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+"$@"}="$@"';; esacAlternatively, 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
"