How I write shell scripts

2021-06-24

I use this shell script formula when writing new shell scripts.

It reminds me how to process arguments and write a decent --help feature, among other things.

When to write shell

I have been satisfied with shell scripts when I’ve written them for the following purposes:

  • When they interact mostly with the shell anyway.

    • E.g. scripts that are run during the prompt or to configure my interactive shell are probably best as scripts written in shell, in part because the shell environment is already in memory and execution of shell-related tasks is so fast.
  • When they’re short and simple.

    • Since shell as a general purpose programming language has so many sharp edges (like parsing, nested quoting, different syntax for various conditionals, and many more), keeping the scripts short and not trying to solve problems that involve those edges keeps me much happier with the scripts I write.

    • One pattern I use a lot is a generic, tested script in Python that takes lots of options, called by a wrapper written in shell that hard-codes those options. I could even deploy the wrapper with Ansible and fill in command-line arguments with Ansible variables.

Rules of shell scripts

These are rules I try to keep in mind, even if I might not follow them all to the letter.

  • The Shell Hater’s Handbook is an excellent index of POSIX shell programming documentation. Strongly recommended.
  • Don’t write shell scripts longer than a few PgDns through a reasonably sized text editor window.
  • Suffer no bashism to live. Don’t rely on extensions from GNU, BSD, or anyone else. If you’re going to ask someone to keep track of a script’s dependency, you might as well ask them to install an actually good one.
  • Exported variables should be in all caps (export VAR=value), while script local variables should be in lower case (var=value). See also.
  • Rely on the PATH variable. Do not assume that binaries are necessarily inside /bin or /usr/bin. For instance, Alpine Linux has /bin/true and /bin/false, but macOS has /usr/bin/true and /usr/bin/false. POSIX does not guarantee the location of binaries, except for /bin/sh!
  • In almost all cases, you want to set -e to exit immediately on a nonzero exit code, and you will usually also want to set -u to make an unset variable an error.
  • Consider using ShellCheck to check for POSIX compitibility. (I rarely do this, but it’s there if you like.)