Go to the first, previous, next, last section, table of contents.


Limitations of Shell Builtins

No, no, we are serious: some shells do have limitations! :)

You should always keep in mind that any built-in 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.

@command{!}
You can't use @command{!}, you'll have to rewrite your code.
@command{break}
The use of `break 2', etcetera, is safe.
@command{case}
You don't need to quote the argument; no splitting is performed. You don't need the final `;;', but you should use it. Because of a bug in its fnmatch, @command{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
@command{echo}
The simple 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 section Preset Output Variables, ECHO_N etc. for a means to simulate @option{-c}. Do not use backslashes in the arguments, as there is no consensus on their handling. On `echo '\n' | wc -l', the @command{sh} of Digital Unix 4.0, MIPS RISC/OS 4.52, answer 2, but the Solaris' @command{sh}, Bash and Zsh (in @command{sh} emulation mode) report 1. Please note that the problem is truly @command{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 @command{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
@command{exit}
The default value of @command{exit} is supposed to be $?; 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 @command{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 @command{sh}, an exit trap ignores the exit command's status. 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.
@command{export}
The builtin @command{export} dubs environment variable a shell variable. Each update of exported variables corresponds to an update of the environment variables. Conversely, each environment variable received by the shell when it is launched should be imported as a shell variable marked as exported. Alas, many shells, such as Solaris 2.5, IRIX 6.3, IRIX 5.2, AIX 4.1.5 and DU 4.0, forget to @command{export} the environment variables they receive. As a result, two variables are coexisting: 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 @command{export} again each environment variable that you update.
@command{false}
Don't expect @command{false} to exit with status 1: in the native Bourne shell of Solaris 8, it exits with status 255.
@command{for}
To loop over positional arguments, use:
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 section Shell Substitutions), use:
for arg in ${1+"$@"}; do
  echo "$arg"
done
@command{if}
Using `!' is not portable. Instead of:
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 @command{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
@command{set}
This builtin faces the usual problem with arguments starting with a dash. Modern shells such as Bash or Zsh understand @option{--} to specify the end of the options (any argument after @option{--} is a parameters, even `-x' for instance), but most shells simply stop the option processing as soon as a non-option argument is found. Therefore, use `dummy' or simply `x' to end the option processing, and use @command{shift} to pop it out:
set x $my_list; shift
@command{shift}
Not only is @command{shift}ing a bad idea when there is nothing left to shift, but in addition it is not portable: the shell of MIPS RISC/OS 4.52 refuses to do it.
@command{test}
The 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 @option{-a} and @option{-o}. On System V, the precedence of @option{-a} and @option{-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 @command{test}, but not with @command{if}: `test ! -r foo || exit 1'.
@command{test (files)}
To enable configure scripts to support cross-compilation, they shouldn't do anything that tests features of the build system instead of the host system. But occasionally you may find it necessary to check whether some arbitrary file exists. To do so, use `test -f' or `test -r'. Do not use `test -x', because 4.3BSD does not have it. Do not use `test -e' either, because Solaris 2.5 does not have it.
@command{test (strings)}
Avoid `test "string"', in particular if string might start with a dash, since 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 frequent 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.2 syntax `[!...]' (when in interactive mode, @command{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.
@command{trap}
It is safe to trap at least the signals 1, 2, 13 and 15. You can also trap 0, i.e., have the @command{trap} run when the script ends (either via an explicit @command{exit}, or the end of the script). 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 @command{exit}, what is the last command run: that before @command{exit}, or @command{exit} itself?" Bash considers @command{exit} to be the last command, while Zsh and Solaris 8 @command{sh} consider that when the trap is run it is still in the @command{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 @command{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 @command{trap}.
$ trap 'false

echo $?' 0
$ exit
0
Fortunately, this bug only affects @command{trap}.
@command{true}
Don't worry: as far as we know @command{true} is portable. Nevertheless, it's not always a builtin (e.g., Bash 1.x), and the portable shell community tends to prefer using @command{:}. This has a funny side effect: when asked whether @command{false} is more portable than @command{true} Alexandre Oliva answered:

In a sense, yes, because if it doesn't exist, the shell will produce an exit status of failure, which is correct for @command{false}, but not for @command{true}.

@command{unset}
You cannot assume the support of @command{unset}, nevertheless, because it is extremely useful to disable embarrassing variables such as CDPATH or LANG, you can test for its existence and use it provided you give a neutralizing value when @command{unset} is not supported:
if (unset FOO) >/dev/null 2>&1; then
  unset=unset
else
  unset=false
fi
$unset CDPATH || CDPATH=:
See section Special Shell Variables, for some neutralizing values. Also, see section Limitations of Shell Builtins, documentation of @command{export}, for the case of environment variables.


Go to the first, previous, next, last section, table of contents.