From b865a2e8a04ddc76e24f8de5c0bc79eb0bf5813e Mon Sep 17 00:00:00 2001 From: Jason Wallace Date: Tue, 6 Jun 2023 17:10:09 +0200 Subject: [PATCH 1/5] Define conventions for parsing command-line flags. --- README.md | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f90a1a5..054f171 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ for URL in "${URLS[@]}"; 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. @@ -162,6 +162,105 @@ 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 +# BAD - parses only short flag names. +# Pros: +# - Uses built-in `getopts` command +# - Code is compact +# Cons: +# - Code is difficult read and write +TARGET_FILE='' +FORCE='false' +while getopts 'hm: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 + +# GOOD - parses only long flag names. +# Pros: +# - Code is easier to read and write +# Cons: +# - Completely custom implementation +# - Can't specify flag values using `=` +# For example: +# script.sh --target-file=/tmp/file +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 + +# BEST - parses both long and short flag names. +# Pros: +# - Relatively little extra work to support both long and short flag names +# Cons: +# - Can't chain short flag names +# For example: +# script.sh -abcd +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 +``` + ### Error messages * Print error messages to stderr. From 3b688a3b883a60f7c9d47e76ce847b2d73b772fe Mon Sep 17 00:00:00 2001 From: Jason Wallace Date: Tue, 6 Jun 2023 17:33:13 +0200 Subject: [PATCH 2/5] Fix typo. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 054f171..b88398e 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ In the same way that we prefer to use long flag names, we also prefer to impleme # - Code is difficult read and write TARGET_FILE='' FORCE='false' -while getopts 'hm:f' opt; do +while getopts 'ht:f' opt; do case "${opt}" in h) echo 'Help is on its way' From 6bf26b1f7428c86fcf00acb15498ff3885e953ff Mon Sep 17 00:00:00 2001 From: Jason Wallace Date: Wed, 7 Jun 2023 15:29:46 +0200 Subject: [PATCH 3/5] Add an example using `getopt`. --- README.md | 117 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 96 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b88398e..30a3b7d 100644 --- a/README.md +++ b/README.md @@ -166,15 +166,24 @@ Make exceptions for flags where the short flags are extremely common and the lon In the same way that we prefer to use long flag names, we also prefer to implement long flag names. +#### BAD - using `getopts` +Pros: +- Uses built-in [`getopts`](https://man7.org/linux/man-pages/man1/getopts.1p.html) command +- Can chain short flag names +- Code is compact + +Cons: +- Only supports short flag names +- Need to learn the options syntax + +Example: + ```bash -# BAD - parses only short flag names. -# Pros: -# - Uses built-in `getopts` command -# - Code is compact -# Cons: -# - Code is difficult read and write 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) @@ -194,15 +203,77 @@ while getopts 'ht:f' opt; do done readonly TARGET_FILE readonly FORCE +``` + +#### BAD - using `getopt` +Pros: +- Supports both short and long flag names +- Can specify flag values using `=` + +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 +``` + +#### GOOD - using custom implementation #1 -# GOOD - parses only long flag names. -# Pros: -# - Code is easier to read and write -# Cons: -# - Completely custom implementation -# - Can't specify flag values using `=` -# For example: -# script.sh --target-file=/tmp/file +Pros: +- Code is easier to read and write + +Cons: +- Completely custom implementation +- No built-in error handling +- Can't specify flag values using `=` + +```bash TARGET_FILE='' FORCE='false' while [[ "$#" -gt 0 ]]; do @@ -227,14 +298,18 @@ while [[ "$#" -gt 0 ]]; do done readonly TARGET_FILE readonly FORCE +``` + +#### OKAY - using custom implementation #2 +Pros: +- Requires little extra work to support both long and short flag names + +Cons: +- Can't chain short flag names -# BEST - parses both long and short flag names. -# Pros: -# - Relatively little extra work to support both long and short flag names -# Cons: -# - Can't chain short flag names -# For example: -# script.sh -abcd +Example: + +```bash TARGET_FILE='' FORCE='false' while [[ "$#" -gt 0 ]]; do From be2599044c18560cc12242af68b22d6264957b51 Mon Sep 17 00:00:00 2001 From: Jason Wallace Date: Mon, 12 Jun 2023 14:23:51 +0200 Subject: [PATCH 4/5] Reduce. --- README.md | 143 +----------------------------------------------------- 1 file changed, 1 insertion(+), 142 deletions(-) diff --git a/README.md b/README.md index 30a3b7d..bee374d 100644 --- a/README.md +++ b/README.md @@ -166,113 +166,6 @@ Make exceptions for flags where the short flags are extremely common and the lon In the same way that we prefer to use long flag names, we also prefer to implement long flag names. -#### BAD - using `getopts` -Pros: -- Uses built-in [`getopts`](https://man7.org/linux/man-pages/man1/getopts.1p.html) command -- Can chain short flag names -- Code is compact - -Cons: -- Only supports short flag names -- Need to learn the options syntax - -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 -``` - -#### BAD - using `getopt` -Pros: -- Supports both short and long flag names -- Can specify flag values using `=` - -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 -``` - -#### GOOD - using custom implementation #1 - -Pros: -- Code is easier to read and write - -Cons: -- Completely custom implementation -- No built-in error handling -- Can't specify flag values using `=` - ```bash TARGET_FILE='' FORCE='false' @@ -300,41 +193,7 @@ readonly TARGET_FILE readonly FORCE ``` -#### OKAY - using custom implementation #2 -Pros: -- Requires little extra work to support both long and short flag names - -Cons: -- 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 -``` +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 From fbce07f408156dce49541117d52f2dd14b0a8411 Mon Sep 17 00:00:00 2001 From: Jason Wallace Date: Thu, 15 Jun 2023 13:39:10 +0200 Subject: [PATCH 5/5] Add print_help. --- README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bee374d..ed3f4e6 100644 --- a/README.md +++ b/README.md @@ -167,12 +167,22 @@ Make exceptions for flags where the short flags are extremely common and the lon 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 echo 'Sorry, invalid option' + >&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.