2518

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?

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
Redwood
  • 66,744
  • 41
  • 126
  • 187
  • 1
    For zsh-users there's a great builtin called zparseopts which can do: `zparseopts -D -E -M -- d=debug -debug=d` And have both `-d` and `--debug` in the `$debug` array `echo $+debug[1]` will return 0 or 1 if one of those are used. Ref: http://www.zsh.org/mla/users/2011/msg00350.html – dza Aug 02 '16 at 02:13
  • 2
    Really good tutorial: http://linuxcommand.org/lc3_wss0120.php. I especially like the "Command Line Options" example. – Gabriel Staples Feb 10 '20 at 18:45
  • I created a script which does it for you, it's called - https://github.com/unfor19/bargs – Meir Gabay Aug 07 '20 at 14:29
  • 2
    See also [Giving a bash script the option to accepts flags, like a command?](https://stackoverflow.com/a/64257864/15168) for an elaborate, ad hoc, long and short option parser. It does not attempt to handle option arguments attached to short options, nor long options with `=` separating option name from option value (in both cases, it simply assumes that the option value is in the next argument). It also doesn't handle short option clustering — the question didn't need it. – Jonathan Leffler Oct 09 '20 at 15:21
  • [This great tutorial by Baeldung](https://www.baeldung.com/linux/use-command-line-arguments-in-bash-script) shows 4 ways to process command-line arguments in bash, including: 1) positional parameters `$1`, `$2`, etc., 2) flags with `getopts` and `${OPTARG}`, 3) looping over all parameters (`$@`), and 4) looping over all parameters using `$#`, `$1`, and the `shift` operator. – Gabriel Staples Dec 26 '20 at 21:05
  • I suggest leogama's [solution](https://stackoverflow.com/a/62616466/4123703) in this thread. – Louis Go Jun 23 '22 at 02:58

41 Answers41

3500

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.

Mateen Ulhaq
  • 24,552
  • 19
  • 101
  • 135
Bruno Bronosky
  • 66,273
  • 12
  • 162
  • 149
  • 58
    Is this really true? According to [Wikipedia](http://en.wikipedia.org/wiki/Getopts) 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. – Livven Jun 06 '13 at 21:19
  • 54
    That something is a certain way on your system is a very weak premise to base asumptions of "being standard" on. – szablica Jul 17 '13 at 15:23
  • 17
    @Livven, that `getopt` is not a GNU utility, it's part of `util-linux`. – Stephane Chazelas Aug 20 '14 at 19:55
  • i used this with a small modification to append all unrecognized options to an array that then becomes our new $@ https://gist.github.com/rciorba/514fd75f4a6471d44d71 – radu.ciorba Nov 11 '15 at 09:33
  • 4
    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: http://pastebin.com/6DJ57HTc –  Jun 19 '16 at 21:22
  • 2
    *grumble* re: using all-uppercase variable names, in contravention of [POSIX convention](http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html) 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). – Charles Duffy Oct 11 '16 at 21:49
  • 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 --) – gcscaglia Mar 03 '17 at 15:18
  • Just wanted to advocate for the recommended solution. After monkeying with several options, this is the most painless and simple. – rjurney Jun 09 '17 at 22:29
  • 2
    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 ]]` – kolydart Jul 10 '17 at 08:11
  • 1
    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 – Bruno Bronosky Sep 23 '17 at 05:13
  • 4
    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.”` – Simon A. Eugster Oct 06 '17 at 07:04
  • 2
    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` – Jakub Kukul Jan 22 '18 at 14:50
  • 1
    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". – lionello Feb 19 '18 at 06:45
  • 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. – Martynas Jusevičius Aug 22 '18 at 12:38
  • @MartynasJusevičius the `shift` commands takes them or of `$@` so limit the number of times you shift. – Bruno Bronosky Aug 22 '18 at 12:41
  • 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 Aug 22 '18 at 12:49
  • 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. – Martynas Jusevičius Aug 22 '18 at 13:05
  • 1
    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. – pappix Nov 22 '18 at 14:55
  • `Bash Equals-Separated (e.g., --option=argument) (without getopt[s])` what changes i will have to do for `-x y` instead of `-x=y`? – Manish Kumar Jan 25 '19 at 12:13
  • 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. – Harlin Apr 19 '19 at 23:23
  • 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? – Tyrel Kostyk Jun 26 '19 at 23:38
  • 1
    @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. – Bruno Bronosky Jun 27 '19 at 11:34
  • 1
    @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?) – Tyrel Kostyk Jun 27 '19 at 16:49
  • 1
    I'm using your first method and it doesn't work if the value has spaces on it – Freedo Jan 29 '20 at 08:34
  • @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. – lxop Feb 21 '20 at 03:03
  • 1
    I downvoted , because i believe the below answer is the correct answer. – Christoph Henrici Apr 07 '20 at 18:32
  • what is this line doing `cat >/tmp/demo-getopts.sh <<'EOF'` – simplename Sep 30 '20 at 18:12
  • In bash space separated, what does this expression means: POSITIONAL=()?? – Jaraws Nov 15 '20 at 13:59
  • @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? – von spotz Jun 01 '21 at 16:05
  • @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. – Bruno Bronosky Jun 01 '21 at 17:39
  • If you want/need long-opts with this portable getopts this query have a nice solution: https://stackoverflow.com/q/35235707/671282 – Samuel Åslund Jun 22 '21 at 11:47
  • 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. – Kvothe Aug 17 '21 at 16:44
  • `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](https://stackoverflow.com/a/70572787/4561887). – Gabriel Staples Jan 04 '22 at 02:06
  • 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](https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world#4-bash). 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. – Gabriel Staples Feb 11 '22 at 18:11
  • `getopt` is POSIX too. – midnite May 11 '22 at 00:03
  • `-*|--*)` seems duplicated: the first match `-*` will already match anything starting with `--` as well. `-*)` would be enough. Found thanks to [shellcheck](https://www.shellcheck.net). – Alexander Klimetschek Jul 15 '22 at 15:40
  • Based on this answer, I realized more features in a single script: https://gist.github.com/iwill/97935594437d560bbaaa20037a7b73bb – Míng Oct 12 '22 at 16:22
774

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 it comes preinstalled on any GNU system (i.e. Linux mostly); see footnote1
  • 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[0]} with a simple $?, but I prefer safety.
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

# option --output/-o requires 1 argument
LONGOPTS=debug,force,output:,verbose
OPTIONS=dfo:v

# -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, brew install util-linux 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)

Robert Siemer
  • 32,405
  • 11
  • 84
  • 94
  • 8
    Thanks for this. Just confirmed from the feature table at https://en.wikipedia.org/wiki/Getopts, if you need support for long options, and you're not on Solaris, `getopt` is the way to go. – johncip Jan 12 '17 at 02:00
  • 8
    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](https://gist.github.com/74e9875d5ab0a2bc1010447f1bee5d0a) on top of your solution.** – Kaushal Modi Apr 27 '17 at 14:02
  • 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. – bobpaul Mar 20 '18 at 16:22
  • 3
    @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)? – Robert Siemer Mar 21 '18 at 09:16
  • 1
    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. – Benjamin W. Jul 23 '18 at 15:15
  • 8
    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... – jjj Apr 10 '19 at 13:12
  • 5
    @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. ;-) – Robert Siemer Apr 11 '19 at 02:24
  • 1
    @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. – Robert Siemer May 15 '19 at 11:49
  • Is there link to doc on `getopt` command? All I see after googling `getopt` is about the c funciton not the terminal command.. – Jay Somedon Jul 14 '19 at 16:34
  • 1
    @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. – Robert Siemer Aug 19 '19 at 23:35
  • 2
    What does the leading exclaimation mark mean? Could anyone add some reference? – Sang Nov 28 '19 at 12:32
  • 3
    @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` – Robert Siemer Nov 29 '19 at 02:04
  • @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”. – Robert Siemer Oct 11 '20 at 10:11
  • @jjj brew install gnu-getopt ... and dont forget the small print about your path. – YvesLeBorg Jul 28 '21 at 11:35
  • @RobertSiemer I just broke your 666 upvotes record, with my upvote :-D Very well explained. Thanks. – vaeVictis Nov 10 '21 at 23:06
  • @taiyodayo How do you test for the return value? What does `getopt --version` say? – Robert Siemer May 10 '22 at 13:58
  • "handles spaces": It doesn't handle several arguments separated by spaces, right? I've been using your script but my use case is something like `--foo1 bar1 bar2 --foo2 bar3 bar4 bar5`. In this example `foo1` and `foo2` are options that can have arbitrarily many arguments separated by spaces. Would it be easy to support that? – Seub Sep 07 '22 at 23:03
  • @Seub “handles spaces in arguments” means enhanced getopt does not choke on `--foo1 "bar1 bar2"`, which can feed `bar1 bar2` as one option argument in the parsing loop. You can also write `--foo1 bar1 --foo1 bar2` to feed arguments twice to `--foo1`’s parsing. – Otherwise the line you wrote equals `bar2 bar4 bar5 --foo1 bar1 --foo2 bar3`. Your wish of interpreting it different is uncommon, incompatible with OPs wish to mix arguments and options and not supported by enhanced getopt (nor any other solution I see here out of the box). – Robert Siemer Sep 15 '22 at 15:32
  • May I use parts of your `myscript` under MIT license @RobertSiemer ? – Boffin Nov 14 '22 at 18:05
  • @Boffin yes, you may – Robert Siemer Nov 16 '22 at 08:50
  • @taiyodayo - `brew install util-linux` and then adding `/opt/homebrew/opt/util-linux/bin`--or better yet, `$(brew --prefix util-linux)/bin`--to PATH yields a properly-working (`getopt --test` yields `4` return value) `getopt` for me on macOS v12.6.3. – Johnny Utahh May 02 '23 at 18:25
  • bobpaul@github's [update](https://gist.github.com/kaushalmodi/74e9875d5ab0a2bc1010447f1bee5d0a?permalink_comment_id=2386807#gistcomment-2386807) to @Kaushal Modi's above getopt-example script: https://gist.github.com/bobpaul/ecd74cdf7681516703f20726431eaceb – Johnny Utahh May 03 '23 at 17:02
385

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
Inanc Gumus
  • 25,195
  • 9
  • 85
  • 101
  • 4
    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: https://gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58 – hfossli Apr 07 '18 at 20:58
  • 30
    Wow! Simple and clean! This is how I'm using this: https://gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58 – hfossli Apr 07 '18 at 21:10
  • 5
    This is much nicer to paste into each script rather than dealing with source or having people wonder where your functionality actually starts. – RealHandy Jan 31 '19 at 20:05
  • 5
    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: – yair Sep 15 '19 at 22:33
  • 1
    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. – EM0 Jan 12 '20 at 07:26
  • 3
    Great answer, tnx! I shortened it a bit - `while (( "$#" )); do` instead of `while [[ "$#" -gt 0 ]]; do` – CIsForCookies Jan 07 '21 at 16:53
  • 2
    @CIsForCookies Thx! That's because, afaik, `((...))` syntax is only exists in ksh, bash, and zsh, but not in plain sh. – Inanc Gumus Jan 07 '21 at 17:36
  • 2
    **Wrong!** Don't work if invoked as `./deploy.sh -ut dev` !! See [Robert Siemers's answer](https://stackoverflow.com/a/29754866/1765658) – F. Hauri - Give Up GitHub Nov 08 '21 at 12:56
  • 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. – pmarreck Feb 23 '22 at 02:48
  • Yes, `target` makes another shift—but `uglify` makes a single one. – Inanc Gumus Feb 23 '22 at 07:25
  • @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. – Inanc Gumus Feb 23 '22 at 07:27
  • @InancGumus This doesn't follow [POSIX recommendations](https://en.wikipedia.org/wiki/Getopt)! `deploy -ut dev` will answer `Unknown parameter passed: -ut`, It's wrong! ... and [tag:shell] is not [tag:go]! – F. Hauri - Give Up GitHub Feb 23 '22 at 07:47
  • 1
    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). – Inanc Gumus Feb 23 '22 at 11:51
  • 2
    @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! – cdgraham Mar 25 '22 at 16:02
  • @hfossli why did you change `-gt` to `>`? – Steven Lu Jan 27 '23 at 00:46
  • No idea. I can't remember – hfossli Jan 28 '23 at 11:56
157

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.

Rob Bednark
  • 25,981
  • 23
  • 80
  • 125
guneysus
  • 6,203
  • 2
  • 45
  • 47
  • 4
    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 – Tobias Kienzler Nov 12 '13 at 12:48
  • 4
    This can’t handle `-vfd` style combined short options. – Robert Siemer Mar 19 '16 at 15:23
  • 1
    If you want to generically evaluate `--option` and `-option` without repeating `OPTION=$i` every time, use `-*=*)` as match pattern and `eval ${i##*-}`. – user8162 Apr 11 '21 at 09:40
118
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
Inanc Gumus
  • 25,195
  • 9
  • 85
  • 101
bronson
  • 5,612
  • 3
  • 31
  • 18
  • 4
    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;;`. – bronson Oct 08 '15 at 20:41
  • 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) – Guilherme Garnier Apr 13 '18 at 16:10
  • 3
    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. – lauksas Apr 27 '19 at 23:22
113

getopt()/getopts() is a good option. Copied from here:

The simple use of "getopt" is shown in this mini-script:

#!/bin/bash
echo "Before getopt"
for i
do
  echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
  echo "-->$i"
done

What we have said is that any of -a, -b, -c or -d will be allowed, but that -c is followed by an argument (the "c:" says that).

If we call this "g" and try it out:

bash-2.05a$ ./g -abc foo
Before getopt
-abc
foo
After getopt
-->-a
-->-b
-->-c
-->foo
-->--

We start with two arguments, and "getopt" breaks apart the options and puts each in its own argument. It also added "--".

Rob Bednark
  • 25,981
  • 23
  • 80
  • 125
Matt J
  • 43,589
  • 7
  • 49
  • 57
  • 5
    Using `$*` is broken usage of `getopt`. (It hoses arguments with spaces.) See [my answer](http://stackoverflow.com/a/29754866/825924) for proper usage. – Robert Siemer Apr 16 '16 at 14:37
  • Why would you want to make it more complicated? – SDsolar Aug 10 '17 at 14:07
  • @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? – thebunnyrules Jun 01 '18 at 01:57
53

I have found the matter to write portable parsing in scripts so frustrating that I have written Argbash - a FOSS code generator that can generate the arguments-parsing code for your script plus it has some nice features:

https://argbash.dev

bubla
  • 963
  • 9
  • 10
  • 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. – RichVel Aug 18 '16 at 05:34
  • 2
    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](http://argbash.readthedocs.io/en/latest/example.html#another-example) to demonstrate that and I have made it more explicit in the documentation, too. – bubla Aug 23 '16 at 20:40
  • 1
    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). – RichVel Aug 24 '16 at 05:47
  • 3
    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 https://argbash.io/generate – bubla Dec 02 '16 at 20:12
44

I used the earlier answers as a starting point to tidy up my old adhoc param parsing. I then refactored out the following template code. It handles both long and short params, using = or space separated arguments, as well as multiple short params grouped together. Finally it re-inserts any non-param arguments back into the $1,$2.. variables.

#!/usr/bin/env bash

# NOTICE: Uncomment if your script depends on bashisms.
#if [ -z "$BASH_VERSION" ]; then bash $0 $@ ; exit $? ; fi

echo "Before"
for i ; do echo - $i ; done


# Code template for parsing command line parameters using only portable shell
# code, while handling both long and short params, handling '-f file' and
# '-f=file' style param data and also capturing non-parameters to be inserted
# back into the shell positional parameters.

while [ -n "$1" ]; do
        # Copy so we can modify it (can't modify $1)
        OPT="$1"
        # Detect argument termination
        if [ x"$OPT" = x"--" ]; then
                shift
                for OPT ; do
                        REMAINS="$REMAINS \"$OPT\""
                done
                break
        fi
        # Parse current opt
        while [ x"$OPT" != x"-" ] ; do
                case "$OPT" in
                        # Handle --flag=value opts like this
                        -c=* | --config=* )
                                CONFIGFILE="${OPT#*=}"
                                shift
                                ;;
                        # and --flag value opts like this
                        -c* | --config )
                                CONFIGFILE="$2"
                                shift
                                ;;
                        -f* | --force )
                                FORCE=true
                                ;;
                        -r* | --retry )
                                RETRY=true
                                ;;
                        # Anything unknown is recorded for later
                        * )
                                REMAINS="$REMAINS \"$OPT\""
                                break
                                ;;
                esac
                # Check for multiple short options
                # NOTICE: be sure to update this pattern to match valid options
                NEXTOPT="${OPT#-[cfr]}" # try removing single short opt
                if [ x"$OPT" != x"$NEXTOPT" ] ; then
                        OPT="-$NEXTOPT"  # multiple short opts, keep going
                else
                        break  # long form, exit inner loop
                fi
        done
        # Done with that param. move to next
        shift
done
# Set the non-parameters back into the positional parameters ($1 $2 ..)
eval set -- $REMAINS


echo -e "After: \n configfile='$CONFIGFILE' \n force='$FORCE' \n retry='$RETRY' \n remains='$REMAINS'"
for i ; do echo - $i ; done
Rob Bednark
  • 25,981
  • 23
  • 80
  • 125
Shane Day
  • 565
  • 4
  • 3
  • 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 Dec 06 '15 at 13:47
  • 2
    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. – sfnd Jun 06 '16 at 19:28
34
# As long as there is at least one more argument, keep looping
while [[ $# -gt 0 ]]; do
    key="$1"
    case "$key" in
        # This is a flag type option. Will catch either -f or --foo
        -f|--foo)
        FOO=1
        ;;
        # Also a flag type option. Will catch either -b or --bar
        -b|--bar)
        BAR=1
        ;;
        # This is an arg value type option. Will catch -o value or --output-file value
        -o|--output-file)
        shift # past the key and to the value
        OUTPUTFILE="$1"
        ;;
        # This is an arg=value type option. Will catch -o=value or --output-file=value
        -o=*|--output-file=*)
        # No need to shift here since the value is part of the same string
        OUTPUTFILE="${key#*=}"
        ;;
        *)
        # Do whatever you want with extra options
        echo "Unknown option '$key'"
        ;;
    esac
    # Shift after checking all the cases to get the next option
    shift
done

This allows you to have both space separated options/values, as well as equal defined values.

So you could run your script using:

./myscript --foo -b -o /fizz/file.txt

as well as:

./myscript -f --bar -o=/fizz/file.txt

and both should have the same end result.

PROS:

  • Allows for both -arg=value and -arg value

  • Works with any arg name that you can use in bash

    • Meaning -a or -arg or --arg or -a-r-g or whatever
  • Pure bash. No need to learn/use getopt or getopts

CONS:

  • Can't combine args

    • Meaning no -abc. You must do -a -b -c
Inanc Gumus
  • 25,195
  • 9
  • 85
  • 101
Ponyboy47
  • 924
  • 9
  • 15
  • 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 – KasRoudra May 21 '22 at 04:05
  • 1
    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 – Ponyboy47 May 25 '22 at 15:25
32

This example shows how to use getopt and eval and HEREDOC and shift to handle short and long parameters with and without a required value that follows. Also the switch/case statement is concise and easy to follow.

#!/usr/bin/env bash

# usage function
function usage()
{
   cat << HEREDOC

   Usage: $progname [--num NUM] [--time TIME_STR] [--verbose] [--dry-run]

   optional arguments:
     -h, --help           show this help message and exit
     -n, --num NUM        pass in a number
     -t, --time TIME_STR  pass in a time string
     -v, --verbose        increase the verbosity of the bash script
     --dry-run            do a dry run, dont change any files

HEREDOC
}  

# initialize variables
progname=$(basename $0)
verbose=0
dryrun=0
num_str=
time_str=

# use getopt and store the output into $OPTS
# note the use of -o for the short options, --long for the long name options
# and a : for any option that takes a parameter
OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; usage; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  # uncomment the next line to see how shift is working
  # echo "\$1:\"$1\" \$2:\"$2\""
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

if (( $verbose > 0 )); then

   # print out all the parameters we read in
   cat <<EOM
   num=$num_str
   time=$time_str
   verbose=$verbose
   dryrun=$dryrun
EOM
fi

# The rest of your script below

The most significant lines of the script above are these:

OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

Short, to the point, readable, and handles just about everything (IMHO).

Hope that helps someone.

phyatt
  • 18,472
  • 5
  • 61
  • 80
32

ASAP: Another Shell Argument Parser

Edit note: version 2.0, now with pure POSIX shell code and gluten free!

TL;DR

This parser uses only POSIX compliant shell code to process options in these formats: -o [ARG], -abo [ARG], --opt [ARG] or --opt=[ARG], where ARG is an optional argument. It can handle intermixed options and arguments, and also "--" to force any argument after it to be treated as positional.

Here is a minimal version that works as long as the command is correct, i.e. it doesn't perform almost any checks. You can paste it at the top of your script —it won't work as a function— and substitute your option definitions.

#!/bin/sh -e

USAGE="Usage:  ${CMD:=${0##*/}} [(-v|--verbose)] [--name=TEXT] [(-o|--output) FILE] [ARGS...]"

# helper functions
exit2 () { printf >&2 "%s:  %s: '%s'\n%s\n" "$CMD" "$1" "$2" "$USAGE"; exit 2; }
check () { { [ "$1" != "$EOL" ] && [ "$1" != '--' ]; } || exit2 "missing argument" "$2"; }  # avoid infinite loop

# parse command-line options
set -- "$@" "${EOL:=$(printf '\1\3\3\7')}"  # end-of-list marker
while [ "$1" != "$EOL" ]; do
  opt="$1"; shift
  case "$opt" in

    #EDIT HERE: defined options
         --name    ) check "$1" "$opt"; opt_name="$1"; shift;;
    -o | --output  ) check "$1" "$opt"; opt_output="$1"; shift;;
    -v | --verbose ) opt_verbose='true';;
    -h | --help    ) printf "%s\n" "$USAGE"; exit 0;;

    # process special cases
    --) while [ "$1" != "$EOL" ]; do set -- "$@" "$1"; shift; done;;   # parse remaining as positional
    --[!=]*=*) set -- "${opt%%=*}" "${opt#*=}" "$@";;                  # "--opt=arg"  ->  "--opt" "arg"
    -[A-Za-z0-9] | -*[!A-Za-z0-9]*) exit2 "invalid option" "$opt";;    # anything invalid like '-*'
    -?*) other="${opt#-?}"; set -- "${opt%$other}" "-${other}" "$@";;  # "-abc"  ->  "-a" "-bc"
    *) set -- "$@" "$opt";;                                            # positional, rotate to the end
  esac
done; shift

printf "name = '%s'\noutput = '%s'\nverbose = '%s'\n\$@ = (%s)\n" \
    "$opt_name" "$opt_output" "$opt_verbose" "$*"

Sample outputs

$ ./asap-example.sh -vo path/to/camelot 'spam?' --name=Arthur 'spam!' -- +42 -17
name = 'Arthur'
output = 'path/to/camelot'
verbose = 'true'
$@ = (spam? spam! +42 -17)
$ ./asap-example.sh -name Lancelot eggs bacon
asap-example.sh:  invalid option: '-n'
Usage:  asap-example.sh [(-v|--verbose)] [--name=TEXT] [(-o|--output) FILE] [ARG...]

Description

I was inspired by the relatively simple answer by @bronson and tempted to try to improve it (without adding too much complexity).

This parser implementation uses pattern matching, parameter expansion and the shell's own positional parameters as an output-restricted queue to loop over and process arguments. Here's the result:

  • Any of the -o [ARG], -abo [ARG], --long-option [ARG] and --long-option=[ARG] styles of options are accepted;
  • Arguments may occur in any order, only positional ones are left in $@ after the loop;
  • Use -- to force remaining arguments to be treated as positional;
  • Portable, compact, quite readable, with orthogonal features;
  • Doesn't depend on getopt(s) or external utilities;
  • Detects invalid options and missing arguments.

Portability

This code was tested and verified to work with a reasonably recent version of: bash, dash, mksh, ksh93, yash, zsh and BusyBox's ash (all called with their standard executable paths, not as /bin/sh).

If you find a bug or that it doesn't work with a particular POSIX compatible shell, please leave a comment.


PS: I know... An argument with the binary value of 0x01030307 could break the logic. However, if anyone is passing around binary arguments in a command-line, this issue should be their last concern.

leogama
  • 898
  • 9
  • 13
  • [In POSIX sh, ^ in place of ! in glob bracket expressions is undefined.](https://github.com/koalaman/shellcheck/wiki/SC3026). – Liso Apr 03 '22 at 09:15
  • 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). – leogama Apr 04 '22 at 12:14
  • 1
    @leogama your `$EOL` is not a binary sequence. It is just a printable string with length 12 (containing four backslashes). Did you mean to use `printf` instead of `echo`? I don't know if this impacts POSIX compliance though. – Pedro A Jul 21 '22 at 02:27
  • 1
    @PedroA Great! From GNU echo info page: "POSIX does not require support for any options, and says that the behavior of ‘echo’ is implementation-defined if any STRING contains a backslash or if the first argument is ‘-n’. Portable programs can use the ‘printf’ command if they need to omit trailing newlines or output control characters or backslashes." At least `printf` **is** in POSIX... – leogama Jul 21 '22 at 11:31
24

Expanding on @bruno-bronosky's answer, I added a "preprocessor" to handle some common formatting:

  • Expands --longopt=val into --longopt val
  • Expands -xyz into -x -y -z
  • Supports -- to indicate the end of flags
  • Shows an error for unexpected options
  • Compact and easy-to-read options switch
#!/bin/bash

# Report usage
usage() {
  echo "Usage:"
  echo "$(basename "$0") [options] [--] [file1, ...]"
}

invalid() {
  echo "ERROR: Unrecognized argument: $1" >&2
  usage
  exit 1
}

# Pre-process options to:
# - expand -xyz into -x -y -z
# - expand --longopt=arg into --longopt arg
ARGV=()
END_OF_OPT=
while [[ $# -gt 0 ]]; do
  arg="$1"; shift
  case "${END_OF_OPT}${arg}" in
    --) ARGV+=("$arg"); END_OF_OPT=1 ;;
    --*=*)ARGV+=("${arg%%=*}" "${arg#*=}") ;;
    --*) ARGV+=("$arg") ;;
    -*) for i in $(seq 2 ${#arg}); do ARGV+=("-${arg:i-1:1}"); done ;;
    *) ARGV+=("$arg") ;;
  esac
done

# Apply pre-processed options
set -- "${ARGV[@]}"

# Parse options
END_OF_OPT=
POSITIONAL=()
while [[ $# -gt 0 ]]; do
  case "${END_OF_OPT}${1}" in
    -h|--help)      usage; exit 0 ;;
    -p|--password)  shift; PASSWORD="$1" ;;
    -u|--username)  shift; USERNAME="$1" ;;
    -n|--name)      shift; names+=("$1") ;;
    -q|--quiet)     QUIET=1 ;;
    -C|--copy)      COPY=1 ;;
    -N|--notify)    NOTIFY=1 ;;
    --stdin)        READ_STDIN=1 ;;
    --)             END_OF_OPT=1 ;;
    -*)             invalid "$1" ;;
    *)              POSITIONAL+=("$1") ;;
  esac
  shift
done

# Restore positional parameters
set -- "${POSITIONAL[@]}"
jchook
  • 6,690
  • 5
  • 38
  • 40
  • 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. – Lukas S. May 01 '21 at 01:03
21

If you are making scripts that are interchangeable with other utilities, below flexibility may be useful.

Either:

command -x=myfilename.ext --another_switch 

Or:

command -x myfilename.ext --another_switch

Here is the code:

STD_IN=0

prefix=""
key=""
value=""
for keyValue in "$@"
do
  case "${prefix}${keyValue}" in
    -i=*|--input_filename=*)  key="-i";     value="${keyValue#*=}";; 
    -ss=*|--seek_from=*)      key="-ss";    value="${keyValue#*=}";;
    -t=*|--play_seconds=*)    key="-t";     value="${keyValue#*=}";;
    -|--stdin)                key="-";      value=1;;
    *)                                      value=$keyValue;;
  esac
  case $key in
    -i) MOVIE=$(resolveMovie "${value}");  prefix=""; key="";;
    -ss) SEEK_FROM="${value}";          prefix=""; key="";;
    -t)  PLAY_SECONDS="${value}";           prefix=""; key="";;
    -)   STD_IN=${value};                   prefix=""; key="";; 
    *)   prefix="${keyValue}=";;
  esac
done
Inanc Gumus
  • 25,195
  • 9
  • 85
  • 101
unsynchronized
  • 4,828
  • 2
  • 31
  • 43
17

I think this one is simple enough to use:

#!/bin/bash
#

readopt='getopts $opts opt;rc=$?;[ "$rc$opt" = "0?" ]&&exit 1;[ $rc = 0 ]||{ shift $[OPTIND-1];false; }'

opts=vfdo:

# Enumerating options
while eval "$readopt"
do
    echo OPT:$opt ${OPTARG+OPTARG:$OPTARG}
done

# Enumerating arguments
for arg
do
    echo ARG:$arg
done

Invocation example:

./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile
OPT:v 
OPT:d 
OPT:o OPTARG:/fizz/someOtherFile
OPT:f 
ARG:./foo/bar/someFile
Alek
  • 634
  • 7
  • 7
  • 1
    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 May 20 '15 at 22:50
  • 1
    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: – m3nda May 20 '15 at 23:25
12

I give you The Function parse_params that will parse params from the command line.

  1. It is a pure Bash solution, no additional utilities.
  2. Does not pollute global scope.
  3. Effortlessly returns you simple to use variables, that you could build further logic on.
  4. Amount of dashes before params does not matter (--all equals -all equals all=all)

The script below is a copy-paste working demonstration. See show_use function to understand how to use parse_params.

Limitations:

  1. Does not support space delimited params (-d 1)
  2. Param names will lose dashes so --any-param and -anyparam are equivalent
  3. eval $(parse_params "$@") must be used inside bash function (it will not work in the global scope)

#!/bin/bash

# Universal Bash parameter parsing
# Parse equal sign separated params into named local variables
# Standalone named parameter value will equal its param name (--force creates variable $force=="force")
# Parses multi-valued named params into an array (--path=path1 --path=path2 creates ${path[*]} array)
# Puts un-named params as-is into ${ARGV[*]} array
# Additionally puts all named params as-is into ${ARGN[*]} array
# Additionally puts all standalone "option" params as-is into ${ARGO[*]} array
# @author Oleksii Chekulaiev
# @version v1.4.1 (Jul-27-2018)
parse_params ()
{
    local existing_named
    local ARGV=() # un-named params
    local ARGN=() # named params
    local ARGO=() # options (--params)
    echo "local ARGV=(); local ARGN=(); local ARGO=();"
    while [[ "$1" != "" ]]; do
        # Escape asterisk to prevent bash asterisk expansion, and quotes to prevent string breakage
        _escaped=${1/\*/\'\"*\"\'}
        _escaped=${_escaped//\'/\\\'}
        _escaped=${_escaped//\"/\\\"}
        # If equals delimited named parameter
        nonspace="[^[:space:]]"
        if [[ "$1" =~ ^${nonspace}${nonspace}*=..* ]]; then
            # Add to named parameters array
            echo "ARGN+=('$_escaped');"
            # key is part before first =
            local _key=$(echo "$1" | cut -d = -f 1)
            # Just add as non-named when key is empty or contains space
            if [[ "$_key" == "" || "$_key" =~ " " ]]; then
                echo "ARGV+=('$_escaped');"
                shift
                continue
            fi
            # val is everything after key and = (protect from param==value error)
            local _val="${1/$_key=}"
            # remove dashes from key name
            _key=${_key//\-}
            # skip when key is empty
            # search for existing parameter name
            if (echo "$existing_named" | grep "\b$_key\b" >/dev/null); then
                # if name already exists then it's a multi-value named parameter
                # re-declare it as an array if needed
                if ! (declare -p _key 2> /dev/null | grep -q 'declare \-a'); then
                    echo "$_key=(\"\$$_key\");"
                fi
                # append new value
                echo "$_key+=('$_val');"
            else
                # single-value named parameter
                echo "local $_key='$_val';"
                existing_named=" $_key"
            fi
        # If standalone named parameter
        elif [[ "$1" =~ ^\-${nonspace}+ ]]; then
            # remove dashes
            local _key=${1//\-}
            # Just add as non-named when key is empty or contains space
            if [[ "$_key" == "" || "$_key" =~ " " ]]; then
                echo "ARGV+=('$_escaped');"
                shift
                continue
            fi
            # Add to options array
            echo "ARGO+=('$_escaped');"
            echo "local $_key=\"$_key\";"
        # non-named parameter
        else
            # Escape asterisk to prevent bash asterisk expansion
            _escaped=${1/\*/\'\"*\"\'}
            echo "ARGV+=('$_escaped');"
        fi
        shift
    done
}

#--------------------------- DEMO OF THE USAGE -------------------------------

show_use ()
{
    eval $(parse_params "$@")
    # --
    echo "${ARGV[0]}" # print first unnamed param
    echo "${ARGV[1]}" # print second unnamed param
    echo "${ARGN[0]}" # print first named param
    echo "${ARG0[0]}" # print first option param (--force)
    echo "$anyparam"  # print --anyparam value
    echo "$k"         # print k=5 value
    echo "${multivalue[0]}" # print first value of multi-value
    echo "${multivalue[1]}" # print second value of multi-value
    [[ "$force" == "force" ]] && echo "\$force is set so let the force be with you"
}

show_use "param 1" --anyparam="my value" param2 k=5 --force --multi-value=test1 --multi-value=test2
12

Yet another option parser (generator)

An elegant option parser for shell scripts (full support for all POSIX shells) https://github.com/ko1nksm/getoptions (Update: v3.3.0 released on 2021-05-02)

getoptions is a new option parser (generator) written in POSIX-compliant shell script and released in august 2020. It is for those who want to support the POSIX / GNU style option syntax in your shell scripts.

The supported syntaxes are -a, +a, -abc, -vvv, -p VALUE, -pVALUE, --flag, --no-flag, --with-flag, --without-flag, --param VALUE, --param=VALUE, --option[=VALUE], --no-option --.

It supports subcommands, validation, abbreviated options, and automatic help generation. And works with all POSIX shells (dash 0.5.4+, bash 2.03+, ksh88+, mksh R28+, zsh 3.1.9+, yash 2.29+, busybox ash 1.1.3+, etc).

#!/bin/sh

VERSION="0.1"

parser_definition() {
  setup   REST help:usage -- "Usage: example.sh [options]... [arguments]..." ''
  msg -- 'Options:'
  flag    FLAG    -f --flag                 -- "takes no arguments"
  param   PARAM   -p --param                -- "takes one argument"
  option  OPTION  -o --option on:"default"  -- "takes one optional argument"
  disp    :usage  -h --help
  disp    VERSION    --version
}

eval "$(getoptions parser_definition) exit 1"

echo "FLAG: $FLAG, PARAM: $PARAM, OPTION: $OPTION"
printf '%s\n' "$@" # rest arguments

It's parses the following arguments:

example.sh -f --flag -p VALUE --param VALUE -o --option -oVALUE --option=VALUE 1 2 3

And automatic help generation.

$ example.sh --help

Usage: example.sh [options]... [arguments]...

Options:
  -f, --flag                  takes no arguments
  -p, --param PARAM           takes one argument
  -o, --option[=OPTION]       takes one optional argument
  -h, --help
      --version

It is also an option parser generator, generates the following simple option parsing code. If you use the generated code, you won't need getoptions. Achieve true portability and zero dependency.

FLAG=''
PARAM=''
OPTION=''
REST=''
getoptions_parse() {
  OPTIND=$(($#+1))
  while OPTARG= && [ $# -gt 0 ]; do
    case $1 in
      --?*=*) OPTARG=$1; shift
        eval 'set -- "${OPTARG%%\=*}" "${OPTARG#*\=}"' ${1+'"$@"'}
        ;;
      --no-*|--without-*) unset OPTARG ;;
      -[po]?*) OPTARG=$1; shift
        eval 'set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}"' ${1+'"$@"'}
        ;;
      -[fh]?*) OPTARG=$1; shift
        eval 'set -- "${OPTARG%"${OPTARG#??}"}" -"${OPTARG#??}"' ${1+'"$@"'}
        OPTARG= ;;
    esac
    case $1 in
      '-f'|'--flag')
        [ "${OPTARG:-}" ] && OPTARG=${OPTARG#*\=} && set "noarg" "$1" && break
        eval '[ ${OPTARG+x} ] &&:' && OPTARG='1' || OPTARG=''
        FLAG="$OPTARG"
        ;;
      '-p'|'--param')
        [ $# -le 1 ] && set "required" "$1" && break
        OPTARG=$2
        PARAM="$OPTARG"
        shift ;;
      '-o'|'--option')
        set -- "$1" "$@"
        [ ${OPTARG+x} ] && {
          case $1 in --no-*|--without-*) set "noarg" "${1%%\=*}"; break; esac
          [ "${OPTARG:-}" ] && { shift; OPTARG=$2; } || OPTARG='default'
        } || OPTARG=''
        OPTION="$OPTARG"
        shift ;;
      '-h'|'--help')
        usage
        exit 0 ;;
      '--version')
        echo "${VERSION}"
        exit 0 ;;
      --)
        shift
        while [ $# -gt 0 ]; do
          REST="${REST} \"\${$(($OPTIND-$#))}\""
          shift
        done
        break ;;
      [-]?*) set "unknown" "$1"; break ;;
      *)
        REST="${REST} \"\${$(($OPTIND-$#))}\""
    esac
    shift
  done
  [ $# -eq 0 ] && { OPTIND=1; unset OPTARG; return 0; }
  case $1 in
    unknown) set "Unrecognized option: $2" "$@" ;;
    noarg) set "Does not allow an argument: $2" "$@" ;;
    required) set "Requires an argument: $2" "$@" ;;
    pattern:*) set "Does not match the pattern (${1#*:}): $2" "$@" ;;
    notcmd) set "Not a command: $2" "$@" ;;
    *) set "Validation error ($1): $2" "$@"
  esac
  echo "$1" >&2
  exit 1
}
usage() {
cat<<'GETOPTIONSHERE'
Usage: example.sh [options]... [arguments]...

Options:
  -f, --flag                  takes no arguments
  -p, --param PARAM           takes one argument
  -o, --option[=OPTION]       takes one optional argument
  -h, --help
      --version
GETOPTIONSHERE
}
Koichi Nakashima
  • 799
  • 8
  • 10
11

getopts works great if #1 you have it installed and #2 you intend to run it on the same platform. OSX and Linux (for example) behave differently in this respect.

Here is a (non getopts) solution that supports equals, non-equals, and boolean flags. For example you could run your script in this way:

./script --arg1=value1 --arg2 value2 --shouldClean

# parse the arguments.
COUNTER=0
ARGS=("$@")
while [ $COUNTER -lt $# ]
do
    arg=${ARGS[$COUNTER]}
    let COUNTER=COUNTER+1
    nextArg=${ARGS[$COUNTER]}

    if [[ $skipNext -eq 1 ]]; then
        echo "Skipping"
        skipNext=0
        continue
    fi

    argKey=""
    argVal=""
    if [[ "$arg" =~ ^\- ]]; then
        # if the format is: -key=value
        if [[ "$arg" =~ \= ]]; then
            argVal=$(echo "$arg" | cut -d'=' -f2)
            argKey=$(echo "$arg" | cut -d'=' -f1)
            skipNext=0

        # if the format is: -key value
        elif [[ ! "$nextArg" =~ ^\- ]]; then
            argKey="$arg"
            argVal="$nextArg"
            skipNext=1

        # if the format is: -key (a boolean flag)
        elif [[ "$nextArg" =~ ^\- ]] || [[ -z "$nextArg" ]]; then
            argKey="$arg"
            argVal=""
            skipNext=0
        fi
    # if the format has not flag, just a value.
    else
        argKey=""
        argVal="$arg"
        skipNext=0
    fi

    case "$argKey" in 
        --source-scmurl)
            SOURCE_URL="$argVal"
        ;;
        --dest-scmurl)
            DEST_URL="$argVal"
        ;;
        --version-num)
            VERSION_NUM="$argVal"
        ;;
        -c|--clean)
            CLEAN_BEFORE_START="1"
        ;;
        -h|--help|-help|--h)
            showUsage
            exit
        ;;
    esac
done
Hive
  • 193
  • 1
  • 4
  • 17
vangorra
  • 1,631
  • 19
  • 24
9

I wanna submit my project : https://github.com/flyingangel/argparser

source argparser.sh
parse_args "$@"

Simple as that. The environment will be populated with variables with the same name as the arguments

Thanh Trung
  • 3,566
  • 3
  • 31
  • 42
7

This is how I do in a function to avoid breaking getopts run at the same time somewhere higher in stack:

function waitForWeb () {
   local OPTIND=1 OPTARG OPTION
   local host=localhost port=8080 proto=http
   while getopts "h:p:r:" OPTION; do
      case "$OPTION" in
      h)
         host="$OPTARG"
         ;;
      p)
         port="$OPTARG"
         ;;
      r)
         proto="$OPTARG"
         ;;
      esac
   done
...
}
akostadinov
  • 17,364
  • 6
  • 77
  • 85
7

I'd like to offer my version of option parsing, that allows for the following:

-s p1
--stage p1
-w somefolder
--workfolder somefolder
-sw p1 somefolder
-e=hello

Also allows for this (could be unwanted):

-s--workfolder p1 somefolder
-se=hello p1
-swe=hello p1 somefolder

You have to decide before use if = is to be used on an option or not. This is to keep the code clean(ish).

while [[ $# > 0 ]]
do
    key="$1"
    while [[ ${key+x} ]]
    do
        case $key in
            -s*|--stage)
                STAGE="$2"
                shift # option has parameter
                ;;
            -w*|--workfolder)
                workfolder="$2"
                shift # option has parameter
                ;;
            -e=*)
                EXAMPLE="${key#*=}"
                break # option has been fully handled
                ;;
            *)
                # unknown option
                echo Unknown option: $key #1>&2
                exit 10 # either this: my preferred way to handle unknown options
                break # or this: do this to signal the option has been handled (if exit isn't used)
                ;;
        esac
        # prepare for next option in this key, if any
        [[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}"
    done
    shift # option(s) fully processed, proceed to next input argument
done
galmok
  • 869
  • 10
  • 21
7

There are several ways to parse cmdline args (e.g. GNU getopt (not portable) vs BSD (MacOS) getopt vs getopts) - all problematic. This solution

  • is portable!
  • has zero dependencies, only relies on bash built-ins
  • allows for both short and long options
  • handles whitespace or simultaneously the use of = separator between option and argument
  • supports concatenated short option style -vxf
  • handles option with optional arguments (E.g. --color vs --color=always),
  • correctly detects and reports unknown options
  • supports -- to signal end of options, and
  • doesn't require code bloat compared with alternatives for the same feature set. I.e. succinct, and therefore easier to maintain

Examples: Any of

# flag
-f
--foo

# option with required argument
-b"Hello World"
-b "Hello World"
--bar "Hello World"
--bar="Hello World"

# option with optional argument
--baz
--baz="Optional Hello"

#!/usr/bin/env bash

usage() {
  cat - >&2 <<EOF
NAME
    program-name.sh - Brief description
 
SYNOPSIS
    program-name.sh [-h|--help]
    program-name.sh [-f|--foo]
                    [-b|--bar <arg>]
                    [--baz[=<arg>]]
                    [--]
                    FILE ...

REQUIRED ARGUMENTS
  FILE ...
          input files

OPTIONS
  -h, --help
          Prints this and exits

  -f, --foo
          A flag option
      
  -b, --bar <arg>
          Option requiring an argument <arg>

  --baz[=<arg>]
          Option that has an optional argument <arg>. If <arg>
          is not specified, defaults to 'DEFAULT'
  --     
          Specify end of options; useful if the first non option
          argument starts with a hyphen

EOF
}

fatal() {
    for i; do
        echo -e "${i}" >&2
    done
    exit 1
}

# For long option processing
next_arg() {
    if [[ $OPTARG == *=* ]]; then
        # for cases like '--opt=arg'
        OPTARG="${OPTARG#*=}"
    else
        # for cases like '--opt arg'
        OPTARG="${args[$OPTIND]}"
        OPTIND=$((OPTIND + 1))
    fi
}

# ':' means preceding option character expects one argument, except
# first ':' which make getopts run in silent mode. We handle errors with
# wildcard case catch. Long options are considered as the '-' character
optspec=":hfb:-:"
args=("" "$@")  # dummy first element so $1 and $args[1] are aligned
while getopts "$optspec" optchar; do
    case "$optchar" in
        h) usage; exit 0 ;;
        f) foo=1 ;;
        b) bar="$OPTARG" ;;
        -) # long option processing
            case "$OPTARG" in
                help)
                    usage; exit 0 ;;
                foo)
                    foo=1 ;;
                bar|bar=*) next_arg
                    bar="$OPTARG" ;;
                baz)
                    baz=DEFAULT ;;
                baz=*) next_arg
                    baz="$OPTARG" ;;
                -) break ;;
                *) fatal "Unknown option '--${OPTARG}'" "see '${0} --help' for usage" ;;
            esac
            ;;
        *) fatal "Unknown option: '-${OPTARG}'" "See '${0} --help' for usage" ;;
    esac
done

shift $((OPTIND-1))

if [ "$#" -eq 0 ]; then
    fatal "Expected at least one required argument FILE" \
    "See '${0} --help' for usage"
fi

echo "foo=$foo, bar=$bar, baz=$baz, files=${@}"
tmoschou
  • 977
  • 1
  • 8
  • 11
  • It's something. I much prefer this over the classic getopts garbage, but the way it parses is not "natural". `./program-name.sh NOTES.md -f` produced `foo=, bar=, baz=, files=NOTES.md -f` and it thought `-f` was part of the file name. It did not set `foo=1`. It doesn't do what I want it to do. – Kalec Jun 16 '23 at 15:01
  • @Kalec that would be because `-f` comes after a non-option argument `NOTES.md` and by design, option processing stops after first non-option argument like many unix commands, since you could have different subcommands with different sub options. `-f` in this case is the stored in `$2` with `NOTES.md` in `$1`. You could repeat option parsing if you so wish, until `$# -eq 0` – tmoschou Jul 06 '23 at 03:59
7

Based on other answers here, this my version:

#!/bin/bash
set -e

function parse() {
    for arg in "$@"; do # transform long options to short ones
        shift
        case "$arg" in
            "--name") set -- "$@" "-n" ;;
            "--verbose") set -- "$@" "-v" ;;
            *) set -- "$@" "$arg"
        esac
    done

    while getopts "n:v" optname  # left to ":" are flags that expect a value, right to the ":" are flags that expect nothing
    do
        case "$optname" in
            "n") name=${OPTARG} ;;
            "v") verbose=true ;;
        esac
    done
    shift "$((OPTIND-1))" # shift out all the already processed options
}


parse "$@"
echo "hello $name"
if [ ! -z $verbose ]; then echo 'nice to meet you!'; fi

Usage:

$ ./parse.sh
hello
$ ./parse.sh -n YOUR_NAME
hello YOUR_NAME
$ ./parse.sh -n YOUR_NAME -v
hello YOUR_NAME
nice to meet you!
$ ./parse.sh -v -n YOUR_NAME
hello YOUR_NAME
nice to meet you!
$ ./parse.sh -v
hello 
nice to meet you!
CIsForCookies
  • 12,097
  • 11
  • 59
  • 124
  • Could you please explain why you think this version is better than others? With no explanation it’s hard to understand why we should use this version rather than another. – bfontaine Oct 25 '22 at 17:08
5

Solution that preserves unhandled arguments. Demos Included.

Here is my solution. It is VERY flexible and unlike others, shouldn't require external packages and handles leftover arguments cleanly.

Usage is: ./myscript -flag flagvariable -otherflag flagvar2

All you have to do is edit the validflags line. It prepends a hyphen and searches all arguments. It then defines the next argument as the flag name e.g.

./myscript -flag flagvariable -otherflag flagvar2
echo $flag $otherflag
flagvariable flagvar2

The main code (short version, verbose with examples further down, also a version with erroring out):

#!/usr/bin/env bash
#shebang.io
validflags="rate time number"
count=1
for arg in $@
do
    match=0
    argval=$1
    for flag in $validflags
    do
        sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers

The verbose version with built in echo demos:

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
echo "all args
$@"
validflags="rate time number"
count=1
for arg in $@
do
    match=0
    argval=$1
#   argval=$(echo $@ | cut -d ' ' -f$count)
    for flag in $validflags
    do
            sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done

#Cleanup then restore the leftovers
echo "pre final clear args:
$@"
shift $#
echo "post final clear args:
$@"
set -- $leftovers
echo "all post set args:
$@"
echo arg1: $1 arg2: $2

echo leftovers: $leftovers
echo rate $rate time $time number $number

Final one, this one errors out if an invalid -argument is passed through.

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
validflags="rate time number"
count=1
for arg in $@
do
    argval=$1
    match=0
        if [ "${argval:0:1}" == "-" ]
    then
        for flag in $validflags
        do
                sflag="-"$flag
            if [ "$argval" == "$sflag" ]
            then
                declare $flag=$2
                match=1
            fi
        done
        if [ "$match" == "0" ]
        then
            echo "Bad argument: $argval"
            exit 1
        fi
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers
echo rate $rate time $time number $number
echo leftovers: $leftovers

Pros: What it does, it handles very well. It preserves unused arguments which a lot of the other solutions here don't. It also allows for variables to be called without being defined by hand in the script. It also allows prepopulation of variables if no corresponding argument is given. (See verbose example).

Cons: Can't parse a single complex arg string e.g. -xcvf would process as a single argument. You could somewhat easily write additional code into mine that adds this functionality though.

4

Here is my approach - using regexp.

  • no getopts
  • it handles block of short parameters -qwerty
  • it handles short parameters -q -w -e
  • it handles long options --qwerty
  • you can pass attribute to short or long option (if you are using block of short options, attribute is attached to the last option)
  • you can use spaces or = to provide attributes, but attribute matches until encountering hyphen+space "delimiter", so in --q=qwe ty qwe ty is one attribute
  • it handles mix of all above so -o a -op attr ibute --option=att ribu te --op-tion attribute --option att-ribute is valid

script:

#!/usr/bin/env sh

help_menu() {
  echo "Usage:

  ${0##*/} [-h][-l FILENAME][-d]

Options:

  -h, --help
    display this help and exit

  -l, --logfile=FILENAME
    filename

  -d, --debug
    enable debug
  "
}

parse_options() {
  case $opt in
    h|help)
      help_menu
      exit
     ;;
    l|logfile)
      logfile=${attr}
      ;;
    d|debug)
      debug=true
      ;;
    *)
      echo "Unknown option: ${opt}\nRun ${0##*/} -h for help.">&2
      exit 1
  esac
}
options=$@

until [ "$options" = "" ]; do
  if [[ $options =~ (^ *(--([a-zA-Z0-9-]+)|-([a-zA-Z0-9-]+))(( |=)(([\_\.\?\/\\a-zA-Z0-9]?[ -]?[\_\.\?a-zA-Z0-9]+)+))?(.*)|(.+)) ]]; then
    if [[ ${BASH_REMATCH[3]} ]]; then # for --option[=][attribute] or --option[=][attribute]
      opt=${BASH_REMATCH[3]}
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    elif [[ ${BASH_REMATCH[4]} ]]; then # for block options -qwert[=][attribute] or single short option -a[=][attribute]
      pile=${BASH_REMATCH[4]}
      while (( ${#pile} > 1 )); do
        opt=${pile:0:1}
        attr=""
        pile=${pile/${pile:0:1}/}
        parse_options
      done
      opt=$pile
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    else # leftovers that don't match
      opt=${BASH_REMATCH[10]}
      options=""
    fi
    parse_options
  fi
done
a_z
  • 133
  • 1
  • 6
3

Mixing positional and flag-based arguments

--param=arg (equals delimited)

Freely mixing flags between positional arguments:

./script.sh dumbo 127.0.0.1 --environment=production -q -d
./script.sh dumbo --environment=production 127.0.0.1 --quiet -d

can be accomplished with a fairly concise approach:

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   param=${!pointer}
   if [[ $param != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      case $param in
         # paramter-flags with arguments
         -e=*|--environment=*) environment="${param#*=}";;
                  --another=*) another="${param#*=}";;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + 1)):$#} \
         || set -- ${@:((pointer + 1)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2

--param arg (space delimited)

It's usualy clearer to not mix --flag=value and --flag value styles.

./script.sh dumbo 127.0.0.1 --environment production -q -d

This is a little dicey to read, but is still valid

./script.sh dumbo --environment production 127.0.0.1 --quiet -d

Source

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   if [[ ${!pointer} != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      param=${!pointer}
      ((pointer_plus = pointer + 1))
      slice_len=1

      case $param in
         # paramter-flags with arguments
         -e|--environment) environment=${!pointer_plus}; ((slice_len++));;
                --another) another=${!pointer_plus}; ((slice_len++));;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + $slice_len)):$#} \
         || set -- ${@:((pointer + $slice_len)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2
Mark Fox
  • 8,694
  • 9
  • 53
  • 75
3

Note that getopt(1) was a short living mistake from AT&T.

getopt was created in 1984 but already buried in 1986 because it was not really usable.

A proof for the fact that getopt is very outdated is that the getopt(1) man page still mentions "$*" instead of "$@", that was added to the Bourne Shell in 1986 together with the getopts(1) shell builtin in order to deal with arguments with spaces inside.

BTW: if you are interested in parsing long options in shell scripts, it may be of interest to know that the getopt(3) implementation from libc (Solaris) and ksh93 both added a uniform long option implementation that supports long options as aliases for short options. This causes ksh93 and the Bourne Shell to implement a uniform interface for long options via getopts.

An example for long options taken from the Bourne Shell man page:

getopts "f:(file)(input-file)o:(output-file)" OPTX "$@"

shows how long option aliases may be used in both Bourne Shell and ksh93.

See the man page of a recent Bourne Shell:

http://schillix.sourceforge.net/man/man1/bosh.1.html

and the man page for getopt(3) from OpenSolaris:

http://schillix.sourceforge.net/man/man3c/getopt.3c.html

and last, the getopt(1) man page to verify the outdated $*:

http://schillix.sourceforge.net/man/man1/getopt.1.html

schily
  • 307
  • 1
  • 5
3

I have write a bash helper to write a nice bash tool

project home: https://gitlab.mbedsys.org/mbedsys/bashopts

example:

#!/bin/bash -ei

# load the library
. bashopts.sh

# Enable backtrace dusplay on error
trap 'bashopts_exit_handle' ERR

# Initialize the library
bashopts_setup -n "$0" -d "This is myapp tool description displayed on help message" -s "$HOME/.config/myapprc"

# Declare the options
bashopts_declare -n first_name -l first -o f -d "First name" -t string -i -s -r
bashopts_declare -n last_name -l last -o l -d "Last name" -t string -i -s -r
bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\$first_name \$last_name"
bashopts_declare -n age -l number -d "Age" -t number
bashopts_declare -n email_list -t string -m add -l email -d "Email adress"

# Parse arguments
bashopts_parse_args "$@"

# Process argument
bashopts_process_args

will give help:

NAME:
    ./example.sh - This is myapp tool description displayed on help message

USAGE:
    [options and commands] [-- [extra args]]

OPTIONS:
    -h,--help                          Display this help
    -n,--non-interactive true          Non interactive mode - [$bashopts_non_interactive] (type:boolean, default:false)
    -f,--first "John"                  First name - [$first_name] (type:string, default:"")
    -l,--last "Smith"                  Last name - [$last_name] (type:string, default:"")
    --display-name "John Smith"        Display name - [$display_name] (type:string, default:"$first_name $last_name")
    --number 0                         Age - [$age] (type:number, default:0)
    --email                            Email adress - [$email_list] (type:string, default:"")

enjoy :)

Emeric Verschuur
  • 553
  • 1
  • 4
  • 9
  • 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 Jun 24 '17 at 18:07
  • You need Bash version 4 to use this. On Mac, the default version is 3. You can use home brew to install bash 4. – Josh Wulf Jun 24 '17 at 18:17
3

Assume we create a shell script named test_args.sh as follow

#!/bin/sh
until [ $# -eq 0 ]
do
  name=${1:1}; shift;
  if [[ -z "$1" || $1 == -* ]] ; then eval "export $name=true"; else eval "export $name=$1"; shift; fi  
done
echo "year=$year month=$month day=$day flag=$flag"

After we run the following command:

sh test_args.sh  -year 2017 -flag  -month 12 -day 22 

The output would be:

year=2017 month=12 day=22 flag=true
John
  • 413
  • 1
  • 5
  • 13
  • 6
    This takes the same approach as [Noah's answer](https://stackoverflow.com/a/39198204/5216668), 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. – Will Barnwell Oct 10 '17 at 23:57
3

Here is a getopts that achieves the parsing with minimal code and allows you to define what you wish to extract in one case using eval with substring.

Basically eval "local key='val'"

function myrsync() {

        local backup=("${@}") args=(); while [[ $# -gt 0 ]]; do k="$1";
                case "$k" in
                    ---sourceuser|---sourceurl|---targetuser|---targeturl|---file|---exclude|---include)
                        eval "local ${k:3}='${2}'"; shift; shift    # Past two arguments
                    ;;
                    *)  # Unknown option  
                        args+=("$1"); shift;                        # Past argument only
                    ;;                                              
                esac                                                
        done; set -- "${backup[@]}"                                 # Restore $@


        echo "${sourceurl}"
}

Declares the variables as locals instead of globals as most answers here.

Called as:

myrsync ---sourceurl http://abc.def.g ---sourceuser myuser ... 

The ${k:3} is basically a substring to remove the first --- from the key.

mjs
  • 21,431
  • 31
  • 118
  • 200
3

I wanted to share what I made for parsing options. Some of my needs were not fulfilled by the answers here so I had to come up with this: https://github.com/MihirLuthra/bash_option_parser

This supports:

  • Suboption parsing
  • Alias names for options
  • Optional args
  • Variable args
  • Printing usage and errors

Let's say we have a command named fruit with usage as follows:

fruit <fruit-name> ...
   [-e|—-eat|—-chew]
   [-c|--cut <how> <why>]
   <command> [<args>] 

-e takes no args
-c takes two args i.e. how to cut and why to cut
fruit itself takes at least one argument.
<command> is for suboptions like apple, orange etc. (similar to git which has suboptions commit, push etc. )

So to parse it:

parse_options \
    'fruit'                         '1 ...'  \
    '-e'     , '--eat' , '--chew'   '0'      \
    '-c'     , '--cut'              '1 1'    \
    'apple'                         'S'      \
    'orange'                        'S'      \
    ';' \
    "$@"

Now if there was any usage error, it can be printed using option_parser_error_msg as follows:

retval=$?

if [ $retval -ne 0 ]; then
    # this will manage error messages if
    # insufficient or extra args are supplied

    option_parser_error_msg "$retval"

    # This will print the usage
    print_usage 'fruit'
    exit 1
fi

To check now if some options was passed,

if [ -n "${OPTIONS[-c]}" ]
then
    echo "-c was passed"

    # args can be accessed in a 2D-array-like format
    echo "Arg1 to -c = ${ARGS[-c,0]}"
    echo "Arg2 to -c = ${ARGS[-c,1]}"

fi

Suboption parsing can also be done by passing $shift_count to parse_options_detailed which makes it start parsing after shifting args to reach args of suboption. It is demonstrated in this example.

A detailed description is provided in the readme and examples in the repository.

Mihir Luthra
  • 6,059
  • 3
  • 14
  • 39
2

Use module "arguments" from bash-modules

Example:

#!/bin/bash
. import.sh log arguments

NAME="world"

parse_arguments "-n|--name)NAME;S" -- "$@" || {
  error "Cannot parse command line."
  exit 1
}

info "Hello, $NAME!"
2

I wrote down a script that can assist with parsing command-line arguments easily - https://github.com/unfor19/bargs

Examples

$ bash example.sh -n Willy --gender male -a 99
Name:      Willy
Age:       99
Gender:    male
Location:  chocolate-factory
$ bash example.sh -n Meir --gender male
[ERROR] Required argument: age

Usage: bash example.sh -n Willy --gender male -a 99

--person_name  |  -n  [Willy]              What is your name?
--age          |  -a  [Required]
--gender       |  -g  [Required]
--location     |  -l  [chocolate-factory]  insert your location
$ bash example.sh -h

Usage: bash example.sh -n Willy --gender male -a 99
--person_name  |  -n  [Willy]              What is your name?
--age          |  -a  [Required]
--gender       |  -g  [Required]
--location     |  -l  [chocolate-factory]  insert your location
Meir Gabay
  • 2,870
  • 1
  • 24
  • 34
2

I ended up implementing the dash (or /bin/sh) version of the accepted answer, basically, without array usage:

while [[ $# -gt 0 ]]; do
    case "$1" in
    -v|--verbose) verbose=1; shift;;
    -o|--output) if [[ $# -gt 1 && "$2" != -* ]]; then
            file=$2; shift 2
        else
            echo "-o requires file-path" 1>&2; exit 1
        fi ;;
    --)
        while [[ $# -gt 0 ]]; do BACKUP="$BACKUP;$1"; shift; done
        break;;
    *)
        BACKUP="$BACKUP;$1"
        shift
        ;;
    esac
done
# Restore unused arguments.
while [ -n "$BACKUP" ] ; do
    [ ! -z "${BACKUP%%;*}" ] && set -- "$@" "${BACKUP%%;*}"
    [ "$BACKUP" = "${BACKUP/;/}" ] && break
    BACKUP="${BACKUP#*;}"
done
Top-Master
  • 7,611
  • 5
  • 39
  • 71
1

This also might be useful to know: you can set a value and if someone provides input, override the default with that value.

myscript.sh -f ./serverlist.txt or just ./myscript.sh (and it takes defaults)

    #!/bin/bash
    # --- set the value, if there is inputs, override the defaults.

    HOME_FOLDER="${HOME}/owned_id_checker"
    SERVER_FILE_LIST="${HOME_FOLDER}/server_list.txt"

    while [[ $# > 1 ]]
    do
    key="$1"
    shift
    
    case $key in
        -i|--inputlist)
        SERVER_FILE_LIST="$1"
        shift
        ;;
    esac
    done

    
    echo "SERVER LIST   = ${SERVER_FILE_LIST}"
Michael
  • 8,362
  • 6
  • 61
  • 88
Mike Q
  • 6,716
  • 5
  • 55
  • 62
1

Here is my improved solution of Bruno Bronosky's answer using variable arrays.

it lets you mix parameters position and give you a parameter array preserving the order without the options

#!/bin/bash

echo $@

PARAMS=()
SOFT=0
SKIP=()
for i in "$@"
do
case $i in
    -n=*|--skip=*)
    SKIP+=("${i#*=}")
    ;;
    -s|--soft)
    SOFT=1
    ;;
    *)
        # unknown option
        PARAMS+=("$i")
    ;;
esac
done
echo "SKIP            = ${SKIP[@]}"
echo "SOFT            = $SOFT"
    echo "Parameters:"
    echo ${PARAMS[@]}

Will output for example:

$ ./test.sh parameter -s somefile --skip=.c --skip=.obj
parameter -s somefile --skip=.c --skip=.obj
SKIP            = .c .obj
SOFT            = 1
Parameters:
parameter somefile
Masadow
  • 762
  • 5
  • 15
  • 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 `#*=` – Jason S Dec 03 '17 at 01:01
  • You're right, in fact, since I build a PARAMS variable, I don't need to use shift at all – Masadow Dec 05 '17 at 09:17
1

Another solution without getopt[s], POSIX, old Unix style

Similar to the solution Bruno Bronosky posted this here is one without the usage of getopt(s).

Main differentiating feature of my solution is that it allows to have options concatenated together just like tar -xzf foo.tar.gz is equal to tar -x -z -f foo.tar.gz. And just like in tar, ps etc. the leading hyphen is optional for a block of short options (but this can be changed easily). Long options are supported as well (but when a block starts with one then two leading hyphens are required).

Code with example options

#!/bin/sh

echo
echo "POSIX-compliant getopt(s)-free old-style-supporting option parser from phk@[se.unix]"
echo

print_usage() {
  echo "Usage:

  $0 {a|b|c} [ARG...]

Options:

  --aaa-0-args
  -a
    Option without arguments.

  --bbb-1-args ARG
  -b ARG
    Option with one argument.

  --ccc-2-args ARG1 ARG2
  -c ARG1 ARG2
    Option with two arguments.

" >&2
}

if [ $# -le 0 ]; then
  print_usage
  exit 1
fi

opt=
while :; do

  if [ $# -le 0 ]; then

    # no parameters remaining -> end option parsing
    break

  elif [ ! "$opt" ]; then

    # we are at the beginning of a fresh block
    # remove optional leading hyphen and strip trailing whitespaces
    opt=$(echo "$1" | sed 's/^-\?\([a-zA-Z0-9\?-]*\)/\1/')

  fi

  # get the first character -> check whether long option
  first_chr=$(echo "$opt" | awk '{print substr($1, 1, 1)}')
  [ "$first_chr" = - ] && long_option=T || long_option=F

  # note to write the options here with a leading hyphen less
  # also do not forget to end short options with a star
  case $opt in

    -)

      # end of options
      shift
      break
      ;;

    a*|-aaa-0-args)

      echo "Option AAA activated!"
      ;;

    b*|-bbb-1-args)

      if [ "$2" ]; then
        echo "Option BBB with argument '$2' activated!"
        shift
      else
        echo "BBB parameters incomplete!" >&2
        print_usage
        exit 1
      fi
      ;;

    c*|-ccc-2-args)

      if [ "$2" ] && [ "$3" ]; then
        echo "Option CCC with arguments '$2' and '$3' activated!"
        shift 2
      else
        echo "CCC parameters incomplete!" >&2
        print_usage
        exit 1
      fi
      ;;

    h*|\?*|-help)

      print_usage
      exit 0
      ;;

    *)

      if [ "$long_option" = T ]; then
        opt=$(echo "$opt" | awk '{print substr($1, 2)}')
      else
        opt=$first_chr
      fi
      printf 'Error: Unknown option: "%s"\n' "$opt" >&2
      print_usage
      exit 1
      ;;

  esac

  if [ "$long_option" = T ]; then

    # if we had a long option then we are going to get a new block next
    shift
    opt=

  else

    # if we had a short option then just move to the next character
    opt=$(echo "$opt" | awk '{print substr($1, 2)}')

    # if block is now empty then shift to the next one
    [ "$opt" ] || shift

  fi

done

echo "Doing something..."

exit 0

For the example usage please see the examples further below.

Position of options with arguments

For what its worth there the options with arguments don't be the last (only long options need to be). So while e.g. in tar (at least in some implementations) the f options needs to be last because the file name follows (tar xzf bar.tar.gz works but tar xfz bar.tar.gz does not) this is not the case here (see the later examples).

Multiple options with arguments

As another bonus the option parameters are consumed in the order of the options by the parameters with required options. Just look at the output of my script here with the command line abc X Y Z (or -abc X Y Z):

Option AAA activated!
Option BBB with argument 'X' activated!
Option CCC with arguments 'Y' and 'Z' activated!

Long options concatenated as well

Also you can also have long options in option block given that they occur last in the block. So the following command lines are all equivalent (including the order in which the options and its arguments are being processed):

  • -cba Z Y X
  • cba Z Y X
  • -cb-aaa-0-args Z Y X
  • -c-bbb-1-args Z Y X -a
  • --ccc-2-args Z Y -ba X
  • c Z Y b X a
  • -c Z Y -b X -a
  • --ccc-2-args Z Y --bbb-1-args X --aaa-0-args

All of these lead to:

Option CCC with arguments 'Z' and 'Y' activated!
Option BBB with argument 'X' activated!
Option AAA activated!
Doing something...

Not in this solution

Optional arguments

Options with optional arguments should be possible with a bit of work, e.g. by looking forward whether there is a block without a hyphen; the user would then need to put a hyphen in front of every block following a block with a parameter having an optional parameter. Maybe this is too complicated to communicate to the user so better just require a leading hyphen altogether in this case.

Things get even more complicated with multiple possible parameters. I would advise against making the options trying to be smart by determining whether the an argument might be for it or not (e.g. with an option just takes a number as an optional argument) because this might break in the future.

I personally favor additional options instead of optional arguments.

Option arguments introduced with an equal sign

Just like with optional arguments I am not a fan of this (BTW, is there a thread for discussing the pros/cons of different parameter styles?) but if you want this you could probably implement it yourself just like done at http://mywiki.wooledge.org/BashFAQ/035#Manual_loop with a --long-with-arg=?* case statement and then stripping the equal sign (this is BTW the site that says that making parameter concatenation is possible with some effort but "left [it] as an exercise for the reader" which made me take them at their word but I started from scratch).

Other notes

POSIX-compliant, works even on ancient Busybox setups I had to deal with (with e.g. cut, head and getopts missing).

Community
  • 1
  • 1
phk
  • 2,002
  • 1
  • 29
  • 54
1

The top answer to this question seemed a bit buggy when I tried it -- here's my solution which I've found to be more robust:

boolean_arg=""
arg_with_value=""

while [[ $# -gt 0 ]]
do
key="$1"
case $key in
    -b|--boolean-arg)
    boolean_arg=true
    shift
    ;;
    -a|--arg-with-value)
    arg_with_value="$2"
    shift
    shift
    ;;
    -*)
    echo "Unknown option: $1"
    exit 1
    ;;
    *)
    arg_num=$(( $arg_num + 1 ))
    case $arg_num in
        1)
        first_normal_arg="$1"
        shift
        ;;
        2)
        second_normal_arg="$1"
        shift
        ;;
        *)
        bad_args=TRUE
    esac
    ;;
esac
done

# Handy to have this here when adding arguments to
# see if they're working. Just edit the '0' to be '1'.
if [[ 0 == 1 ]]; then
    echo "first_normal_arg: $first_normal_arg"
    echo "second_normal_arg: $second_normal_arg"
    echo "boolean_arg: $boolean_arg"
    echo "arg_with_value: $arg_with_value"
    exit 0
fi

if [[ $bad_args == TRUE || $arg_num < 2 ]]; then
    echo "Usage: $(basename "$0") <first-normal-arg> <second-normal-arg> [--boolean-arg] [--arg-with-value VALUE]"
    exit 1
fi
Daniel Bigham
  • 187
  • 3
  • 8
1

Simple and easy to modify, parameters can be in any order. this can be modified to take parameters in any form (-a, --a, a, etc).

for arg in "$@"
do
   key=$(echo $arg | cut -f1 -d=)`
   value=$(echo $arg | cut -f2 -d=)`
   case "$key" in
        name|-name)      read_name=$value;;
        id|-id)          read_id=$value;;
        *)               echo "I dont know what to do with this"
   ease
done
terijo001
  • 48
  • 6
1

I use it to iterate over key => value from the end. A first optional argument is caught after the loop.

Usage is ./script.sh optional-first-arg -key value -key2 value2

#!/bin/sh

a=$(($#-1))
b=$(($#))
while [ $a -gt 0 ]; do
    eval 'key="$'$a'"; value="$'$b'"'
    echo "$key => $value"
    b=$(($b-2))
    a=$(($a-2))
done
unset a b key value

[ $(($#%2)) -ne 0 ] && echo "first_arg = $1"

Sure you can do it from the left to the right with a few changes.

This snippet code shows the key => value pairs and the first argument if it exists.

#!/bin/sh

a=$((1+$#%2))
b=$((1+$a))

[ $(($#%2)) -ne 0 ] && echo "first_arg = $1"

while [ $a -lt $# ]; do
    eval 'key="$'$a'"; value="$'$b'"'
    echo "$key => $value"
    b=$(($b+2))
    a=$(($a+2))
done

unset a b key value

Tested with 100,000 arguments, fast.

You can also iterate key => value and first optional arg from the left to the right without eval :

#!/bin/sh

a=$(($#%2))
b=0

[ $a -eq 1 ] && echo "first_arg = $1"

for value; do
    if [ $b -gt $a -a $(($b%2)) -ne $a ]; then
        echo "$key => $value"
    fi
    key="$value"
    b=$((1+$b))
done

unset a b key value

1

I'm using a combination of optget and optgets to parse short and long options with or without arguments and even non-options (those without - or --):

# catch wrong options and move non-options to the end of the string
args=$(getopt -l "$opt_long" "$opt_short" "$@" 2> >(sed -e 's/^/stderr/g')) || echo -n "Error: " && echo "$args" | grep -oP "(?<=^stderr).*" && exit 1
mapfile -t args < <(xargs -n1 <<< "$(echo "$args" | sed -E "s/(--[^ ]+) /\1=/g")" )
set -- "${args[@]}"

# parse short and long options
while getopts "$opt_short-:" opt; do
  ...
done

# remove all parsed options from $@
shift $((OPTIND-1)

By that I'm able to access all options with a variable like $opt_verbose and the non-options are accessible through the default variables $1, $2, etc.:

echo "help:$opt_help"
echo "file:$opt_file"
echo "verbose:$opt_verbose"
echo "long_only:$opt_long_only"
echo "short_only:$opt_s"
echo "path:$1"
echo "mail:$2"

One of the main features is, that I'm able to pass all options and non-options in a complete random order:

#             $opt_file     $1        $2          $opt_... $opt_... $opt_...
# /demo.sh --file=file.txt /dir info@example.com -V -h --long_only=yes -s
help:1
file:file.txt
verbose:1
long_only:yes
short_only:1
path:/dir
mail:info@example.com

More details: https://stackoverflow.com/a/74275254/318765

mgutt
  • 5,867
  • 2
  • 50
  • 77
0

I have browsed through all of the answers to this question. And although some contain wealth of information, I was specifically looking for the answers that allow us to describe the supported command line arguments declaratively and get the help text generated automatically from the spec.

And I found 5 such answers (sorry, if I missed yours):

  1. argbash
  2. getoptions
  3. bashopts
  4. bash_option_parser
  5. bargs

I am also guilty of writing my own command line parser, which seems to be different from all the rest, because I use JSON to describe the supported command line arguments and thus my implementation depends on jq.

Check it out here - https://github.com/MarkKharitonov/bash-parse-command-line-args. The repo contains an example script and the README shows off various invocation scenarios.

mark
  • 59,016
  • 79
  • 296
  • 580