Intro

It is nearly impossible to write shell scripts in safe manner. Even more difficult to make it work across all POSIX-conformant shells. Here follow some tricks to get a little closer to the ideal scripting.

Header

set -euC
# bash specific
shopt -s expand_aliases 2>>/dev/null ||:
# zsh specific
setopt shwordsplit 2>>/dev/null ||:

# (not POSIX) ksh specific
(exec >>/dev/null 2>&1; f() { local a; }; f) ||
	alias local=typeset

errexit and nounset are recommended very often, skpping the explanation here. noclobber just makes difference for > and >|, would not harm if one always keeps this in mind.

expand_aliases makes bash to expand aliases like all other shells do.

shwordsplit makes zsh to perform word splitting as in POSIX.

Variable scoping is supported in all nowadays shells, but not specified in POSIX yet. All shells has local builtin for this, but ksh defines typeset instead. A workaround here is to create an alias in ksh and use local everywhere. With no parameters, just for scoping. It should be kept in mind that alias substitution might not happen under some unobvious conditions:

# 'local' gets replaced with 'typeset' here
$ cat <<-"EOF" | ksh93 -euC /dev/stdin
alias local=typeset
foo() { local a ; echo OK ; }
foo
EOF

OK

# 'local' does not undergo alias replacement here
$ cat <<-"EOF" | ksh93 -euCc '. /dev/stdin'
alias local=typeset
foo() { local a ; echo OK ; }
foo
EOF

ksh93: .[2]: local: not found [No such file or directory]

pipefail is often recommended as well. It is not mentioned in POSIX and it is easy to find shells which do not support the feature in any form. The same applies to nullglob/failglob.

Notes

noclobber

Using noclobber option implies a little brain overhead. The most common thing would be to get used to writting 2>>/dev/null in place of widespread 2>/dev/null. Besides that >| redirection form is not of a wide use.

# appending to /dev/null, not overwritting
command set -o pipefail 2>>/dev/null

# intentionally overwrite file if exists
tmp_fn="$(mktemp)"
… some code follows …
LC_ALL=C TZ=UTC date +%F >|"$tmp_fn"

errexit

errexit helps to interrupt script execution when things go wrong in some cases. To make it work bash to work identical to other shells, the option should also be set again in functions and subshells:

# functions
foo() {
	set -e
	… function body …
}

# subshells
v="$(set -e ; … command(s) follow …)"

One more drawback of errexit is that there is no debug output produced, just a non-zero exit code. It might be nontrivial to find out where it stopped exactly.

trap - EXIT

It is not defined what «EXIT» means. Explicit exit makes that condition. Regular commands, interrupted due to errexit, do too. Special built-ins are different, unless prepended with command. A different story when script is interrupted by a signal. One more case when script stops due to nounset option. Briefly saying «EXIT» trap is not very reliable.

Variable Scoping

This is likely to be included in POSIX eventually, but not yet. For now positional parameters are the only «variables» that are local to a function. Taking into consideration that shells tend to implement dynamic scoping, standardization of local variables will not change the statement.

Subshells are also of use for limiting variable scope. Might be useful to unset introduced variables in order to avoid them propagated.

bar() {
	# no sane way 'alpha' 'beta' 'gamma' can be found here
	… useful work with 'one' 'two' 'three' …
}

# local variables needed, spot the subshell here
foo() (
	i=0
	… work with local i …
	# make sure i is not propagated to bar()
	unset i
	bar 'one' 'two' 'three'
)

foo 'alpha' 'beta' 'gamma'

Related Links