Skip to content

Commit

Permalink
Define a convention for parsing command-line flags (#10)
Browse files Browse the repository at this point in the history
Resolves #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


<a data-ca-tag
href="https://codeapprove.com/pr/tiny-pilot/style-guides/10"><img
src="https://codeapprove.com/external/github-tag-allbg.png" alt="Review
on CodeApprove" /></a>
  • Loading branch information
jdeanwallace authored Jun 20, 2023
1 parent 824bb4c commit 4d09df9
Showing 1 changed file with 50 additions and 1 deletion.
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 <<EOF
Usage: ${0##*/} [--help] [--force] --target-file TARGET_FILE
Creates an empty file at the target path.
--help Display this help and exit.
--force Overwrite the target file, if it already exists.
--target-file TARGET_FILE The target path of the empty file.
EOF
}

TARGET_FILE=''
FORCE='false'
while [[ "$#" -gt 0 ]]; do
case "$1" in
--help)
print_help
exit
;;
--target-file)
TARGET_FILE="$2"
shift # For flag name.
shift # For flag value.
;;
--force)
FORCE='true'
shift # For flag name.
;;
*)
>&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.
Expand Down

0 comments on commit 4d09df9

Please sign in to comment.