为啥 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 string
和search 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 |
没有在另一个管道之前退出?
【问题讨论】:
变量OPTIND
和opt
都是全局变量,这意味着每次运行该函数时,它都会继承之前运行的剩余值。将它们设为本地 (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
问题:
正如我在评论中所说,您需要将 OPTIND
和 opt
设置为函数的局部变量,因此它不会继承函数之前运行的值。要理解为什么会这样,让我从您的原始函数开始(从您的问题的第一个版本开始),并以 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
的参数列表中。
不过,在继续之前,我应该稍微解释一下OPTIND
。 getopts
旨在重复运行以迭代标志(又名选项)参数,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
糟糕,现在一切都搞砸了,这是因为它从上一次运行中继承了 OPTIND
和 opt
。 OPTIND
是 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"
第三,也是最重要的一点,管道的元素在子流程中运行。对于像exit
和return
这样的shell 命令,这意味着它们在子shell 中运行,而在子shell 中运行exit
或return
(或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 会弄乱我的位置参数?的主要内容,如果未能解决你的问题,请参考以下文章