0

I would like either of these inputs to work. That is, the -n option itself is optional – I already know how to do that – but it then may have an optional parameter on top. If no parameter is given, a fallback value will be applied.

command -n 100
command -n

I can only make the former input type work or the latter, but not both.

HAS_NICE_THINGS=0
NICE_THINGS=50       # default value.

while getopts n: option; do #while getopts n option; do # NICE_THINGS would always be that default value. #while getopts nn: option; do # same. case "${option}" in n) HAS_NICE_THINGS=1 if [[ ! -z "${OPTARG}" ]] && (( "${OPTARG}" > 0 )) && (( "${OPTARG}" <= 100 )); then NICE_THINGS=${OPTARG} fi;; esac done

error message:

option requires an argument -- n

I'm not entirely sure yet if I would need a boolean for my script, but so far, just in case, I am logging one (HAS_NICE_THINGS).

The end goal I had in mind was to set the JPG quality when eventually saving an image. Though, I can imagine this construct being useful elsewhere as well.

I'm using Ubuntu 18.04.5 and GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu).

  • 1
    I don't quite understand what is supposed to happen. If -n takes a value, what should happen if the user passes -n with no value? Should it behave as though -n were not passed? Should -n result in one value for NICE_THINGS while running without -n should result in another value? Please [edit] your question and explain what each scenario is. – terdon Aug 08 '21 at 13:16
  • @terdon, looking at the code, they have a default value for NICE_THINGS, which would be used with just plain -n (with no value). – ilkkachu Aug 08 '21 at 13:36
  • Do you have an example of some other tool that behaves like this? – Kusalananda Aug 08 '21 at 15:39
  • @Kusalananda, another tool that takes options with optional option-arguments? GNU sed and the -i option. In contrast to e.g. the FreeBSD version where the option-argument is mandatory. – ilkkachu Aug 09 '21 at 09:26
  • (TBH, I can't help wondering why our esteemed moderators are double-guessing the aims of the asker; focusing on the what should happen, when the question seems to be quite clearly about (if and) how this could be done.) – ilkkachu Aug 09 '21 at 09:27
  • @ilkkachu Sorry, but I'm not "esteemed" anything. I was just asking because it's such an unusual thing to want to do that I thought that there must be better ways of doing it. The -i option to sed is a good example of a hideous design decision, because virtually no GNU user knows what the option is actually for (it's for setting a backup suffix) and it's hopelessly non-portable. – Kusalananda Aug 09 '21 at 09:30
  • @Kusalananda, well, I don't know, it may be the angle of coming from Linux and being influenced by GNU, but on that side, optional opt-args aren't unheard of. Probably more common with long options though, and less awkward with those, too. As for sed -i, come on, why would GNU users in particular be unfamiliar with it, and not sed users in general? But sure, it's not POSIX, though seems to be mentioned in at least the GNU, FreeBSD, OpenBSD and Busybox versions of sed. ...and apparently OpenBSD and Busybox also take it with an optional opt-arg. (And yes, I meant both of you mods.) – ilkkachu Aug 09 '21 at 09:42
  • What Unix are you using? – Kusalananda Aug 09 '21 at 13:24
  • Sorry, busy week. I've edited the question, though @ilkkachu correctly interpreted my intentions. I do not have an example of another tool with that behavior, off the top of my head. – WoodrowShigeru Aug 15 '21 at 16:11
  • @WoodrowShigeru, hmm, if you're going to set the JPG quality, there doesn't seem to be an enable/disable value involved? File type would be a bit different, if you allow more than just jpg. So you might be able to do with just something like -t jpg for the type and -q 99 for the quality. But even if you end up doing that, it doesn't invalidate the question about optional opt-args. (well, not as far as I can see anyway.) – ilkkachu Aug 15 '21 at 16:39
  • I only need to distinguish between jpg and png, and I only need the quality value for the jpg, so I wanted to combine the two. Set jpg instead of default png with a -j flag, and then tweak the quality if necessary (run script again if image is too bad or large). But I didn't expect this to be such a complex problem. – WoodrowShigeru Aug 15 '21 at 19:38
  • @WoodrowShigeru, I also didn't expect this to be such a controversial problem. – ilkkachu Sep 05 '21 at 10:03

2 Answers2

1

Not sensibly with Bash's/POSIX getopts, but you could do it with the "enhanced" getopt (without an s) from util-linux or Busybox. (And those only, in particular many "traditional" getopt implementations are broken wrt. whitespace also)

The man page says of getopts:

optstring contains the option characters to be recognized; if a character is followed by a colon, the option is expected to have an argument, which should be separated from it by white space.

there's no mention of optional option-arguments.

Of course you could have another optional option to give the non-default value. E.g. let -n take no argument and just enable nice things, and let -N <arg> take the argument, enable nice things and set the value.

E.g. something like this:

#!/bin/bash
HAS_NICE_THINGS=0
NICE_THINGS_VALUE=50

while getopts nN: option; do case "${option}" in n) HAS_NICE_THINGS=1 shift;; N) HAS_NICE_THINGS=1 NICE_THINGS_VALUE=$OPTARG shift; shift;; esac done

if [ "$HAS_NICE_THINGS" = 1 ]; then echo "nice things enabled with value $NICE_THINGS_VALUE" fi

would give

$ bash nice-getotps.sh -n
nice things enabled with value 50
$ bash nice-getopts.sh -N42
nice things enabled with value 42

The util-linux getopt takes optional option-arguments with the double-colon syntax. It's a bit awkward to use, and you need to mess with eval, but done correctly, it seems to work.

Man page:

-o shortopts [...] 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.

With a script to just print the raw values so we can check it works properly (getopt-optional.sh):

#!/bin/bash
getopt -T
if [ "$?" -ne 4 ]; then
    echo "wrong version of 'getopt' installed, exiting..." >&2
    exit 1
fi 
params="$(getopt -o an:: -- "$@")"
eval set -- "$params"
while [ "$#" -gt 0 ]; do
    case "$1" in
    -n)
        echo "option -n with arg '$2'"
        shift 2;;
    -a)
        echo "option -a"
        shift;;
    --) 
        shift
        break;;
     *) 
        echo "something else: '$1'"
        shift;;
    esac
done
echo "remaining arguments ($#):"
printf "<%s> " "$@"
echo

we get

$ bash getopt-optional.sh -n -a
option -n with arg ''
option -a
remaining arguments (0):
<> 
$ bash getopt-optional.sh -n'blah blah' -a
 -n 'blah blah' -a --
option -n with arg 'blah blah'
option -a
remaining arguments (0):
<> 

No argument to -n shows up as an empty argument.

Not that you could pass an explicit empty argument anyway, since the option-argument needs to be within the same command line argument as the option itself, and -n is the same as -n"" after the quotes are removed. That makes optional option-arguments awkward to use in that you need to use -nx, as -n x would be taken as the option -n (without an opt-arg), followed by a regular non-option command line argument x. Which is unlike what would happen if -n took a mandatory option-argument.

More about getopt on this Stackoverflow answer to How do I parse command line arguments in Bash?


Note that that appears limited to that particular implementation of getopt, one that happens to be common on Linux systems, but probably not on others. Other implementations of getopt might not even support whitespace in arguments (the program has to do shell quoting for them). The "enhanced" util-linux version has the -T option to test if you have that particular version installed. There's some discussion on the limitations and caveats with getopt here: getopt, getopts or manual parsing - what to use when I want to support both short and long options?

Also, getopt is not a standard tool like getopts is.

ilkkachu
  • 138,973
-1

Let's assume that we want to provide an option -a that takes an optional option-argument. Using the option should set the shell variable a_opt in the shell, but it should set it to the value $a_default if no option-argument is given, otherwise it sets it to the value of $OPTARG as usual.

To determine when -a is used with no argument, we have two cases:

  1. The option-argument that getopts believes the user is using is actually one of the other options, or
  2. The -a option is used at the end of the list command line (getopts would usually generate an error about a missing option in this case).

The first case is handled by looking at $OPTARG to determine whether it looks like one of the other options. If it does, then use the default value, otherwise use $OPTARG as the value. When the default value is used, we need to restart option parsing at the point after the -a, which means shifting off a few arguments (OPTIND-1 of them), resetting OPTIND to 1 and then adding $OPTARG to the front of the list of positional parameters.

This all means that we can't use -a with an argument that looks like one of the options.

The second case is handled by letting the first character of the option string given to getopts be :. This has two effects:

  1. getopts no longer emits errors by itself.
  2. When an option requiring an option-argument does not get an option-argument, the name of that option is placed in $OPTARG and an : is put into the option variable used with getopts.

This means that we can add a : case to our usual option parsing case statement which tests $OPTARG against a to see whether the user used -a at the very end of the command line.

Code:

#!/usr/local/bin/bash

a_default='A default value' d_opt=false

while getopts :a:b:c:d opt; do case $opt in a) case $OPTARG in -[a-d-]) shift "$(( OPTIND - 1 ))" OPTIND=1 set -- "$OPTARG" "$@" a_opt=$a_default ;; ) a_opt=$OPTARG esac ;;

            b)      b_opt=$OPTARG ;;
            c)      c_opt=$OPTARG ;;
            d)      d_opt=true ;;

            :)
                    if [ &quot;$OPTARG&quot; = a ]; then
                            a_opt=$a_default
                    else
                            printf '%s: option requires an argument -- %s\n' \
                                    &quot;$0&quot; &quot;$OPTARG&quot; &gt;&amp;2
                            exit 1
                    fi
                    ;;

            *)      printf '%s: generic option parsing error\n' &quot;$0&quot; &gt;&amp;2
                    exit 1
    esac

done

shift "$(( OPTIND - 1 ))"

printf '%s = "%s"\n'
'a_opt' "${a_opt-(Not set)}"
'b_opt' "${b_opt-(Not set)}"
'c_opt' "${c_opt-(Not set)}"
'd_opt' "${d_opt-(Not set)}"

if [ "$#" -gt 0 ]; then printf 'Extra argument: "%s"\n' "$@" fi

Testing:

a_opt gets a default value if -a is used with no option-argument:

$ ./script -a
a_opt = "A default value"
b_opt = "(Not set)"
c_opt = "(Not set)"
d_opt = "false"

a_opt does not get a value if -a isn't used:

$ ./script -b bval
a_opt = "(Not set)"
b_opt = "bval"
c_opt = "(Not set)"
d_opt = "false"

We can give a_opt a value as usual by providing an option-argument with -a:

$ ./script -da aval -c cval
a_opt = "aval"
b_opt = "(Not set)"
c_opt = "cval"
d_opt = "true"

We may still use -- to end option parsing:

$ ./script -d -a -- aval -c cval hello world
a_opt = "A default value"
b_opt = "(Not set)"
c_opt = "(Not set)"
d_opt = "true"
Extra argument: "aval"
Extra argument: "-c"
Extra argument: "cval"
Extra argument: "hello"
Extra argument: "world"
Kusalananda
  • 333,661
  • While a nice hack, the caveat with messing with OPTIND is that it doesn't export the position within a single command line argument. This goes into an endless loop if it sees the single argument -a-b. And also it's incompatible with at least GNU-style optional opt-args, since it takes something like -a foo as -a with the opt-arg foo, instead of -a without an opt-arg and foo as a regular non-option command line arg. (Same with Busybox's sed, sed -e ... -i file.txt takes file.txt as a filename, not an opt-arg to -i. Don't know if I have other similar non-GNU things to test) – ilkkachu Aug 09 '21 at 12:32
  • @ilkkachu Yes, -a-b causing an infinite loop is an issue, however -a foo not using foo as an operand but as an option argument to -a doesn't seem like a bug to me. If you want option parsing to end before it sees foo, then use -a -- foo (this is standard procedure). All of this points to the fact that this is the sort of option processing Unix utilities do not generally do and that one should ideally try to solve this in other ways. – Kusalananda Aug 09 '21 at 13:14
  • Related to the -a-b issue, it's even worse in that it's actually impossible to give a negative number as an opt-arg to -a here (or any other string that starts with a dash). -a-123 also enters the loop, and -a -123 gives an error (since -1 isn't a valid option). Regardless of what one thinks -a foo should do, the fact is that having it treat foo as the opt-arg to -a is incompatible with every other existing implementation I can find that supports optional opt-args, and with no upsides either. – ilkkachu Aug 09 '21 at 20:48
  • @ilkkachu Could you test again, please? I think the only thing you can't do now is to use one of the options to the script as the option-argument to -a. – Kusalananda Aug 09 '21 at 21:23
  • Can't use as opt-arg anything starting with one of the options, I think. -a-beep puts eep as the opt-arg for -b. So yeah, negative numbers work as long as there are no numeric options. But it leads to repetition in the code (all the options have to be listed in two places), and is still rather non-general opposed to the getopt(3) interpretation of taking whatever (if anything) following the -a in the same command line arg as the opt-arg and never looking at the following command line arg for an optional opt-arg. – ilkkachu Aug 09 '21 at 22:00
  • I'm not sure what you think I was arguing, but I meant to argue against a hacky implementation that's incompatible with the existing similar implementations. Just saying "no" is better, IMO. Also I don't think I ever said the solution with the getopt(1) was particularly portable: I explicitly mentioned the one implementation I know that works properly and the fact that others might not. So yeah, if portability is a concern, it's probably better to just work around the need for optional opt-args. (at least in shell scripts. getopt(3) seems to have the support more widely) – ilkkachu Aug 09 '21 at 22:03