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 Link to this section

Create empty files without spawning touch — the colon is a no-op and redirection creates the file:

:> filename
>file
printf '' >file

History expansion Link to this section

SyntaxEffect
!!Repeat last command
!prefixRun last command starting with prefix
!$Last argument of previous command
!#:1First argument of current command line
!:1First argument of previous command
^old^newFind 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 Link to this section

  • $$ — PID of the current shell. echo $$.
  • $_ — last argument of the previous command. ls -la mydir, then echo $_ prints mydir.

Expansion and globs Link to this section

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 Link to this section

$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 Link to this section

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 Link to this section

Strings Link to this section

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 Link to this section

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 Link to this section

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 Link to this section

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 Link to this section

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 Link to this section

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 Link to this section

ExpansionEffect
${!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 Link to this section

  • 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 Link to this section

((var=1+2))
((var++))
((var+=1))
((var=var2>var?var2:var))   # ternary

Operators: +, -, *, /, **, %, &, |, ^, <<, >>.

Traps Link to this section

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 Link to this section

Text colours:

SequenceEffect
\e[38;5;<N>mForeground color (0-255)
\e[48;5;<N>mBackground color (0-255)
\e[38;2;<R>;<G>;<B>mForeground RGB
\e[48;2;<R>;<G>;<B>mBackground 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 Link to this section

VariableEffect
$BASHPath to bash binary
$BASH_VERSIONBash version string
${BASH_VERSINFO[@]}Bash version array
${EDITOR:-vi}User’s editor
${FUNCNAME[0]}Current function name
$HOSTNAMESystem hostname
$HOSTTYPEArchitecture
$OSTYPEOS/kernel name
$PWDCurrent directory
$SECONDSScript runtime in seconds
$RANDOMPseudorandom integer (0-32767)
$_Last argument of previous command
$$Current shell PID

Terminal Link to this section

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 Link to this section

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 Link to this section

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 Link to this section

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
SyntaxOperationExample
${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 Link to this section

ShebangUse When
#!/bin/bashNeed bash features (arrays, [[ ]], brace expansion)
#!/usr/bin/env shMaximum 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 Link to this section

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 Link to this section

  • 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.