From 4d09df9bb66e5b35025f4c31ecae0de1a4d85f21 Mon Sep 17 00:00:00 2001 From: Jason Wallace Date: Tue, 20 Jun 2023 14:30:25 +0200 Subject: [PATCH] Define a convention for parsing command-line flags (#10) Resolves https://github.com/tiny-pilot/style-guides/issues/3 Looking at the shell scripts in our repos, we seem to parse command-line flags in basically four different ways. This PR compares these methods and picks a convention based on our preference. ## 1. Using `getopts` ### Pros: - Uses bash built-in [`getopts`](https://man7.org/linux/man-pages/man1/getopts.1p.html) command - Can chain short flag names (e.g. `script.sh -abcd`) - Built-in error handling - Code is compact - Currently, the majority of our shell scripts parse command-line flags in this way ### Cons: - Only supports short flag names - Need to learn the options syntax (e.g. `getopts 'ht:f'`) ### Example: ```bash TARGET_FILE='' FORCE='false' # Parse command-line flags. # One colon indicates that the previous flag expects a required value, while # two colons to indicate an optional value. while getopts 'ht:f' opt; do case "${opt}" in h) echo 'Help is on its way' exit ;; t) TARGET_FILE="${OPTARG}" ;; f) FORCE='true' ;; *) >&2 echo 'Sorry, invalid option' exit 1 esac done readonly TARGET_FILE readonly FORCE ``` ## 2. Using `getopt` ### Pros: - Supports both short and long flag names - Can specify flag values using `=` (e.g. `script.sh --target-file=/tmp/file`) - Built-in error handling ### Cons: - Uses GNU based [`getopt`](https://linux.die.net/man/1/getopt) command (i.e., not available on macOS) - Need to learn the options syntax - Performs unexpected flag matching For example, `script.sh -target /tmp/file` results in `TARGET_FILE=arget`. This isn't an issue in the other parsing methods. ### Example: ```bash # Parse command-line flags. # One colon indicates that the previous flag expects a required value, while # two colons to indicate an optional value. OPTIONS="$(getopt \ --options 'ht:f' \ --longoptions 'help,target-file:,force' \ -- \ "$@")" # Process command-line flags. eval set -- "${OPTIONS}" TARGET_FILE='' FORCE='false' while true; do case "$1" in -h|--help) echo 'Help is on its way' exit ;; -t|--target-file) TARGET_FILE="$2" shift # For flag name. shift # For flag value. ;; -f|--force) FORCE='true' shift # For flag name. ;; --) shift break ;; *) >&2 echo 'Sorry, invalid option' exit 1 esac done readonly TARGET_FILE readonly FORCE ``` ## 3. Using a custom implementation for long flags ### Pros: - Code is easier to read and write than `getopt` / `getopts` ### Cons: - Completely custom implementation - No built-in error handling - Can't specify flag values using `=` ### Example: ```bash TARGET_FILE='' FORCE='false' while [[ "$#" -gt 0 ]]; do case "$1" in --help) echo 'Help is on its way' exit ;; --target-file) TARGET_FILE="$2" shift # For flag name. shift # For flag value. ;; --force) FORCE='true' shift # For flag name. ;; *) >&2 echo 'Sorry, invalid option' exit 1 esac done readonly TARGET_FILE readonly FORCE ``` ## 4. Using a custom implementation for short and long flags ### Pros: - Code is easier to read and write than `getopt` / `getopts` - Requires little extra work to support both long and short flag names ### Cons: - Completely custom implementation - No built-in error handling - Can't specify flag values using `=` - Can't chain short flag names ### Example: ```bash TARGET_FILE='' FORCE='false' while [[ "$#" -gt 0 ]]; do case "$1" in -h|--help) echo 'Help is on its way' exit ;; -t|--target-file) TARGET_FILE="$2" shift # For flag name. shift # For flag value. ;; -f|--force) FORCE='true' shift # For flag name. ;; *) >&2 echo 'Sorry, invalid option' exit 1 esac done readonly TARGET_FILE readonly FORCE ``` --- Source material * https://stackoverflow.com/a/402410/3769045 * https://stackoverflow.com/a/34531699/3769045 * https://stackoverflow.com/a/7069755/3769045 Review
on CodeApprove --- README.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9df6b80..171bf58 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ for value in 1 2 3; do done ``` -### Command-line flags +### Using command-line flags If a bash script calls another application that accepts command-line flags, use long flag names where available. @@ -177,6 +177,55 @@ Make exceptions for flags where the short flags are extremely common and the lon * `mkdir -p` +### Parsing command-line flags + +In the same way that we prefer to use long flag names, we also prefer to implement long flag names. + +```bash +print_help() { + cat <&2 print_help + exit 1 + esac +done +readonly TARGET_FILE +readonly FORCE + +if [[ -z "${TARGET_FILE}" ]]; then + >&2 echo 'Missing parameter: TARGET_FILE' + >&2 print_help + exit 1 +fi +``` + +There's no need to implement short flag names because our scripts are either being called by other scripts, internally, or users are copy/pasting commands from examples we've given them. Either way, we prefer to see long flag names being used. + ### Error messages * Print error messages to stderr.