Search code examples
bashcommand-linescriptingargumentsgetopts

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?


Solution

  • 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.