Bash Golf Part 4
Published at 2025-09-13T12:04:03+03:00
This is the fourth blog post about my Bash Golf series. This series is random Bash tips, tricks, and weirdnesses I have encountered over time.
'\ '\ '\ '\ . . |>18>>
\ \ \ \ . ' . |
O>> O>> O>> O>> . 'o |
\ .\. .. .\. .. .\. .. . |
/\ . /\ . /\ . /\ . . |
/ / . / / .'. / / .'. / / .' . |
jgs^^^^^^^`^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Art by Joan Stark, mod. by Paul Buetow
Table of Contents
- ⇢ Bash Golf Part 4
- ⇢ ⇢ Split pipelines with tee + process substitution
- ⇢ ⇢ Heredocs for remote sessions (and their gotchas)
- ⇢ ⇢ Namespacing and dynamic dispatch with `::`
- ⇢ ⇢ Indirect references with namerefs
- ⇢ ⇢ Function declaration forms
- ⇢ ⇢ Chaining function calls in conditionals
- ⇢ ⇢ Grep, sed, awk quickies
- ⇢ ⇢ Safe xargs with NULs
- ⇢ ⇢ Efficient file-to-variable and arrays
- ⇢ ⇢ Quick password generator
- ⇢ ⇢ `yes` for automation
- ⇢ ⇢ Forcing `true` to fail (and vice versa)
- ⇢ ⇢ Restricted Bash
- ⇢ ⇢ Useless use of cat (and when it’s ok)
- ⇢ ⇢ Atomic locking with `mkdir`
- ⇢ ⇢ Smarter globs and faster find-exec
Split pipelines with tee + process substitution
Sometimes you want to fan out one stream to multiple consumers and still continue the original pipeline. `tee` plus process substitution does exactly that:
somecommand \
| tee >(command1) >(command2) \
| command3
All of `command1`, `command2`, and `command3` see the output of `somecommand`. Example:
printf 'a\nb\n' \
| tee >(sed 's/.*/X:&/; s/$/ :c1/') >(tr a-z A-Z | sed 's/$/ :c2/') \
| sed 's/$/ :c3/'
Output:
a :c3 b :c3 A :c2 :c3 B :c2 :c3 X:a :c1 :c3 X:b :c1 :c3
This relies on Bash process substitution (`>(...)`). Make sure your shell is Bash and not a POSIX `/bin/sh`.
Example (fails under `dash`/POSIX sh):
/bin/sh -c 'echo hi | tee >(cat)'
# /bin/sh: 1: Syntax error: "(" unexpected
Combine with `set -o pipefail` if failures in side branches should fail the whole pipeline.
Example:
set -o pipefail printf 'ok\n' | tee >(false) | cat >/dev/null echo $? # 1 because a side branch failed
Further reading:
Heredocs for remote sessions (and their gotchas)
Heredocs are great to send multiple commands over SSH in a readable way:
ssh "$SSH_USER@$SSH_HOST" <Tips:
Quoting the delimiter changes interpolation. Use `<<'EOF'` to avoid local expansion and send the content literally.
Example:
FOO=bar cat <<'EOF' $FOO is not expanded here EOFPrefer explicit quoting for variables (as above) to avoid surprises. Example (spaces preserved only when quoted):
WORK_DIR="/tmp/my work" ssh host <Consider `set -euo pipefail` at the top of the remote block for stricter error handling. Example:
ssh host <<'EOF' set -euo pipefail false # causes immediate failure echo never EOFIndent-friendly variant: use a dash to strip leading tabs in the body:
cat <<-EOF > script.sh #!/usr/bin/env bash echo "tab-indented content is dedented" EOFFurther reading:
Namespacing and dynamic dispatch with `::`
You can emulate simple namespacing by encoding hierarchy in function names. One neat pattern is pseudo-inheritance via a tiny `super` helper that maps `pkg::lang::action` to a `pkg::base::action` default.
#!/usr/bin/env bash set -euo pipefail super() { local -r fn=${FUNCNAME[1]} # Split name on :: and dispatch to base implementation local -a parts=( ${fn//::/ } ) "${parts[0]}::base::${parts[2]}" "$@" } foo::base::greet() { echo "base: $@"; } foo::german::greet() { super "Guten Tag, $@!"; } foo::english::greet() { super "Good day, $@!"; } for lang in german english; do foo::$lang::greet Paul doneOutput:
base: Guten Tag, Paul! base: Good day, Paul!Indirect references with namerefs
`declare -n` creates a name reference — a variable that points to another variable. It’s cleaner than `eval` for indirection:
user_name=paul declare -n ref=user_name echo "$ref" # paul ref=julia echo "$user_name" # juliaOutput:
paul juliaNamerefs are local to functions when declared with `local -n`. Requires Bash ≥4.3.
You can also construct the target name dynamically:
make_var() { local idx=$1; shift local name="slot_$idx" printf -v "$name" '%s' "$*" # create variable slot_$idx } get_var() { local idx=$1 local -n ref="slot_$idx" # bind ref to slot_$idx printf '%s\n' "$ref" } make_var 7 "seven" get_var 7Output:
sevenFunction declaration forms
All of these work in Bash, but only the first one is POSIX-ish:
foo() { echo foo; } function foo { echo foo; } function foo() { echo foo; }Recommendation: prefer `name() { ... }` for portability and consistency.
Chaining function calls in conditionals
Functions return a status like commands. You can short-circuit them in conditionals:
deploy_check() { test -f deploy.yaml; } smoke_test() { curl -fsS http://localhost/healthz >/dev/null; } if deploy_check || smoke_test; then echo "All good." else echo "Something failed." >&2 fiYou can also compress it golf-style:
deploy_check || smoke_test && echo ok || echo fail >&2Grep, sed, awk quickies
Word match and context: `grep -w word file`; with context: `grep -C3 foo file` (same as `-A3 -B3`). Example:
cat > /tmp/ctx.txt <Output:
one foo twoSkip a directory while recursing: `grep -R --exclude-dir=foo 'bar' /path`. Example:
mkdir -p /tmp/golf/foo /tmp/golf/src printf 'bar\n' > /tmp/golf/src/a.txt printf 'bar\n' > /tmp/golf/foo/skip.txt grep -R --exclude-dir=foo 'bar' /tmp/golfOutput:
/tmp/golf/src/a.txt:barInsert lines with sed: `sed -e '1isomething' -e '3isomething' file`. Example:
printf 'A\nB\nC\n' > /tmp/s.txt sed -e '1iHEAD' -e '3iMID' /tmp/s.txtOutput:
HEAD A B MID CDrop last column with awk: `awk 'NF{NF-=1};1' file`. Example:
printf 'a b c\nx y z\n' > /tmp/t.txt cat /tmp/t.txt echo awk 'NF{NF-=1};1' /tmp/t.txtOutput:
a b c x y z a b x ySafe xargs with NULs
Avoid breaking on spaces/newlines by pairing `find -print0` with `xargs -0`:
find . -type f -name '*.log' -print0 | xargs -0 rm -fExample with spaces and NULs only:
printf 'a\0b c\0' | xargs -0 -I{} printf '<%s>\n' {}Output:
Efficient file-to-variable and arrays
Read a whole file into a variable without spawning `cat`:
cfg=$(Read lines into an array safely with `mapfile` (aka `readarray`):
mapfile -t lines < <(grep -v '^#' config.ini) printf '%s\n' "${lines[@]}"Assign formatted strings without a subshell using `printf -v`:
printf -v msg 'Hello %s, id=%04d' "$USER" 42 echo "$msg"Output:
Hello paul, id=0042Read NUL-delimited data (pairs well with `-print0`):
mapfile -d '' -t files < <(find . -type f -print0) printf '%s\n' "${files[@]}"Quick password generator
Pure Bash with `/dev/urandom`:
LC_ALL=C tr -dc 'A-Za-z0-9_'Alternative using `openssl`:
openssl rand -base64 16 | tr -d '\n' | cut -c1-22`yes` for automation
`yes` streams a string repeatedly; handy for feeding interactive commands or quick load generation:
yes | rm -r large_directory # auto-confirm yes n | dangerous-command # auto-decline yes anything | head -n1 # prints one line: anythingForcing `true` to fail (and vice versa)
You can shadow builtins with functions:
true() { return 1; } false() { return 0; } true || echo 'true failed' false && echo 'false succeeded' # Bypass function with builtin/command builtin true # returns 0 command true # returns 0To disable a builtin entirely: `enable -n true` (re-enable with `enable true`).
Further reading:
Restricted Bash
`bash -r` (or `rbash`) starts a restricted shell that limits potentially dangerous actions, for example:
- Changing directories (`cd`).
- Modifying `PATH`, `SHELL`, `BASH_ENV`, or `ENV`.
- Redirecting output.
- Running commands with `/` in the name.
- Using `exec`.
It’s a coarse sandbox for highly constrained shells; read `man bash` (RESTRICTED SHELL) for details and caveats.
Example session:
rbash -c 'cd /' # cd: restricted rbash -c 'PATH=/tmp' # PATH: restricted rbash -c 'echo hi > out' # redirection: restricted rbash -c '/bin/echo hi' # commands with /: restricted rbash -c 'exec ls' # exec: restrictedUseless use of cat (and when it’s ok)
Avoid the extra process if a command already reads files or `STDIN`:
# Prefer grep -i foo fileBut for interactive composition, or when you truly need to concatenate multiple sources into a single stream, `cat` is fine, as you may think, "First I need the content, then I do X." Changing the "useless use of cat" in retrospect is really a waste of time for one-time interactive use:
cat file1 file2 | grep -i fooFrom notes: “Good for interactivity; Useless use of cat” — use judgment.
Atomic locking with `mkdir`
Portable advisory locks can be emulated with `mkdir` because it’s atomic:
lockdir=/tmp/myjob.lock if mkdir "$lockdir" 2>/dev/null; then trap 'rmdir "$lockdir"' EXIT INT TERM # critical section do_work else echo "Another instance is running" >&2 exit 1 fiThis works well on Linux. Remove the lock in `trap` so crashes don’t leave stale locks.
Smarter globs and faster find-exec
- Enable extended globs when useful: `shopt -s extglob`; then patterns like `!(tmp|cache)` work.
- Use `-exec ... {} +` to batch many paths in fewer process invocations:
find . -name '*.log' -exec gzip -9 {} +Example for extglob (exclude two dirs from listing):
shopt -s extglob ls -d -- !(.git|node_modules) 2>/dev/nullE-Mail your comments to `paul@nospam.buetow.org` :-)
Other related posts are: