Shell Scripts/CLIs
Some Principles
Useless use of cat
The classic doc Useless Use of Cat is a classic resource.
Use ShellCheck
ShellCheck is an amazing static analysis tool that nags you about nearly everything that was taught to me by the elders of the internet.
Don’t fear mktemp
Proper use of mktemp
is super useful, whether a temporary file or directory. Just make sure that you handle cleanup on the very next line.
Don’t invent your own tempfile creation like mkdir .foo.$$
. It’s a scary race condition that there is no need for. mktemp
is there for you. Just use it.
The following is very typical of my usage:
# Securely create a temp directory and capture the name
TDIR=$(mktemp -d "/tmp/${prog}.XXXXXX"
# this deletes the files on the pseudo-signal EXIT.
# I use outer single quotes and inner double. The variable will be expanded
# when it runs, and the double quotes handle accidental spaces.
trap 'rm -fr "$TDIR"' EXIT
Use getopts
Inventing your own option handling is an exercise in pain. Just use getopts
. You could use the similarly named getopt
. I don’t.
Pedantic Principles
Don’t get fancy
Support the least common denominator of shell features as possible. Stick to Borne Shell (eg. sh
) features when you can. Use POSIX shell spec features next. If I find myself having to wander into Bashisms, Zshisms, etc., it’s time for me to use a “real” programming language.
Usually arrays can be avoided with structure or using temporary files.
Don’t use backticks
Seriously. Stop it. They’ve been deprecated for decades now and are so difficult to read. Use $()
instead. You may violate the previous with it, but whatever. If the shell doesn’t support $()
, it’s stupid.
Don’t create unnecessary processes
If there’s a common shell builtin, use it.
# POSIX string manipulation
prog="${0##*/}"
# unnecessary process
prog="$(basename "$0")"
It is almost never useful to pipe grep
with awk
or sed
.
# boo
grep '^switch_[ab]' hosts.txt | awk '{print $2}' | tee ips.txt
# yay
awk '/^switch_[ab]{print $2}' | tee ips.txt
Sample
This is an example of a simple script that demonstrates most of the above doing 87 lines of sanity checking before 1 line of “business logic.”
#!/bin/bash
# This is a simplified wrapper around pandoc that makes sure the dependencies
# exist.
prog=${0##*/}
# standard erro.h codes we use
EINVAL=22
ENOENT=2
# usage message
usage() {
cat <<EOM
${prog} is a simplified wrapper around pandoc that makes sure the dependencies
exist.
USAGE
${prog} [-t OUTPUT_TYPE] [-o OUTPUT_FILE] INPUT_FILE
${prog} -h
ARGUMENTS
INPUT_FILE Markdown file to convert
OPTIONS
-h This friendly help message
-o OUTPUT_FILE Path to output file
Default: ./[current file name].[expected extention]
-t OUTPUT_TYPE Type of file to output: html, pdf, word
Default: pdf
EXAMPLES
# Generate pdf as ./foo.pdf
./${prog} foo.md
# Generate word file as ~/Documents/foo.docx
./${prog} -t word -o ~/Document/foo.docx foo.md
EOM
}
# default values
OUTPUT_TYPE=pdf
OUTPUT_FILE=""
# make sure we have pandoc
if ! command -v pandoc >/dev/null 2>&1; then
echo "${prog}: Missing pandoc, please install" >&2
exit $ENOENT
fi
# process options
while getopts ho:t: OPT; do
case "$OPT" in
h)
usage
exit 0
;;
o)
OUTPUT_FILE="$OPTARG"
;;
t)
OUTPUT_TYPE="$OPTARG"
;;
*)
echo "${prog}: Invalid option $OPT" >&2
usage
exit $EINVAL
;;
esac
done
shift $(( OPTIND - 1 ))
# figure out extension, bail if something not supported
case "$OUTPUT_TYPE" in
html)
EXTENSION=".html"
;;
pdf)
EXTENSION=".pdf"
;;
word)
EXTENSION=".docx"
;;
*)
echo "${prog}: Invalid output type $OUTPUT_TYPE" >&2
exit $EINVAL
;;
esac
# get out input file
INPUT_FILE="$1"
# determine output file name if not specified
[ -z "$OUTPUT_FILE" ] && OUTPUT_FILE="${INPUT_FILE%%.*}${EXTENSION}"
# make sure we have pdflatex to pdf
if [ "$OUTPUT_TYPE" = "pdf" ]; then
if ! command -v pdflatex >/dev/null 2>&1; then
echo "${prog}: missing \`pdflatex' command needed for pandoc to pdf" \
>/dev/null 2>&1
exit $ENOENT
fi
fi
exec pandoc -o "$OUTPUT_FILE" "$INPUT_FILE"