Bash reference: interactive history tricks, a pure-bash scripting library based on pure-bash-bible, parameter expansion, and the shell’s weirder corners.
Interactive shell tricks
Create empty files without spawning touch — the colon is a no-op and redirection creates the file:
:> filename
>file
printf '' >file
History expansion
| Syntax | Effect |
|---|---|
!! | Repeat last command |
!prefix | Run last command starting with prefix |
!$ | Last argument of previous command |
!#:1 | First argument of current command line |
!:1 | First argument of previous command |
^old^new | Find and replace in previous command |
!CDruns the last command that started withCD.!$reruns the previous command’s argument — reuse the path from anlsin another command:envm !$.!#:1references the first argument on the current line;!:1the first argument of the previous command.^fuzzy^fzfcorrects a typo or command name in the previous command in place.
Special parameters
$$— PID of the current shell.echo $$.$_— last argument of the previous command.ls -la mydir, thenecho $_printsmydir.
Expansion and globs
mkdir -p mydir2/{dir1,dir2,dir3} # nested dirs in one command
rm file[14] # glob removal: file1 and file4
cp file.txt{,.back} # creates file.txt.back
Brace expansion covers numbers, characters, nesting, zero-padding, and steps:
echo {1..100} # numbers
echo {a..z} # chars
echo {A..Z}{0..9} # nested
echo {01..100} # zero-padded (bash 4+)
echo {1..10..2} # increment by 2 (bash 4+)
echo {apples,oranges} # string list
Randomness
$RANDOM prints a pseudo-random integer from 0 to 32767 (a 16-bit maximum). Arithmetic limits the range: echo $((RANDOM % 10 + 1)) gives 1–10. It is not truly random and the modulo operator introduces bias.
shuf (GNU coreutils) is the unbiased tool:
shuf my_file # randomize lines
shuf -n 1 my_file # one random line
shuf -i 1-1000 -n 1 # random number in range
shuf -i 1-1000 # random permutation of 1..1000
yes repeatedly prints a string (default y) until killed — yes | sudo apt-get install -y zip automates confirmations, though it is rarely a good idea.
Prompts and animations
Prompt (PS1) can embed commands: branch the arrow colour on the previous exit status ($?, tested with test), or show git branch. Keep PS1 commands lightweight — they run every time the prompt is drawn.
A spinner proves a long-running script has not hung: a while true loop printing characters with echo -n, \r to return the cursor, and sleep for pacing. A one-line clock is the same shape:
while true; do clear; date +%T; sleep 1; done
Pure-bash scripting library
Strings
trim_string() {
: "${1#"${1%%[![:space:]]*}"}"
: "${_%"${_##*[![:space:]]}"}"
printf '%s\n' "$_"
}
trim_all() {
set -f
set -- $*
printf '%s\n' "$*"
set +f
}
regex() {
[[ $1 =~ $2 ]] && printf '%s\n' "${BASH_REMATCH[1]}"
}
split() { # bash 4+
IFS=$'\n' read -d "" -ra arr <<< "${1//$2/$'\n'}"
printf '%s\n' "${arr[@]}"
}
lower() { printf '%s\n' "${1,,}"; } # bash 4+
upper() { printf '%s\n' "${1^^}"; }
reverse_case() { printf '%s\n' "${1~~}"; }
Substring checks and pattern stripping:
[[ $var == *sub_string* ]] # contains
[[ $var == sub_string* ]] # starts with
[[ $var == *sub_string ]] # ends with
"${var/pattern}" # first match
"${var//pattern}" # all matches
"${var##pattern}" # from start (longest)
"${var%%pattern}" # from end (longest)
URL encode/decode:
urlencode() {
local LC_ALL=C
for (( i = 0; i < ${#1}; i++ )); do
: "${1:i:1}"
case "$_" in
[a-zA-Z0-9.~_-]) printf '%s' "$_" ;;
*) printf '%%%02X' "'$_" ;;
esac
done
printf '\n'
}
urldecode() {
: "${1//+/ }"
printf '%b\n' "${_//%/\\x}"
}
Arrays
reverse_array() {
shopt -s extdebug
f()(printf '%s\n' "${BASH_ARGV[@]}"); f "$@"
shopt -u extdebug
}
remove_array_dups() { # bash 4+
declare -A tmp_array
for i in "$@"; do
[[ $i ]] && IFS=" " tmp_array["${i:- }"]=1
done
printf '%s\n' "${!tmp_array[@]}"
}
random_array_element() {
local arr=("$@")
printf '%s\n' "${arr[RANDOM % $#]}"
}
Loops
for i in {0..100}; do printf '%s\n' "$i"; done # fixed range
for ((i=0;i<=VAR;i++)); do printf '%s\n' "$i"; done # variable range
for element in "${arr[@]}"; do printf '%s\n' "$element"; done
for i in "${!arr[@]}"; do printf '%s\n' "${arr[i]}"; done
Iterate files without parsing ls:
for file in *; do printf '%s\n' "$file"; done
for file in ~/Pictures/*.png; do printf '%s\n' "$file"; done
for dir in ~/Downloads/*/; do printf '%s\n' "$dir"; done
shopt -s globstar
for file in ~/Pictures/**/*; do printf '%s\n' "$file"; done
shopt -u globstar
File handling
file_data="$(<"file")" # to string
mapfile -t file_data < "file" # to array (bash 4+)
head() { mapfile -tn "$1" line < "$2"; printf '%s\n' "${line[@]}"; } # bash 4+
tail() { mapfile -tn 0 line < "$2"; printf '%s\n' "${line[@]: -$1}"; }
lines() { mapfile -tn 0 lines < "$1"; printf '%s\n' "${#lines[@]}"; }
extract() {
while IFS=$'\n' read -r line; do
[[ $extract && $line != "$3" ]] && printf '%s\n' "$line"
[[ $line == "$2" ]] && extract=1
[[ $line == "$3" ]] && extract=
done < "$1"
}
File paths
dirname() {
local tmp=${1:-.}
[[ $tmp != *[!/]* ]] && { printf '/\n'; return; }
tmp=${tmp%%"${tmp##*[!/]}"}
[[ $tmp != */* ]] && { printf '.\n'; return; }
tmp=${tmp%/*}
tmp=${tmp%%"${tmp##*[!/]}"}
printf '%s\n' "${tmp:-/}"
}
basename() {
local tmp=${1%"${1##*[!/]}"}
tmp=${tmp##*/}
tmp=${tmp%"${2/"$tmp"}"}
printf '%s\n' "${tmp:-/}"
}
Variables
hello_world="value"
var="world"
ref="hello_$var"
printf '%s\n' "${!ref}" # value — indirection
declare -n ref=hello_$var # bash 4.3+ — nameref
printf '%s\n' "$ref"
Parameter expansion
| Expansion | Effect |
|---|---|
${!VAR} | Access variable by name stored in VAR |
${VAR#P} | Remove shortest match from start |
${VAR##P} | Remove longest match from start |
${VAR%P} | Remove shortest match from end |
${VAR%%P} | Remove longest match from end |
${VAR/P/R} | Replace first match |
${VAR//P/R} | Replace all matches |
${#VAR} | Length in characters |
${VAR:OFFSET:LEN} | Substring |
${VAR:-DEFAULT} | Default if empty/unset |
${VAR:=DEFAULT} | Set default if empty/unset |
${VAR:?ERROR} | Error if empty |
Conditionals
- File tests:
-eexists,-ddirectory,-fregular file,-rreadable,-wwritable,-xexecutable,-snon-empty,-Lsymlink,-Nmodified since last read. - File comparison:
file -nt file2newer,file -ot file2older,file -ef file2same inode. - Variable tests:
-zzero length,-nnon-zero,-vhas value,-Ris nameref.
Arithmetic
((var=1+2))
((var++))
((var+=1))
((var=var2>var?var2:var)) # ternary
Operators: +, -, *, /, **, %, &, |, ^, <<, >>.
Traps
trap 'printf \\e[2J\\e[H\\e[m' EXIT # on exit
trap '' INT # ignore Ctrl+C
trap 'code_here' SIGWINCH # on resize
trap 'code_here' DEBUG # before every command
trap 'code_here' RETURN # after function returns
Escape sequences
Text colours:
| Sequence | Effect |
|---|---|
\e[38;5;<N>m | Foreground color (0-255) |
\e[48;5;<N>m | Background color (0-255) |
\e[38;2;<R>;<G>;<B>m | Foreground RGB |
\e[48;2;<R>;<G>;<B>m | Background RGB |
Attributes: \e[1m bold, \e[3m italic, \e[4m underline, \e[7m highlight, \e[9m strike-through, \e[m reset.
Cursor: \e[<LINE>;<COLUMN>H absolute, \e[A up, \e[B down, \e[C right, \e[D left, \e[s save, \e[u restore.
Internal variables
| Variable | Effect |
|---|---|
$BASH | Path to bash binary |
$BASH_VERSION | Bash version string |
${BASH_VERSINFO[@]} | Bash version array |
${EDITOR:-vi} | User’s editor |
${FUNCNAME[0]} | Current function name |
$HOSTNAME | System hostname |
$HOSTTYPE | Architecture |
$OSTYPE | OS/kernel name |
$PWD | Current directory |
$SECONDS | Script runtime in seconds |
$RANDOM | Pseudorandom integer (0-32767) |
$_ | Last argument of previous command |
$$ | Current shell PID |
Terminal
get_term_size() {
shopt -s checkwinsize; (:;:)
printf '%s\n' "$LINES $COLUMNS"
}
get_cursor_pos() {
IFS='[;' read -p $'\e[6n' -d R -rs _ y x _
printf '%s\n' "$x $y"
}
Conversion
hex_to_rgb() {
: "${1/\#}"
((r=16#${_:0:2},g=16#${_:2:2},b=16#${_:4:2}))
printf '%s\n' "$r $g $b"
}
rgb_to_hex() { printf '#%02x%02x%02x\n' "$1" "$2" "$3"; }
Other tricks
read_sleep() { read -rt "$1" <> <(:) || :; } # sleep alternative (bash 4+)
type -p executable_name &>/dev/null # check program in PATH
command -v executable_name &>/dev/null
printf '%(%a %d %b - %l:%M %p)T\n' "-1" # date via printf (bash 4+)
\ls # bypass alias
command ls # bypass function
bkr() { (nohup "$@" &>/dev/null &); } # background command
bar() { # progress bar
((elapsed=$1*$2/100))
printf -v prog "%${elapsed}s"
printf -v total "%$(($2-elapsed))s"
printf '%s\r' "[${prog// /-}${total}]"
}
UUID v4 in pure bash:
uuid() {
C="89ab"
for ((N=0;N<16;++N)); do
B="$((RANDOM%256))"
case "$N" in
6) printf '4%x' "$((B%16))" ;;
7) printf '%c%x' "${C:$RANDOM%${#C}:1}" "$((B%16))" ;;
3|5|7|9) printf '%02x-' "$B" ;;
*) printf '%02x' "$B" ;;
esac
done
printf '\n'
}
Parameter expansion in practice
Prefix and suffix removal, and the #/% versus / distinction:
VERSION=${IMAGE_TAG#v} # v1.2.3 → 1.2.3 (shortest prefix)
${VAR##pattern} # longest prefix
VERSION=${IMAGE_TAG%-beta*} # 1.2.3-beta45 → 1.2.3 (shortest suffix)
${VAR%%pattern} # longest suffix
${VAR/pattern/replacement} # replace first match
${VAR//pattern/replacement} # replace all matches
branch=${1//\//_} # feature/test → feature_test
${VAR:offset}
${VAR:offset:length}
${VAR:-default} # use default if unset
${VAR:=default} # assign default if unset
| Syntax | Operation | Example |
|---|---|---|
${VAR#pattern} | Remove prefix (shortest) | ${IMAGE_TAG#v} |
${VAR##pattern} | Remove prefix (longest) | |
${VAR%pattern} | Remove suffix (shortest) | ${IMAGE_TAG%-beta*} |
${VAR%%pattern} | Remove suffix (longest) | |
${VAR/pattern/} | Substitute first match | ${IMAGE_TAG/-beta*/} |
Dockerfile ENV and ARG do not support bash parameter expansion. Do it in a RUN:
| |
Shebang choice
| Shebang | Use When |
|---|---|
#!/bin/bash | Need bash features (arrays, [[ ]], brace expansion) |
#!/usr/bin/env sh | Maximum portability, POSIX compliance only |
#!/usr/bin/env sh finds sh via PATH and is portable across Unix-like systems; #!/bin/bash assumes bash at /bin/bash — predictable but less portable.
AWK path extraction
echo "/path/to/dir abc123 [branch]" | awk '{print $1}'
# Output: /path/to/dir
Default field separator is whitespace; use -F' +' for runs of spaces.
Shell magic and easter eggs
- Zsh Tetris. Zsh ships a native
tetriscursesmodule:autoload -Uz tetriscurses, thentetriscurses. It needs thecursesmodule; arrow keys move/rotate, Down drops, no Vim bindings. Zsh stays smaller than bash even with these modules. - The fork bomb.
:(){ :|:& };:defines a function:that pipes itself into itself in the background and immediately calls it —:()defines it,:|:pipes it,&backgrounds it,;:runs it. Process count grows exponentially until the OS cannot cope and evenhtopmay fail to open. - Calendar oddity.
cal 1752shows September 1752 missing 11 days (Sept 3–13) — the British Empire’s switch to the Gregorian calendar is preserved in the utility.
🚨 Never run the fork bomb on a production system or any machine you cannot reboot.
PS1 logic and : > file.txt are the interactive tricks above; cal and the fork bomb are the historical colour — command-line utilities keep cultural shifts baked into their logic.