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

filbranden
  • 21,751
  • 4
  • 63
  • 86

2 Answers2

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)

Fabby
  • 5,384
  • 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? – Mikhail Ramendik Nov 16 '18 at 01: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. – Gordon Davisson Nov 16 '18 at 07:09
  • @GordonDavisson Thanks for responding while I was asleep. ;-) Code edited to clarify as well. – Fabby Nov 16 '18 at 08: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. –  Nov 16 '18 at 12:47
  • also, getopt is not able to handle empty strings as argument values; example: getopt aq: -q '' -a –  Nov 16 '18 at 12:53
  • @mosvy The GNU version of getopt can handle empty strings if you use its nonstandard -o | --options option for the format string: getopt -o aq: -- -q '' -a prints " -q '' -a -- " (which, when double-quoted and evaled, gives the correct result). Other versions of getopt... not so much. – Gordon Davisson Nov 20 '18 at 08:21
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.

  • 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... – Gordon Davisson Nov 16 '18 at 07: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. – Fabby Nov 16 '18 at 08: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 –  Nov 16 '18 at 10:11
  • @GordonDavisson I've updated the second example with something that should be able to handle that, too. –  Nov 16 '18 at 12:40