lintsh is a Bourne shell that optionally warns about suspicious
or nonportable constructs. It is intended to help script authors write
correct, portable scripts; it will also be suitable for use as
/bin/sh. It does not exist yet, and it might never, or perhaps
the warning functionality could be added to an existing shell. Regardless,
this list of gotchas will be maintained, and you can get into the habit of
coding to avoid them.
The following programs are known to be in use as /bin/sh,
in addition to the /bin/sh programs maintained by OS
vendors. Please let me know about any not listed here.
bash (many GNU/Linux systems)
pdksh (OpenBSD)
zsh (Darwin, Mac OS X)
ash (Almquist shell: FreeBSD, NetBSD, DragonFly, Android)
dash (Debian Almquist shell)
ksh - maybe?
ksh88 - maybe?
ksh-93 - maybe?
You can also contribute by emailing me suggestions for constructs to warn
about. My experience is mostly with bash and
pdksh, so information about issues with other shells is
especially appreciated. I am not interested in how shells behave in
interactive mode; scripts are only affected by differences in behavior in
noninteractive mode. Here's the list of issues so far. (Some are bugs in
particular versions; these will still be warned about as long as buggy
installations are thought to be common, or as long as I forget to update this
page.)
IFS=whatever set $variable"; this
setting of IFS will not be used to split
$variable. Use
"IFS=whatever; set $variable"
instead.
sh) or the new
value (bash).
bash automatically sets the following variables if they
are not already set:
$BASH,
$BASHOPTS,
$BASH_ALIASES,
$BASH_ARGC,
$BASH_ARGV,
$BASH_CMDS,
$BASH_EXECUTION_STRING,
$BASH_LINENO,
$BASH_SOURCE,
$BASH_VERSINFO,
$BASH_VERSION,
$DIRSTACK,
$EUID,
$GROUPS,
$HOSTNAME,
$HOSTTYPE,
$IFS,
$MACHTYPE,
$OPTERR,
$OPTIND,
$OSTYPE,
$PATH,
$POSIXLY_CORRECT,
$PPID,
$PS4,
$PWD,
$SHELL,
$SHELLOPTS,
$SHLVL,
$TERM,
$UID,
$_.
bash automatically set the following
variables (clobbering any inherited value):
$BASH, $BASH_VERSINFO,
$BASH_VERSION, $DIRSTACK,
$EUID, $GROUPS, $HISTCMD,
$HOSTNAME, $HOSTTYPE, $IFS,
$LINENO, $MACHTYPE, $OLDPWD,
$OPTERR, $OPTIND, $OSTYPE,
$PPID, $PWD, $RANDOM,
$SECONDS, $SHELLOPTS,
$SHLVL, $UID, $_.
bash automatically sets the following variables if
they are not already set: $COLUMNS,
$HISTFILE, $HISTFILESIZE,
$HISTSIZE, $LINES,
$MAILCHECK, $PATH, $PS4,
$SHELL, $TERM.
bash automatically unsets $PS1 and
$PS2.
bash automatically exports the following variables:
$PWD, $SHLVL, $_. Older versions
also exported $HOSTNAME, $HOSTTYPE,
$MACHTYPE, $OSTYPE, $PATH,
$SHELL, $TERM.
bash treats $BASH_VERSINFO,
$EUID, $PPID, $SHELLOPTS,
and $UID as read-only.
ksh automatically sets $SHELL (to
/bin/sh) if it is not already set.
sh does not allow $PATH or
$MAILCHECK to be unset.
echo" command may treat
backslashes in its arguments as literal backslashes
(bash), or as escape characters
(pdksh).
echo" command may accept
options such as "-n" (bash),
or may treat all arguments as data to print (Solaris
sh).
ksh does not fork for the last command in
the last pipeline in a script or subshell.
sh does not fork for the last command in
the last pipeline in a subshell.
$*" may expand to the positional parameters
separated by the first character of $IFS, or by the empty
string if $IFS is empty (bash,
pdksh, Solaris ksh), or it may expand to the
positional parameters separated by spaces (Solaris sh).
bash and pdksh use $IFS for word
splitting only for the results of unquoted expansions (variable
expansions, command substitution, etc.). Solaris sh uses it
for all unquoted strings. Solaris ksh uses it for the
unquoted parts of any word which includes a (quoted or unquoted)
expansion.
\"" appears in a here-document,
pdksh interprets the backslash as an escape character and
removes it. bash and Solaris sh preserve the
backslash. Workaround: backslash-escape the backslash.
var=value command", if it the command is a
shell function or one of certain builtins, may leave the variable set
after the command completes (pdksh, Solaris
sh), or may revert it to its previous state for subsequent
commands (bash).
var=value exec cmd" may set
$var in the environment for cmd
(bash, pdksh) or not (Solaris
sh). Workaround:
"exec env var=value cmd".
var= shell_function" may make
$var unset during the call to the function
(bash before version 2.05b), may set it to the empty string
during the call (bash 2.05b and later, pdksh)
or may not affect $var at all during the call (Solaris
sh).
var is already unset,
"unset var" may give exit status 0
(bash 2.05b and later, Solaris sh) or nonzero
(bash before version 2.05b, pdksh, Solaris
ksh).
. directory" may fail with an error message
(bash) or may be a successful no-op (pdksh).
bash allows arbitrary file descriptors to be
redirected. pdksh doesn't redirect file descriptors
larger than 9; any digit strings longer than a single character
are treated as a command or argument, even if followed immediately
by ">" or
"<".
sh will lose its script fd if a redirection
in the script happens to use that fd. bash dups the
script fd before performing the redirection. pdksh
initially moves the script to an fd larger than 9, so it cannot be
redirected.
bash,
"command &> file" runs
command with standard output and error redirected to
file. In pdksh, it runs
command in the background and then truncates
file, as if it were written
"command & > file".
bash accepts both "!" and
"^" to negate character classes in pattern
matching; pdksh accepts only
"!".
pdksh and bash treat an unquoted
"^" as a non-special character; Solaris
sh treats it as equivalent to "|".
sh forks for compound commands with redirections
such as
"while read var; do something; done < file",
so variable assignments and exit behave differently
(although errors still cause the top-level shell to exit if
set -e is in effect). bash and
pdksh do not fork. As a workaround, the compound command
can be wrapped in a shell function, and the function can be invoked with
redirections; this will not cause Solaris sh to fork.
if...elif...fi chain, any
redirections appplied to the final fi may take effect only
for the final then clause (and the accompanying
elif condition, if there is one) (Solaris sh),
or for the entire compound command (bash). Workaround: put
the whole compound command inside {...}, and
move the redirections outside the braces.
set -e is in effect,
"false && true" does not cause
Solaris sh to exit.
set -e is in effect,
"foo() { return 1; }; foo"
does not cause Solaris sh to exit.
set -e is in effect, a shell function that
returns with a nonzero status always causes BSD/OS sh to
exit, even if the call is the condition of an if statement,
etc. Workaround: call the shell function in a subshell.
set -e is in effect,
"foo() { true; false; }; foo || :"
causes Solaris sh to exit.
set -e is in effect,
eval false may cause the shell to exit unconditionally,
(pdksh, FreeBSD sh), or may not, depending on
the context of the eval command (bash).
Workaround: wrap the eval in a subshell.
set -e is not in effect, a builtin command
with failing redirections, called from eval, may cause the
shell to exit (FreeBSD sh, NetBSD sh) or not
(bash, pdksh, Solaris sh). It may
even depend on which builtin command is used - bash exits
for :, but not for true.
set -e is in effect and a subshell command exits
nonzero, Solaris sh and ksh and FreeBSD
sh exit; bash, FreeBSD ksh, and
NetBSD sh and ksh do not exit.
pdksh exits in some circumstances but not others.
test command is a builtin and returns false, the
shell may exit when set -e is in effect
(pdksh, bash) or not (Solaris
sh).
test" builtin command may treat the
following as operators (bash) or as plain strings
(Solaris sh):
">",
"<",
"-G",
"-O",
"-S",
"-e",
"-ef",
"-nt",
"-o"
"-ot",
"==".
test '!' = '!'" may treat
"=" as a binary operator (bash,
pdksh), or may treat the first "!" as
negation.
sh) or have separate namespaces
(bash).
bash, pdksh), or may fail with an error or
crash the shell (Solaris sh).
$? may be reset upon the appearance of redirections
(Free/NetBSD sh), or only after whole commands.
pdksh), or to set $? to
nonzero for the command (bash).
cd command may cause the shell to exit even when
set -e is not in effect (Solaris sh), or
may return a nonzero exit status (bash,
pdksh).
set -e is in effect, a failing command in backticks
appearing as an argument of another command, as in
"echo `false`", may cause the shell to exit
(Solaris sh), or not (bash, pdksh,
Solaris ksh). Workaround: use backticks only in variable
assignments. All shells exit if such a command exits nonzero:
"var=`false`; echo "$var"".
!", "[[",
"function" and
"time" may be shell keywords, having
special effects on parsing (bash), or may be treated
as ordinary external command names (Solaris sh).
alias",
"bind",
"builtin",
"command",
"compgen",
"complete",
"declare",
"dirs",
"disown",
"enable",
"fc",
"getconf",
"help",
"hist",
"history",
"let",
"local",
"logout",
"newgrp",
"popd",
"print",
"printf",
"pushd",
"shopt",
"sleep",
"stop",
"typeset",
"unalias",
"whence".
$'foo\nbar'" may be expanded as
"foo<newline>bar"
(bash), or plainly quoted as
"$foo\nbar" (Solaris sh).
a{b,c}d" may expand to
"abd acd" (bash), or may
be treated literally (Solaris sh).
`< file`" may expand to the
contents of file (bash), or to an empty
string (Solaris sh).
"`echo \"foo\"`"" may
produce the string ""foo""
(pdksh) or "foo" (bash).
The results are more consistent when the backtick command is not
contained in an outer set of double quotes.
-"
may be treated as an option/flag (bash), or as a script
filename (Solaris sh).
[[:alnum:]]",
"[[=c=]]", or
"[[.symbol.]]" may be treated as an extension
pattern (bash) or as a simple character-class pattern
followed by a literal "]"
(pdksh).
~" in certain contexts may be
subject to tilde expansion (bash), or may be treated
literally (Solaris sh).
set --" may be a no-op (Solaris
sh), or may unset all positional parameters
(bash, pdksh). To unset all positional
parameters portably, use
"set x && shift".
"$@"" may
expand to a single empty argument (some older sh
implementations) or to no arguments (bash,
pdksh, Solaris sh, zsh). This is
commonly worked around by using
"${1+"$@"}". However, this workaround
fails for zsh: each argument will be subjected to word
splitting. To avoid this, define this global alias before using the
workaround:
case $ZSH_VERSION in ?*) alias -g '${1+"$@"}="$@"';; esac
Alternatively, explicitly check for arguments and use separate command
lines:
case $# in 0) foo;; *) foo "$@";; esacBut this kind of code is harder to maintain.
case
pattern, then any wildcard characters in its value may be interpreted
either as literal characters (bash, pdksh,
Solaris sh, OpenBSD sh) or as wildcards
(FreeBSD sh, NetBSD sh before NetBSD 2.0). To
force the literal interpretation, assign the value of the positional
parameter to a named parameter, and use the named parameter in the
case pattern:
var=$1; case foo in "$var") ...;; esac
case statement in which none of the patterns match may
return successfully (bash, pdksh) or may
preserve $? as the exit code of the previous command
(Solaris sh). Workaround: include a final pattern clause
like "*) :;;" to ensure there is a match.
for loop with an empty list of iteration values (such as
"for var in $empty ...") may
return successfully (bash, pdksh) or may
preserve $? as the exit code of the previous command
(Solaris sh). Workaround: insert a
":" command before the for loop to
ensure that the status of the loop will be 0 if it never iterates.
case
pattern) ending in "*/" may expand to a list of
directories (bash, pdksh) or may be preserved
as if it were quoted (Solaris sh). Workaround: use
"...*/.".
sh script-basename" the
script may be searched for only in the current directory
(pdksh; Solaris, NetBSD, FreeBSD, OpenBSD sh),
or in the current directory followed by $PATH
(bash), or in $PATH followed by the current directory
(Solaris ksh).
These will also be erroneous for lintsh; no separate warning
will be necessary. These include:
for i; do ...; done" is a
syntax error for Solaris sh. However,
"for i do ...; done" works
portably.
$(command)" is a syntax error for Solaris
sh. However, "`command`" works
portably.
echo 'foo", is
tolerated by Solaris sh, but not by pdksh or
bash.
${var-multiple words}" (also with
+ instead of -) is a syntax error for Solaris
sh. Workaround:
"tmp='multiple words'; echo ${var-$tmp}"
bash,
"command >& file" runs
command with standard output and standard error redirected
to file. In pdksh, it is a syntax error.
Portable syntax:
"command > file 2>&1"