在Shell脚本(POSIX)中删除最后一个参数

3

我目前正在开发一种编译成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。我还希望它速度合理;打开外部进程/子Shell的程序会对小型参数计数过重,但如果您有创造性的解决方案,我也不介意看到它(并且它们仍然可以用作大型参数计数的备选方案)。像那些数组方法一样快速的东西是理想的。

4个回答

4

这是我的当前回答:

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 #=> 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% \"*}"
}

pop_next 是一个比在posix shells中的pop更简单的操作(虽然它比zsh和bash中的pop稍微复杂一些)。

它的使用方式如下:

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

main 1 2 3 #=> 1

POP_EXPR和变量作用域

请注意,如果您在poppop_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 #=> g=1, f=a b

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 \ # 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的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        \ # 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

它将输出一个带有实际值、用户值、系统值和内部值(使用 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 # or without --fast if you have a day to spare
$ sh poptable.sh -g >benchmarks.md

结论

这是对该主题进行了一周的研究得出的结果,我想分享一下。希望它不会太长,我试图削减掉次要信息并提供指向要点的链接。这最初是作为答案制作的 (删除shell脚本(bash)参数列表中的最后一个参数),但我感到重点放在POSIX上使其离题。

这里链接的所有代码都使用MIT许可证授权。


local 不是 POSIX 的一部分。 - chepner
是的,这就是为什么我在popgen.sh中有一个选项不使用它。local在我需要的所有shell上都存在,包括unix ksh,这就是为什么我的函数使用它的原因,但也许为了这个答案的缘故,我应该将其删除。 - phicr
1
坦白说,我没有读到那么远。这已经超出了 Stack Overflow 问题的范围;它更像是一篇博客文章。 - chepner
我知道答案很长,但它完全致力于回答一个简单的问题(这个问题曾经被问过,并添加了posix和速度的要求)。我认为提供详细的答案并没有什么坏处,特别是因为我已经把大多数人感兴趣的部分(代码)放在了最前面。 - phicr
1
我已经将这里的信息和代码整理成了一个适当的 Github 存储库:https://github.com/fcard/pop.sh;如果人们真的认为这里有太多信息,我现在可以在适当的地方链接到它。 - phicr

2
alias pop='set -- $(eval printf '\''%s\\n'\'' $(seq $(expr $# - 1) | sed '\''s/^/\$/;H;$!d;x;s/\n/ /g'\'') )'

编辑:

这是一个使用别名而不是函数的POSIX shell解决方案;如果在函数中调用,它会产生所需的效果(通过使用相同数量的参数减去最后一个来重置函数参数;作为别名,并且使用eval,它可以更改封闭函数的值):

func () {
    pop
    pop
    echo "$@"
}
func a b c d e      # prints a b c

请问您能否解释一下这个别名的执行流程,或者至少是sed部分吗? - saulius2
1
@saulius2,sed 部分的作用是将 seq 生成的数字列转换为一行,该行包括除最后一个位置参数以外的所有位置参数,它通过将换行符替换为空格并在每个数字前面添加 '$' 来实现。这可以用多种其他方式实现。 - phranz

1
pop () {
    i=0
    while [ $((i+=1)) -lt $# ]; do
        set -- "$@" "$1"
        shift
    done # 1 2 3 -> 3 1 2
    printf '%s' "$1" # last argument
    shift # $@ is now without last argument
}

1

& 纯净破折号兼容... :)

使用方法:

test () { echo "$@" ; } ;

with_init test 1 2 3 4 ;  # test will be called with: 1 2 3

图书馆:

#!/bin/sh

init_arguments_ () {  #L variable [#arguments] last ;

  local ia_i=0 ia_v="$1" ia_tr= ;
      # index  variable  to_return

  shift ;

  while [ $(( ia_i += 1 )) -lt $# ] ; do

    ia_tr=$ia_tr" \"\$$ia_i\"" ;  done ;

  eval "$ia_v=\$ia_tr" ;
} ;

unshift () {  #L [#arguments ;

  local args= ;  init_arguments_ args "$@" ;

  echo "eval set $args" ;
} ;


unshift_ () {  #L arguments_variable

  eval "$1=\${$1"'% \"*}' ;
} ;

with_init () {
#L command [#arguments] last

  local command=$1 args= ;  shift ;

  # Maybe get last before removal

    # eval 'local last="$'$#\" ;

  # Shorter

    # $(unshift "$@") ;

    # "$command" "$@" ;

  # Faster

  init_arguments_ args "$@" ;

  ## Maybe unshift another, nice for loops

    # unshift_ args ;

  eval "$command$args" ;

    # Or:  eval "set $args" ;
    #      "$command" "$@" ;

} ;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接