【问题标题】:Remove last argument in shell script (POSIX)删除 shell 脚本中的最后一个参数 (POSIX)
【发布时间】:2020-12-31 00:30:58
【问题描述】:

我目前正在研究一种旨在编译为 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的东西对于小参数计数来说太重了,如果你有一个创造性的解决方案我不介意看到它(并且它们仍然可以用作大参数计数的后备)。与那些数组方法一样快的东西是理想的。

【问题讨论】:

    标签: shell sh posix


    【解决方案1】:

    这是我目前的答案:

    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 shell 中的 pop 简单得多(尽管它是 在 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 的 gist 中,您将找到一个 popsh.c 文件,该文件可以 编译并用作默认 shell 的专用、更快的替代方案 外部工具,它将被使用生成的任何函数使用 popgen.sh -c ... 如果它可以被 shell 访问为 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
    

    它将输出带有realusersys时间值的time -p样式表, 以及一个int 值,用于内部,在基准内计算 使用date处理。

    以下是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 上

    对于较大的参数计数,使用 eval 设置 set -- ... 在 zsh no 上非常慢 不管是什么方法,保存为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 许可下获得许可。

    【讨论】:

    • local 不是 POSIX 的一部分。
    • 是的,这就是为什么我在 popgen.sh 中有一个不使用它的选项。 local 在我提到的所有外壳上,包括 unix ksh,这就是我的函数使用它的原因,但也许为了这个答案,我应该删除它。
    • 坦率地说,我没有读那么远。这确实超出了 Stack Overflow 问题的范围;它更像是一篇博文。
    • 我知道答案很长,但它仍然完全致力于回答一个简单的问题。 (之前有人问过,加上对 posix 和速度的附加要求)我不认为有一个详细的答案有什么害处,特别是因为我已经强调了我知道大多数人会感兴趣的部分(代码)在顶部。
    • 我已将此处的信息和代码组织到一个适当的 github 存储库中:github.com/fcard/pop.sh;如果人们真的认为这里的信息太多,我现在可以在适当的地方链接到它。
    【解决方案2】:
    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
    }
    

    【讨论】:

      【解决方案3】:
      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
      

      【讨论】:

        猜你喜欢
        • 2013-12-22
        • 1970-01-01
        • 2013-11-09
        • 1970-01-01
        • 2012-04-01
        • 2010-12-23
        • 2011-09-14
        • 1970-01-01
        相关资源
        最近更新 更多