这是我的当前回答:
pop() {
local n=$(($1 - ${2:-1}))
if [ -n "$ZSH_VERSION" -o -n "$BASH_VERSION" ]; then
POP_EXPR='set -- "${@:1:'$n'}"'
elif [ $n -ge 500 ]; then
POP_EXPR="set -- $(seq -s " " 1 $n | sed 's/[0-9]\+/"${\0}"/g')"
else
local index=0
local arguments=""
while [ $index -lt $n ]; do
index=$((index+1))
arguments="$arguments \"\${$index}\""
done
POP_EXPR="set -- $arguments"
fi
}
请注意,
local
不是 POSIX 标准,但由于所有主流的
sh
shell 都支持它(特别是我在问题中提到的那些),不使用它可能会导致严重的错误。因此,我决定在这个主函数中包含它。以下是一个完全符合 POSIX 标准的版本,使用混淆的参数以减少错误的机会:
pop() {
__pop_n=$(($1 - ${2:-1}))
if [ -n "$ZSH_VERSION" -o -n "$BASH_VERSION" ]; then
POP_EXPR='set -- "${@:1:'$__pop_n'}"'
elif [ $__pop_n -ge 500 ]; then
POP_EXPR="set -- $(seq -s " " 1 $__pop_n | sed 's/[0-9]\+/"${\0}"/g')"
else
__pop_index=0
__pop_arguments=""
while [ $__pop_index -lt $__pop_n ]; do
__pop_index=$((__pop_index+1))
__pop_arguments="$__pop_arguments \"\${$__pop_index}\""
done
POP_EXPR="set -- $__pop_arguments"
fi
}
用法
pop1() {
pop $#
eval "$POP_EXPR"
echo "$@"
}
pop2() {
pop $# 2
eval "$POP_EXPR"
echo "$@"
}
pop1 a b c
pop1 $(seq 1 1000)
pop2 $(seq 1 1000)
pop_next
使用pop创建POP_EXPR
变量后,您可以使用以下函数将其更改为省略后续的参数:
pop_next() {
if [ -n "$BASH_VERSION" -o -n "$ZSH_VERSION" ]; then
local np="${POP_EXPR##*:}"
np="${np%\}*}"
POP_EXPR="${POP_EXPR%:*}:$((np == 0 ? 0 : np - 1))}\""
return
fi
POP_EXPR="${POP_EXPR% \"*}"
}
pop_next
是一个比在posix shells中的pop
更简单的操作(虽然它比zsh和bash中的pop
稍微复杂一些)。
它的使用方式如下:
main() {
pop $#
pop_next
eval "$POP_EXPR"
}
main 1 2 3
POP_EXPR和变量作用域
请注意,如果您在pop
和pop_next
之后不立即使用eval "$POP_EXPR"
,并且在操作之间没有小心处理作用域的一些函数调用,可能会更改POP_EXPR
变量并弄乱事情。为了避免这种情况,请在每个使用pop
的函数开头加上local POP_EXPR
(如果可用)。
f() {
local POP_EXPR
pop $#
g 1 2
eval "$POP_EXPR"
printf '%s' "f=$*"
}
g() {
local POP_EXPR
pop $#
eval "$POP_EXPR"
printf '%s, ' "g=$*"
}
f a b c
popgen.sh
这个特定的函数对我的目的而言已经足够好了,但我创建了一个脚本来生成更优化的函数。
https://gist.github.com/fcard/e26c5a1f7c8b0674c17c7554fb0cd35c#file-popgen-sh
在这里提高性能的一种方法是意识到拼接几个小字符串会很慢,因此批量处理它们可以使函数变得更快。调用脚本popgen.sh -gN1,N2,N3
将创建一个处理操作的pop函数,在批处理N1、N2或N3个参数时会更快。该脚本还包含其他技巧,下面进行了举例并加以解释:
$ sh popgen \
> -g 10,100 \
> -w \
> -x9 \
> -t1000 \
> -p posix \
> -s '' \
> -c \
> -@ \
> -+ \
> -nl \
> -f \
> -o pop.sh
可以使用popgen.sh -t500 -g1 -@
生成与上述函数等效的函数。
在包含popgen.sh
的gist中,您将找到一个popsh.c
文件,可以将其编译并用作默认shell外部工具的专用、更快速的替代品,如果它通过shell可访问为popsh
,则会被任何使用popgen.sh -c ...
生成的函数使用。
或者,您可以创建任何名为popsh
的函数或工具,并在其位置使用它。
基准测试
基准测试函数:
用于基准测试的脚本可以在此gist中找到:
https://gist.github.com/fcard/f4aec7e567da2a8e97962d5d3f025ad4#file-popbench-sh
基准测试函数在这些行中找到:
https://gist.github.com/fcard/f4aec7e567da2a8e97962d5d3f025ad4#file-popbench-sh-L233-L301
脚本可以使用如下:
$ sh popbench.sh \
> -s dash \
> -f posix \
> -i 10000 \
> -a '\0' \
> -o /dev/stdout \
> -n 5,10,1000
它将输出一个带有实际值、用户值、系统值和内部值(使用 date 在基准测试进程内计算)的 time -p 样式表中的 int 值。
时间
以下是对调用的 int 结果的描述:
$ sh popbench.sh -s $shell -f $function -i 10000 -n 1,5,10,100,1000,10000
posix
指的是第二条和第三条款,subarray
指的是第一条,而final
则指整体。
value count 1 5 10 100 1000 10000
---------------------------------------------------------------------------------------
dash/final 0m0.109s 0m0.183s 0m0.275s 0m2.270s 0m16.122s 1m10.239s
ash/final 0m0.104s 0m0.175s 0m0.273s 0m2.337s 0m15.428s 1m11.673s
ksh/final 0m0.409s 0m0.557s 0m0.737s 0m3.558s 0m19.200s 1m40.264s
bash/final 0m0.343s 0m0.414s 0m0.470s 0m1.719s 0m17.508s 3m12.496s
---------------------------------------------------------------------------------------
bash/subarray 0m0.135s 0m0.179s 0m0.224s 0m1.357s 0m18.911s 3m18.007s
dash/posix 0m0.171s 0m0.290s 0m0.447s 0m3.610s 0m17.376s 1m8.852s
ash/posix 0m0.109s 0m0.192s 0m0.285s 0m2.457s 0m14.942s 1m10.062s
ksh/posix 0m0.416s 0m0.581s 0m0.768s 0m4.677s 0m18.790s 1m40.407s
bash/posix 0m0.409s 0m0.739s 0m1.145s 0m10.048s 0m58.449s 40m33.024s
关于zsh
在zsh中,对于大量参数的设定,无论使用何种方法,使用set -- ...
与eval结合会非常慢,除了eval 'set -- "${@:1:$# - 1}"'
。即使仅仅是将其修改为eval "set -- ${@:1:$# - 1}"
(忽略带有空格的参数不起作用),速度也会变慢两个数量级。
value count 1 5 10 100 1000 10000
---------------------------------------------------------------------------------------
zsh/subarray 0m0.203s 0m0.227s 0m0.233s 0m0.461s 0m3.643s 0m38.396s
zsh/final 0m0.399s 0m0.416s 0m0.441s 0m0.722s 0m4.205s 0m37.217s
zsh/posix 0m0.718s 0m0.913s 0m1.182s 0m6.200s 0m46.516s 42m27.224s
zsh/eval-zsh 0m0.419s 0m0.353s 0m0.375s 0m0.853s 0m5.771s 32m59.576s
更多基准测试
如需更多基准测试,包括仅使用外部工具、c popsh工具或naive算法,请参阅此文件:
https://gist.github.com/fcard/f4aec7e567da2a8e97962d5d3f025ad4#file-benchmarks-md
它是这样生成的:
$ git clone https://gist.github.com/f4aec7e567da2a8e97962d5d3f025ad4.git popbench
$ cd popbench
$ sh popgen_run.sh
$ sh popbench_run.sh --fast
$ sh poptable.sh -g >benchmarks.md
结论
这是对该主题进行了一周的研究得出的结果,我想分享一下。希望它不会太长,我试图削减掉次要信息并提供指向要点的链接。这最初是作为答案制作的
(删除shell脚本(bash)参数列表中的最后一个参数),但我感到重点放在POSIX上使其离题。
这里链接的所有代码都使用MIT许可证授权。
local
不是 POSIX 的一部分。 - chepnerlocal
在我需要的所有shell上都存在,包括unix ksh,这就是为什么我的函数使用它的原因,但也许为了这个答案的缘故,我应该将其删除。 - phicr