Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I parse command line arguments in Bash?

Say, I have a script that gets called with this line:

./myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile 

or this one:

./myscript -v -f -d -o /fizz/someOtherFile ./foo/bar/someFile  

What's the accepted way of parsing this such that in each case (or some combination of the two) $v, $f, and $d will all be set to true and $outFile will be equal to /fizz/someOtherFile?

like image 656
Lawrence Johnston Avatar asked Oct 10 '08 16:10

Lawrence Johnston


People also ask

How do you parse arguments in bash?

We can use the getopts program/ command to parse the arguments passed to the script in the command line/ terminal by using loops and switch-case statements. Using getopts, we can assign the positional arguments/ parameters from the command line to the bash variables directly.

How do I parse a command-line in bash?

To parse short command-line options, we can use bash's built-in function getopts. It parses positional parameters as options.

How does bash handle command-line arguments?

You can handle command-line arguments in a bash script in two ways. One is by using argument variables, and another is by using the getopts function.

How do you read an argument in shell script?

The special character $# stores the total number of arguments. We also have $@ and $* as wildcard characters which are used to denote all the arguments. We use $$ to find the process ID of the current shell script, while $? can be used to print the exit code for our script.


1 Answers

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.

like image 97
Bruno Bronosky Avatar answered Oct 02 '22 07:10

Bruno Bronosky