555

I want to call myscript file in this way:

$ ./myscript -s 45 -p any_string

or

$ ./myscript -h  #should display help
$ ./myscript     #should display help

My requirements are:

  • getopt here to get the input arguments
  • check that -s exists, if not return an error
  • check that the value after the -s is 45 or 90
  • check that the -p exists and there is an input string after
  • if the user enters ./myscript -h or just ./myscript then display help

I tried so far this code:

#!/bin/bash
while getopts "h:s:" arg; do
  case $arg in
    h)
      echo "usage" 
      ;;
    s)
      strength=$OPTARG
      echo $strength
      ;;
  esac
done

But with that code I get errors. How to do it with Bash and getopt?

Mohammad Kholghi
  • 533
  • 2
  • 7
  • 21
MOHAMED
  • 41,599
  • 58
  • 163
  • 268
  • 7
    Options are supposed to be optional. If you require the value specified by `-s`, make it a positional argument: `./myscript 45 anystring`. – chepner May 10 '13 at 13:19
  • 1
    @chepner `$./myscript -s 45 -p any_string` – MOHAMED May 10 '13 at 13:21
  • 2
    It's fine if `-p` is actually an option (that is, your program can proceed if it's not present). In this case, `./myscript 45 -p any_string`. (I think that `getopt` can handle mixed options and positional arguments, whereas the `bash` built-in command `getopts` requires all positional arguments to be placed after options.) – chepner May 10 '13 at 13:42

8 Answers8

734
#!/bin/bash

usage() { echo "Usage: $0 [-s <45|90>] [-p <string>]" 1>&2; exit 1; }

while getopts ":s:p:" o; do
    case "${o}" in
        s)
            s=${OPTARG}
            ((s == 45 || s == 90)) || usage
            ;;
        p)
            p=${OPTARG}
            ;;
        *)
            usage
            ;;
    esac
done
shift $((OPTIND-1))

if [ -z "${s}" ] || [ -z "${p}" ]; then
    usage
fi

echo "s = ${s}"
echo "p = ${p}"

Example runs:

$ ./myscript.sh
Usage: ./myscript.sh [-s <45|90>] [-p <string>]

$ ./myscript.sh -h
Usage: ./myscript.sh [-s <45|90>] [-p <string>]

$ ./myscript.sh -s "" -p ""
Usage: ./myscript.sh [-s <45|90>] [-p <string>]

$ ./myscript.sh -s 10 -p foo
Usage: ./myscript.sh [-s <45|90>] [-p <string>]

$ ./myscript.sh -s 45 -p foo
s = 45
p = foo

$ ./myscript.sh -s 90 -p bar
s = 90
p = bar
Adrian Frühwirth
  • 42,970
  • 10
  • 60
  • 71
  • 56
    In the getopts call, why is there a leading colon? When does "h" have a colon after it? – e40 Aug 14 '13 at 14:43
  • @e40 Good catch, thanks! Probably a copy-paste mistake on the `h`. The leading colon is explained in the man page for `getopts`. Try `$ ./myscript.sh -s 45 -p` with and without it and the difference will be obvious. Whether you want to use this feature is up to you and how you want to handle the whole thing. – Adrian Frühwirth Aug 14 '13 at 15:02
  • 14
    Should `usage()` really return 1? – Pithikos Sep 10 '14 at 08:21
  • 9
    @Pithikos Good point. Common sense tells me that when invoked via `-h` it should return `0`, upon hitting a non-existing flag it should return `>0` (for the sake of simplicity I didn't differentiate between those cases and nobody forces you to print the usage text in the latter case). I have seen programs which always return `!= 0`, though, even on `-h/--help`. Maybe I should update the snippet in case people use this as boilerplate (I hope not)? – Adrian Frühwirth Sep 10 '14 at 09:04
  • Seems to work but the problem with getopts is that optional arguments aren't really optional, with your code if someone doesn't put a value for -s and it is the first option then in the option handler OPTARG will get -p for its value. `./myscript -sh -p -s s = p = -s` <-- note you must comment out the check for null to see the output –  Mar 12 '15 at 23:26
  • 2
    @A.Danischewski This is by (`getopts`') design, there is no such thing as "optional arguments" with `getopts`. The parser simply cannot know if the next token is an argument to the current option or an option by itself since `-p` might be the intended value. You *can* hack around this if you absolutely know that an option parameter cannot look like another valid option, yes, but one could say there's a reason optional arguments are not defined in POSIX. – Adrian Frühwirth Mar 13 '15 at 09:40
  • `getopts` is poorly designed in my opinion the documentation says it should have optional arguments with the double colon syntax :: yet Bash does not support it. It should not be necessary to have to make an elaborate workaround to provide optional arguments, a sensible default behavior in my opinion would be to consider leading dashes by default to indicate that it is an option and not an argument to an option. Then if you want to take in leading dashes as an option argument you can jump through hoops to express that to getopts. –  Mar 13 '15 at 10:31
  • 2
    @A.Danischewski You are mixing `getopt` and `getopts`. `getopt` does optional arguments with the double colon syntax and is an external utility, however, it is highly unportable and not recommended to rely on it (see Brian's answer and stArdustͲ's comment). `getopts` is (usually) a shell builtin and [it's behaviour is defined by POSIX](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/getopts.html). It's portable, but doesn't do fancy GNU stuff. – Adrian Frühwirth Mar 13 '15 at 11:07
  • Why do you use curly braces around variable names inside strings? I thought those were for separating variables from normal text like `"${foo}bar"` but that isn't the case with any of your uses. Is my understanding incorrect? – user1011471 Aug 11 '15 at 18:13
  • 9
    @user1011471 You are correct! Curly braces, so to speak, just aid the `bash` lexer in identifying variables. They are in many cases unneeded and the fact that I always use them is just a matter of personal coding style. To me, it's easier (and prettier) to just always use them instead of remembering the parsing rules with regards to ambiguity. Pretty much the same why one would write `if (foo) { bar; }` instead of `if (foo) bar;` in a C-style language (aesthetics and/or avoiding silly mistakes). – Adrian Frühwirth Aug 12 '15 at 09:17
  • @AdrianFrühwirth- How can I check one or both conditions. Any one from -s or -p OR both -s & -p ? – S R May 20 '16 at 03:05
  • Is this possible with numerical argument names (e.g. `-2`)? – noɥʇʎԀʎzɐɹƆ Aug 11 '16 at 15:32
  • @uoɥʇʎPʎzɐɹC Why not try it? Hint: It is indeed supported. `getopts` follows the [POSIX Utility Syntax Guidelines](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02) which states that *"Each option name should be a single alphanumeric character (the alnum character classification) from the portable character set.*". This includes single-digit options. – Adrian Frühwirth Aug 12 '16 at 11:17
  • how to parse long option? e.g.: --include=file.c – Lion Lai Mar 24 '17 at 10:28
  • 2
    "there is no such thing as 'optional arguments' with getopts" <-- Sadly, exactly why I usually prefer to just use while/case… (sorry for the huge one-liner): `usage() { printf "Usage: %s [<-s|--sopt> <45|90>] [<-p|--popt> ]\n" "$0"; return 1; }; main() { req="${1:?$(usage)}"; shift; s=""; p=""; while [ "$#" -ge 1 ]; do case "$1" in -s|--sopt) shift; s="${1:?$(usage)}"; [ "$s" -eq 45 ] || [ "$s" -eq 90 ] || { usage; return 1; } ;; -p|--popt) shift; p="${1:?$(usage)}" ;; *) usage; return 1 ;; esac; shift; done; printf "req = %s\ns = %s\np = %s\n" "$req" "$s" "$p"; }; main "$@"` – Mark G. Apr 04 '17 at 03:23
  • 1
    @MarkG. Maybe you'd like to make this an answer instead of a comment? It's certainly a valid one. – I0_ol Apr 16 '17 at 00:15
  • 1
    @LionLai There's no way to parse long options in a POSIX compilant way. On GNU systems you can use GNU's `getopt` (which comes with the util-linux bundle). But of course it's not portable. – n.caillou Nov 22 '17 at 22:35
  • 1
    @MarkG. This is not POSIX compliant; it will fail if there's no space between the options and their argument. – n.caillou Nov 22 '17 at 22:52
  • 1
    @I0_ol The question specifically asks for an example of "How to use getopts," and my comment completely shirks that request. :P (I have a personal dislike of answers that answer "How do I do X?" with "Don't, do Y instead," because it pollutes searches for people who are *legitimately* wanting to do X. So, I figured I'd try to avoid contributing to that phenomenon…) @n.caillou True. I didn't try to make it compliant because the one-liner was already getting pretty long. It would be trivial to make it comply though, with e.g. `case "$1" in -s*) s=${1#-s}; if [ -z "$s" ]; shift; s=$1; fi` etc. – Mark G. Aug 23 '18 at 15:09
  • @MarkG. sometimes I legitimately appreciating the answers that tell me a better way to do the thing I'm trying to do wrong! Don't worry, the upvotes and downvotes will sort you into the right place. – Buttle Butkus Mar 05 '20 at 03:03
  • for safest & (my preferred) canonical usage, I might recommend the following pattern: `local OPTARGS OPTIND` (if used within a function), and the loop: `while getopts ":abc:de:f" opt; do ... done; shift "$((OPTIND-1)); OPTIND=1;` then handle cases for `\?) invalid option` and `:) missing arg`, and set `OPTIND` because shell does not reset it automatically, if using `getopts` repeatedly (see also: https://www.gnu.org/software/bash/manual/bash.html#getopts ) – michael Dec 22 '20 at 10:57
  • Minus one for naming variable 'o'. Caused confusion when I had an -o flag. Use what is the standard variable name 'arg'. Please edit answer. – Matthaeus Gaius Caesar Apr 24 '21 at 22:33
  • 5
    Sadly there's no explaination at all in this answer, yet it gets upvoted. Would be nice if more information was added. – user Apr 29 '21 at 12:19
  • @Pithikos In this case, as script does not have a `-h` or `--help` option then `*` means unknown option and exit with `1` would be correct. If it had a `-h` option, it *should* exit with `0` if that is given, else `1` on unknown. Personally I typically do `-h) usage >&2; exit 0;;` and `*) usage >&2; exit 1;;` optionally with an extra error message if unknown option or bad / missing argument. That said, the `usage` after the loop is somewhat ambiguous as the `usage` does not demand any options at all :P – user3342816 Aug 29 '21 at 03:14
  • You have to **not use square bracket** in `usage()` if arguments are mendatory! See [How to denote that a command line argument is optional when printing usage](https://stackoverflow.com/a/36787811/1765658) – F. Hauri - Give Up GitHub Feb 12 '22 at 08:20
240

The problem with the original code is that:

  • h: expects parameter where it shouldn't, so change it into just h (without colon)
  • to expect -p any_string, you need to add p: to the argument list

Basically : after the option means it requires the argument.


The basic syntax of getopts is (see: man bash):

getopts OPTSTRING VARNAME [ARGS...]

where:

  • OPTSTRING is string with list of expected arguments,

    • h - check for option -h without parameters; gives error on unsupported options;

    • h: - check for option -h with parameter; gives errors on unsupported options;

    • abc - check for options -a, -b, -c; gives errors on unsupported options;

    • :abc - check for options -a, -b, -c; silences errors on unsupported options;

      Notes: In other words, colon in front of options allows you handle the errors in your code. Variable will contain ? in the case of unsupported option, : in the case of missing value.

  • OPTARG - is set to current argument value,

  • OPTERR - indicates if Bash should display error messages.

So the code can be:

#!/usr/bin/env bash
usage() { echo "$0 usage:" && grep " .)\ #" $0; exit 0; }
[ $# -eq 0 ] && usage
while getopts ":hs:p:" arg; do
  case $arg in
    p) # Specify p value.
      echo "p is ${OPTARG}"
      ;;
    s) # Specify strength, either 45 or 90.
      strength=${OPTARG}
      [ $strength -eq 45 -o $strength -eq 90 ] \
        && echo "Strength is $strength." \
        || echo "Strength needs to be either 45 or 90, $strength found instead."
      ;;
    h | *) # Display help.
      usage
      exit 0
      ;;
  esac
done

Example usage:

$ ./foo.sh 
./foo.sh usage:
    p) # Specify p value.
    s) # Specify strength, either 45 or 90.
    h | *) # Display help.
$ ./foo.sh -s 123 -p any_string
Strength needs to be either 45 or 90, 123 found instead.
p is any_string
$ ./foo.sh -s 90 -p any_string
Strength is 90.
p is any_string

See: Small getopts tutorial at Bash Hackers Wiki

gronostaj
  • 2,231
  • 2
  • 23
  • 43
kenorb
  • 155,785
  • 88
  • 678
  • 743
  • 4
    Change the usage function to this: `usage() { echo "$0 usage:" && grep "[[:space:]].)\ #" $0 | sed 's/#//' | sed -r 's/([a-z])\)/-\1/'; exit 0; }`. It only accounts for a single whitespace character before the letter option, removes the # from the comment and prepends a '-' before the letter option making it clearer for the command. – poagester Oct 05 '16 at 19:55
  • 4
    @kenorb: Colon in front of options doesn't ignore unsupported options but silences errors from bash and allows you handle it in your code. Variable will contain '?' in the case of unsupported option and ':' in the case of missing value. – Hynek -Pichi- Vychodil Feb 06 '17 at 07:42
  • 1
    Thanks for the detailed docs, wasn't able to get the `:` right until I saw these notes. We need to add a `:` to the options where we are expecting an argument. – Aukhan Feb 25 '19 at 07:22
  • This is a great example, and I've used it in my scripts. However, under the case for option "h", the `exit 0` isn't needed (nor is it ever reached) as usage calls `exit 0` instead. – Greenonline Oct 23 '22 at 10:03
186

Use getopt

Why getopt?

To parse elaborated command-line arguments to avoid confusion and clarify the options we are parsing so that reader of the commands can understand what's happening.

What is getopt?

getopt is used to break up (parse) options in command lines for easy parsing by shell procedures, and to check for legal options. It uses the GNU getopt(3) routines to do this.

getopt can have following types of options.

  1. No-value options
  2. key-value pair options

Note: In this document, during explaining syntax:

  • Anything inside [ ] is optional parameter in the syntax/examples.
  • <value> is a place holder, which mean it should be substituted with an actual value.

HOW TO USE getopt?

Syntax: First Form

getopt optstring parameters

Examples:

# This is correct
getopt "hv:t::" -v 123 -t123  
getopt "hv:t::" -v123 -t123  # -v and 123 doesn't have whitespace

# -h takes no value.
getopt "hv:t::" -h -v123


# This is wrong. after -t can't have whitespace.
# Only optional params cannot have whitespace between key and value
getopt "hv:t::" -v 123 -t 123

# Multiple arguments that takes value.
getopt "h:v:t::g::" -h abc -v 123 -t21

# Multiple arguments without value
# All of these are correct
getopt "hvt" -htv
getopt "hvt" -h -t -v
getopt "hvt" -tv -h

Here h,v,t are the options and -h -v -t is how options should be given in command-line.

  1. 'h' is a no-value option.
  2. 'v:' implies that option -v has value and is a mandatory option. ':' means has a value.
  3. 't::' implies that option -t has value but is optional. '::' means optional.

In optional param, value cannot have whitespace separation with the option. So, in "-t123" example, -t is option 123 is value.

Syntax: Second Form

getopt [getopt_options] [--] optstring parameters

Here after getopt is split into five parts

  • The command itself i.e. getopt
  • The getopt_options, it describes how to parse the arguments. single dash long options, double dash options.
  • --, separates out the getopt_options from the options you want to parse and the allowed short options
  • The short options, is taken immediately after -- is found. Just like the Form first syntax.
  • The parameters, these are the options that you have passed into the program. The options you want to parse and get the actual values set on them.

Examples

getopt -l "name:,version::,verbose" -- "n:v::V" --name=Karthik -version=5.2 -verbose

Syntax: Third Form

getopt [getopt_options] -o|--options optstring [getopt_options] [--] [parameters]

Here after getopt is split into five parts

  • The command itself i.e. getopt
  • The getopt_options, it describes how to parse the arguments. single dash long options, double dash options.
  • The short options i.e. -o or --options. Just like the Form first syntax but with option "-o" and before the "--" (double dash).
  • --, separates out the getopt_options from the options you want to parse and the allowed short options
  • The parameters, these are the options that you have passed into the program. The options you want to parse and get the actual values set on them.

Examples

getopt -l "name:,version::,verbose" -a -o "n:v::V" -- -name=Karthik -version=5.2 -verbose

GETOPT_OPTIONS

getopt_options changes the way command-line params are parsed.

Below are some of the getopt_options

Option: -l or --longoptions

Means getopt command should allow multi-character options to be recognised. Multiple options are separated by comma.

For example, --name=Karthik is a long option sent in command line. In getopt, usage of long options are like

getopt -l "name:,version" -- "" --name=Karthik

Since name: is specified, the option should contain a value

Option: -a or --alternative

Means getopt command should allow long option to have a single dash '-' rather than double dash '--'.

Example, instead of --name=Karthik you could use just -name=Karthik

getopt -a -l "name:,version" -- "" -name=Karthik

A complete script example with the code:

#!/bin/bash

# filename: commandLine.sh
# author: @theBuzzyCoder

showHelp() {
# `cat << EOF` This means that cat should stop reading when EOF is detected
cat << EOF  
Usage: ./installer -v <espo-version> [-hrV]
Install Pre-requisites for EspoCRM with docker in Development mode

-h, -help,          --help                  Display help

-v, -espo-version,  --espo-version          Set and Download specific version of EspoCRM

-r, -rebuild,       --rebuild               Rebuild php vendor directory using composer and compiled css using grunt

-V, -verbose,       --verbose               Run script in verbose mode. Will print out each step of execution.

EOF
# EOF is found above and hence cat command stops reading. This is equivalent to echo but much neater when printing out.
}


export version=0
export verbose=0
export rebuilt=0

# $@ is all command line parameters passed to the script.
# -o is for short options like -v
# -l is for long options with double dash like --version
# the comma separates different long options
# -a is for long options with single dash like -version
options=$(getopt -l "help,version:,verbose,rebuild,dryrun" -o "hv:Vrd" -a -- "$@")

# set --:
# If no arguments follow this option, then the positional parameters are unset. Otherwise, the positional parameters 
# are set to the arguments, even if some of them begin with a ‘-’.
eval set -- "$options"

while true
do
case "$1" in
-h|--help) 
    showHelp
    exit 0
    ;;
-v|--version) 
    shift
    export version="$1"
    ;;
-V|--verbose)
    export verbose=1
    set -xv  # Set xtrace and verbose mode.
    ;;
-r|--rebuild)
    export rebuild=1
    ;;
--)
    shift
    break;;
esac
shift
done

Running this script file:

# With short options grouped together and long option
# With double dash '--version'

bash commandLine.sh --version=1.0 -rV
# With short options grouped together and long option
# With single dash '-version'

bash commandLine.sh -version=1.0 -rV

# OR with short option that takes value, value separated by whitespace
# by key

bash commandLine.sh -v 1.0 -rV

# OR with short option that takes value, value without whitespace
# separation from key.

bash commandLine.sh -v1.0 -rV

# OR Separating individual short options

bash commandLine.sh -v1.0 -r -V
onlycparra
  • 607
  • 4
  • 22
theBuzzyCoder
  • 2,652
  • 2
  • 31
  • 26
  • 6
    source: https://www.linkedin.com/pulse/command-line-named-parameters-bash-karthik-bhat-k/?published=t – theBuzzyCoder Oct 05 '18 at 23:13
  • 12
    getopt vs getopts .. very different cross platform compliance – shadowbq Jan 29 '20 at 14:29
  • 2
    While I too find GNU getopt superior, it's not built in on BSD systems (like macOS) so if your goal is cross platform compatibility, go with getopts – Győri Sándor Jun 16 '21 at 10:17
  • How to explain `getopt "hv:t::" -tv123`? Is is equivalent to `-t -v 123` or `-t v123` – DawnSong May 22 '22 at 08:09
  • 2
    `getopt` from **macOS Monterey** is very **old** (1999) and simple. A better one is `brew install getopt`. So I don't suggest `getopt` any more. – DawnSong May 26 '22 at 02:57
  • Are there different manuals available? I'm missing an explanation for `'::' means optional`. Thank you for mentioning this! – mgutt Oct 31 '22 at 12:08
  • The original question asks to show help (and probably exit quickly) when *no* parameter/option/argument/flag is given. But the "complete script example with the code" (in its form as of _this_ writing) will not show help and will continue executing. Correct? – Abdull Feb 03 '23 at 14:11
  • @mgutt: in `man getopt` I can see *"Each short option character in shortopts may be followed by one colon to indicate it has a required argument, and by two colons to indicate it has an optional argument. [...] util-linux 2.38 2022-02-17"* – Ganton Mar 17 '23 at 00:42
  • In `man getopt` I can also see *"Long options may be abbreviated, as long as the abbreviation is not ambiguous"*, so executing `bash commandLine.sh --ver=1.0 -rV` shows `getopt: option '--ver=1.0' is ambiguous; possibilities: '--version' '--verbose'` but executing `bash commandLine.sh --vers=1.0 -rV` shows no error message (and works like if `--version=1.0` had been used). – Ganton Mar 17 '23 at 00:43
  • You should consider writing a book on getopt for O'Reilly. More seriously, the shortest answer are the best. People who get here already know what is getopt, and if they don't it's not your role to explain them in lengthy details every aspect of the command. – Eric Apr 18 '23 at 09:22
46

The example packaged with getopt (my distro put it in /usr/share/getopt/getopt-parse.bash) looks like it covers all of your cases:

#!/bin/bash

# A small example program for using the new getopt(1) program.
# This program will only work with bash(1)
# An similar program using the tcsh(1) script language can be found
# as parse.tcsh

# Example input and output (from the bash prompt):
# ./parse.bash -a par1 'another arg' --c-long 'wow!*\?' -cmore -b " very long "
# Option a
# Option c, no argument
# Option c, argument 'more'
# Option b, argument ' very long '
# Remaining arguments:
# --> 'par1'
# --> 'another arg'
# --> 'wow!*\?'

# Note that we use `"$@"' to let each command-line parameter expand to a 
# separate word. The quotes around '$@' are essential!
# We need TEMP as the `eval set --' would nuke the return value of getopt.
TEMP=$(getopt -o ab:c:: --long a-long,b-long:,c-long:: \
              -n 'example.bash' -- "$@")

if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi

# Note the quotes around '$TEMP': they are essential!
eval set -- "$TEMP"

while true ; do
    case "$1" in
        -a|--a-long) echo "Option a" ; shift ;;
        -b|--b-long) echo "Option b, argument '$2'" ; shift 2 ;;
        -c|--c-long) 
            # c has an optional argument. As we are in quoted mode,
            # an empty parameter will be generated if its optional
            # argument is not found.
            case "$2" in
                "") echo "Option c, no argument"; shift 2 ;;
                *)  echo "Option c, argument '$2'" ; shift 2 ;;
            esac ;;
        --) shift ; break ;;
        *) echo "Internal error!" ; exit 1 ;;
    esac
done
echo "Remaining arguments:"
for arg do echo '--> '"'$arg'" ; done
vaeVictis
  • 484
  • 1
  • 3
  • 13
Brian Cain
  • 14,403
  • 3
  • 50
  • 88
  • 12
    The external command getopt(1) is never safe to use, unless you *know* it is GNU getopt, you call it in a GNU-specific way, *and* you ensure that GETOPT_COMPATIBLE is not in the environment. Use getopts (shell builtin) instead, or simply loop over the positional parameters. – Gilles Quénot Aug 14 '13 at 16:52
  • 18
    Eh, no external command is safe to use by that standard. Built in getopts is missing crucial features and if you want to check for GETOPT_COMPATIBLE that's easier than porting getopt's features. – Michael Terry Sep 12 '14 at 18:26
  • `getopt` is in the `util-linux` package, so unless you're trying to support many different *nix'es at once, you'll probably be fine, but it's of course a good thing to keep in mind. – Steen Schütt Jun 04 '21 at 08:16
  • 1
    why `eval set -- "$TEMP"` and not just `set -- "$TEMP"` ? – onlycparra Dec 11 '22 at 09:19
14

POSIX 7 example

It is also worth checking the example from the standard: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/getopts.html

aflag=
bflag=
while getopts ab: name
do
    case $name in
    a)    aflag=1;;
    b)    bflag=1
          bval="$OPTARG";;
    ?)   printf "Usage: %s: [-a] [-b value] args\n" $0
          exit 2;;
    esac
done
if [ ! -z "$aflag" ]; then
    printf "Option -a specified\n"
fi
if [ ! -z "$bflag" ]; then
    printf 'Option -b "%s" specified\n' "$bval"
fi
shift $(($OPTIND - 1))
printf "Remaining arguments are: %s\n" "$*"

And then we can try it out:

$ sh a.sh
Remaining arguments are: 
$ sh a.sh -a
Option -a specified
Remaining arguments are: 
$ sh a.sh -b
No arg for -b option
Usage: a.sh: [-a] [-b value] args
$ sh a.sh -b myval
Option -b "myval" specified
Remaining arguments are: 
$ sh a.sh -a -b myval
Option -a specified
Option -b "myval" specified
Remaining arguments are: 
$ sh a.sh remain
Remaining arguments are: remain
$ sh a.sh -- -a remain
Remaining arguments are: -a remain

Tested in Ubuntu 17.10, sh is dash 0.5.8.

Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
13

I know that this is already answered, but for the record and for anyone with the same requeriments as me I decided to post this related answer. The code is flooded with comments to explain the code.

Updated answer:

Save the file as getopt.sh:

#!/bin/bash

function get_variable_name_for_option {
    local OPT_DESC=${1}
    local OPTION=${2}
    local VAR=$(echo ${OPT_DESC} | sed -e "s/.*\[\?-${OPTION} \([A-Z_]\+\).*/\1/g" -e "s/.*\[\?-\(${OPTION}\).*/\1FLAG/g")

    if [[ "${VAR}" == "${1}" ]]; then
        echo ""
    else
        echo ${VAR}
    fi
}

function parse_options {
    local OPT_DESC=${1}
    local INPUT=$(get_input_for_getopts "${OPT_DESC}")

    shift
    while getopts ${INPUT} OPTION ${@};
    do
        [ ${OPTION} == "?" ] && usage
        VARNAME=$(get_variable_name_for_option "${OPT_DESC}" "${OPTION}")
            [ "${VARNAME}" != "" ] && eval "${VARNAME}=${OPTARG:-true}" # && printf "\t%s\n" "* Declaring ${VARNAME}=${!VARNAME} -- OPTIONS='$OPTION'"
    done

    check_for_required "${OPT_DESC}"

}

function check_for_required {
    local OPT_DESC=${1}
    local REQUIRED=$(get_required "${OPT_DESC}" | sed -e "s/\://g")
    while test -n "${REQUIRED}"; do
        OPTION=${REQUIRED:0:1}
        VARNAME=$(get_variable_name_for_option "${OPT_DESC}" "${OPTION}")
                [ -z "${!VARNAME}" ] && printf "ERROR: %s\n" "Option -${OPTION} must been set." && usage
        REQUIRED=${REQUIRED:1}
    done
}

function get_input_for_getopts {
    local OPT_DESC=${1}
    echo ${OPT_DESC} | sed -e "s/\([a-zA-Z]\) [A-Z_]\+/\1:/g" -e "s/[][ -]//g"
}

function get_optional {
    local OPT_DESC=${1}
    echo ${OPT_DESC} | sed -e "s/[^[]*\(\[[^]]*\]\)[^[]*/\1/g" -e "s/\([a-zA-Z]\) [A-Z_]\+/\1:/g" -e "s/[][ -]//g"
}

function get_required {
    local OPT_DESC=${1}
    echo ${OPT_DESC} | sed -e "s/\([a-zA-Z]\) [A-Z_]\+/\1:/g" -e "s/\[[^[]*\]//g" -e "s/[][ -]//g"
}

function usage {
    printf "Usage:\n\t%s\n" "${0} ${OPT_DESC}"
    exit 10
}

Then you can use it like this:

#!/bin/bash
#
# [ and ] defines optional arguments
#

# location to getopts.sh file
source ./getopt.sh
USAGE="-u USER -d DATABASE -p PASS -s SID [ -a START_DATE_TIME ]"
parse_options "${USAGE}" ${@}

echo ${USER}
echo ${START_DATE_TIME}

Old answer:

I recently needed to use a generic approach. I came across with this solution:

#!/bin/bash
# Option Description:
# -------------------
#
# Option description is based on getopts bash builtin. The description adds a variable name feature to be used
# on future checks for required or optional values.
# The option description adds "=>VARIABLE_NAME" string. Variable name should be UPPERCASE. Valid characters
# are [A-Z_]*.
#
# A option description example:
#   OPT_DESC="a:=>A_VARIABLE|b:=>B_VARIABLE|c=>C_VARIABLE"
#
# -a option will require a value (the colon means that) and should be saved in variable A_VARIABLE.
# "|" is used to separate options description.
# -b option rule applies the same as -a.
# -c option doesn't require a value (the colon absense means that) and its existence should be set in C_VARIABLE
#
#   ~$ echo get_options ${OPT_DESC}
#   a:b:c
#   ~$
#


# Required options 
REQUIRED_DESC="a:=>REQ_A_VAR_VALUE|B:=>REQ_B_VAR_VALUE|c=>REQ_C_VAR_FLAG"

# Optional options (duh)
OPTIONAL_DESC="P:=>OPT_P_VAR_VALUE|r=>OPT_R_VAR_FLAG"

function usage {
    IFS="|"
    printf "%s" ${0}
    for i in ${REQUIRED_DESC};
    do
        VARNAME=$(echo $i | sed -e "s/.*=>//g")
    printf " %s" "-${i:0:1} $VARNAME"
    done

    for i in ${OPTIONAL_DESC};
    do
        VARNAME=$(echo $i | sed -e "s/.*=>//g")
        printf " %s" "[-${i:0:1} $VARNAME]"
    done
    printf "\n"
    unset IFS
    exit
}

# Auxiliary function that returns options characters to be passed
# into 'getopts' from a option description.
# Arguments:
#   $1: The options description (SEE TOP)
#
# Example:
#   OPT_DESC="h:=>H_VAR|f:=>F_VAR|P=>P_VAR|W=>W_VAR"
#   OPTIONS=$(get_options ${OPT_DESC})
#   echo "${OPTIONS}"
#
# Output:
#   "h:f:PW"
function get_options {
    echo ${1} | sed -e "s/\([a-zA-Z]\:\?\)=>[A-Z_]*|\?/\1/g"
}

# Auxiliary function that returns all variable names separated by '|'
# Arguments:
#       $1: The options description (SEE TOP)
#
# Example:
#       OPT_DESC="h:=>H_VAR|f:=>F_VAR|P=>P_VAR|W=>W_VAR"
#       VARNAMES=$(get_values ${OPT_DESC})
#       echo "${VARNAMES}"
#
# Output:
#       "H_VAR|F_VAR|P_VAR|W_VAR"
function get_variables {
    echo ${1} | sed -e "s/[a-zA-Z]\:\?=>\([^|]*\)/\1/g"
}

# Auxiliary function that returns the variable name based on the
# option passed by.
# Arguments:
#   $1: The options description (SEE TOP)
#   $2: The option which the variable name wants to be retrieved
#
# Example:
#   OPT_DESC="h:=>H_VAR|f:=>F_VAR|P=>P_VAR|W=>W_VAR"
#   H_VAR=$(get_variable_name ${OPT_DESC} "h")
#   echo "${H_VAR}"
#
# Output:
#   "H_VAR"
function get_variable_name {
    VAR=$(echo ${1} | sed -e "s/.*${2}\:\?=>\([^|]*\).*/\1/g")
    if [[ ${VAR} == ${1} ]]; then
        echo ""
    else
        echo ${VAR}
    fi
}

# Gets the required options from the required description
REQUIRED=$(get_options ${REQUIRED_DESC})

# Gets the optional options (duh) from the optional description
OPTIONAL=$(get_options ${OPTIONAL_DESC})

# or... $(get_options "${OPTIONAL_DESC}|${REQUIRED_DESC}")

# The colon at starts instructs getopts to remain silent
while getopts ":${REQUIRED}${OPTIONAL}" OPTION
do
    [[ ${OPTION} == ":" ]] && usage
    VAR=$(get_variable_name "${REQUIRED_DESC}|${OPTIONAL_DESC}" ${OPTION})
    [[ -n ${VAR} ]] && eval "$VAR=${OPTARG}"
done

shift $(($OPTIND - 1))

# Checks for required options. Report an error and exits if
# required options are missing.

# Using function version ...
VARS=$(get_variables ${REQUIRED_DESC})
IFS="|"
for VARNAME in $VARS;
do
    [[ -v ${VARNAME} ]] || usage
done
unset IFS

# ... or using IFS Version (no function)
OLDIFS=${IFS}
IFS="|"
for i in ${REQUIRED_DESC};
do
    VARNAME=$(echo $i | sed -e "s/.*=>//g")
    [[ -v ${VARNAME} ]] || usage
    printf "%s %s %s\n" "-${i:0:1}" "${!VARNAME:=present}" "${VARNAME}"
done
IFS=${OLDIFS}

I didn't test this roughly, so I could have some bugs in there.

Sebastian
  • 4,770
  • 4
  • 42
  • 43
  • 1
    If you're using `getopts` in a function, add `local OPTIND OPTARG` to the function – glenn jackman Mar 09 '20 at 17:30
  • @glennjackman actually it's more like a sed approach rather than using `getopts` – Sebastian Mar 10 '20 at 20:47
  • I get `./getopt.sh: line 38: : invalid variable name` which points to the second last line in the while loop of `check_for_required()` when running the example. – pat-s Nov 22 '21 at 08:30
  • @pat-s can you post a gist to your example so maybe I can help you? – Sebastian Nov 23 '21 at 10:19
  • @Sebastian Appreciated. I went with the approach of https://stackoverflow.com/a/61055114/4185785 for now, which seems to work. I assume the mistake must be on my side. Thanks for sharing your solution! – pat-s Dec 01 '21 at 07:59
  • Please [avoid useless forks](https://stackoverflow.com/a/41236640/1765658)! Write `sed <<<"${VAR}" ... ` instead of `echo "$VAR" | sed ...`! – F. Hauri - Give Up GitHub Feb 12 '22 at 08:27
7

getopts and getopt are very limited. While getopt is suggested not to be used at all, it does offer long options. Whereas getopts does only allow single character options such as -a -b. There are a few more disadvantages when using either one.

So I've written a small script that replaces getopts and getopt. It's a start, it could probably be improved a lot.

Update 08-04-2020: I've added support for hyphens e.g. --package-name.

Usage: "./script.sh package install --package "name with space" --build --archive"

# Example:
# parseArguments "${@}"
# echo "${ARG_0}" -> package
# echo "${ARG_1}" -> install
# echo "${ARG_PACKAGE}" -> "name with space"
# echo "${ARG_BUILD}" -> 1 (true)
# echo "${ARG_ARCHIVE}" -> 1 (true)
function parseArguments() {
  PREVIOUS_ITEM=''
  COUNT=0
  for CURRENT_ITEM in "${@}"
  do
    if [[ ${CURRENT_ITEM} == "--"* ]]; then
      printf -v "ARG_$(formatArgument "${CURRENT_ITEM}")" "%s" "1" # could set this to empty string and check with [ -z "${ARG_ITEM-x}" ] if it's set, but empty.
    else
      if [[ $PREVIOUS_ITEM == "--"* ]]; then
        printf -v "ARG_$(formatArgument "${PREVIOUS_ITEM}")" "%s" "${CURRENT_ITEM}"
      else
        printf -v "ARG_${COUNT}" "%s" "${CURRENT_ITEM}"
      fi
    fi

    PREVIOUS_ITEM="${CURRENT_ITEM}"
    (( COUNT++ ))
  done
}

# Format argument.
function formatArgument() {
  ARGUMENT="${1^^}" # Capitalize.
  ARGUMENT="${ARGUMENT/--/}" # Remove "--".
  ARGUMENT="${ARGUMENT//-/_}" # Replace "-" with "_".
  echo "${ARGUMENT}"
}
Greenonline
  • 1,330
  • 8
  • 23
  • 31
Twoez
  • 540
  • 1
  • 5
  • 17
  • This doesn't answer the question but this is some very interesting code that provides an alternative approach to the problem. Thanks for sharing. – TonyG Sep 22 '20 at 22:26
  • 1
    +1 - You should probably put this on github/gitlab, to allow for a collaborative effort (people raising issues for bugs/improvements, etc.). Then add the link to this answer. – Greenonline Dec 06 '22 at 22:38
1

Turning the huge one-liner in Mark G.'s comment (under Adrian Frühwirth's answer) into a more readable answer - this shows how to avoid using getopts in order to get optional arguments:

usage() { 
    printf "Usage: %s <req> [<-s|--sopt> <45|90>] [<-p|--popt> <string>]\n" "$0"; 
    return 1; 
}; 

main() { 
    req="${1:?$(usage)}";
    shift; 
    s="";
    p="";
    while [ "$#" -ge 1 ]; do
        case "$1" in 
            -s|--sopt) 
                shift;
                s="${1:?$(usage)}";
                [ "$s" -eq 45 ] || [ "$s" -eq 90 ] || { 
                    usage; 
                    return 1; 
                } 
                ;; 
            -p|--popt) 
                shift; 
                p="${1:?$(usage)}" 
                ;; 
            *) 
                usage;
                return 1 
                ;; 
        esac; 
        shift;
    done; 
    printf "req = %s\ns = %s\np = %s\n" "$req" "$s" "$p"; 
};

main "$@"

As noted in n.caillou's comment:

it will fail if there's no space between the options and their argument.

However, to make it more POSIX compliant (from Mark G.'s other comment):

        case "$1" in 
            -s*)
                s=${1#-s}; 
                if [ -z "$s" ]; 
                    shift; 
                    s=$1; 
                fi
Greenonline
  • 1,330
  • 8
  • 23
  • 31