Next: Limitations of Usual Tools, Previous: Special Shell Variables, Up: Portable Shell
No, no, we are serious: some shells do have limitations! :)
You should always keep in mind that any builtin or command may support
options, and therefore have a very different behavior with arguments
starting with a dash. For instance, the innocent `echo "$word"'
can give unexpected results when word starts with a dash. It is
often possible to avoid this problem using `echo "x$word"', taking
the `x' into account later in the pipe.
Portable scripts should assume neither option is supported, and should
assume neither behavior is the default. This can be a bit tricky,
since the POSIX default behavior means that, for example,
`ls ..' and `cd ..' may refer to different directories if
the current logical directory is a symbolic link. It is safe to use
cd dir if dir contains no .. components.
Also, Autoconf-generated scripts check for this problem when computing
variables like ac_top_srcdir (see Configuration Actions),
so it is safe to cd to these variables.
Also please see the discussion of the pwd command.
You don't need the final `;;', but you should use it.
Because of a bug in its fnmatch, bash fails to properly
handle backslashes in character classes:
bash-2.02$ case /tmp in [/\\]*) echo OK;; esac
bash-2.02$
This is extremely unfortunate, since you are likely to use this code to handle unix or ms-dos absolute paths. To work around this bug, always put the backslash first:
bash-2.02$ case '\TMP' in [\\/]*) echo OK;; esac
OK
bash-2.02$ case /tmp in [\\/]*) echo OK;; esac
OK
Some shells, such as Ash 0.3.8, are confused by an empty
case/esac:
ash-0.3.8 $ case foo in esac;
error-->Syntax error: ";" unexpected (expecting ")")
Many shells still do not support parenthesized cases, which is a pity for those of us using tools that rely on balanced parentheses. For instance, Solaris 8's Bourne shell:
$ case foo in (foo) echo foo;; esac
error-->syntax error: `(' unexpected
echo is probably the most surprising source of
portability troubles. It is not possible to use `echo' portably
unless both options and escape sequences are omitted. New applications
which are not aiming at portability should use `printf' instead of
`echo'.
Don't expect any option. See Preset Output Variables, ECHO_N
etc. for a means to simulate -n.
Do not use backslashes in the arguments, as there is no consensus on their handling. On `echo '\n' | wc -l', the sh of Digital Unix 4.0 and MIPS RISC/OS 4.52, answer 2, but the Solaris' sh, Bash, and Zsh (in sh emulation mode) report 1. Please note that the problem is truly echo: all the shells understand `'\n'' as the string composed of a backslash and an `n'.
Because of these problems, do not pass a string containing arbitrary characters to echo. For example, `echo "$foo"' is safe if you know that foo's value cannot contain backslashes and cannot start with `-', but otherwise you should use a here-document like this:
cat <<EOF
$foo
EOF
$?;
unfortunately, some shells, such as the DJGPP port of Bash 2.04, just
perform `exit 0'.
bash-2.04$ foo=`exit 1` || echo fail
fail
bash-2.04$ foo=`(exit 1)` || echo fail
fail
bash-2.04$ foo=`(exit 1); exit` || echo fail
bash-2.04$
Using `exit $?' restores the expected behavior.
Some shell scripts, such as those generated by autoconf, use a trap to clean up before exiting. If the last shell command exited with nonzero status, the trap also exits with nonzero status so that the invoker can tell that an error occurred.
Unfortunately, in some shells, such as Solaris 8 sh, an exit
trap ignores the exit command's argument. In these shells, a trap
cannot determine whether it was invoked by plain exit or by
exit 1. Instead of calling exit directly, use the
AC_MSG_ERROR macro that has a workaround for this problem.
Alas, many shells, such as Solaris 2.5, irix 6.3, irix 5.2, AIX 4.1.5, and Digital unix 4.0, forget to export the environment variables they receive. As a result, two variables coexist: the environment variable and the shell variable. The following code demonstrates this failure:
#! /bin/sh
echo $FOO
FOO=bar
echo $FOO
exec /bin/sh $0
when run with `FOO=foo' in the environment, these shells will print alternately `foo' and `bar', although it should only print `foo' and then a sequence of `bar's.
Therefore you should export again each environment variable
that you update.
for arg
do
echo "$arg"
done
You may not leave the do on the same line as for,
since some shells improperly grok:
for arg; do
echo "$arg"
done
If you want to explicitly refer to the positional arguments, given the `$@' bug (see Shell Substitutions), use:
for arg in ${1+"$@"}; do
echo "$arg"
done
But keep in mind that Zsh, even in Bourne shell emulation mode, performs
word splitting on `${1+"$@"}'; see Shell Substitutions,
item `$@', for more.
if ! cmp -s file file.new; then
mv file.new file
fi
use:
if cmp -s file file.new; then :; else
mv file.new file
fi
There are shells that do not reset the exit status from an if:
$ if (exit 42); then true; fi; echo $?
42
whereas a proper shell should have printed `0'. This is especially bad in Makefiles since it produces false failures. This is why properly written Makefiles, such as Automake's, have such hairy constructs:
if test -f "$file"; then
install "$file" "$dest"
else
:
fi
printf %s -foo
POSIX 1003.1-2001 requires that pwd must support the -L (“logical”) and -P (“physical”) options, with -L being the default. However, traditional shells do not support these options, and their pwd command has the -P behavior.
Portable scripts should assume neither option is supported, and should assume neither behavior is the default. Also, on many hosts `/bin/pwd' is equivalent to `pwd -P', but POSIX does not require this behavior and portable scripts should not rely on it.
Typically it's best to use plain pwd. On modern hosts this outputs logical directory names, which have the following advantages:
Also please see the discussion of the cd command.
set x $my_list; shift
Some shells have the "opposite" problem of not recognizing all options (e.g., `set -e -x' assigns `-x' to the command line). It is better to elide these:
set -ex
test program is the way to perform many file and string
tests. It is often invoked by the alternate name `[', but using
that name in Autoconf code is asking for trouble since it is an M4 quote
character.
If you need to make multiple checks using test, combine them with
the shell operators `&&' and `||' instead of using the
test operators -a and -o. On System V, the
precedence of -a and -o is wrong relative to the unary
operators; consequently, POSIX does not specify them, so using them
is nonportable. If you combine `&&' and `||' in the same
statement, keep in mind that they have equal precedence.
You may use `!' with test, but not with if:
`test ! -r foo || exit 1'.
/bin/sh support only -h.
test might interpret its argument as an
option (e.g., `string = "-n"').
Contrary to a common belief, `test -n string' and `test -z string' are portable. Nevertheless many shells (such as Solaris 2.5, AIX 3.2, unicos 10.0.0.6, Digital Unix 4 etc.) have bizarre precedence and may be confused if string looks like an operator:
$ test -n =
test: argument expected
If there are risks, use `test "xstring" = x' or `test "xstring" != x' instead.
It is common to find variations of the following idiom:
test -n "`echo $ac_feature | sed 's/[-a-zA-Z0-9_]//g'`" &&
action
to take an action when a token matches a given pattern. Such constructs should always be avoided by using:
echo "$ac_feature" | grep '[^-a-zA-Z0-9_]' >/dev/null 2>&1 &&
action
Use case where possible since it is faster, being a shell builtin:
case $ac_feature in
*[!-a-zA-Z0-9_]*) action;;
esac
Alas, negated character classes are probably not portable, although no shell is known to not support the POSIX syntax `[!...]' (when in interactive mode, zsh is confused by the `[!...]' syntax and looks for an event in its history because of `!'). Many shells do not support the alternative syntax `[^...]' (Solaris, Digital Unix, etc.).
One solution can be:
expr "$ac_feature" : '.*[^-a-zA-Z0-9_]' >/dev/null &&
action
or better yet
expr "x$ac_feature" : '.*[^-a-zA-Z0-9_]' >/dev/null &&
action
`expr "Xfoo" : "Xbar"' is more robust than `echo
"Xfoo" | grep "^Xbar"', because it avoids problems when
`foo' contains backslashes.
Although POSIX is not absolutely clear on this point, it is widely admitted that when entering the trap `$?' should be set to the exit status of the last command run before the trap. The ambiguity can be summarized as: “when the trap is launched by an exit, what is the last command run: that before exit, or exit itself?”
Bash considers exit to be the last command, while Zsh and Solaris 8 sh consider that when the trap is run it is still in the exit, hence it is the previous exit status that the trap receives:
$ cat trap.sh
trap 'echo $?' 0
(exit 42); exit 0
$ zsh trap.sh
42
$ bash trap.sh
0
The portable solution is then simple: when you want to `exit 42', run `(exit 42); exit 42', the first exit being used to set the exit status to 42 for Zsh, and the second to trigger the trap and pass 42 as exit status for Bash.
The shell in FreeBSD 4.0 has the following bug: `$?' is reset to 0 by empty lines if the code is inside trap.
$ trap 'false
echo $?' 0
$ exit
0
Fortunately, this bug only affects trap.
In a sense, yes, because if it doesn't exist, the shell will produce an exit status of failure, which is correct for false, but not for true.
PS1, you can test for its existence and use
it provided you give a neutralizing value when unset is
not supported:
if (unset FOO) >/dev/null 2>&1; then
unset=unset
else
unset=false
fi
$unset PS1 || PS1='$ '
See Special Shell Variables, for some neutralizing values. Also, see Limitations of Builtins, documentation of export, for the case of environment variables.