This post is inspired by Nochlin’s blog post on creating a great user experience for shell scripts.
Here are 6 techniques that can be used to make shell scripts robust, safe and fun:
1. Comprehensive Error Handling and Input Validation
For clear error messages and good user experience:
if [ -z "$1" ]
then
echo "Usage: evaluate.sh <fork name> (<fork name 2> ...)"
echo " for each fork, there must be a 'calculate*average*<fork name>.sh' script and an optional 'prepare\_<fork name>.sh'."
exit 1
fi
This approach helps users quickly identify and resolve issues, saving them time and frustration.
2. Clear and Colorful Output
BOLD*RED='\033[1;31m'
RESET='\033[0m'
echo -e "${BOLD_RED}ERROR${RESET}: ./calculate_average*$fork.sh does not exist." >&2
This visual distinction helps users quickly grasp the nature of each message.
3. Detailed Progress Reporting
function print_and_execute() {
echo "+ $@" >&2
"$@"
}
This matches the output format of Bash’s builtin set -x tracing, but gives the script author more granular control of what is printed.
This level of transparency not only keeps users informed but also aids in debugging if something goes wrong.
4. Strategic Error Handling with “set -e” and “set +e”
# At the beginning of the script
set -eo pipefail
# Before running tests and benchmarks for each fork
for fork in "$@"; do
set +e # we don't want prepare.sh, test.sh or hyperfine failing on 1 fork to exit the script early
# Run prepare script (simplified)
print*and_execute source "./prepare*$fork.sh"
# Run the test suite (simplified)
print_and_execute $TIMEOUT ./test.sh $fork
# ... (other fork-specific operations)
done
set -e # Re-enable exit on error after the fork-specific operations
This approach gives the script author fine-grained control over which errors cause the script to exit and which can be handled in other ways.
5. Platform-Specific Adaptations
if [ "$(uname -s)" == "Linux" ]; then
TIMEOUT="timeout -v $RUN_TIME_LIMIT"
else # Assume MacOS
if [ -x "$(command -v gtimeout)" ]; then
TIMEOUT="gtimeout -v $RUN_TIME_LIMIT"
else
echo -e "${BOLD_YELLOW}WARNING${RESET} gtimeout not available, install with `brew install coreutils` or benchmark runs may take indefinitely long."
fi
fi
This ensures a consistent experience across different environments.
6. Timestamped File Outputs for Multiple Runs
This allows users to run the script multiple times and keep a historical record of all results. Here’s how I did it:
filetimestamp=$(date +"%Y%m%d%H%M%S")
# ... (in the loop for each fork)
HYPERFINE_OPTS="--warmup 0 --runs $RUNS --export-json $fork-$filetimestamp-timing.json --output ./$fork-$filetimestamp.out"
# ... (after the benchmarks)
echo "Raw results saved to file(s):"
for fork in "$@"; do
if [ -f "$fork-$filetimestamp-timing.json" ]; then
cat $fork-$filetimestamp-timing.json >> $fork-$filetimestamp.out
rm $fork-$filetimestamp-timing.json
fi
if [ -f "$fork-$filetimestamp.out" ]; then
echo " $fork-$filetimestamp.out"
fi
done