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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
|
# 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.
=> ./2021-11-29-bash-golf-part-1.gmi 2021-11-29 Bash Golf Part 1
=> ./2022-01-01-bash-golf-part-2.gmi 2022-01-01 Bash Golf Part 2
=> ./2023-12-10-bash-golf-part-3.gmi 2023-12-10 Bash Golf Part 3
=> ./2025-09-14-bash-golf-part-4.gmi 2025-09-14 Bash Golf Part 4 (You are currently reading this)
```
'\ '\ '\ '\ . . |>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:
```bash
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):
```bash
/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:
```bash
set -o pipefail
printf 'ok\n' | tee >(false) | cat >/dev/null
echo $? # 1 because a side branch failed
```
Further reading:
=> https://blogtitle.github.io/splitting-pipelines/ Splitting pipelines with tee
## Heredocs for remote sessions (and their gotchas)
Heredocs are great to send multiple commands over SSH in a readable way:
```bash
ssh "$SSH_USER@$SSH_HOST" <<EOF
# Go to the work directory
cd "$WORK_DIR"
# Make a git pull
git pull
# Export environment variables required for the service to run
export AUTH_TOKEN="$APP_AUTH_TOKEN"
# Start the service
docker compose up -d --build
EOF
```
Tips:
Quoting the delimiter changes interpolation. Use `<<'EOF'` to avoid local expansion and send the content literally.
Example:
```bash
FOO=bar
cat <<'EOF'
$FOO is not expanded here
EOF
```
Prefer explicit quoting for variables (as above) to avoid surprises. Example (spaces preserved only when quoted):
```bash
WORK_DIR="/tmp/my work"
ssh host <<EOF
cd $WORK_DIR # may break if unquoted
cd "$WORK_DIR" # safe
EOF
```
Consider `set -euo pipefail` at the top of the remote block for stricter error handling. Example:
```bash
ssh host <<'EOF'
set -euo pipefail
false # causes immediate failure
echo never
EOF
```
Indent-friendly variant: use a dash to strip leading tabs in the body:
```bash
cat <<-EOF > script.sh
#!/usr/bin/env bash
echo "tab-indented content is dedented"
EOF
```
Further reading:
=> https://rednafi.com/misc/heredoc_headache/ Heredoc headaches and fixes
## 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.
```bash
#!/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
done
```
Output:
```
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:
```bash
user_name=paul
declare -n ref=user_name
echo "$ref" # paul
ref=julia
echo "$user_name" # julia
```
Output:
```
paul
julia
```
Namerefs are local to functions when declared with `local -n`. Requires Bash ≥4.3.
You can also construct the target name dynamically:
```bash
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 7
```
Output:
```
seven
```
## Function declaration forms
All of these work in Bash, but only the first one is POSIX-ish:
```bash
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:
```bash
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
fi
```
You can also compress it golf-style:
```bash
deploy_check || smoke_test && echo ok || echo fail >&2
```
## Grep, sed, awk quickies
Word match and context: `grep -w word file`; with context: `grep -C3 foo file` (same as `-A3 -B3`). Example:
```bash
cat > /tmp/ctx.txt <<EOF
one
foo
two
three
bar
EOF
grep -C1 foo /tmp/ctx.txt
```
Output:
```
one
foo
two
```
Skip a directory while recursing: `grep -R --exclude-dir=foo 'bar' /path`. Example:
```bash
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/golf
```
Output:
```
/tmp/golf/src/a.txt:bar
```
Insert lines with sed: `sed -e '1isomething' -e '3isomething' file`. Example:
```bash
printf 'A\nB\nC\n' > /tmp/s.txt
sed -e '1iHEAD' -e '3iMID' /tmp/s.txt
```
Output:
```
HEAD
A
B
MID
C
```
Drop last column with awk: `awk 'NF{NF-=1};1' file`. Example:
```bash
printf 'a b c\nx y z\n' > /tmp/t.txt
cat /tmp/t.txt
echo
awk 'NF{NF-=1};1' /tmp/t.txt
```
Output:
```
a b c
x y z
a b
x y
```
## Safe xargs with NULs
Avoid breaking on spaces/newlines by pairing `find -print0` with `xargs -0`:
```bash
find . -type f -name '*.log' -print0 | xargs -0 rm -f
```
Example with spaces and NULs only:
```bash
printf 'a\0b c\0' | xargs -0 -I{} printf '<%s>\n' {}
```
Output:
```
<a>
<b c>
```
## Efficient file-to-variable and arrays
Read a whole file into a variable without spawning `cat`:
```bash
cfg=$(<config.ini)
```
Read lines into an array safely with `mapfile` (aka `readarray`):
```bash
mapfile -t lines < <(grep -v '^#' config.ini)
printf '%s\n' "${lines[@]}"
```
Assign formatted strings without a subshell using `printf -v`:
```bash
printf -v msg 'Hello %s, id=%04d' "$USER" 42
echo "$msg"
```
Output:
```
Hello paul, id=0042
```
Read NUL-delimited data (pairs well with `-print0`):
```bash
mapfile -d '' -t files < <(find . -type f -print0)
printf '%s\n' "${files[@]}"
```
## Quick password generator
Pure Bash with `/dev/urandom`:
```bash
LC_ALL=C tr -dc 'A-Za-z0-9_' </dev/urandom | head -c 16; echo
```
Alternative using `openssl`:
```bash
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:
```bash
yes | rm -r large_directory # auto-confirm
yes n | dangerous-command # auto-decline
yes anything | head -n1 # prints one line: anything
```
## Forcing `true` to fail (and vice versa)
You can shadow builtins with functions:
```bash
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 0
```
To disable a builtin entirely: `enable -n true` (re-enable with `enable true`).
Further reading:
=> https://blog.robertelder.org/force-true-command-to-return-false/ Force true to return false
## 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:
```bash
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: restricted
```
## Useless use of cat (and when it’s ok)
Avoid the extra process if a command already reads files or `STDIN`:
```bash
# Prefer
grep -i foo file
<file grep -i foo # or feed via redirection
# Over
cat file | grep -i foo
```
But 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:
```bash
cat file1 file2 | grep -i foo
```
From 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:
```bash
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
fi
```
This 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:
```bash
find . -name '*.log' -exec gzip -9 {} +
```
Example for extglob (exclude two dirs from listing):
```bash
shopt -s extglob
ls -d -- !(.git|node_modules) 2>/dev/null
```
E-Mail your comments to `paul@nospam.buetow.org` :-)
Other related posts are:
=> ./2025-09-14-bash-golf-part-4.gmi 2025-09-14 Bash Golf Part 4 (You are currently reading this)
=> ./2023-12-10-bash-golf-part-3.gmi 2023-12-10 Bash Golf Part 3
=> ./2022-01-01-bash-golf-part-2.gmi 2022-01-01 Bash Golf Part 2
=> ./2021-11-29-bash-golf-part-1.gmi 2021-11-29 Bash Golf Part 1
=> ./2021-06-05-gemtexter-one-bash-script-to-rule-it-all.gmi 2021-06-05 Gemtexter - One Bash script to rule it all
=> ./2021-05-16-personal-bash-coding-style-guide.gmi 2021-05-16 Personal Bash coding style guide
=> ../ Back to the main site
|