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"