Notes on shell programming

The point of a standard isn't even whether it's ideal or particularly sensible, it's that a compliant program produces consistent results on compliant platforms. Diverging in the name of whatever benefit just means that one has to work harder to produce a portable program. -- Richard L. Hamilton, in comp.unix.questions

I occasionally have to write Bourne shell scripts. Often enough that I've a small bag on knowledge about it, rarely enough that I tend to rediscover it far too often for my liking. Here is a repository of some of them, mainly for my use, but they could be useful for others.

On the given quote, my view is that portability is not a goal, it is a mean. Thus one shouldn't jeopardize the goal neither in the name of portability nor in the one of convenience.


#! /bin/sh -

set -e

usage() {
    printf "Usage: prog [options] args...\n"

help() {
    printf "Description...\n"
    printf "\nArguments\n"
    printf "\nOptions\n"

while getopts hab: opt ; do
    case $opt in
            exit 0
            printf "Received -a\n"
            printf "Received -b %s\n" "$OPTARG" 
            exit 1
shift $((OPTIND - 1))


Some recommendations

Parameter quoting

Parameter should be expanded between double quotes "${var}" if one doesn't want to get their value split. There are a few contexts where the quoting isn't needed.

Parameter expansion

var is not set var is null var is not null
${var:-txt} txt txt $var
${var-txt} txt $var
${var:=txt} txt txt $var
${var=txt} txt $var
${var:+txt} txt
${var+txt} txt txt

The form with = also set $var. This is often used in a context where the parameter expansion isn't needed, just to set a default value. For instance as argument of : (which is a variant of true which never use its argument, GNU true outputs something with --version).

: ${var:=default}

A common use is to set the default value of optional arguments when the default is set from other (potentially optional) arguments. For instance:

: ${prefix:=/usr/local}
: ${bindir:=${prefix}/bin}
: ${libdir:=${prefix}/lib}

will set prefix, bindir and libdir if they are not already defined by the preceding arguments parsing.

To test if a parameter is set, the idiom is [ -n "${var+set}" ] and to test if it is not set, use [ -z "${var+set}" ]. The variants with :+ instead of + are handling a set but null parameter in the same way as an unset one.

There is also some way to remove a pattern at the start or end of a parameter:

remove the shortest pattern from the end of V.
remove the longest pattern from the end of V.
remove the shortest pattern from the start of V.
remove the longest pattern from the start of V.


[ (or test) can be used to compare strings and numbers:

strings integers
not equal!=-ne
less or equal<=-le
greater or equal>=-ge

-z and -n test for (non) null strings.

Starting with ! negates the test.

Testing files

-t fdfd is a terminal
-e pathentry exists
-f pathnormal file
-d pathdirectory
-L pathsymbolic link
-r pathreadable
-w pathwritable
-x pathexecutable
-s pathfile not empty

See also the note on test X"$var" = X"value".

Arithmetic expansion

Expressions in $(( ... )) are replaced by their value. Parameters in these expressions do not need to be preceded by $. The operators and their precedence are

unary +, unary -, ~, !
*, /, %
+, -
<<, >>
<, <=, >=, >
==, !=
=, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=


expr is a standalone program which allows to evaluate expressions. For arithmetic expressions, $((...)) is probably more convenient and avoid to launch another process, for comparisons, test avoid the issue of different interpretations determined if the values are decimal representations or not, so the main usage of expr is the regular expression matching.

( expr ) Grouping
expr1 : expr2 regular expression matching, either the number of matched characters or the match of the first subexpression \( ...\).
expr1 * expr2Multiplication of decimal integers.
expr1 / expr2Integer division of decimal integers.
expr1 % expr2Remainder of integer division of decimal integers.
expr1 + expr2Addition of decimal integers.
expr1 - expr2Subtraction of decimal integer.
expr1 = expr2Equal.
expr1 > expr2Greater than.
expr1 >= expr2Greater than or equal.
expr1 < expr2Less than.
expr1 <= expr2Less than or equal.
expr1 != expr2Not equal.
expr1 & expr2Returns the evaluation of expr1 if neither expression evaluates to null or zero; otherwise, returns zero.
expr1 | expr2Returns the evaluation of expr1 if it is neither null nor zero; otherwise, returns the evaluation of expr2 if it is not null; otherwise, zero.

Comparisons return the result of a decimal integer comparison if both arguments are integers; otherwise, returns the result of a string comparison using the locale-specific collation sequence. The result of each comparison is 1 if the specified relationship is true, or 0 if the relationship is false.

Forwarding arguments

Forwarding arguments to another program should be done with "$@" so that they are not split before the passing. If one want to process some arguments and pass the others, something like this is in order to handle the argument parsing.

#! /bin/sh -

set -e

checkarg() {
    if [ -z "$2" ] ; then
       printf "%s: argument expected\n" "$1"
       exit 1

while [ $cnt -ne $argc ] ; do
    case $1 in
            printf "Got -mine\n"
            checkarg "$1" "${2+set}"
            printf "Got -with '%s'\n" "$2"
            cnt=$((cnt + 1))
            cnt=$((cnt + 1))
            set -- "$@" "$1"
    cnt=$((cnt + 1))

while [ $cnt -ne $argc ] ; do
    set -- "$@" "$1"
    cnt=$((cnt + 1))

showargs prog "$@"

Choice of shell

Although I'm somewhat concerned about portability, I don't write scripts for unconditional one. For instance all the machines I care about had a /bin/ksh and thus I used write my scripts for it (trying to keep to POSIX features) instead of writing them to the lowest common denominator of /bin/sh (that would be the one from Solaris) or doing epic effort to respawn a better shell. Nowadays I'm using /bin/sh and using POSIX features without caring about the old Solaris Bourne shell.


Rationale for things one often see in scripts

Here are some rationales for usage often seen in scripts but that I don't follow.

test X"$var" = X"value"

The idiom test X"$var" = X"value" is used traditionally to handle correctly the case where $var begins in a way which could confuse test about the fact it is something to be compared. Personally, I avoid -a, -o and the parenthesis in test arguments (thus I use the shell && and || and grouping) as they are marked as obsolete by POSIX and this avoidance suppresses the need of the trick.

Test or [

Some scripts are using

if test cond ; then
while others are using
if [ cond ] ; then

The only reason I know to choose the former is that the use of [ doesn't work with some preprocessing tools (m4 for instance for autotools scripts).

Use of negation

Some scripts are using

if cond ; then

Instead of

if ! cond ; then

The only shell I know which doesn't support ! is /bin/sh on Solaris. Better use some other shell on Solaris, that one is stable but has been frozen for so long that supporting it is painful.

! in test is AFAIK supported everywhere.

Conditional expression with [[

Some shells, at least bash and zsh, allow conditional expressions with [[. Those behave mostly like test but with some additional capabilities such as using && and || to combine subexpressions.

Arithmetic expression with ((

Some shells, at least bash and zsh, allow arithmetic expressions with ((. Note that like expr it has an non-null exit status when the result is 0, so that makes it often less convenient than $((...)).