Parse Args in Bash Scripts

There is something to be said for the immediacy of using Bash scripts, especially when dealing with relatively simple system operations; however, parsing command line arguments has always been rather cumbersome and usually done along the lines of painful if [[ ${1} == '--build' ]]; then ....

On the other hand, Python is pretty convenient for system operations (especially when using the sh module) but sometimes a bit of an overkill, or just missing the immediacy of Bash: however, the argparse module is nothing short of awesome, when it comes to power and flexibility in parsing command line options.

This simple Python script tries to marry the best of both worlds, allowing with a relatively simple setup to parse arbitrary command line options, and then having their values reflected in the corresponding local environment variables.

Usage

The usage is rather straightforward: we invoke it with a list of the desired option names, followed by the actual command line arguments ($@) separated with --:

# The `-` indicates a bool flag (its presence will set the associated variable, no
# value expected); the `!` indicates a required argument.
PARSED=$(./parse_args keep- take counts! mount -- $@)
source ${PARSED}

the values of the arguments (if any) are then available via the ${ } operator:

if [[ -n ${keep} ]]; then
  echo "Keeping mount: ${mount}"
fi

For example:

└─( ./test --keep --mount /var/loc/bac --take 3 --counts yes  
Keeping mount: /var/loc/bac
Take: 3, counts: yes

The trailing - (simple dash) indicates a “flag” (a boolean option, which takes no value and whose presence will result in the corresponding variable to be set), while a trailing ! indicates a required argument:

└─( ./test --keep --mount /var/loc/bac --take 3             
usage: [-h] [--keep] [--take TAKE] --counts COUNTS [--mount MOUNT]
ERROR: the following arguments are required: --counts

Implementation

The source code is available here and revolves around adding arguments to argparse.ArgumentParser dynamically:

    for arg in args:
        required = False
        if arg.endswith('!'):
            required = True
            arg = arg[:-1]
        if arg.endswith('-'):
            parser.add_argument(f"--{arg[:-1]}", required=required, action='store_true')
        else:
            parser.add_argument(f"--{arg}", required=required)

We have subclassed the ArgumentParser with a StderrParser so that:

  • when erroring out, we emit error messages to stderr so they don’t get “swallowed” in the bash script; and
  • we need to exit with an error code, so that using set -e in our shell script will cause it to terminate, instead of executing the source command with potentially unexpected consequences.
class StderrParser(argparse.ArgumentParser):
    def __init__(self, **kwargs):
        super().__init__(prog='', **kwargs)

    def exit(self, status=0, message=None):
        if message:
            print(message, file=sys.stderr)
        exit(status)

    def error(self, message):
        self.print_usage(file=sys.stderr)
        self.exit(status=1, message=f"ERROR: {message}")
Advertisement

3 responses to “Parse Args in Bash Scripts”

    1. To be honest, the pain of `getopts` was the main motivator behind my writing `parse-args` 😀
      IMHO it is too limiting and too cumbersome to use.

  1. Correct link to script (linked to from “This simple Python script…”):

    https://github.com/massenz/common-utils/blob/main/parse_args.py

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: