-
All scripts should pass Shellcheck
Add explicit annotations as needed. When adding new scripts, confirm that our CI machinery is configured to run on your files.
-
All scripts end in a
.sh
extensionThis is typically an anti-pattern, but we currently rely on it to locate shell scripts within the source tree for the purposes of Shellcheck on CI.
-
Always use double-quotes, except where interpolation must be prevented
NOTE: It's acceptable to use single-quotes as a matter of course for script arguments to other interpreters like
sed
,awk
, andpsql
even if there would be no unintentional interpolations currently.Tip: quoting a here-doc terminator will prevent interpolation:
cat <<EOM Uh-oh, I need to \$escape this EOM cat <<'EOM' But not in $this case. EOM
-
Prefer Bash
We write all of our scripts as Bash. It's available everywhere we need scripts to run and selecting an explicit shell will ensure consistent behavior.
-
Use
#!/usr/bin/env bash
, not#!/bin/bash
This is specifically for OSX folks, where
/bin/bash
is severely out of date. A newer version, installed via Homebrew, may appear first in$PATH
and should be used when possible.
Though our scripts are using a Bash shebang, prefer POSIX alternatives where possible. This is not about portability, rather it's to reduce to scope of understanding required on the part of the reader by preferring simpler, more-common, and well-defined constructs.
If you decide to use a Bashism, be consistent within that script. For example,
if you use [[
because you want regex or glob matching in one place, don't use
[
in other places in the same script.
-
Use
set -eu
-e
ensures that any unchecked, non-zero exit code halts the script. For cases where a non-zero exit is OK, explicitly append|| true
.-u
will halt the script if an unset variable is used.This avoids bugs like:
dir=/tmp/foo rm -rf "$dirr/bar" # typo made this rm -rf /bar
-
Avoid
set -x
This option outputs every statement before it's executed. Only use this if your script has no function definitions, control flow structures, or conditional expressions; otherwise it is overly noisy.
# Bad set -eux heyoh() { tar ... } if [ this ]; then curl ... fi heyoh || mv ... # OK set -eux curl ... tar ... mv ...
-
Use
;
to joinif
,for
andwhile
statements# Bad if then fi while do done for x do done # Good if; then fi while; do done for x; do done
-
Avoid redundant braces:
# Bad touch "foo/${bar}/baz" # Good touch "foo/$bar/baz" # But required here touch "foo/${bar}_backup/bat
-
Avoid redundant quoting on assignment:
# Bad, suprisingly foo="barbaz" foo="$bar$baz" # Good foo=barbaz foo=$bar$baz # But required for foo="bar baz" foo="$bar $baz"
-
Scripts should have a header comment, formatted like so:
#!/usr/bin/env bash # # Do the thing with the other thing. Assumes you've got this that and the # other present. Invoke with NO_FOOBAR=1 set to prevent a foobar. # set -eu
-
Scripts should validate their options and error with proper [usage syntax][usage-syntax] and conventional [exit code][exit-code]:
[usage-syntax]: https://stackoverflow.com/a/9727046 [exit-code]: https://www.freebsd.org/cgi/man.cgi?query=sysexits&apropos=0&sektion=0&manpath=FreeBSD%204.3-RELEASE&format=html
if [ "$#" -lt 2 ]; then echo "usage: foo-bar count file [limit]" >&2 exit 64 fi
-
Scripts should be quiet when successful. Ideally the output of the commands invoked by the script stands on their own, without additional print statements. See the Rule of Silence here.
-
Scripts should output error information to
stderr
and use exit codes responsibly. -
If a script uses
sudo
on all or most commands, consider assuming and requiring the script is itself run as root. -
Scripts should be good Unix citizens and accept input on
stdin
and produce results tostdout
. Be a filter. If you can't seem to do this because your operation is multi-stage or writes multiple inputs and outputs, consider writing more, smaller scripts that each do one thing.# Bad ./some-script -i input.txt -o output.txt # Good ./some-script < input.txt > output.txt
# Bad ./some-script *.input.txt -o output.txt # Better ./some-script *.input.txt > output.txt # Best for x in *.input.txt; do ./some-script < "$x" done > output.txt
-
Define function-local variables with
local
# Bad foo() { bar=$1 # ... } # Good foo() { local bar=$1 # ... }
Tip: perform multiple assignments in one statement:
foo() { local bar=$1 baz=$2 # ... }
-
Use
mktemp
andtrap EXIT
to ensure uniqueness and clean up of temporary resources:tmp=$(mktemp) trap 'rm $tmp' EXIT # use $tmp file
tmp=$(mktemp -d) trap 'rm -f $tmp' EXIT # use $tmp directory
-
Only use
echo
for the simplest case ofecho "static string"
, otherwise useprintf
.echo
with options, escapes, or variable interpolation is a portability nightmare. See here as just one example reference.# Bad echo -e "Results written to $output\nThanks!" # Good printf "Results written to %s\nThanks!\n" "$output" # Also good printf "Results written to %s\n" "$output" echo "Thanks!"
Shells are frequently glue code, pulling together other tools such as grep
or
sed
. These guides are about how to best use those tools.
-
Avoid any Useless Use of Cat
# Bad cat PATH | grep ... cat PATH | sed ... cat PATH | awk ... # Good grep ... PATH sed ... PATH awk ... PATH
-
Use
-F
/-E
, notfgrep
/egrep
-
Use
grep -F
if you don't require regex -
Use
grep -x
, notgrep '^foo$'
-
grep re | awk '{ cmd }'
can beawk '/re/ { cmd }'
-
grep re | sed 's/re/x/'
can besed '/re/!d; s//x/'
(notice the reuse ofre
through an emptys//
) -
awk
is usually better thancut
for splitting on whitespace; the former handles tab-separation better and has$NF
available for "last"
The following features should give you pause:
- A line-count approaching 100
- Complex options, use of
getopts
or similar - Bash arrays, especially associative arrays (i.e. hashes)
- Multiple-levels of
source
dependency
Some alternatives:
- Is this part of CI/Deployment? Try writing it in Shake
- Is this called from something like
yarn
? Consider using js-specific tooling - Is the process infrequent and made simpler by hard-coding some information about the local environment (paths, passwords, etc)? Consider just documenting the command with dummy values to be copied, edited, and run
- Is the process frequent, useful, and complex? Consider adding to FRTool or creating a standalone Haskell binary