![如何反转 shell 参数?](https://linux22.com/image/13122/%E5%A6%82%E4%BD%95%E5%8F%8D%E8%BD%AC%20shell%20%E5%8F%82%E6%95%B0%EF%BC%9F.png)
我知道可以"$@"
使用数组来反转:
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
(即使它为空)。因此,当flag
is时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 秒。"$@"
flag
bash
zsh
我假设这是由于 shell 如何执行测试$flag
和/或扩展"$@"
扩展的一些实现细节${flag-"$@"}
(shell 可能会"$@"
在内部扩展两次?)。
如果允许我们使用数组作为临时存储(这不会是标准,但仍然相当便携的因为我们通常知道我们正在为哪个 shell 编写脚本),所以我们可以使用该值$#
(位置参数的数量)作为索引,在循环位置参数时存储当前值。在每次迭代中减少此值shift
会产生从数组末尾向开头插入值的效果。
在 中bash
,数组从索引 0 开始,并且由于shift
出现在赋值之后,因此最后一个位置参数将存储在索引 1 而不是 0 处。这对于代码在 中的工作方式没有影响bash
,它仍然会生成正确的结果,但是它使得它也可以工作zsh
(默认使用基于 1 的数组索引)。
代码:
tmp=()
for value do
tmp[$#]=$value
shift
done
set -- "${tmp[@]}"
使用bash
or 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 分钟(zsh
的Oa
方法大约需要 2 毫秒)。
在busybox 1.30.1sh
中awk
,这些时间分别变为:0.06s、11s、11s。