Why is OPTIND messing my positional params?

后端 未结 2 940
时光取名叫无心
时光取名叫无心 2021-01-28 07:22

I have this function:

    sgrep () 
{ 
    local OPTIND;
    if getopts i o; then
        grep --color=auto -P -in \"$1\" \"$2\";
        shift $((OPTIND-1));
           


        
相关标签:
2条回答
  • 2021-01-28 08:08

    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
    
    0 讨论(0)
  • 2021-01-28 08:10

    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.

    0 讨论(0)
提交回复
热议问题