如何反转 shell 参数?

如何反转 shell 参数?

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

arr=( "$@" )

使用这个答案,反转数组。

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

也可以使用tac

set -- $( printf '%s\n' "$@" | tac )

但是,如果参数包含空格、制表符或换行符(假设默认值为$IFS)或包含通配符(除非预先禁用通配符)并删除空元素,则该情况会中断,并且需要 GNUtac命令(tail -r在 GNU 系统之外使用稍微更便携)但某些实现在大输入时失败)。

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

答案1

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

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

例子:

$ 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>

的值flag控制 的扩展${flag-"$@"}。设置后flag,它会扩展为 的值flag(即使它为空)。因此,当flagis时flag=''${flag....}展开为空值,并且由于未加引号而被 shell 删除。当flag被取消设置时, 的值${flag-"$@"}会扩展为 右侧的值-,即 的扩展"$@",因此它成为所有位置参数(带引号,不会删除任何空值)。此外,该变量flag最终被删除(未设置),不会影响后续代码。

答案2

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

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

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

    set -- "$value" "$@"
done

不幸的是,这非常慢,因为位置参数列表实际上是在每次迭代中重建set -- some-list设置所有位置参数)。 shellbash大约需要 50 秒来反转 1 到 10000 之间的整数,而zsh只需要 15 秒多一点。

使用艾萨克的诡计with (仅在未设置时才${flag-"$@"}扩展)实际上会使整个运行速度变慢; 1 分 50 秒 (!)和 25 秒。"$@"flagbashzsh

我假设这是由于 shell 如何执行测试$flag和/或扩展"$@"扩展的一些实现细节${flag-"$@"}(shell 可能会"$@"在内部扩展两次?)。


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

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

代码:

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

set -- "${tmp[@]}"

使用bashor zsh,大约需要 0.6 秒来反转 1 到 10000 之间的整数。

答案3

复制自我的这个答案Bash - 使用 glob 打印反向文件列表,以 POSIX 方式反转位置参数列表:

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

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

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

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

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

在 中zsh,您可以使用Oa明确设计用于反转数组的参数标志:

set -- "${(Oa)@}"

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

在busybox 1.30.1shawk,这些时间分别变为:0.06s、11s、11s。

相关内容