koalaman / shellcheck

ShellCheck, a static analysis tool for shell scripts
https://www.shellcheck.net
GNU General Public License v3.0
36.36k stars 1.77k forks source link

Suggest `getopt` instead of any kind of manual options parsing #2595

Open l0b0 opened 2 years ago

l0b0 commented 2 years ago

For new checks and feature suggestions

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 getopt to 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:

Alhadis commented 1 year ago

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 ```sh # # 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).

Jorenar commented 4 months ago

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

ale5000-git commented 4 months ago

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.

ko1nksm commented 2 months ago

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