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:
1
2
3
| :> 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 |
!CD runs the last command that started with CD.!$ reruns the previous command’s argument — reuse the path from an ls in another command: envm !$.!#:1 references the first argument on the current line; !:1 the first argument of the previous command.^fuzzy^fzf corrects 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, then echo $_ prints mydir.
Expansion and globs
1
2
3
| 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:
1
2
3
4
5
6
| 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:
1
2
3
4
| 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:
1
| while true; do clear; date +%T; sleep 1; done
|
Pure-bash scripting library
Strings
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| 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:
1
2
3
4
5
6
7
8
| [[ $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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 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
1
2
3
4
5
| 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:
1
2
3
4
5
6
| 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 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
1
2
3
4
5
6
7
| 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:
-e exists, -d directory, -f regular file, -r readable, -w writable, -x executable, -s non-empty, -L symlink, -N modified since last read. - File comparison:
file -nt file2 newer, file -ot file2 older, file -ef file2 same inode. - Variable tests:
-z zero length, -n non-zero, -v has value, -R is nameref.
Arithmetic
1
2
3
4
| ((var=1+2))
((var++))
((var+=1))
((var=var2>var?var2:var)) # ternary
|
Operators: +, -, *, /, **, %, &, |, ^, <<, >>.
Traps
1
2
3
4
5
| 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
1
2
3
4
5
6
7
8
9
| 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
1
2
3
4
5
6
7
| 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
| 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
| 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:
1
2
| ARG IMAGE_TAG
RUN VERSION=${IMAGE_TAG%-beta*} && echo "VERSION=$VERSION" > /version
|
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.
1
2
| 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
tetriscurses module: autoload -Uz tetriscurses, then tetriscurses. It needs the curses module; 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 even htop may fail to open. - Calendar oddity.
cal 1752 shows 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.