![创建并附加到数组,mapfile 与 arr+=(input) 相同,还是我遗漏了什么?](https://linux22.com/image/221699/%E5%88%9B%E5%BB%BA%E5%B9%B6%E9%99%84%E5%8A%A0%E5%88%B0%E6%95%B0%E7%BB%84%EF%BC%8Cmapfile%20%E4%B8%8E%20arr%2B%3D(input)%20%E7%9B%B8%E5%90%8C%EF%BC%8C%E8%BF%98%E6%98%AF%E6%88%91%E9%81%97%E6%BC%8F%E4%BA%86%E4%BB%80%E4%B9%88%EF%BC%9F.png)
有mapfile
没有好处超过的情况arr+=(input)
?
简单的例子
映射文件数组名称,arr:
mkdir {1,2,3}
mapfile -t arr < <(ls)
declare -p arr
输出:
declare -a arr=([0])="1" [1]="2" [2]="3")
编辑:
更改了以下标题;正文具有y
数组名称,但标题具有arr
名称,这可能会导致混乱。
y+=(输入)
IFS=$'\n'
y+=($(ls))
declare -p y
输出:
declare -a y=([0])="1" [1]="2" [2]="3")
我认为一个优点mapfile
是你不必担心分词。
对于另一种方式,您可以通过设置来避免分词IFS=$'\n'
,但对于本例来说,无需担心。
第二个例子似乎更容易写,我错过了什么吗?
答案1
即使在之后,它们也根本不是同一件事IFS=$'\n'
。
特别是在 bash 中(尽管该语法是从 zsh 借用的):
arr=( $(cmd) )
(arr+=( $(cmd) )
将用于附加数组中的元素;因此将与keys=( -1 "${!arr[@]}" ); readarray -tO "$(( ${keys[@]: -1} + 1))" arr < <(cmd)
²)进行比较。
做:
- 在子 shell 中运行
cmd
,其标准输出在管道的写入端打开。 - 同时,父 shell 进程从管道的另一端读取数据并:
- 删除 NUL 字符和尾随换行符
- 根据
$IFS
特殊变量的内容分割结果字符串。对于其中的$IFS
空白字符(例如换行符),行为更加复杂:- 前导和尾随的被删除(在换行符的情况下,它们已被命令替换删除,如上所示)
- 一个或多个序列被视为一分隔器。例如, 的输出
printf '\n\n\na\n\n\nb\n\n\n'
仅分为两个元素:a
和b
。
- 然后,这些单词中的每一个都将受到文件名生成(又名通配符)的影响,其行为受到许多选项的影响,包括
noglob
,nullglob
,failglob
,extglob
,globasciiranges
,glabstar
,nocaseglob
。这适用于那些包含*
、?
、[
、 和 某些 bash 版本的单词,如果启用,\
则更多。extglob
- 然后将得到的单词作为元素分配给
$arr
数组。
例子:
bash-5.1$ touch x '\x' '?x' aX $'foo\n\n\n\n*'
bash-5.1$ IFS=$'\n'
bash-5.1$ ls | cat
aX
foo
*
?x
\x
x
bash-5.1$ arr=( $(ls) )
bash-5.1$ typeset -p arr
declare -a arr=([0]="aX" [1]="foo" [2]="aX" [3]=$'foo\n\n\n\n*' [4]="?x" [5]="\\x" [6]="x" [7]="?x" [8]="\\x" [9]="\\x" [10]="x")
正如您所看到的,该$'foo\n\n\n\n*'
文件被拆分为foo
和 ,*
并*
扩展为当前工作目录中的文件列表,这解释了为什么我们同时得到foo
和$'foo\n\n\n\n*'
,同样?x
解释了为什么我们得到\x
(显示为"\\x"
) 3 次,因为有\x
行的输出与和ls
都匹配。*
?x
使用 bash 5.0,我们得到:
bash-5.0$ arr=( $(ls) )
bash-5.0$ typeset -p arr
declare -a arr=([0]="aX" [1]="foo" [2]="aX" [3]=$'foo\n\n\n\n*' [4]="?x" [5]="\\x" [6]="x" [7]="?x" [8]="\\x" [9]="x" [10]="x")
在该版本中,反斜杠只有两次但三倍,即使后面没有跟随一个通配符,反斜杠也是一个通配符,因此作为\x
通配符匹配。x
\x
x
之后shopt nocaseglob
,我们得到:
bash-5.1$ shopt -s nocaseglob
bash-5.1$ arr=( $(ls) )
bash-5.1$ typeset -p arr
declare -a arr=([0]="aX" [1]="foo" [2]="aX" [3]=$'foo\n\n\n\n*' [4]="?x" [5]="\\x" [6]="x" [7]="aX" [8]="?x" [9]="\\x" [10]="\\x" [11]="x")
显示aX
3 次,因为它?x
也匹配。
后shopt -s failglob
:
bash-5.0$ shopt -s failglob
bash-5.0$ arr=( $(printf '\\z\n') )
bash: no match: \z
bash-5.0$ arr=( $(printf 'WTF\n?') )
bash: no match: WTF?
和arr=( $(echo '/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*') )
使系统无法使用几分钟后内存不足。
因此,总而言之,IFS=$'\n'; arr=( $(cmd) )
不会将 的输出行存储cmd
在数组中,而是将其输出的非空行扩展所产生的文件名cmd
视为 glob 模式。
使用mapfile
或较少误导性的readarray
别名:
readarray -t arr < <(cmd)
- 如上所述,
cmd
在子 shell 中运行,其标准输出在管道的写入端打开。 - the
<(...)
被扩展为类似/dev/fd/63
或/proc/self/fd/63
where63
is a file descriptor of theparent shell open on the read end of the pipeline. - 使用
<
的重定向缩写0<
,打开 /dev/fd/63 以在 fd 0 上读取,这意味着 的 stdinreadarray
也将是该管道的读取端。 readarray
从该管道读取每一行(同时cmd
写入它),丢弃行分隔符 (-t
),并将其存储在$arr
.
所以最后$arr
,假设cmd
输出没有 NUL 将包含输出的每一行的内容cmd
,无论它们是否为空或是否包含全局字符。
以上面的例子:
bash-5.1$ readarray -t arr < <(ls)
bash-5.1$ typeset -p arr
declare -a arr=([0]="aX" [1]="foo" [2]="" [3]="" [4]="" [5]="*" [6]="?x" [7]="\\x" [8]="x")
这与我们在之前的输出中看到的一致ls | cat
,但如果目的是获取当前工作目录中的文件列表,那么这仍然是错误的。的输出ls
无法进行后处理,除非您使用 GNU 实现的某些扩展,ls
例如--quoting-style=shell-always
或--zero
最新版本(9.0 或更高版本):
bash-5.2$ readarray -td '' arr < <(ls --zero)
bash-5.2$ typeset -p arr
declare -a arr=([0]="aX" [1]=$'foo\n\n\n\n*' [2]="?x" [3]="\\x" [4]="x")
这次,将 NUL限制记录readarray
的内容存储到 中。不能用于,因为不能在其变量中存储 NUL。d
$arr
IFS=$'\0'
bash
bash
或者:
bash-5.1$ eval "arr=( $(ls --quoting-style=shell-always) )"
bash-5.1$ typeset -p arr
declare -a arr=([0]="aX" [1]=$'foo\n\n\n\n*' [2]="?x" [3]="\\x" [4]="x")
无论如何,将当前工作目录中的文件列表放入数组的正确方法是:
shopt -s nullglob
arr=( * )
仅ls --zero
当您希望列表按大小或修改时间排序时,您才会使用 bash glob(与 zsh 相反)无法做到的事情。
如:
桀骜 | 最近的 GNU bash + GNU coreutils |
---|---|
new_to_old=( *.txt(Nom) ) |
readarray -td '' new_to_old < <(ls -td --zero -- *.txt) |
four_largest=( *.txt(NOL[1,4]) ) |
readarray -td '' four_largest < <(ls -tdrS --zero -- *.txt | head -zn4) |
a=($(cmd))
和之间的另一个区别readarray < <(cmd)
是退出状态,前者是cmd
,后者是readarray
。使用最新版本的,您可以使用 获取后者bash
的存在状态。cmd
wait "$!"; cmd_status=$?
¹arr=( ... )
语法来自 zsh(bash 直到 1996 年 2.0 才出现数组),但请注意,在 zsh 中,命令替换虽然也会删除尾随换行符并受$IFS
-stripping 约束,但不会丢弃 NUL(NUL 甚至在那里的默认值$IFS
)并且不像其他类似 Bourne 的 shell 那样受到通配符的影响,这有助于使其成为一个更安全的 shell。
²readarray
又名mapfile
没有追加模式,但在最近的版本中,您可以告诉它第一个元素的索引,从哪里开始存储元素,如下-O
所示。要找出 bash 中最后一个元素的索引(其中数组像 ksh 中一样稀疏!),这是非常困难的。在这里附加cmd
to的输出行$arr
,而不是那些非常复杂的代码,您不妨将这些行读入临时数组 withreadarray -r tmp < <(cmd)
并将元素附加到$arr
with arr+=( "${tmp[@]}" )
。另请注意,如果arr
变量被声明为标量或关联,则这些方法之间的行为会有所不同。