Shell Script Argument Parsing

How do I parse command line arguments in Bash?

Bash Space-Separated (e.g., --option argument)

cat >/tmp/demo-space-separated.sh <<'EOF'
#!/bin/bash

POSITIONAL_ARGS=()

while [[ $# -gt 0 ]]; do
case $1 in
-e|--extension)
EXTENSION="$2"
shift # past argument
shift # past value
;;
-s|--searchpath)
SEARCHPATH="$2"
shift # past argument
shift # past value
;;
--default)
DEFAULT=YES
shift # past argument
;;
-*|--*)
echo "Unknown option $1"
exit 1
;;
*)
POSITIONAL_ARGS+=("$1") # save positional arg
shift # past argument
;;
esac
done

set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters

echo "FILE EXTENSION = ${EXTENSION}"
echo "SEARCH PATH = ${SEARCHPATH}"
echo "DEFAULT = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)

if [[ -n $1 ]]; then
echo "Last line of file specified as non-opt/last argument:"
tail -1 "$1"
fi
EOF

chmod +x /tmp/demo-space-separated.sh

/tmp/demo-space-separated.sh -e conf -s /etc /etc/hosts
Output from copy-pasting the block above
FILE EXTENSION  = conf
SEARCH PATH = /etc
DEFAULT =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34 example.com
Usage
demo-space-separated.sh -e conf -s /etc /etc/hosts


Bash Equals-Separated (e.g., --option=argument)

cat >/tmp/demo-equals-separated.sh <<'EOF'
#!/bin/bash

for i in "$@"; do
case $i in
-e=*|--extension=*)
EXTENSION="${i#*=}"
shift # past argument=value
;;
-s=*|--searchpath=*)
SEARCHPATH="${i#*=}"
shift # past argument=value
;;
--default)
DEFAULT=YES
shift # past argument with no value
;;
-*|--*)
echo "Unknown option $i"
exit 1
;;
*)
;;
esac
done

echo "FILE EXTENSION = ${EXTENSION}"
echo "SEARCH PATH = ${SEARCHPATH}"
echo "DEFAULT = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)

if [[ -n $1 ]]; then
echo "Last line of file specified as non-opt/last argument:"
tail -1 $1
fi
EOF

chmod +x /tmp/demo-equals-separated.sh

/tmp/demo-equals-separated.sh -e=conf -s=/etc /etc/hosts
Output from copy-pasting the block above
FILE EXTENSION  = conf
SEARCH PATH = /etc
DEFAULT =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34 example.com
Usage
demo-equals-separated.sh -e=conf -s=/etc /etc/hosts

To better understand ${i#*=} search for "Substring Removal" in this guide. It is functionally equivalent to `sed 's/[^=]*=//' <<< "$i"` which calls a needless subprocess or `echo "$i" | sed 's/[^=]*=//'` which calls two needless subprocesses.



Using bash with getopt[s]

getopt(1) limitations (older, relatively-recent getopt versions):

  • can't handle arguments that are empty strings
  • can't handle arguments with embedded whitespace

More recent getopt versions don't have these limitations. For more information, see these docs.



POSIX getopts

Additionally, the POSIX shell and others offer getopts which doen't have these limitations. I've included a simplistic getopts example.

cat >/tmp/demo-getopts.sh <<'EOF'
#!/bin/sh

# A POSIX variable
OPTIND=1 # Reset in case getopts has been used previously in the shell.

# Initialize our own variables:
output_file=""
verbose=0

while getopts "h?vf:" opt; do
case "$opt" in
h|\?)
show_help
exit 0
;;
v) verbose=1
;;
f) output_file=$OPTARG
;;
esac
done

shift $((OPTIND-1))

[ "${1:-}" = "--" ] && shift

echo "verbose=$verbose, output_file='$output_file', Leftovers: $@"
EOF

chmod +x /tmp/demo-getopts.sh

/tmp/demo-getopts.sh -vf /etc/hosts foo bar
Output from copy-pasting the block above
verbose=1, output_file='/etc/hosts', Leftovers: foo bar
Usage
demo-getopts.sh -vf /etc/hosts foo bar

The advantages of getopts are:

  1. It's more portable, and will work in other shells like dash.
  2. It can handle multiple single options like -vf filename in the typical Unix way, automatically.

The disadvantage of getopts is that it can only handle short options (-h, not --help) without additional code.

There is a getopts tutorial which explains what all of the syntax and variables mean. In bash, there is also help getopts, which might be informative.

Parsing command line arguments in a shell script function

Function can have arguments passed. so $@ inside function becomes functions args not the shell arguments at command line.

ALL_ARGS_PASSED="$@" is wrong $@ is already quoted args list. If you quote again it becomes single string and hence only first argument is parsed rest are the value.

""aa=1 bb=2 cc=3""
so if you parse this aa is key and value is "1 bb=2 cc=3"

so the solution is not to quote for ALL_ARGS_PASSED

ALL_PASSED_ARGS=$@

Shell script argument parsing

You want to use getopt with long and short options. An example from working code:

# Parse arguments
TEMP=$(getopt -n $PROGRAM_NAME -o p:P:cCkhnvVS \
--long domain-password:,pop3-password:\
,create,cron,kill,help,no-sync-passwords,version,verbose,skip-pop3 \
-- "$@")

# Die if they fat finger arguments, this program will be run as root
[ $? = 0 ] || die "Error parsing arguments. Try $PROGRAM_NAME --help"

eval set -- "$TEMP"
while true; do
case $1 in
-c|--create)
MODE="CREATE"; shift; continue
;;
-C|--cron)
MODE="CRON"; shift; continue
;;
-k|--kill)
MODE="KILL"; shift; continue
;;
-h|--help)
usage
exit 0
;;
-n|--no-sync-passwords)
SYNC_VHOST=0; shift; continue
;;
-p|--domain-password)
DOMAIN_PASS="$2"; shift; shift; continue
;;
-P|--pop3-password)
POP3_PASS="$2"; shift; shift; continue
;;
-v|--version)
printf "%s, version %s\n" "$PROGRAM_NAME" "$PROGRAM_VERSION"
exit 0
;;
-v|--verbose)
VERBOSE=1; shift; continue
;;
-S|--skip-pop3)
SKIP_POP=1; shift; continue
;;
--)
# no more arguments to parse
break
;;
*)
printf "Unknown option %s\n" "$1"
exit 1
;;
esac
done

Note, die is a function that was defined previously (not shown).

The -n option tells getopt to report errors as the name of my program, not as getopt. -o defines a list of short options (: after an option indicates a needed argument) and --long specifies the list of long options (corresponding in order to the short options).

The rest is just a simple switch, calling shift appropriately to advance the argument pointer. Note, calling shift; shift; is just a die hard habit. In the currently modern world, shift 2 would probably suffice.

The modern getopt is pretty consistent over newer platforms, however you may encounter some portability problems on older (circa pre Redhat 9) systems. See man getopt for information about backwards compatibility. However it's unlikely that you'll run into the need for it.

Finally, after parsing options, you can once again call:

eval set -- "$@"

This will move the argument pointer to anything else left on the command line after getopt was done parsing options. You can then just shift to keep reading them. For instance, if a command looked like this:

./foo --option bar file1.txt file2.txt file3.txt

Don't forget to make a handy -h / --help option to print your new fancy options once you're done. :) If you make that output help2man friendly, you have an instant man page to go with your new tool.

Edit

On most distributions, you can find more example getopt code in /usr/share/doc/util-linux/examples, which should have been installed by default.

How Bash parse multi-flag commands?

The shell does not attempt to parse command arguments; that's the responsibility of the utility. The range of possible command argument syntaxes, both in use and potentially useful, is far too great to attempt that.

On Unix-like systems, the shell identifies individual arguments from the command line, mostly by splitting at whitespace but also taking into account the use of quotes and a variety of other transformations, such as "glob expansion". It then makes a vector of these arguments ("argv") and passes the vector to execve, which hands them to the newly created process.

On Windows systems, the shell doesn't even do that. It just hands over the command-line as a string, and leaves it to the command-line tool to do everything. (In order to provide a modicum of compatibility, there's an intermediate layer which is called by the application initialization code, which eventually calls main(). This does some basic argument-splitting, although its quoting algorithm is quite a bit simplified from that used by a Unix shell.)

No command-line shell that I know of attempts to identify command-line flags. And neither should you.

For a bit of extracurricular reading, here's the description of shell parsing from the Posix standard: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html. Trying to implement all that goes far beyond the requirements given to you for this assignment, and I'm certainly not recommending that you do that. But it might still be interesting, and understanding it will help you immensely if you start using a shell.

Alternatively, you could try reading the Bash manual, which might be easier to understand. Note that Bash implements a lot of extensions to the Posix standard.

Argument parsing in bash

VMMOUNT=""
BOOTSTRAP=""
IMAGE_FILE=""
TARGET_EXE=""
INTERNAL_EXE=""
while : ; do
case "$1" in
--vmmount)
[ -n "${VMMOUNT}" ] && usage
VMMOUNT="$2"
shift 2 ;;
--bootstrap)
[ -n "${BOOTSTRAP}" ] && usage
BOOTSTRAP="$2"
shift 2 ;;
--image)
[ -n "${IMAGE_FILE}" ] && usage
IMAGE_FILE="$2"
shift 2 ;;
--target-exe)
[ -n "${TARGET_EXE}" ] && usage
TARGET_EXE="$2"
shift 2 ;;
--internal-exe)
[ -n "${INTERNAL_EXE}" ] && usage
INTERNAL_EXE="true"
shift ;;
*)
break ;;
esac
done
my_method "${IMAGE_FILE}" "${VMMOUNT}" "${BOOTSTRAP}" "${TARGET_EXE}" "${INTERNAL_EXE}" "$@"

Don't forget to enclose $@ in double quotes.

Shell how to make some of the script arguments mandatory

I made this work by checking that the variables req and out have some value after the for loop with the below code:

  if [[ -z $out  ||  -z $req ]]
then
echo "ERROR: -q and -o are mandatory arguments. See usage: \n";
exit 1;
fi


Related Topics



Leave a reply



Submit