Why is OPTIND messing my positional params?

淺唱寂寞╮ 提交于 2020-05-24 06:08:28

问题


I have this function:

    sgrep () 
{ 
    local OPTIND;
    if getopts i o; then
        grep --color=auto -P -in "$1" "$2";
        shift $((OPTIND-1));
    else
        grep --color=auto -P -n "$1" "$2";
    fi | sed -E -n 's/^([0-9]+).*/\1/p' | xargs -I{} vim +"{}" "$2";
    stty sane
}

It should use grep case sensitive, if it is invoke with -i. But when it is, the it put -i is in place of search string and search string is in place of somefile:

$ set -x
$ sgrep 'somesearch' 'somefile'
---#output---
+ sgrep -i 'somesearch' 'somefile'
+ local OPTIND
+ sed -E -n 's/^([0-9]+).*/\1/p'
+ getopts i o
+ grep --color=auto -P -in -i 'somesearch'

In invocation, the grep takes the $1 (which should be search string), as -i, so the search string is in place of file and therefor not invokes (respect. waiting for file or stdin - as grep does without file specified). I thought the $((OPTIND-1)) would shift the one option out according to this Explain the shell command: shift $(($optind - 1)) but it does not. 1) Can someone explain? + little explanation of the $OPTIND in my case would be good as well. 2) last question: why does || exit 1 | does not exit before another pipe when grep fail?


回答1:


Question 1, getopts problems:

As I said in a comment, you need to make OPTIND and opt local to the function, so it doesn't inherit values from previous runs of the function. To understand why this is, let me start with your original function (from the first version of your question), and add some instrumentation in the form of echo commands to show how things change as it runs:

sgrep () 
{ 
    echo "Starting sgrep, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
    if getopts "i" i; then
        opt="-i";
        shift $((OPTIND-1));
        echo "Parsed -$i flag, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
    fi;
    echo "Done parsing,   OPTIND='$OPTIND', opt='$opt', args=$*" >&2
    # grep --color=auto -P ${opt} "$1" "$2" || exit 1 | sed -E -n 's/^([0-9]+)/\1/p' | xargs -I{} vim +"{}" "$2";
}

...and try running that, first with no -i flag:

$ sgrep 'somesearch' 'somefile'
Starting sgrep, OPTIND='1', opt='', args=somesearch somefile
Done parsing,   OPTIND='1', opt='', args=somesearch somefile

And it worked fine! After parsing, opt is empty (as it should be), and both "somesearch" and "somefile" remain in the argument list to be passed to grep.

I should explain a little about OPTIND, though, before going on. getopts is designed to be run repeatedly to iterate through the flag (aka option) arguments, and OPTIND is part of how it keeps track of where it is in processing the argument list. In particular, it's the number of the next argument that it needs to examine to see if it's a flag (and process it if it is). In this case, it start off at 1 (i.e. $1 is the next arg to examine), and it stays there because $1 is a regular argument, not a flag.

BTW, if you'd done shift $((OPTIND-1)) after processing as usual, it'd do shift 0, which would all zero flag arguments from the arg list. Just as it should. (On the other hand, if you had a loop and put shift inside the loop, it'd change the arg list out from under getopts, causing it to lose track of its place and get very confused. That's why you put the shift after the loop.)

Ok, let's try it with an actual flag:

$ sgrep -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='1', opt='', args=-i somesearch somefile
Parsed -i flag, OPTIND='2', opt='-i', args=somesearch somefile
Done parsing,   OPTIND='2', opt='-i', args=somesearch somefile

Again, it worked properly! It parsed the -i, set opt appropriately, incremented OPTIND to 2 so if you'd had a loop it would've examined the second argument, found it's a regular argument, and stopped the loop. And then shift $((OPTIND-1)) shifted off the one flag argument, leaving the non-flag ones to be passed to grep.

Let's try it again, with the same flag:

$ sgrep -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='2', opt='-i', args=-i somesearch somefile
Done parsing,   OPTIND='2', opt='-i', args=-i somesearch somefile

Oops, now it's all gone screwy, and it's because it inherited OPTIND and opt from the previous run. OPTIND being 2 tells getopts that it's already examined $1 and doesn't have to process it again; it looks at $2, sees it doesn't start with - so it isn't a flag, so it returns false, and the if doesn't run and the flag argument doesn't get shifted away. Meanwhile, opt is still set to "-i" from the last run.

That is why getopts hasn't been working right for you. To prove it, let's modify the function to make both variables local:

sgrep ()
{
    local OPTIND opt    # <- This is the only change here
    echo "Starting sgrep, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
    if getopts "i" i; then
        opt="-i";
        shift $((OPTIND-1));
        echo "Parsed -$i flag, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
    fi;
    echo "Done parsing,   OPTIND='$OPTIND', opt='$opt', args=$*" >&2
    # grep --color=auto -P ${opt} "$1" "$2" || exit 1 | sed -E -n 's/^([0-9]+)/\1/p' | xargs -I{} vim +"{}" "$2";
}

And try it out:

$ sgrep -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-i somesearch somefile
Parsed -i flag, OPTIND='2', opt='-i', args=somesearch somefile
Done parsing,   OPTIND='2', opt='-i', args=somesearch somefile
$ sgrep -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-i somesearch somefile
Parsed -i flag, OPTIND='2', opt='-i', args=somesearch somefile
Done parsing,   OPTIND='2', opt='-i', args=somesearch somefile

Now it starts off a little weird because OPTIND is blank instead of 1, but that's not actually a problem because getopts assumes it should start at 1. So it parses the argument, sets opt (which didn't inherit a bogus value from before), and shifts the flag out of the argument list.

There is a problem, though. Suppose we pass an illegal (/unsupported) flag:

$ sgrep -k 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-k somesearch somefile
-bash: illegal option -- k
Parsed -? flag, OPTIND='2', opt='-i', args=somesearch somefile
Done parsing,   OPTIND='2', opt='-i', args=somesearch somefile

Oops again. Since getopts processed an argument starting with -, it printed an error but went ahead and returned true with the variable i set to "?" to indicate there was a problem. Your system didn't check that, it just assumed it must be -i.

Now, let me show you the standard (recommended) version, with a while loop and a case on the flag, with an error handler. I've also taken the liberty of removing single semicolons from the end of lines, 'cause they're useless in shell:

sgrep ()
{
    local OPTIND opt
    echo "Starting sgrep, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
    while getopts "i" i; do
        case "$i" in
            i )
                opt="-$i"
                echo "Parsed -$i flag, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
                ;;
            * )
                return 1 ;;
        esac
    done
    shift $((OPTIND-1))
    echo "Done parsing,   OPTIND='$OPTIND', opt='$opt', args=$*" >&2
    # grep --color=auto -P ${opt} "$1" "$2" || exit 1 | sed -E -n 's/^([0-9]+)/\1/p' | xargs -I{} vim +"{}" "$2"
}

And run it:

$ sgrep 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=somesearch somefile
Done parsing,   OPTIND='1', opt='', args=somesearch somefile
$ sgrep -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-i somesearch somefile
Parsed -i flag, OPTIND='2', opt='-i', args=-i somesearch somefile
Done parsing,   OPTIND='2', opt='-i', args=somesearch somefile
$ sgrep 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=somesearch somefile
Done parsing,   OPTIND='1', opt='', args=somesearch somefile
$ sgrep -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-i somesearch somefile
Parsed -i flag, OPTIND='2', opt='-i', args=-i somesearch somefile
Done parsing,   OPTIND='2', opt='-i', args=somesearch somefile

...Parsing works as expected, even with repeated runs. Check the error handling:

$ sgrep -k 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-k somesearch somefile
-bash: illegal option -- k

And since there's a loop, it handles multiple flags (even if there's only one defined flag):

$ sgrep -i -i -i -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-i -i -i -i somesearch somefile
Parsed -i flag, OPTIND='2', opt='-i', args=-i -i -i -i somesearch somefile
Parsed -i flag, OPTIND='3', opt='-i', args=-i -i -i -i somesearch somefile
Parsed -i flag, OPTIND='4', opt='-i', args=-i -i -i -i somesearch somefile
Parsed -i flag, OPTIND='5', opt='-i', args=-i -i -i -i somesearch somefile
Done parsing,   OPTIND='5', opt='-i', args=somesearch somefile

Now, you might complain that that's a lot of code for such a simple task (just one possible flag!), and you'd be right. But it's basically boilerplate; you don't have to write that whole thing every time, just copy a standard example, fill in the options string and cases to handle them, and that's pretty much it. If it weren't in a function you wouldn't have the local command, and you'd use exit 1 instead of return 1 to bail out, but that's about it.

If you really want it to be simple, just use if [ "$1" = "-i" ], and don't get involved with the complexities of using getopts.

Question 2, why does || exit 1 | does not exit before another pipe when grep fail?:

There are actually three problems with that approach: first, to exit a function you use return instead of exit.

Second, the shell parses pipes with higher precedence than || so the command was treated as:

    grep --color=auto -P ${opt} "$1" "$2"
Logical or'ed with:
    exit 1 | sed -E -n 's/^([0-9]+)/\1/p' | xargs -I{} vim +"{}" "$2"

Rather than

    grep --color=auto -P ${opt} "$1" "$2" || exit 1
Piped to:
    sed -E -n 's/^([0-9]+)/\1/p' | xargs -I{} vim +"{}" "$2"

Third, and most importantly, the elements of a pipeline run in subprocesses. For shell commands like exit and return, that means they run in subshells, and running exit or return (or break or ...) in a subshell doesn't have that effect on the parent shell (i.e the one running the function). That means there's nothing you can do within the pipeline to make the function return directly.

In this case, I think your best option is something like:

grep ... | otherstuff
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
    return 1
fi



回答2:


I have changed the positional params to one bigger:

sgrep () 
    { 
        local OPTIND;
        if getopts i o; then
            grep --color=auto -P -in "$2" "$3";
            shift $((OPTIND-1));
        else
            grep --color=auto -P -n "$1" "$2";
        fi | sed -E -n 's/^([0-9]+).*/\1/p' | xargs -I{} vim +"{}" "$2";
        stty sane
    }

Which works, but I don't like it. In one branch I have to use bigger positions, because of the option, but on the other branch without option used, positions do not change. It is messy

I have tried to put shift $((OPTIND-1)) immediately after then but for no avail.



来源:https://stackoverflow.com/questions/61566331/why-is-optind-messing-my-positional-params

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!