问题描述
我目前正在研究一种旨在编译为POSIX Shell语言的语言,并且我想引入一种pop
功能。就像您可以使用“ shift”删除传递给函数的第一个参数一样:
f() {
shift
printf '%s' "$*"
}
f 1 2 3 #=> 2 3
g() {
# pop
printf '%s' "$*"
}
g 1 2 3 #=> 1 2
我知道(Remove last argument from argument list of shell script (bash))中详细介绍的数组方法,但是我想要一种可移植的东西,这些东西至少可以在以下shell中工作:ash,dash,ksh(Unix),bash和zsh。我还想要一些速度较快的东西;对于小数量的参数而言,打开外部进程/子外壳的内容就太繁琐了,以为如果您有一个创新的解决方案,我将不介意看到它(它们仍然可以用作大参数数量的后备设备)。与那些数组方法一样快的速度将是理想的。
解决方法
这是我当前的答案:
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
外壳程序都支持它(特别是我在问题中要求的外壳程序)并且没有它会导致严重的错误,所以我决定将其包含在此主要功能中。但是,这是一个完全兼容的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 #=> a b
pop1 $(seq 1 1000) #=> 1 .. 999
pop2 $(seq 1 1000) #=> 1 .. 998
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% \"*}"
}
在posix shell中, pop_next
比pop
简单得多(尽管它是
在zsh和bash上比pop
稍微复杂一点)
它是这样使用的:
main() {
pop $#
pop_next
eval "$POP_EXPR"
}
main 1 2 3 #=> 1
POP_EXPR和变量范围
请注意,如果您不打算在使用之后立即使用eval "$POP_EXPR"
pop
和pop_next
,如果您对范围内的函数调用不谨慎
操作之间可能会更改POP_EXPR
变量并弄乱事情
向上。为避免这种情况,只需将local POP_EXPR
放在每个函数的开头
(如果可用)使用pop
。
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 #=> g=1,f=a b
popgen.sh
这个特殊功能足以满足我的目的,但是我确实创建了一个 脚本以生成进一步优化的功能。
https://gist.github.com/fcard/e26c5a1f7c8b0674c17c7554fb0cd35c#file-popgen-sh
此处不使用外部工具即可提高性能的方法之一是
意识到有几个小的字符串连接很慢,所以
分批进行它们可以使功能大大加快。调用脚本
popgen.sh -gN1,N2,N3
创建一个处理操作的弹出函数
以N1,N2或N3为批次,具体取决于参数计数。该脚本也
包含其他技巧,示例如下:
$ sh popgen \
> -g 10,100 \ # concatenate strings in batches\
> -w \ # overwrite current file\
> -x9 \ # hardcode the result of the first 9 argument counts\
> -t1000 \ # starting at argument count 1000,use external tools\
> -p posix \ # prefix to add to the function name (with a underscore)\
> -s '' \ # suffix to add to the function name (with a underscore)\
> -c \ # use the command popsh instead of seq/sed as the external tool\
> -@ \ # on zsh and bash,use the subarray method (checks on runtime)\
> -+ \ # use bash/zsh extensions (removes runtime check from -@)\
> -nl \ # don't use 'local'\
> -f \ # use 'function' syntax\
> -o pop.sh # output file
可以使用popgen.sh -t500 -g1 -@
生成与上述函数等效的函数。
在包含popgen.sh
的要点中,您将找到一个popsh.c
文件,该文件可以是
编译并用作默认Shell的专用,更快的替代方法
外部工具,它将由
popgen.sh -c ...
(如果外壳程序可以通过popsh
对其进行访问)。
另外,您可以创建任何名为popsh
的函数或工具并使用
代替它。
基准
基准功能:
我用于基准测试的脚本可以在以下要点找到: 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 \ # shell used by the benchmark,can be dash/bash/ash/zsh/ksh.\
> -f posix \ # function to be tested\
> -i 10000 \ # number of times that the function will be called per test\
> -a '\0' \ # replacement pattern to model arguments by index (uses sed)\
> -o /dev/stdout \ # where to print the results to (concatenates,defaults to stdout)\
> -n 5,10,1000 # argument sizes to test
它将输出带有time -p
,real
和user
时间值的sys
样式表,
以及内部基准的int
值,它是在基准内计算的
date
处理。
次
以下是int
的调用结果
$ sh popbench.sh -s $shell -f $function -i 10000 -n 1,5,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上用eval设置set -- ...
非常慢
无论使用哪种方法,都保存为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工具或朴素算法的基准),请参见以下文件:
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 # or without --fast if you have a day to spare
$ sh poptable.sh -g >benchmarks.md
结论
这是对该主题进行了为期一周的研究的结果,我认为 我会分享。希望它不是太长,我试图将其修剪到主要 带有要点链接的信息。最初是为了解决 (Remove last argument from argument list of shell script (bash)),但我感到重点放在POSIX上 成为话题。
此处链接的要点中的所有代码均已获得MIT许可。