如何反转shell参数?

ImH*_*ere 10 shell-script posix

我知道可以"$@"使用数组进行反转:

arr=( "$@" )
Run Code Online (Sandbox Code Playgroud)

使用此答案,反转数组。

但这需要一个具有数组的外壳。

也可以使用tac

set -- $( printf '%s\n' "$@" | tac )
Run Code Online (Sandbox Code Playgroud)

但是,断裂,如果参数有空格,制表符或换行符(假设的缺省值$IFS),或者包含通配符(除非通配符被禁用预先)并移除空元素,并需要GNUtac命令(使用tail -r是GNU系统的稍微更便携式外但某些实现在大输入时失败)。

有没有一种方法可以在不使用数组的情况下可移植地反转 shell 位置参数,即使参数包含空格、换行符或通配符或可能为空也能正常工作?

ImH*_*ere 14

可移植地,不需要数组(只有位置参数)并且可以使用空格和换行符:

flag=''; for a in "$@"; do set -- "$a" ${flag-"$@"}; unset flag; done
Run Code Online (Sandbox Code Playgroud)

例子:

$ set -- one "two 22" "three
> 333" four

$ printf '<%s>' "$@"; echo
<one><two 22><three
333><four>

$ flag=''; for a in "$@"; do set -- "$a" ${flag-"$@"}; unset flag; done

$ printf '<%s>' "$@"; echo
<four><three
333><two 22><one>
Run Code Online (Sandbox Code Playgroud)

的值flag控制 的扩展${flag-"$@"}。当flag被设置时,它膨胀到的值flag(即使它是空的)。所以,当flagflag=''${flag....}扩展到一个空值,并得到由壳去除,因为它是不带引号的。当 未flag设置时, 的值${flag-"$@"}将扩展为 右侧的值-,即 的扩展"$@",因此它成为所有位置参数(引用,不会删除任何空值)。此外,该变量flag最终被擦除(未设置)而不影响以下代码。


Kus*_*nda 10

当不想使用任何数组作为临时存储时,我们可以使用这样一个事实,即for循环始终迭代一组不变的静态元素。从某种意义上说,我们可以将循环本身用作位置参数的临时存储,同时以相反的顺序重建列表。

为了能够做到这一点,我们还需要在第一次迭代时清空列表。下面的代码使用一个简单的标志来检测是否必须这样做。当列表被清空时,标志被切换。

flag=true
for value do
    if "$flag"; then
        set --
        flag=false
    fi

    set -- "$value" "$@"
done
Run Code Online (Sandbox Code Playgroud)

不幸的是,这很慢,因为位置参数列表在每次迭代中都有效地重建set -- some-list设置所有位置参数)。所述bash壳大约需要50秒至逆转1和10000之间的整数,而zsh只需在15秒内。

使用Isaac 的技巧with ${flag-"$@"}(扩展为"$@"仅当flag未设置时)实际上会使整个事情运行得更慢;1 分 50 秒 (!)bash和 25 秒zsh

我猜想这是由于一些实施特殊性在壳上如何进行测试$flag和/或扩大"$@"${flag-"$@"}扩张(外壳有可能会扩大"$@"内部两次?)。


如果允许我们使用数组作为临时存储(这不是标准的,但仍然相当可移植,因为我们经常知道我们正在为哪个 shell 编写脚本),我们可以使用值$#(位置参数的数量)作为在循环位置参数时存储当前值的索引。shift在每次迭代中使用减少此值会产生从数组末尾向开头插入值的效果。

在 中bash,数组从索引 0 开始,并且由于在shift赋值之后出现,最后一个位置参数将存储在索引 1 而不是 0。这对代码的工作方式没有影响bash,它仍然会生成正确的结果,但是它使它也可以工作zsh(默认情况下使用基于 1 的数组索引)。

代码:

tmp=()
for value do
    tmp[$#]=$value
    shift
done

set -- "${tmp[@]}"
Run Code Online (Sandbox Code Playgroud)

使用bashzsh,这使用大约 0.6 秒来反转 1 到 10000 之间的整数。


Sté*_*las 10

从我的这个答案复制到Bash - 使用 glob 打印反转文件列表,以反转位置参数列表 POSIXly:

eval "set -- $(awk 'BEGIN {for (i = ARGV[1]; i; i--) printf " \"${"i"}\""}' "$#")"
Run Code Online (Sandbox Code Playgroud)

或者在几行上稍微更清晰:

eval "set -- $(
  awk '
    BEGIN {
      for (i = ARGV[1]; i; i--)
        printf " \"${" i "}\""
    }' "$#"
)"
Run Code Online (Sandbox Code Playgroud)

这个想法是用来awk帮助生成set -- "${3}" "${2}" "${1}"shell 代码eval来解释例如何时"$@"有 3 个元素。

对于大型列表,它可能比使用 shell 循环快得多,尤其是在每次迭代时重建列表的循环。该awk代码可以由壳循环,提供同样的输出(如@mosvy注释中显示)所取代,但在我与bash5 + gawk4.1的测试中,它仍然是两倍,除了非常短名单慢。

在 中zsh,您将使用Oa显式设计用于反转数组的参数标志:

set -- "${(Oa)@}"
Run Code Online (Sandbox Code Playgroud)

在我的系统上(比@Kusalananda 稍慢),在set $(seq 10000)使用 bash5 + gawk4.2.1获得的位置参数列表上,该eval方法需要0.4 秒,而@Kusalananda需要 1 分钟,@Isaac需要 2 分钟(zshOa方法需要约 2 毫秒)。

shawk从busybox的1.30.1,那些定时成为:分别0.06S,11S,11S。

  • @mtraceur 你可以在普通 shell 中做同样的把戏,而且不会慢很多:`eval "set -- $(i=$#; for a do printf ' "${%d}"' "$i "; i=$((i-1)); done)"`。我经常使用这个技巧,尤其是在构建 `--foo arg1 --foo arg2 ...` cmd 列表时,但在这里回答问题时我并没有得到太多关注(可能是因为常见的货物崇拜教条是 eval = bad ;-)) (2认同)