Learn Docker With My Newest Course

Dive into Docker takes you from "What is Docker?" to confidently applying Docker to your own projects. It's packed with best practices and examples. Start Learning Docker →

Parse Command Line Positional Arguments and Flags with Bash

parse-command-line-positional-arguments-and-flags-with-bash.jpg

We'll go over using short flags, long flags, flags with and without values and required positional arguments.

Quick Jump:

There’s a number of ways to handle user input in your scripts. Using command line flags and positional arguments are a popular approach.

When I’m looking to build a portable and zero dependency command line tool I often use the tactics described below.

# The Script

Since the script has a decent amount going on, I’ve littered it with a bunch of comments. The demo video covers everything in more detail as well as seeing it run.

#!/usr/bin/env bash

set -o errexit
set -o pipefail
set -o nounset

# A help menu showing how to use the script and what the expected output is.
_usage() {
cat << EOF
  ${0} NAME COUNTRY -i|--interests X[,Y] [-c|--cool]

  Output:
    Name: NAME
    Country: COUNTRY
    Interests: X[,Y]
    Cool?: (true|false)
EOF
}

main() {
  # Positional arguments.
  local position=0
  local name=
  local country=

  # Flag arguments.
  local interests=
  local cool="false"

  # Count all of the args passed into the script and continue looping while we have args.
  while [[ "${#}" -gt 0 ]]; do
    # Determine which arg we're working with.
    case "${1}" in
      # Optimize for happiness and support multiple variants.
      -h|--help|help)
        _usage

        # Since there's nothing to do, we're done.
        exit 0
        ;;
      -i|--interests)
        # Set the value so we can use it later in the script.
        interests="${2:-}"

        # Interests requires a value, so we make sure it's not empty.
        # If it's empty we'll let the user know and print a message to STDERR.
        [[ -z "${interests}" ]] && printf "%s must have a value\n\n" "${1}" >&2 && _usage >&2 && exit 1

        # Pop the first 2 elements off the list of arguments in $@, in this case
        # that's the -i|--interests flag and its value. This lets us break out
        # of the while loop because eventually we'll reach 0 args.
        shift 2
        ;;
      -c|--cool)
        # Set the value so we can use it later in the script.
        cool="true"

        # We only need to pop the first element off the list of arguments
        # since this flag doesn't have a value.
        shift
        ;;
      # We've handled all of our flags, now it's onto anything else (the positional args).
      *)
        # Determine which position we're at (this value defaults to 0 earlier in the script).
        case "${position}" in
          0)
            # Set the value so we can use it later in the script.
            name="${1}"

            # We've processed this position so let's increment it by 1 and
            # pop the first element off the list of args so we can continue in the loop.
            position=1
            shift
            ;;
          1)
            # The same as above.
            country="${1}"
            position=2
            shift
            ;;
          2)
            # The user called the script with an unexpected argument, so let's bail.
            printf "Unknown argument: %s\n\n" "${1}" >&2
            _usage >&2
            exit 1
            ;;
        esac
        ;;
    esac
  done

  # Validation.
  [[ -z "${name}" ]] && printf "Requires NAME\n\n" >&2 && _usage >&2 && exit 1
  [[ -z "${country}" ]] && printf "Requires COUNTRY\n\n" >&2 && _usage >&2 && exit 1
  [[ -z "${interests}" ]] && printf "Requires --interests X[,Y]\n\n" >&2 && _usage >&2 && exit 1

  # The script's logic now that the inputs are defined.
  echo "Name: ${name}"
  echo "Country: ${country}"
  echo "Interests: ${interests}"
  [[ "${cool}" == "true" ]] && echo "Cool?: ${cool}"

  return 0
}

main "${@:-}"

If you end up not needing flags then you can simplify all of this by accessing ${1}, ${2} or whatever positional args you need without the while loop and case statements.

# Demo Video

Timestamps

  • 0:56 – Displaying a usage message
  • 2:44 – Seeing how $@ works
  • 3:19 – Setting up variables for our inputs
  • 4:26 – A while loop and reducing the args until it’s 0
  • 6:23 – Handling the help menu
  • 7:41 – Handling the interests (required flag)
  • 9:11 – Understanding how shift works
  • 10:53 – Handling being cool or not (optional flag)
  • 12:05 – Handling positional arguments
  • 13:32 – Exiting out if there’s too many arguments
  • 14:08 – Validating our inputs
  • 15:45 – Performing your script’s main purpose
  • 16:12 – Why I explicitly return 0 in the main function
  • 16:52 – For basic parsing you can skip the loop and read $1, $2, etc.

References

Never Miss a Tip, Trick or Tutorial

Like you, I'm super protective of my inbox, so don't worry about getting spammed. You can expect a few emails per year (at most), and you can 1-click unsubscribe at any time. See what else you'll get too.



Comments