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.

  • 1
    getopts is not able to process double dash arguments – LinuxSecurityFreak Nov 16 '18 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. – Thomas Dickey Nov 16 '18 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! – filbranden Nov 16 '18 at 2:08
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)

| improve this answer | |
  • 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 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. – Gordon Davisson Nov 16 '18 at 7:09
  • @GordonDavisson Thanks for responding while I was asleep. ;-) Code edited to clarify as well. – Fabby Nov 16 '18 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. – mosvy Nov 16 '18 at 12:47
  • also, getopt is not able to handle empty strings as argument values; example: getopt aq: -q '' -a – mosvy Nov 16 '18 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.

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.