Parse Command Line Positional Arguments and Flags with Bash
We'll go over using short flags, long flags, flags with and without values and required positional arguments.
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.