为啥 OPTIND 会弄乱我的位置参数?

Posted

技术标签:

【中文标题】为啥 OPTIND 会弄乱我的位置参数?【英文标题】:Why is OPTIND messing my positional params?为什么 OPTIND 会弄乱我的位置参数? 【发布时间】:2020-08-17 08:24:56 【问题描述】:

我有这个功能:

    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

如果使用-i 调用,它应该使用grep 区分大小写。但是当它是时,它把-i 代替search stringsearch string 代替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'

在调用中,grep 将$1(应该是搜索字符串)作为-i,因此搜索字符串代替了file,因此不调用(尊重。等待文件或标准输入 -正如 grep 没有指定文件一样)。我认为$((OPTIND-1)) 会根据Explain the shell command: shift $(($optind - 1)) 将一个选项移出,但事实并非如此。 1)有人可以解释吗? + 在我的情况下,对$OPTIND 的一点解释也很好。 2)最后一个问题:当grep失败时,为什么|| exit 1 |没有在另一个管道之前退出?

【问题讨论】:

变量OPTINDopt 都是全局变量,这意味着每次运行该函数时,它都会继承之前运行的剩余值。将它们设为本地 (see here)。此外,getopts 的通常形式是while 循环(而不是if),其中shift $((OPTIND-1))` after 它。 @GordonDavisson 我确实使用了if,因为我只有一个选择。 2)我不明白为什么 global opt 会混淆参数的索引。它存储一个值而不是索引,因此它不会对我的问题产生任何影响。所以宁愿只做本地$OPTIND 看看我的更新。添加了local,但没有帮助 你不能用 grep —color=auto -P -n $@ 或类似的东西替换整个 if-else-fi 树吗?这应该复制你给函数的所有参数,包括-i(如果有) @JimDanner 我可以,但这会让getopts 失去作用。我想了解它是如何工作的,并解析它的论点。所以这个案子没有 【参考方案1】:

问题一,getopts问题:

正如我在评论中所说,您需要将 OPTINDopt 设置为函数的局部变量,因此它不会继承函数之前运行的值。要理解为什么会这样,让我从您的原始函数开始(从您的问题的第一个版本开始),并以 echo 命令的形式添加一些工具,以显示运行时事情的变化:

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";

...并尝试运行它,首先没有-i 标志:

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

而且效果很好!解析后,opt 为空(应该是),“somesearch”和“somefile”都保留在要传递给grep 的参数列表中。

不过,在继续之前,我应该稍微解释一下OPTINDgetopts 旨在重复运行以迭代标志(又名选项)参数,OPTIND 是它跟踪处理参数列表的位置的一部分。特别是,它是 next 参数的编号,它需要检查它是否是一个标志(如果是,则处理它)。在这种情况下,它从 1 开始(即 $1 是下一个要检查的参数),并且它一直留在那里,因为 $1 是一个常规参数,而不是一个标志。

顺便说一句,如果您在照常处理后完成了shift $((OPTIND-1)),它会执行shift 0,这将所有零标记参数列表中的参数。正如它应该的那样。 (另一方面,如果你有一个循环并将shift 放入循环中,它会将arg 列表从getopts 下更改出来,导致它失去对它的位置的跟踪并且变得非常混乱。这就是为什么你将shift 放在循环之后。)

好的,让我们用一个实际的标志来试试吧:

$ 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

再次,它工作正常!它解析了-i,适当地设置opt,将OPTIND 增加到2,所以如果你有一个循环,它会检查第二个参数,发现它是一个常规参数,然后停止循环。然后shift $((OPTIND-1)) 转移了一个标志参数,将非标志参数传递给grep

让我们再试一次,使用相同的标志:

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

糟糕,现在一切都搞砸了,这是因为它从上一次运行中继承了 OPTINDoptOPTIND 是 2 告诉 getopts 它已经检查过 $1 并且不必再次处理它;它查看$2,发现它不是以- 开头的,所以它不是标志,所以它返回false,if 没有运行,标志参数也没有被移开。同时,opt 仍设置为上次运行时的“-i”。

就是为什么getopts 一直不适合你。为了证明这一点,让我们修改函数以使两个变量都本地化:

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";

并尝试一下:

$ 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

现在它开始有点奇怪,因为OPTIND 是空白而不是 1,但这实际上不是问题,因为getopts 假设它应该从 1 开始。所以它解析参数,设置 opt(其中 没有从以前继承一个虚假的值),并将标志移出参数列表。

但是有一个问题。假设我们传递了一个非法(/unsupported)标志:

$ 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

再次糟糕。由于getopts 处理了一个以- 开头的参数,它打印了一个错误但继续返回true,变量i 设置为“?”表示有问题。你的系统没有检查,它只是假设它一定是-i

现在,让我向您展示标准(推荐)版本,带有while 循环和标志上的case,带有错误处理程序。我还冒昧地从行尾删除了单个分号,因为它们在 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"

然后运行它:

$ 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

...解析按预期工作,即使重复运行。检查错误处理:

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

而且由于有一个循环,它处理多个标志(即使只有一个定义的标志):

$ 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

现在,您可能会抱怨对于这样一个简单的任务有很多代码(只是一个可能的标志!),您是对的。但它基本上是样板文件;您不必每次都编写整个内容,只需复制一个标准示例,填写选项字符串和案例来处理它们,就差不多了。如果它不在函数中,您将不会有 local 命令,并且您将使用 exit 1 而不是 return 1 来解决问题,但仅此而已。

如果您真的希望它简单,只需使用if [ "$1" = "-i" ],不要涉及使用getopts 的复杂性。

问题2,为什么grep失败时|| exit 1 |没有在另一个管道之前退出?:

这种方法实际上存在三个问题:首先,要退出使用return 而不是exit 的函数。

其次,shell解析管道的优先级高于||,因此命令被视为:

    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"

而不是

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

第三,也是最重要的一点,管道的元素在子流程中运行。对于像exitreturn 这样的shell 命令,这意味着它们在子shell 中运行,而在子shell 中运行exitreturn(或break 或...)对父shell 没有这种影响shell(即运行函数的那个​​)。这意味着您无法在管道中执行任何操作来使函数直接返回。

在这种情况下,我认为您最好的选择是:

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

【讨论】:

我怎么只有一个问题。在命令中,我是按照多行打开vim。问题是,它很慢。是否可以在我的函数中异步打开它?还是在后台?异步管道怎么做?【参考方案2】:

我已将位置参数更改为更大:

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
    

哪个有效,但我不喜欢它。在一个分支中,由于该选项,我必须使用更大的位置,但在没有使用选项的另一个分支上,位置不会改变。很乱

我曾尝试将shift $((OPTIND-1)) 紧跟在then 之后,但无济于事。

【讨论】:

以上是关于为啥 OPTIND 会弄乱我的位置参数?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 PHP 会弄乱我的 CSS?

为啥画布会弄乱我的图像颜色?

为啥浏览器的后退按钮会弄乱我的 Vue 组件?

为啥 Python 多处理队列会弄乱字典?

为啥 matplotlib.PatchCollection 会弄乱补丁的颜色?

如何使这个 MySQL 函数不会弄乱我的字符集?