shellcheck
shellcheck copied to clipboard
Suggest `getopt` instead of any kind of manual options parsing
For new checks and feature suggestions
- [x] https://www.shellcheck.net/ (i.e. the latest commit) currently gives no useful warnings about this
- [x] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related
Here's a snippet that shows the problem:
#!/usr/bin/env bash
if [[ "$1" == "--foo" ]]; then
…
fi
Here's what shellcheck currently says:
No issues detected!
Here's what I wanted or expected to see:
Use
getoptto parse options.
Rationale: It’s tempting to implement basic option handling yourself when there’s only a single option. But there are plenty of pitfalls, and I’d recommend using getopt from the start. For example, here's a bog standard use of getopt AFAICT covering the vast majority of use cases:
#!/usr/bin/env bash
arguments="$(getopt --options='' \
--longoptions=configuration:,help,include:,verbose --name=foo -- "$@")"
eval set -- "$arguments"
unset arguments
while true
do
case "$1" in
--configuration)
configuration="$2"
shift 2
;;
--help)
usage
exit
;;
--include)
includes+=("$2")
shift 2
;;
--verbose)
verbose=1
shift
;;
--)
shift
break
;;
*)
printf 'Not implemented: %q\n' "$1" >&2
exit 1
;;
esac
done
Doing everything this does manually would be a lot of work:
- Supports both
--configuration=fooand--configuration foocall styles. - Supports optional
--option/argument separator. If this separator is not part of the original command it's added after all recognised flags, so there's no ambiguity about when to break off the loop. - Supports arbitrary option order.
- Supports arbitrary values. Newlines, spaces, backslashes, you name it. If you escape/quote the inputs properly, they'll be part of the value, with no extra hacks.
- Supports long options with minimal hacks (
--options=''is unfortunately still necessary to specify that I definitely don't want to support any short option names). - Prints a useful error message including the program name if parsing fails, which is useful when you're deep in a call stack.
- If
--configurationis specified more than once, the last value is used. This is a common solution to allow a default set of options (for example in a configuration file) which can then be overridden by command-line options. It would be easy to change the example toexitinstead, if that's what you want. - Accumulates
--includevalues in an array, making them safe to reuse. - If we ever hit the default case then there's a flag in the
getoptcall which is not yet handled by thecase, which would be a programmer error. This case is handled gracefully.
For example, here's a bog standard use of getopt AFAICT covering the vast majority of use cases:
I might as well leave this here…
Shell-script boilerplate with full getopts support
#
# name: Short description of your program.
#
usage="${0##*/} [-h|--help] [-v|--version] ...files"
version='v1.0.0'
# Parse command-line switches
while [ -n "$1" ]; do case $1 in
# Print a brief usage summary and exit
-h|--help|-\?)
printf 'Usage: %s\n' "$usage"
exit ;;
# Print a version string and exit
-v|--version)
printf '%s\n' "$version"
exit ;;
# Unbundle short options
-[niladic-short-opts]?*)
tail="${1#??}"
head=${1%"$tail"}
shift
set -- "$head" "-$tail" "$@"
continue ;;
# Expand parametric values
-[monadic-short-opts]?*|--[!=]*=*)
case $1 in
--*) tail=${1#*=}; head=${1%%=*} ;;
*) tail=${1#??}; head=${1%"$tail"} ;;
esac
shift
set -- "$head" "$tail" "$@"
continue ;;
# Add new switch checks here
--option-name)
break ;;
# Double-dash: Terminate option parsing
--)
shift
break ;;
# Invalid option: abort
--*|-?*)
>&2 printf '%s: Invalid option: "%s"\n' "${0##*/}" "$1"
>&2 printf 'Usage: %s\n' "$usage"
exit 1 ;;
# Argument not prefixed with a dash
*) break ;;
esac; shift
done
Here's the snippet version for editors that use TextMate-flavoured snippets (VS Code, Atom, Sublime, TextMate, etc).
Isn't getopt broken everywhere outside Linux? (https://mywiki.wooledge.org/ComplexOptionParsing)
I've actually came across this issue, because I was wondering how to make ShellCheck warn against use of getopt
Using getopt would be really good if the support was fine but there may be cases where getopt isn't supported (most Android), but also case where it is supported but not completely (for example lack of support for long options). So for those that need compatibility it is mostly a problem.
Do not use getopt for portability. Except for the GNU version of getopt, it not only cannot handle long options, but also cannot handle arguments that contain spaces. Below is the macOS (FreeBSD) version of getopt in action.
$ ./test.sh -a -o "a b"
-a
-o: a
Not implemented: b
$ ./test.sh -a "a b"
-a
rest arguments:
a
b
#!/usr/bin/env bash
arguments="$(getopt abco: "$@")"
eval set -- "$arguments"
unset arguments
while true
do
case "$1" in
-a | -b | c) echo "$1"; shift ;;
-o) echo "$1: $2"; shift 2 ;;
--) shift; break ;;
*)
printf 'Not implemented: %q\n' "$1" >&2
exit 1
;;
esac
done
echo "rest arguments:"
for i in "$@"; do
echo "$i"
done