0

I was playing with getopts, which has a shell variable OPTERR that influences its behavior. I wanted to change the value of OPTERR and invoke getopts on the same line, to affect the behavior of getopts but restore OPTERR to its default value thereafter. So, given that I'm a little fuzzy on the process of mixing a variable change a command invocation, I thought I'd experiment with just a humble echo. Here is a series of shell commands with their outputs, some of which don't make sense to me.

$ echo OPTERR
1
$ OPTERR=0 echo $OPTERR
1

Now, let's stop there. Shouldn't the output for the second command be '0'? So before asking the question, I wracked my brains a bit and thought it must be because following command should be in a sub shell. So then I tried...

$ OPTERR=0 (echo $OPTERR)
-bash: syntax error near unexpected token `('

This was an even bigger surprise. How does one change the value of shell variable for the run of a sub shell, if the above construct is a syntax error? (I also tried putting a terminal ";" after the echo command with the same result). I also tried...

$OPTERR=0 bash -c 'echo $OPTERR'
1

So, no joy there either. I took a different tack...

$function sf() { echo $OPTERR }
$sf
1
$OPTERR=0 sf
0
$sf
1

Success!! But why? Functions are specifically not sub shells (I don't think) and this isn't really what I want (though, I could probably make it work for my application). Here's another formula that worked (sort of):

$function sf() { (OPTERR=0; echo $OPTERR;) }
$sf
0
$echo $OPTERR
1

This is PRETTY close to what I want, but of course the intended target for the actual script (not my trivial echo) is getopts, and getopts has to change variables that the rest of the script can see. So, I changed my trivial echo script to a simple variable set, as in ...

$function sf() { foo='bar'; (foo='check'; echo $foo;); echo $foo; }
$sf
check
bar

D'oh!! So I can call getopts with the environment I want, but I can't get any values back out of the sub shell. I tried exporting foo and all kinds of things too embarrassing to list here, but here's my question: if you put all my experiments together, they must reveal a fundamentally wrong model of how all this should be working. What is the missing piece to my understanding, and what's the right thing to do?

Just to re-iterate, all I'm trying to do is build a function, that uses getopts with OPTERR set to 0, ideally without setting OPTERR on the line where the function is invoked, and in a way that OPTERR is restored outside the function, and most importantly in a way that allows me to actually process the output of getopts. It sounds like a lot to ask, but it's pretty sensible and the existence of this OPTERR variable suggests it should be possible.

cycollins
  • 151
  • 2
    Declare it local inside the function; see this near-dupe answer. As for prefixing an assignment to a command, such assignments only apply within the execution of that command; with echo $OPTERR, variable expansion happens before echo is executed; with a function, bash basically sets it temporarily within the function. As for using it with bash -c, the new shell resets it to default ("1") before executing the command. – Gordon Davisson Jul 02 '19 at 21:52
  • Ah... so one wrong model of the world I had is that sub shells inherit the environment of the calling shell. So you're right on all counts. The following worked perfectly:
    `$function sf() { local OPTERR=0; echo $OPTERR; }
    0
    $echo $OPTERR
    1`
    
    

    It makes sense that it would be vastly simpler than most of my contorted experiments. Thanks!

    – cycollins Jul 02 '19 at 22:14
  • Another reason I didn't think of this is that I though the "built-in" shell variables used for such things couldn't be made local because they had some special status, such that they couldn't be covered by a local variable or if they were, that getopts would still be influenced by the one from the calling shell. Another incorrect underlying model. – cycollins Jul 02 '19 at 22:24
  • I also would've assumed special variables couldn't be localized, but it seems to work! BTW, subshells do inherit environment variables, but OPTERR is a special case because bash initializes it. Compare foo=bar OPTERR=0 bash -c 'echo "foo=$foo, OPTERR=$OPTERR"' (which prints "foo=bar, OPTERR=1") with the equivalent using dash instead of bash, which prints "foo=bar, OPTERR=0". – Gordon Davisson Jul 02 '19 at 22:39
  • Ah...that's an important subtlety. What a delightfully whimsical idiosyncrasy. It explains why this case was so filled with false cues. If you hadn't pointed that out, I might have gone on to get what seemed like still more false cues, so thanks. – cycollins Jul 04 '19 at 00:16

2 Answers2

1
$ echo $VAR

This gives the old value of $VAR because the shell applies variable assignments only after the expansions on the command line.

The IFS= read -r foo construct you often see here on the site works, however, since read doesn't take IFS from the command line, but uses it internally. Similarly, OPTERR=0 getopts ... should also work.

$ cat arg.sh
#!/bin/bash
OPTERR=0 getopts a:b var
echo "var: $var OPTERR: $OPTERR"

$ bash arg.sh -z
var: ? OPTERR: 1

No error message even though -z is an invalid option.


$ OPTERR=0 bash -c 'echo $OPTERR'

This is a bit of a special case, because OPTERR is special. Bash's man page says:

OPTERR If set to the value 1, Bash displays error messages generated by the getopts builtin command. OPTERR is initialized to 1 each time the shell is invoked or a shell script is executed.

The idea is right, though, it works with regular variables:

$ FOO=123 bash -c 'echo $FOO'
123

all I'm trying to do is build a function, that uses getopts

If you're parsing the arguments to the function, and calling it multiple times from the main program (e.g. dothis -a; dothis -b -c), and not using the function to parse arguments to the shell (parse_args "$@"), then you'll also need to remember that getopts modifies the OPTIND variable, too. It will also need to be made local to the function.

E.g. here, the second function call doesn't see any of its options:

foo() { while getopts "abcd" opt; do echo "$opt"; done; };
foo -a -a
foo -b -b

Combined with the previous, this function would work, silently:

foo() {
    local OPTIND=1
    while OPTERR=0 getopts "abcd" opt; do
        echo "$opt"
    done
}
ilkkachu
  • 138,973
  • Holy s... Ahem.... Well, let's just say that was also a good heads up, because that would have confused me mightily. Though after this above discussion, I might have tried to localize OPTIND just as a defensive measure. So, is the more-fundamental problem here that using getopts in a function, as opposed to a script running as a sub shell is just plain fraught with issues and out of its intended usage model. Seems weird because while it is appropriate for stand-alone script "executables", it's also ideal for use in functions. – cycollins Jul 04 '19 at 00:31
  • It also means I'm going to take a good hard look at all the "OPT*" variables and either localize them all or at least give some thought as to how having them changed globally would be a risk. – cycollins Jul 04 '19 at 00:33
0

If you need OPTERR to be 0 during a function call, set it as a local variable, as Gordon Davisson indicated:

#!/bin/bash

myfunc() {
  local OPTERR=0
  printf "Inside myfunc, OPTERR=%d\n" "$OPTERR"
  while getopts ":a:" opt; do
    echo $opt is $OPTARG
  done
}

printf "Before calling myfunc, OPTERR=%d\n" "$OPTERR"
myfunc
printf "After calling myfunc, OPTERR=%d\n" "$OPTERR"

The result is:

Before calling myfunc, OPTERR=1
Inside myfunc, OPTERR=0
After calling myfunc, OPTERR=1
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
  • Yeah, I adopted Gordon Davisson's approach. But half of my experiments would have worked had it not been for the quirk that OPTERR (unlike the state of other special variables) is not inherited by a sub shell, but rather re-initialized to 1 when the sub-shell is initialized. Or maybe that's the distinction between a special variable like this and an environment variable. The former get initialized to defaults and the latter get inherited. – cycollins Jul 04 '19 at 00:22