Parse Args in Bash Scripts
By: Marco Massenzio
Tags: Python, Coding, Tech, Bash, Programming

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 several painful if [[ ${1} == '--build' ]]; then ... .

And, yes, I know about getopts : hopefully, no one here is going to argue that it’s either user-friendly or feature rich, now, are we?

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

Modifiers

Simple arguments (without any trailing “modifier”) will be assumed to be optional arguments of the --arg type; there are three modifiers:

! indicates a required option, of the --arg type;

indicates a option, of the type; + indicates a required “positional” argument (no flag, simply the value — as in tar /var/loc/my.tar )

indicates a “positional” argument (no flag, simply the value — as in ) ~ indicates an optional “positional” argument

Implementation

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

for arg in args:

# we first check if there is a modifier using a RegEx

m = re.match(MODIFIED_PATTERN, arg)

if m:

mod = m.group('modifier')

# Depending on the modifier, we add the

# necessary arguments for add_argument()

... parser.add_argument(f"{prefix}{m.group('opt')}",

**kwargs)

the regular expression that matches the ‘option’ and the ‘modifier’ takes advantage of Python’s “named groups” (of the form: (?P<name>...) ):

MODIFIED_PATTERN=re.compile(r"(?P<opt>\w+)(?P<modifier>[-!~\+])?")

so we can then use their names from the smatches object:

mod = m.group('modifier')

Finally, instead of having many “call points” for add_argument() depending on the modifier, we simply build the method’s named arguments into kwargs and pass it in the call:

if mod == "!":

kwargs["required"] = True

kwargs['action'] = 'store_true' if mod == '-' else 'store'

if mod == '+':

prefix = ''

elif mod == '~':

prefix = ''

kwargs['nargs'] = '?' ...

parser.add_argument(f"{prefix}{m.group('opt')}", **kwargs)

We have subclassed 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;

for the same reason, we need to override the print_help() method, which will only emit the `usage` line (to `stderr`) and then exit with a non-zero code, to force the bash script termination.