-1

So I have a shell script that does some preparation and then runs a utility.

The preparation can be affected by switches like -a or -n. The contents of the command line that are not such switches are to be passed to the utility. Currently my script has:

while getopts ":an" opt; do case $opt in a) #something ;; n) #something else ;; \?) echo "Invalid option: -$OPTARG" >&2 ;; esac done shift $((OPTIND-1)) #more prep my_utility $@ 

However, this fails when I want to pass long-form options to the utility, such as --abc as that is interpreted as a, b, and c options by getopts.

So how can I process options in such a way that -a or -n is processed, but --abc remains untouched? Unless, of course, I dump Shell and do it in Python - then options would be very easy, but I also need to copy files and run utilities, and Python makes this awkward.

3
  • 1
    getopts is not able to process double dash arguments Commented Nov 16, 2018 at 0:07
  • You could do this by copying the unrecognized options to a shell variable, and passing that to my_utility, but it requires keeping in mind special cases where the getopts parameter must handle options with values. Commented Nov 16, 2018 at 0:46
  • I disagree with "I also need to copy files and run utilities, and Python makes this awkward." Use shutil.copy2() for the former and subprocess.check_call([...]) and subprocess.check_output([...]) for the latter. In many cases you can still keep these as one-liners. And the language expressiveness you gain for the rest of the script is definitely worth it! Commented Nov 16, 2018 at 2:08

2 Answers 2

2

To interpret double dash commands you need GNU's getopt instead of the built-in getopts.

There is a way to use the bash builtin getopts to mimic long options, but you should not do that as it's a kludge and not worth your effort as getopts cannot enforce the long specification.

So your script becomes:

#!/bin/bash # Get options OPTS=`getopt --options an: --long a_something,n_something_else: -n 'parse-options' -- "$@"` if [ $? != 0 ] ; then echo "Failed parsing options." >&2 ; exit 1 ; fi #echo parsed and cleaned up options echo "$OPTS" eval set -- "$OPTS" bSomething=false bSomethingElse=false # "Endless" loop over the parsed options while true; do case "$1" in -a | --a_something ) bSomething=true; shift ;; -n | --n_something_else ) bSomethingElse=true; shift ;; -- ) shift; break ;; * ) break ;; esac done 

For more information: man getopt
For even more information: man getopt(3)

6
  • Thank you! But I dont understand how this works. The while loop just goes through $1 until $1 becomes unrecognizeable, I like this. But what does getopts do, as that loop does not invoke it? Commented Nov 16, 2018 at 1:21
  • 2
    @MikhailRamendik getopts parses the options, then prints them in a cleaned-up, standardized format. The set command then replaces the passed arguments with the cleaned-up version, so that the while loop can parse them easily. Commented Nov 16, 2018 at 7:09
  • @GordonDavisson Thanks for responding while I was asleep. ;-) Code edited to clarify as well. Commented Nov 16, 2018 at 8:14
  • 1
    getopt, unlike getopts, is non-standard. also notice that getopts is not just a "bash builtin", but a posix mandated utility, present in all shells on all modern systems. Commented Nov 16, 2018 at 12:47
  • also, getopt is not able to handle empty strings as argument values; example: getopt aq: -q '' -a Commented Nov 16, 2018 at 12:53
2

I don't see why you couldn't simulate long options by treating - as a short option requiring an argument:

$ cat /tmp/foo while getopts :aq:-: opt; do case $opt in a) echo option -a;; q) echo option -q with value "$OPTARG";; -) case $OPTARG in abc) echo option --abc;; def=*) echo option --def with value "${OPTARG#*=}";; *) echo >&2 "unknown option --$OPTARG";; esac;; :) echo >&2 "-$OPTARG needs an argument";; *) echo >&2 "unknown option -$OPTARG";; esac done shift "$((OPTIND - 1))" echo argv: "$@" $ sh /tmp/foo -a -qt --abc --def=ghi -- foo bar option -a option -q with value t option --abc option --def with value ghi argv: foo bar 

If you want to make a list of arguments (eg. to call another command with it), and your shell doesn't support arrays (eg. debian's or busybox's /bin/sh), you can use the following trick, which will pass to eval a list of argument indexes instead of actual strings; that will avoid any IFS splitting/globbing/whitespace annoyances:

$ cat /tmp/foo # handle -a and -qval # call another command with any long options and non-option arguments av= while getopts :aq:-: opt; do case $opt in a) echo option -a;; q) echo option -q with value "'$OPTARG'";; -) av="$av \"\${$((OPTIND-1))}\"" ;; # pass long options unchanged :) echo >&2 "-$OPTARG needs an argument";; *) echo >&2 "unknown option -$OPTARG";; esac done i=$OPTIND; while [ "$i" -le "$#" ]; do av="$av \"\${$i}\"" i=$((i + 1)); done print_its_args(){ for a; do printf ' {%s}' "$a"; done; echo; } echo "print_its_args $av" eval "print_its_args $av" 

Then:

$ sh /tmp/foo -aqval --foo='a ** b' --abc -- '(moo)' option -a option -q with value 'val' print_its_args "$2" "$3" "$5" {--foo=a ** b} {--abc} {(moo)} 

This trick could be used in other situations; but this is a case where a simpler solution like set -- "$@" arg to push args into $@ cannot be used, because modifying $@ inside the getopts loop cannot be done in a portable way.

4
  • 1
    Note that the second option won't be very robust at handling options with values (e.g. --long-opt='some value'). As usual, eval is a huge source of trouble... Commented Nov 16, 2018 at 7:13
  • That's what I was warning about when stating There is a way to use the bash builtin getopts to mimic long options, but you should not do that as it's a kludge and not worth your effort as getopts cannot enforce the long specification. It's still elegant code, so not downvoting. Commented Nov 16, 2018 at 8:25
  • Please explain how this "does not enforce the long specification". As to the eval + long opts with spaces in them in the 2nd ex, that could be easily worked around by a) using arrays b) using IFS and set -f c) using another portable trick that I'll edit in when I get something else than a phone to type on Commented Nov 16, 2018 at 10:11
  • @GordonDavisson I've updated the second example with something that should be able to handle that, too. Commented Nov 16, 2018 at 12:40

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.