删除Shell脚本POSIX中的最后一个参数

问题描述

我目前正在研究一种旨在编译为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_nextpop简单得多(尽管它是 在zsh和bash上比pop稍微复杂一点)

它是这样使用的:

main() {
  pop $#
  pop_next
  eval "$POP_EXPR"
}

main 1 2 3 #=> 1

POP_EXPR和变量范围

请注意,如果您不打算在使用之后立即使用eval "$POP_EXPR" poppop_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 -prealuser时间值的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许可。