Shell snippets
Some advanced shell stuff I need from time to time, perhaps this helps others as well.
- shell built-ins don’t have real manpages - they are documented in the manpage of their shell
- POSIX.1-2024 Shell & Utilities 2. Shell Command Language
- Allow comments in interactive zsh shells with
set -k(otherwise you might get some errors with the examples)
Enable “abort on error”¶
Abort shell scripts if a command fails. It doesn’t trigger in anything
called from if, elif, while and until conditions, and only in
the last component of && and || expressions (read the man page for
details of your shell).
Shells usually accept -e as parameter too, like #!/bin/bash -e
Example 1¶
This prints only the line with “a” - then the shell exits (f is called in a subshell so your real shell doesn’t close if you try this)
Example 2¶
This prints “a” and then “b” - set -e doesn’t have any effect as it is
called from an || expression, it doesn’t exit on false, and echo "b"
returns success. This time the shell doesn’t exit, so the subshell (…)
could be removed.
So don’t use set -e in scripts where you call complex functions in
conditionals - it probably doesn’t work like you want it to, but it is
nice in small scripts to simplify error handling.
References¶
- POSIX set built-in
- bash set
- Part of the zshoptions “sh/ksh emulation set” (ERR_EXIT)
Quoting¶
Quoting is needed to deal with variables containing spaces (and similar special characters).
Quoting single arguments is usually easy, just put them in double quotes and everythings works fine, like this:
You don’t need quotes to copy a variable (although I like to add them):
The curly braces are only needed in special cases like this:
To build large argument lists you need arrays:
Arrays¶
Not supported by POSIX sh, except for this feature: you can loop through all arguments like this:
bash and zsh support named arrays:
# print each parameter on a single line
pa() { local x; for x in "$@"; do echo "$x"; done; }
# create an array with zero elements
a=()
# append elements to an array
text="abc xyz"
a+=("$text" Z)
# expand each array entry to a separate parameter
pa "${a[@]}"
echo "Entries: ${#a[@]}"
If you need two dimensional arrays you have to use some way to “flatten” it - if the second dimension is fixed this is easy, otherwise you could store offset and length of each subarray in the flattened array.
Associative arrays are also supported:
# Need to tell the shell that "a" is an associatve array.
unset a; declare -A a
a[a]="abc"
echo "Entry 'a': ${a[a]}"
You can get a list of all keys with "${!t[@]}" in bash and
"${(k)t[@]}" in zsh. The list of all entries works the same as in
normal arrays.
Temporary files¶
If you create temporary stuff in your scripts, make sure you clean it up afterwards. Usually you should clean up even if errors happened, and a simple way to do this are EXIT traps.
tmpdir=$(mktemp --tmpdir -d myscriptname-XXXXXXX)
trap 'rm -rf "${tmpdir}"' EXIT
# now put all your stuff in "${tmpdir}/"
You can have only one exit trap - if you need a more dynamic hook system you have to build it yourself (register functions to execeute in an array or something like that).
Cron jobs and locking¶
Sometimes your cron jobs are so slow, the next call starts before your old finished (or you started it manually). In order to prevent your script from interfering with itself you want to use locking. Each job should use a different lockfile of course.
LOCKFILE=~/.lock/my-scriptname.lock
(
if ! flock -n 9; then
echo "Couldn't lock '${LOCKFILE}', exit" >&2
exit 1
fi
(
# do stuff
echo "long job!"
sleep 100
echo "done"
) 9>&-
) 9>>"${LOCKFILE}"
The inner subshell is used to hide the lockfile (in case you start
daemons), 9>&- closes the file descriptor for the inner shell.
The outer subshell keeps the lockfile open, which is needed to hold the
lock - the lock is released if either all fds are closed that held the
lock, or if you call flock -u on one of them.
(I couldn’t find flock in posix specs - it works on linux and freebsd, but probably only on local filesystems. Shell doesn’t matter as flock is not a built-in.)
Security note
This does not create the LOCKFILE in a secure way, so
don’t use world writable locations with this snippet.
I haven’t found a way yet to do this in a secure way; creating files
with O_EXCL is not enough, if creation fails you also need to check the
existing file to not be a symlink and be owned by you in one step
(using results from the same lstat() call), and the standard “test”
binary doesn’t provide this.
Also have a look at BashFAQ/045, it has some examples without flock (but they don’t recover if a previous run didn’t delete the lock file/directory).
Subshells¶
There is probably much more to say on this subject.. so just some small examples.
Subshells can be created with (commandlist), also asynchronous
commands are executed in a subshell. Changes in subshells don’t affect
the parent:
Commands in a pipe may be executed in a subshell (bash does this for all commands in a pipe, zsh for all but the last one), so this works in zsh but not in bash:
Subshells inherit everything from the parent shell apart from the trap functions which are reset to their default behaviour.
Redirects¶
Note
POSIX only requires support for file descriptors 0 - 9 - for example dash and zsh are limited to single digit file descriptors.
This is only a short list - for details lookup some shell man pages (this is from the dash man page).
[n]> file-
Redirect standard output (or n) to file; fails if “noclobber” option (-C) is active and the file exists and is a regular file.
[n]>| file-
Same, but ignores noclobber.
[n]>> file-
Append standard output (or n) to file.
[n]< file-
Redirect standard input (or n) from file.
[n]<&m-
Duplicate standard input (or n) from file descriptor m.
[n]<&--
Close standard input (or n). Not POSIX.
[n]>&m-
Duplicate standard output (or n) to m.
[n]>&--
Close standard output (or n). Not POSIX.
[n]<>-
Open file for reading and writing on standard input (or n).
Special redirects¶
“Executing” redirects applies the redirects to the current (sub)shell.
Redirect stderr to a logfile:
Redirect stderr to stdout
Moving filedescriptors (bash only)¶
Move fd 6 to fd 5. Same as exec 6>&5 5>&-:
Reverse pipe, process substitution (bash, zsh)¶
Above there was an example with subshells that fails in bash. Here is how to fix it:
<(...) (and >(...)) is replaced by a filename to a pipe which is
connected to the specified command.
Redirect multiple pipes to one process:
Safe iterating over file list from find (using NUL-separated filenames;
note the space between -d and ''):
Be careful with using stdin in such loops - some programs read from stdin if it isn’t a tty device, assuming you piped them some commands. This is why I used fd 3; if you want to be extra careful you can close fd 3 for some commands/subshells in the loop.
Also check out Process Substitution