How do I parse command line arguments in Bash?

1,814,096

Solution 1

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.

Solution 2

No answer showcases enhanced getopt. And the top-voted answer is misleading: It either ignores -⁠vfd style short options (requested by the OP) or options after positional arguments (also requested by the OP); and it ignores parsing-errors. Instead:

  • Use enhanced getopt from util-linux or formerly GNU glibc.1
  • It works with getopt_long() the C function of GNU glibc.
  • no other solution on this page can do all this:
    • handles spaces, quoting characters and even binary in arguments2 (non-enhanced getopt can’t do this)
    • it can handle options at the end: script.sh -o outFile file1 file2 -v (getopts doesn’t do this)
    • allows =-style long options: script.sh --outfile=fileOut --infile fileIn (allowing both is lengthy if self parsing)
    • allows combined short options, e.g. -vfd (real work if self parsing)
    • allows touching option-arguments, e.g. -oOutfile or -vfdoOutfile
  • Is so old already3 that no GNU system is missing this (e.g. any Linux has it).
  • You can test for its existence with: getopt --test → return value 4.
  • Other getopt or shell-builtin getopts are of limited use.

The following calls

myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile
myscript -v -f -d -o/fizz/someOtherFile -- ./foo/bar/someFile
myscript --verbose --force --debug ./foo/bar/someFile -o/fizz/someOtherFile
myscript --output=/fizz/someOtherFile ./foo/bar/someFile -vfd
myscript ./foo/bar/someFile -df -v --output /fizz/someOtherFile

all return

verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile

with the following myscript

#!/bin/bash
# More safety, by turning some bugs into errors.
# Without `errexit` you don’t need ! and can replace
# PIPESTATUS with a simple $?, but I don’t do that.
set -o errexit -o pipefail -o noclobber -o nounset

# -allow a command to fail with !’s side effect on errexit
# -use return value from ${PIPESTATUS[0]}, because ! hosed $?
! getopt --test > /dev/null 
if [[ ${PIPESTATUS[0]} -ne 4 ]]; then
    echo 'I’m sorry, `getopt --test` failed in this environment.'
    exit 1
fi

OPTIONS=dfo:v
LONGOPTS=debug,force,output:,verbose

# -regarding ! and PIPESTATUS see above
# -temporarily store output to be able to check for errors
# -activate quoting/enhanced mode (e.g. by writing out “--options”)
# -pass arguments only via   -- "$@"   to separate them correctly
! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@")
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    # e.g. return value is 1
    #  then getopt has complained about wrong arguments to stdout
    exit 2
fi
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED"

d=n f=n v=n outFile=-
# now enjoy the options in order and nicely split until we see --
while true; do
    case "$1" in
        -d|--debug)
            d=y
            shift
            ;;
        -f|--force)
            f=y
            shift
            ;;
        -v|--verbose)
            v=y
            shift
            ;;
        -o|--output)
            outFile="$2"
            shift 2
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Programming error"
            exit 3
            ;;
    esac
done

# handle non-option arguments
if [[ $# -ne 1 ]]; then
    echo "$0: A single input file is required."
    exit 4
fi

echo "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile"

1 enhanced getopt is available on most “bash-systems”, including Cygwin; on OS X try brew install gnu-getopt or sudo port install getopt
2 the POSIX exec() conventions have no reliable way to pass binary NULL in command line arguments; those bytes prematurely end the argument
3 first version released in 1997 or before (I only tracked it back to 1997)

Solution 3

deploy.sh

#!/bin/bash

while [[ "$#" -gt 0 ]]; do
    case $1 in
        -t|--target) target="$2"; shift ;;
        -u|--uglify) uglify=1 ;;
        *) echo "Unknown parameter passed: $1"; exit 1 ;;
    esac
    shift
done

echo "Where to deploy: $target"
echo "Should uglify  : $uglify"

Usage:

./deploy.sh -t dev -u

# OR:

./deploy.sh --target dev --uglify

Solution 4

From digitalpeer.com with minor modifications:

Usage myscript.sh -p=my_prefix -s=dirname -l=libname

#!/bin/bash
for i in "$@"
do
case $i in
    -p=*|--prefix=*)
    PREFIX="${i#*=}"

    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    ;;
    -l=*|--lib=*)
    DIR="${i#*=}"
    ;;
    --default)
    DEFAULT=YES
    ;;
    *)
            # unknown option
    ;;
esac
done
echo PREFIX = ${PREFIX}
echo SEARCH PATH = ${SEARCHPATH}
echo DIRS = ${DIR}
echo DEFAULT = ${DEFAULT}

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.

Solution 5

while [ "$#" -gt 0 ]; do
  case "$1" in
    -n) name="$2"; shift 2;;
    -p) pidfile="$2"; shift 2;;
    -l) logfile="$2"; shift 2;;

    --name=*) name="${1#*=}"; shift 1;;
    --pidfile=*) pidfile="${1#*=}"; shift 1;;
    --logfile=*) logfile="${1#*=}"; shift 1;;
    --name|--pidfile|--logfile) echo "$1 requires an argument" >&2; exit 1;;
    
    -*) echo "unknown option: $1" >&2; exit 1;;
    *) handle_argument "$1"; shift 1;;
  esac
done

This solution:

  • handles -n arg and --name=arg
  • allows arguments at the end
  • shows sane errors if anything is misspelled
  • compatible, doesn't use bashisms
  • readable, doesn't require maintaining state in a loop
Share:
1,814,096
Redwood
Author by

Redwood

I'm a San Francisco Bay Area programmer working in Objective-C/Cocoa and C#/.NET.

Updated on January 27, 2022

Comments

  • Redwood
    Redwood over 2 years

    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?

  • Livven
    Livven about 11 years
    Is this really true? According to Wikipedia there's a newer GNU enhanced version of getopt which includes all the functionality of getopts and then some. man getopt on Ubuntu 13.04 outputs getopt - parse command options (enhanced) as the name, so I presume this enhanced version is standard now.
  • szablica
    szablica almost 11 years
    That something is a certain way on your system is a very weak premise to base asumptions of "being standard" on.
  • Tobias Kienzler
    Tobias Kienzler over 10 years
    Neat! Though this won't work for space-separated arguments à la mount -t tempfs .... One can probably fix this via something like while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;; etc
  • Stephane Chazelas
    Stephane Chazelas almost 10 years
    @Livven, that getopt is not a GNU utility, it's part of util-linux.
  • m3nda
    m3nda about 9 years
    I read all and this one is my preferred one. I don't like to use -a=1 as argc style. I prefer to put first the main option -options and later the special ones with single spacing -o option. Im looking for the simplest-vs-better way to read argvs.
  • m3nda
    m3nda about 9 years
    It's working really well but if you pass an argument to a non a: option all the following options would be taken as arguments. You can check this line ./myscript -v -d fail -o /fizz/someOtherFile -f ./foo/bar/someFile with your own script. -d option is not set as d:
  • bronson
    bronson over 8 years
    Sorry for the delay. In my script, the handle_argument function receives all the non-option arguments. You can replace that line with whatever you'd like, maybe *) die "unrecognized argument: $1" or collect the args into a variable *) args+="$1"; shift 1;;.
  • radu.ciorba
    radu.ciorba over 8 years
    i used this with a small modification to append all unrecognized options to an array that then becomes our new $@ gist.github.com/rciorba/514fd75f4a6471d44d71
  • Robert Siemer
    Robert Siemer over 8 years
    This code can’t handle options with arguments like this: -c1. And the use of = to separate short options from their arguments is unusual...
  • Robert Siemer
    Robert Siemer over 8 years
    This can’t handle -vfd style combined short options.
  • Robert Siemer
    Robert Siemer about 8 years
    Using $* is broken usage of getopt. (It hoses arguments with spaces.) See my answer for proper usage.
  • sfnd
    sfnd about 8 years
    I ran into two problems with this useful chunk of code: 1) the "shift" in the case of "-c=foo" ends up eating the next parameter; and 2) 'c' should not be included in the "[cfr]" pattern for combinable short options.
  • Nicolas Lacombe
    Nicolas Lacombe about 8 years
    If you use -gt 0, remove your shift after the esac, augment all the shift by 1 and add this case: *) break;; you can handle non optionnal arguments. Ex: pastebin.com/6DJ57HTc
  • RichVel
    RichVel almost 8 years
    Thanks for writing argbash, I just used it and found it works well. I mostly went for argbash because it's a code generator supporting the older bash 3.x found on OS X 10.11 El Capitan. The only downside is that the code-generator approach means quite a lot of code in your main script, compared to calling a module.
  • bubla
    bubla almost 8 years
    You can actually use Argbash in a way that it produces tailor-made parsing library just for you that you can have included in your script or you can have it in a separate file and just source it. I have added an example to demonstrate that and I have made it more explicit in the documentation, too.
  • RichVel
    RichVel almost 8 years
    Good to know. That example is interesting but still not really clear - maybe you can change name of the generated script to 'parse_lib.sh' or similar and show where the main script calls it (like in the wrapping script section which is more complex use case).
  • Oleksii Chekulaiev
    Oleksii Chekulaiev over 7 years
    To use the demo to parse params that come into your bash script you just do show_use "$@"
  • Oleksii Chekulaiev
    Oleksii Chekulaiev over 7 years
    Basically I found out that github.com/renatosilva/easyoptions does the same in the same way but is a bit more massive than this function.
  • Charles Duffy
    Charles Duffy over 7 years
    grumble re: using all-uppercase variable names, in contravention of POSIX convention specifying that upper-case variables are used for names with meaning to POSIX-specified tools, and lower-case names are reserved for application use. (This is relevant to shell variables as well, as a shell variable assignment will overwrite any like-named environment variable that exists).
  • Luca Davanzo
    Luca Davanzo over 7 years
    what's the meaning for "+x" on ${key+x} ?
  • galmok
    galmok over 7 years
    It is a test to see if 'key' is present or not. Further down I unset key and this breaks the inner while loop.
  • bubla
    bubla over 7 years
    The issues were addressed in recent version of argbash: Documentation has been improved, a quickstart argbash-init script has been introduced and you can even use argbash online at argbash.io/generate
  • johncip
    johncip over 7 years
    Thanks for this. Just confirmed from the feature table at en.wikipedia.org/wiki/Getopts, if you need support for long options, and you're not on Solaris, getopt is the way to go.
  • gcscaglia
    gcscaglia over 7 years
    While parsing arguments yourself using a while loop and a case is the right way to go, please don't use the example code of this answer. It has nasty problems, like silently ignoring unknown or malformed options and no support for trailing positional parameters (i.e. things after --)
  • Kaushal Modi
    Kaushal Modi about 7 years
    I believe that the only caveat with getopt is that it cannot be used conveniently in wrapper scripts where one might have few options specific to the wrapper script, and then pass the non-wrapper-script options to the wrapped executable, intact. Let's say I have a grep wrapper called mygrep and I have an option --foo specific to mygrep, then I cannot do mygrep --foo -A 2, and have the -A 2 passed automatically to grep; I need to do mygrep --foo -- -A 2. Here is my implementation on top of your solution.
  • Tom N Tech
    Tom N Tech about 7 years
    Just wanted to advocate for the recommended solution. After monkeying with several options, this is the most painless and simple.
  • mauron85
    mauron85 about 7 years
    Like this one. Maybe just add -e param to echo with new line.
  • Josh Wulf
    Josh Wulf about 7 years
    I get this on Mac OS X: ``` lib/bashopts.sh: line 138: declare: -A: invalid option declare: usage: declare [-afFirtx] [-p] [name[=value] ...] Error in lib/bashopts.sh:138. 'declare -x -A bashopts_optprop_name' exited with status 2 Call tree: 1: lib/controller.sh:4 source(...) Exiting with status 1 ```
  • Josh Wulf
    Josh Wulf about 7 years
    You need Bash version 4 to use this. On Mac, the default version is 3. You can use home brew to install bash 4.
  • kolydart
    kolydart almost 7 years
    You do not echo –default. In the first example, I notice that if –default is the last argument, it is not processed (considered as non-opt), unless while [[ $# -gt 1 ]] is set as while [[ $# -gt 0 ]]
  • SDsolar
    SDsolar almost 7 years
    Why would you want to make it more complicated?
  • Bruno Bronosky
    Bruno Bronosky almost 7 years
    I finally fixed the issue in the "Straight Bash Space Separated" example with -gt 0 vs -gt 1 as described by @NicolasMongrain-Lacombe and @kolydart
  • Simon A. Eugster
    Simon A. Eugster over 6 years
    The getopts "h?vf:" should be getopts "hvf:" without question mark. Arguments which are not recognized are stored as ? in $opt. Quote from man builtins: “The colon and question mark characters may not be used as option characters.”
  • Will Barnwell
    Will Barnwell over 6 years
    This takes the same approach as Noah's answer, but has less safety checks / safeguards. This allows us to write arbitrary arguments into the script's environment and I'm pretty sure your use of eval here may allow command injection.
  • Jason S
    Jason S over 6 years
    You use shift on the known arguments and not on the unknown ones so your remaining $@ will be all but the first two arguments (in the order they are passed in), which could lead to some mistakes if you try to use $@ later. You don't need the shift for the = parameters, since you're not handling spaces and you're getting the value with the substring removal #*=
  • Masadow
    Masadow over 6 years
    You're right, in fact, since I build a PARAMS variable, I don't need to use shift at all
  • Jakub Kukul
    Jakub Kukul over 6 years
    If you're running in the bash "strict mode" (#!/bin/bash -u or set -eu), only restore positional parameters if POSITIONAL array is not empty, to avoid unbound variable error, i.e.: if [ ${#POSITIONAL[@]} -gt 0 ]; then set -- "${POSITIONAL[@]}" ; fi # restore positional parameters
  • lionello
    lionello over 6 years
    First sample doesn't appear to handle positional arguments with spaces: test.sh "a b" will end up having $1 set to "a" and $2 set to "b".
  • bobpaul
    bobpaul over 6 years
    I like this answer. I find the statement from "wooledge.org" that using enhanced getopt means you'll do twice as much work rather misleading. Most of the scripts I write for me. They need to run on my system. I never give them to anyone else (except maybe others at work, if it's that sort of script). At most I need to test to ensure enhanced getopt is installed and then exit with a message "enhanced getopt is required to run this script". Easy peasy. That said, I'm not sure I've ever seen enhanced getopt installed by default. It's part of linux-util, which is a dev package on most distros.
  • Robert Siemer
    Robert Siemer over 6 years
    @bobpaul Your statement about util-linux is wrong and misleading as well: the package is marked “essential” on Ubuntu/Debian. As such, it is always installed. – Which distros are you talking about (where you say it needs to be installed on purpose)?
  • hfossli
    hfossli about 6 years
    This is what I am doing. Have to while [[ "$#" > 1 ]] if I want to support ending the line with a boolean flag ./script.sh --debug dev --uglify fast --verbose. Example: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
  • hfossli
    hfossli about 6 years
    Wow! Simple and clean! This is how I'm using this: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
  • Guilherme Garnier
    Guilherme Garnier about 6 years
    Amazing! I've tested a couple of answers, but this is the only one that worked for all cases, including many positional parameters (both before and after flags)
  • thebunnyrules
    thebunnyrules about 6 years
    @Matt J, the first part of the script (for i) would be able to handle arguments with spaces in them if you use "$i" instead of $i. The getopts does not seem to be able to handle arguments with spaces. What would be the advantage of using getopt over the for i loop?
  • Benjamin W.
    Benjamin W. almost 6 years
    I don't know about the timelines of the different answers, but the top answer does cover -vfd style short options by way of the getopts built-in.
  • Martynas Jusevičius
    Martynas Jusevičius almost 6 years
    How can I process a portion of the arguments recognized by the current script, and pass the rest to another script using $@? $@ is empty after arguments are parsed as shown in the answer.
  • Bruno Bronosky
    Bruno Bronosky almost 6 years
    @MartynasJusevičius the shift commands takes them or of $@ so limit the number of times you shift.
  • Martynas Jusevičius
    Martynas Jusevičius almost 6 years
    So I would need to collect unknown options into my own array instead of $@? I thought $POSITIONAL was doing something similar, but looks like it only stores keys, not values. What is the purpose of it?
  • Martynas Jusevičius
    Martynas Jusevičius almost 6 years
    Actually, if the +=("$1") syntax to append to an array, it's not working for me. I only get the very first option when I print $POSITIONAL. Edit: nevermind, ${POSITIONAL[@]} works :) Thanks.
  • pappix
    pappix over 5 years
    when using "shift" in a loop, I would advise to do something like: shift || exit 1 This is because if shift misbehaves and fails for some reason to shift the arguments, you might get caught in an infinite loop. This way instead if shift has a return value different from 0 you can safely exit and report an error has occurred.
  • Manish Kumar
    Manish Kumar over 5 years
    Bash Equals-Separated (e.g., --option=argument) (without getopt[s]) what changes i will have to do for -x y instead of -x=y?
  • RealHandy
    RealHandy over 5 years
    This is much nicer to paste into each script rather than dealing with source or having people wonder where your functionality actually starts.
  • jjj
    jjj about 5 years
    Note this doesn't work on Mac at least up to the current 10.14.3. The getopt that ships is BSD getopt from 1999...
  • Robert Siemer
    Robert Siemer about 5 years
    @jjj footnote 1 covers OS X. – For OS X out-of-the-box solution check other questions and answers. Or to be honest: for real programming don’t use bash. ;-)
  • Harlin
    Harlin about 5 years
    This is a very good method to do this (without getopts) if you know you can use bash. For me, that's close to 99% of the time. I use this with most of my intermediate-level scripts in my own environment or as helper scripts in my own software.
  • lauksas
    lauksas about 5 years
    nice succinct code, but using -n and no other arg causes infinite loop due to error on shift 2, issuing shift twice instead of shift 2. Suggested the edit.
  • Robert Siemer
    Robert Siemer about 5 years
    @BenjaminW. The top-voted answer covers two self parsing solutions and getopts. The former don’t do combined short options, the latter doesn’t parse options after non-option arguments.
  • thibmaek
    thibmaek about 5 years
    In this script, ease is incorrect and should be replaced with esac to close the case block
  • Tyrel Kostyk
    Tyrel Kostyk about 5 years
    In the Bash Equals-Separated (e.g., --option=argument) (without getopt[s]) method, why is the shift built-in needed? Shouldn't the for loop iterate through each option automatically? If you used the supplied example as-is but just removed the shift commands, would it not be better as that way you don't discard your inputs?
  • Bruno Bronosky
    Bruno Bronosky about 5 years
    @TyrelKostyk if you actually try your suggestion, you'll see that the shift is what makes $1 point to the first non-option argument /etc/hosts. If you don't shift, you'll have to increment your own ((counter++)) in the loop and then call ${!counter} to get the "variable variable". If that's better for you, do it. But I think that's needless complexity to avoid discarding inputs that are stored as well named variables.
  • Tyrel Kostyk
    Tyrel Kostyk about 5 years
    @BrunoBronosky Thanks for the reply! Makes sense. I discovered something from my confusion: I was applying your answer in the form of a "parse_inputs" function. And I found that when using your exact implementation, and calling parse_inputs "$@", the shift operator has no effect, and whether you use it or not, it doesn't discard your inputs. (Should this be added onto your post as an edit?)
  • Jay Somedon
    Jay Somedon almost 5 years
    Is there link to doc on getopt command? All I see after googling getopt is about the c funciton not the terminal command..
  • Robert Siemer
    Robert Siemer almost 5 years
    @JaySomedon man getopt works on the command line and pretty good with Google. Go for section 1, i.e. getopt(1), because getopt(3) is the C function you talk about.
  • yair
    yair almost 5 years
    Warning: this tolerates duplicated arguments, the latest argument prevails. e.g. ./script.sh -d dev -d prod would result in deploy == 'prod'. I used it anyway :P :) :+1:
  • transang
    transang over 4 years
    What does the leading exclaimation mark mean? Could anyone add some reference?
  • Robert Siemer
    Robert Siemer over 4 years
    @transang Boolean negation of the return value. And its side effect: allow a command to fail (otherwise errexit would abort the program on error). -- The comments in the script tell you more. Otherwise: man bash
  • EM0
    EM0 over 4 years
    I'm using this (thanks!) but note that it allows empty argument value, e.g. ./script.sh -d would not generate an error but just set $deploy to an empty string.
  • Freedo
    Freedo over 4 years
    I'm using your first method and it doesn't work if the value has spaces on it
  • lxop
    lxop over 4 years
    @TyrelKostyk your problem is caused by the function getting its own arguments array. So when you shift inside the function, it drops elements from the function's argument array, not that of the overall script. A solution might be to build a positional array in your function, then use set -- ${positional[@]} immediately after calling the parse function.
  • Mr. Polywhirl
    Mr. Polywhirl over 4 years
    This is one of the best answers.
  • chenrici
    chenrici about 4 years
    I downvoted , because i believe the below answer is the correct answer.
  • leogama
    leogama about 4 years
    I've made an edit (it's waiting for review) to add some useful features while keeping the code straightforward and small. For fancier features, like multiple one letter options in a single argument, you should try getopt or getopts.
  • simplename
    simplename over 3 years
    what is this line doing cat >/tmp/demo-getopts.sh <<'EOF'
  • Robert Siemer
    Robert Siemer over 3 years
    @einpoklum There are two set and I believe you mean the first: I disagree. I see your point, but I do not wish to provide an answer which does not work in the environment I consider “best practice”.
  • Jaraws
    Jaraws over 3 years
    In bash space separated, what does this expression means: POSITIONAL=()??
  • Enissay
    Enissay over 3 years
    Good smart way to do it. I am using it from now on until a better way or a bug is found maybe ;-)
  • CIsForCookies
    CIsForCookies over 3 years
    Great answer, tnx! I shortened it a bit - while (( "$#" )); do instead of while [[ "$#" -gt 0 ]]; do
  • Inanc Gumus
    Inanc Gumus over 3 years
    @CIsForCookies Thx! That's because, afaik, ((...)) syntax is only exists in ksh, bash, and zsh, but not in plain sh.
  • user8162
    user8162 about 3 years
    If you want to generically evaluate --option and -option without repeating OPTION=$i every time, use -*=*) as match pattern and eval ${i##*-}.
  • Lukas S.
    Lukas S. about 3 years
    This looks great - but wondering if END_OF_OPT=1 is actually necessary on this line: --*) ARGV+=("$arg"); END_OF_OPT=1 ;;. If left in there, it fails to parse --username=fred if it's included after --quiet (or any other long-style boolean option). For example, script.sh --quiet --username=fred fails with Unrecognized argument: --username=fred (though script.sh --quiet --username fred works fine). I took out that END_OF_OPT=1 in my script and now it works, but not sure if maybe that breaks some other scenario I'm not aware of.
  • von spotz
    von spotz about 3 years
    @BrunoBronosky could you please elaborate what the line cat >/tmp/demo-space-separated.sh <<'EOF' is doing and why you put an EOF at the end of the file?
  • Bruno Bronosky
    Bruno Bronosky about 3 years
    @vonspotz Notice that the after each code sample block, there is a block labeled "Output from copy-pasting the block above:" That is because the previous block is written in a way that can be copy-pasted into your [bash] shell, as-is, no modification. That first line says cat, redirecting stdout into blah.sh. Since cat is not given a file to read, it tries to read stdin. I use a bash here-doc to stream a string of text in. wrapping the delimiter (which could be anything but I chose EOL) in single quotes prevent bash from performing expansions within the string.
  • madhukar93
    madhukar93 about 3 years
    works well for my usecase of mixing positional and optional args in any order, thanks.
  • Samuel Åslund
    Samuel Åslund about 3 years
    If you want/need long-opts with this portable getopts this query have a nice solution: stackoverflow.com/q/35235707/671282
  • YvesLeBorg
    YvesLeBorg almost 3 years
    @jjj brew install gnu-getopt ... and dont forget the small print about your path.
  • Kvothe
    Kvothe almost 3 years
    I want to allow options to take multiple arguments, an unknown number a priori. Can I use something based on this for that purpose? So script -opt1 1 2 3 4 -opt2 5 6 7 should allow me to refer to elements of the set 1 2 3 4 and of 5 6 7 in the script.
  • F. Hauri  - Give Up GitHub
    F. Hauri - Give Up GitHub over 2 years
    Wrong! Don't work if invoked as ./deploy.sh -ut dev !! See Robert Siemers's answer
  • vaeVictis
    vaeVictis over 2 years
    @RobertSiemer I just broke your 666 upvotes record, with my upvote :-D Very well explained. Thanks.
  • Gabriel Staples
    Gabriel Staples over 2 years
    shift removes arguments from the input arguments array, "$@". If you get into a situation where after parsing you still need to restore that array to re-use all of the input arguments somewhere else (not just the positional parameters via the POSITIONAL_ARGS array as shown in this answer), it can be done by backing up the full input arguments array and using the set command later, as shown here.
  • Gabriel Staples
    Gabriel Staples over 2 years
    If anyone is looking to see advanced argument parsing from this answer in a full, advanced bash program with help menu, argument parsing, main function, automatic execute vs source detection (akin to if __name__ == "__main__": in Python), etc, see my demo/template program in this list here. It is currently called argument_parsing__3_advanced__gen_prog_template.sh, but if that name changes in the future I'll update it in the list at the link just above.
  • pmarreck
    pmarreck over 2 years
    In your example, don't you actually have to shift twice if you pass a flag that has a parameter? I like the cleanliness of this otherwise, even if it won't accept "=" between an option and value.
  • Inanc Gumus
    Inanc Gumus over 2 years
    Yes, target makes another shift—but uglify makes a single one.
  • Inanc Gumus
    Inanc Gumus over 2 years
    @F.Hauri It's not wrong, please be kind. This follows Go's way of handling parameters. For a more complex scenario, yes, take a look at that answer as well.
  • F. Hauri  - Give Up GitHub
    F. Hauri - Give Up GitHub over 2 years
    @InancGumus This doesn't follow POSIX recommendations! deploy -ut dev will answer Unknown parameter passed: -ut, It's wrong! ... and shell is not go!
  • Inanc Gumus
    Inanc Gumus over 2 years
    You could have said: It's not a POSIX compliant way instead of rudely saying "wrong" with a bold style (OMG). It's not wrong as long as it works for some people (including me).
  • cdgraham
    cdgraham over 2 years
    @yair That's how CLI arguments usually work as far as I've seen. It's nice because you can override options. For example I can alias build='./script.sh -a 1 -b 2 -c 3' for my default options, then build -b 6 to override individuals!
  • Liso
    Liso about 2 years
  • leogama
    leogama about 2 years
    Thank you, @Liso! I need to update this answer. I've analyzed the regular expressions' decision tree and found some minor errors (nothing serious though).
  • Liso
    Liso about 2 years
    @leogama Yeah, I've used this code in my script, works great overall ! Keep up the good work 👍👍
  • taiyodayo
    taiyodayo about 2 years
    on MacOS 10.16, even after brew install gnu-getopt && brew link --force gnu-getopt , getopt --test doesn't return the value 4. I wonder why? (which getopt is correctly linked to /usr/local/opt/gnu-getopt/bin/getopt) Original BSD getopt returns --.
  • Robert Siemer
    Robert Siemer about 2 years
    @taiyodayo How do you test for the return value? What does getopt --version say?
  • midnite
    midnite about 2 years
    getopt is POSIX too.
  • KasRoudra
    KasRoudra about 2 years
    I have a question here. Why did you use shift; OUTPUTFILE="$1" instead of OUTPUTFILE="$2"? Maybe it has an easy answer but I am a newbie in bash
  • Ponyboy47
    Ponyboy47 about 2 years
    I believe you could do either and it really just comes down to personal preference. In this case I just wanted to keep $1 as the "active" argument everywhere