How to write POSIX shell script

Published on: 2023-02-26

Introduction

Bourne shell - was the default shell for Version 7 Unix. Unix-like systems continue to have /bin/sh—which will be the Bourne shell, but rather a symbolic link or hard link to a compatible shell(ksh, bash, zsh, …)—even when other shells are used by most users.[3]

POSIX - The Portable Operating System Interface (POSIX, with pos pronounced as in positive, not as in pose[1]) is a family of standards specified by the IEEE Computer Society for maintaining compatibility between operating systems.[2]

Nowadays /bin/sh is not a Bourne shell on any system anymore. Even the Solaris was one of the last major system which included it has now switched to a POSIX sh for its /bin/sh in Solaris 11.[1] During early 70s the /bin/sh was the Thompson shell.[1] The Bourne shell replaced the Thompson shell in Unix V7 in 1979.[1] The /bin/sh has been the Bourne shell for many years thereafter (or the Almquist shell, a free reimplementation on BSDs).[1]

One shell that is compatible with most other simple shells, is dash, Debian default system shell, which is a derivative of the older BSD ash.[6]

Portability

Unfortunately, making a shell script ‘POSIX-compliant’ is usually easier than making it run on any real-world shell. The only real-world sensible recommendation is test your script in several shells. Like the list: dash, posh, lksh, and bash –posix. Solaris is a world on its own, probably you will need to test against /bin/sh and xpg4/sh.[6]

Unfortunately, ‘portable’ is usually a stronger requirement than ‘POSIX-compliant’ for shell scripts. That is, writing something that runs on any POSIX shell isn’t too hard, but getting it to run on any real-world shell is harder.[7]

You can start by installing every shell in your package manager, in particular debian’s posh sounds like what you want (Policy-compliant Ordinary SHell). Debian’s policy is POSIX with a few exceptions (echo -n specified, local…).[7]

Beyond that though, testing has to cover a few shells (/bin/sh especially) on a range of platforms. I test on Solaris (/bin/sh and xpg4/sh), and BSD. AIX and HP-UX are very compliant and don’t cause problems. bash is a little world of its own.[7]

I’d recommend the Autoconf guide to portable shell, which is absolutely brilliant and saves a lot of time. Large chunks of it are obsolete, but that’s OK; just skip TruUnix and Ultrix and so on if you don’t care![7]

Arrays

The Bourne shell or the POSIX sh language specification don’t support arrays. Or rather they have only one array: the positional parameters ($1, $2, $@, so one array per function as well).[1]

ksh88 did have arrays which you set with $ set -A, but that didn’t get specified in the POSIX sh as the syntax is awkward and not very usable!!![1] Example: $ set -A array_name 1 2 3, but when you run this line in bash you have a problem. Do not use this kind of array in POSIX shell script.

Other shells with array/lists variables include: csh/tcsh, rc, es, bash, yash, zsh, fish each handle arrays with a different syntax. csh/tcsh, rc, es, bash mostly copied the ksh syntax the ksh93 way. rc the shell of the once to-be successor of Unix. fish and zsh being the most consistent ones.[1]

In standard sh (also works in modern versions of the Bourne shell):

set '1st element' 2 3 # setting the array

set -- "$@" more # adding elements to the end of the array

shift 2 # removing elements (here 2) from the beginning of the array

printf '<%s>\n' "$@"    # passing all the elements of the $@ array
                        # as arguments to a command

for i do # looping over the  elements of the $@ array ($1, $2...)
  printf 'Looping over "%s"\n' "$i"
done

printf '%s\n' "$1" # accessing individual element of the array.
                   # up to the 9th only with the Bourne shell though
                   # (only the Bourne shell), and note that you need
                   # the braces (as in "${10}") past the 9th in other
                   # shells (except zsh, when not in sh emulation and
                   # most ash-based shells).

printf '%s\n' "$# elements in the array"

printf '%s\n' "$*" # join the elements of the array with the
                   # first character (byte in some implementations)
                   # of $IFS (not in the Bourne shell where it's on
                   # space instead regardless of the value of $IFS)

Note: In the Bourne shell and ksh88, $IFS must contain the space character for “$@” to work properly (a bug), and in the Bourne shell, you can’t access elements above $9 (${10} won’t work, you can still do shift 1; echo “$9” or loop over them)).[1]

Also check: POSIX shell array article, where is recommend to use the command set with double-dash. The double-dash mark the end of options.

$ set -- 1 2 3

Manual page of ksh93 say about set -- following:

-- Do not change any of the options; useful in setting $1 to a value beginning with -. If no arguments follow this option then the positional parameters are unset.

stackoverflow topic

Indirect variable reference

If we want to get value from variable, which name is saved to another variable.

$ x=10
$ myvar=x
$ eval "value=\${$myvar}"
$ echo "$value"
10

Sources