I’ve started putting all my configuration files (which include many shell files) under version control. In the Git repository, I’ve configured a pre-commit hook that runs the
checkbashisms utility from Debian’s devscripts package on each
sh file in the repository as a simple sanity check to ensure that I don’t inadvertently introduce Bash-specific syntax into scripts that should be POSIX compatible.
Reason for using POSIX sh
/bin/sh as the interpreter for this pre-commit hook as it’s quicker for systems where
/bin/sh is a link to
dash. Committing files shouldn’t take a noticeable amount of time.
I don’t want to run
checkbashisms if the only files staged for committing are non-shell files and for performance, I only want to execute
checkbashisms command once and supply as arguments the names of all the relevant files that should be checked.
Arguments that may contain spaces
While I don’t normally create files with names containing unusual characters, I tried to write this script to be reasonably robust (without becoming overly complex, harder to read and prone to error on various inputs).
In a more fully-featured shell such as Bash, I’d process a list of file name arguments using an array. However, a plain POSIX shell (such as dash) doesn’t support array variables.
When using a POSIX shell, the positional parameters can often be used as a substitute but in this case, I couldn’t see how I could easily modify them to only add the filenames of shell scripts. The technique I used was to split fields using newlines (not spaces) and to use a string to store the arguments to be passed to
I’ve verified that it works with filenames containing spaces and single quotes – but due to to how the shell represents names containing other characters, it doesn’t work with filenames containing double quotes, tabs, etc. This is fine as I really don’t expect to have to process such filenames.
However, it feels hacky and I don’t know if I’m introducing the potential for other errors (“unknown unknowns”). Modifying
IFS is explicitly not recommended by Greg’s Wiki. I thought I’d post here in the hope that the robustness and/or maintainability of the code can be improved.
#!/bin/sh # Check shell scripts for Bashisms if the `checkbashisms` utility is installed. # Called by "git commit" with no arguments. This hook should exit with a # non-zero status after issuing an appropriate message if it wants to stop the # commit. # Allows for files with spaces or single quotes in their name – but not any # other unusual characters. set -u # -o nounset – exit with failure if an unset shell variable is referenced. set -e # -o errexit – exit script if any command fails. set -o noclobber # prevent shell redirection from over-writing files. if ! command -v checkbashisms >/dev/null 2>&1; then echo "‘checkbashisms’ is not available; it can be installed from the ‘devscripts’ package." else echo "Checking for Bashisms in shell scripts..." # Use only newline characters (not spaces) to split filenames. IFS=" " # POSIX way to set IFS to newline (be careful with indentation). set -f # Disable globbing of pathnames. # Arrays are not defined by POSIX so store arguments in a string variable. filenames="" # Get list of filenames that have been staged for committing. for file in $(git diff --cached --name-only); do # Only process shell scripts. case "$file" in *.sh|shell/*.sh) # Add file to list of arguments (separated by newlines). filenames="$filenames $file" # again, be careful with indentation of the code. esac done; # If any shell files have been staged, check their syntax for Bashisms. if [ "$filenames" ]; then if checkbashisms $filenames; then # no quotes for field-splitting. printf "(No Bashisms found)" else exit 1 fi fi # Restore file globbing and IFS. unset IFS set +f fi # Further code to check for other issues ...