具有空格元素的Bash数组

193

我正在尝试在Bash中构建一个由我的相机文件名组成的数组:

FILES=(2011-09-04 21.43.02.jpg
2011-09-05 10.23.14.jpg
2011-09-09 12.31.16.jpg
2011-09-11 08.43.12.jpg)

正如您所看到的,每个文件名中间都有一个空格。

我已经尝试将每个名称用引号括起来,并使用反斜杠转义空格,但两者都不起作用。

当我尝试访问数组元素时,它继续将空格视为元素分隔符。

我该如何正确地捕获带有空格的文件名?


你试过以老派的方式添加文件吗?像 FILES[0] = ... 这样?(编辑:我刚刚试了一下,不起作用。有趣)。 - Dan Fego
POSIX:https://dev59.com/questions/3E7Sa4cB1Zd3GeqP0xsq - Ciro Santilli OurBigBook.com
所有这里的答案在我使用Cygwin时都会出问题。如果文件名中有空格,它会做一些奇怪的事情。我通过在文本文件列表中创建一个“数组”,并迭代文件中的行来解决它:格式化混淆了括号中命令周围的反引号:IFS=""; array=(find . -maxdepth 1 -type f -iname \*.$1 -printf '%f\n'); for element in ${array[@]}; do echo $element; done - Alex Hall
14个回答

161

我认为问题可能部分地与您访问元素的方式有关。如果我只是简单地使用for elem in $FILES,我会像你一样遇到同样的问题。但是,如果我通过索引访问数组,就像这样,如果我使用数字或转义字符添加元素,则它可以正常工作:

for ((i = 0; i < ${#FILES[@]}; i++))
do
    echo "${FILES[$i]}"
done

下面任何一种声明 $FILES 都可以使用:

FILES=(2011-09-04\ 21.43.02.jpg
2011-09-05\ 10.23.14.jpg
2011-09-09\ 12.31.16.jpg
2011-09-11\ 08.43.12.jpg)
或者
FILES=("2011-09-04 21.43.02.jpg"
"2011-09-05 10.23.14.jpg"
"2011-09-09 12.31.16.jpg"
"2011-09-11 08.43.12.jpg")
或者
FILES[0]="2011-09-04 21.43.02.jpg"
FILES[1]="2011-09-05 10.23.14.jpg"
FILES[2]="2011-09-09 12.31.16.jpg"
FILES[3]="2011-09-11 08.43.12.jpg"

10
注意,在使用数组元素时应使用双引号(例如,echo "${FILES[$i]}")。对于 echo 来说没有关系,但对于将其用作文件名的任何内容来说都会有影响。 - Gordon Davisson
34
当您可以使用for f in "${FILES[@]}"循环遍历元素时,不必循环遍历索引。 - Mark Edgar
12
@MarkEdgar,当数组成员包含空格时,我在使用 for f in ${FILES[@]} 时遇到了问题。似乎整个数组被重新解释了一遍,空格使您现有的成员分裂成两个或更多元素。看起来 " " 非常重要。 - Michael Shaw
1
for ((i = 0; i < ${#FILES[@]}; i++)) 语句中,井号 (#) 符号的作用是什么? - Michal Vician
4
我在六年前已回答过这个问题,但我认为它的目的是获取数组FILES中元素的数量 - Dan Fego
显示剩余2条评论

124

你访问数组元素的方式可能有问题。下面是正确的访问方式:

for elem in "${files[@]}"
...

来自Bash手册:

使用${name[subscript]}可以引用数组中的任何元素。... 如果subscript为@或*,则该单词扩展为name的所有成员。只有当单词出现在双引号中时,这些下标才有所不同。如果单词被双引号引起来,${name[*]}会将每个数组成员的值连接成一个单词,并用IFS特殊变量的第一个字符分隔,${name[@]}将name的每个元素扩展为一个独立的单词

当访问单个成员时,当然也应该使用双引号。

cp "${files[0]}" /tmp

3
这一堆中,这是最清洁、最优雅的解决方案,不过应该重申数组中定义的每个元素都应该加上引号。 - maverick
虽然Dan Fego的回答是有效的,但这是处理元素中空格的更习惯用法。 - Daniel Zhang
3
来自其他编程语言的人,可能很难理解那段摘录中的术语。加上语法也令人困惑。如果您能详细解释一下,我将不胜感激。特别是其中的“扩展为一个单词,该单词的值为每个数组成员用IFS特殊变量的第一个字符分隔”的部分。 - Jodes
2
是的,同意双引号解决了这个问题,这比其他解决方案更好。进一步解释一下 - 大多数其他解决方案都缺少双引号。你使用了正确的写法:for elem in "${files[@]}",而他们使用了 for elem in ${files[@]} - 所以空格会混淆扩展,导致 for 命令尝试对单独的单词进行操作。 - arntg
这在我使用的macOS 10.14.4上不起作用,它使用的是“GNU bash,版本3.2.57(1)-release(x86_64-apple-darwin18)”。也许是旧版bash中的一个错误? - Mark
IFS=$'\n' 重要提示 - IFS=内部字段分隔符,更改数组的断点位置!!!空格不会被视为断点。 - user2718593

59
你需要使用IFS来停止空格作为元素分隔符。
FILES=("2011-09-04 21.43.02.jpg"
       "2011-09-05 10.23.14.jpg"
       "2011-09-09 12.31.16.jpg"
       "2011-09-11 08.43.12.jpg")
IFS=""
for jpg in ${FILES[*]}
do
    echo "${jpg}"
done

如果你想基于.分隔,那么只需要设置IFS="."。


5
我不得不将IFS = ""移动到数组分配之前,但这是正确的答案。 - rob
我正在使用多个数组来解析信息,但IFS = ""的效果只会在其中一个数组中起作用。一旦我使用IFS = "",所有其他数组都无法正确解析。有什么提示吗? - Paulo Pedroso
Paulo,你可以在这里看到另一个答案,它可能更适合你的情况:https://dev59.com/xmox5IYBdhLWcg3wnllI#9089186。虽然我没有尝试过IFS="",但它似乎可以优雅地解决问题 - 但是你的示例说明了为什么在某些情况下可能会遇到问题。可能可以在单行上设置IFS="",但这可能比其他解决方案更令人困惑。 - arntg
它在我的bash上也起作用了。感谢@Khushneet,我找了半个小时... - csonuryilmaz
太好了,这个页面上唯一有效的答案。但是我还必须在数组构建之前移动 IFS="" - pkamb
这是什么邪恶的黑魔法?!(显然是这个 - Code Commander

17

我同意其他人的看法,很可能是您访问元素的方式有问题。在数组赋值中引用文件名是正确的:

FILES=(
  "2011-09-04 21.43.02.jpg"
  "2011-09-05 10.23.14.jpg"
  "2011-09-09 12.31.16.jpg"
  "2011-09-11 08.43.12.jpg"
)

for f in "${FILES[@]}"
do
  echo "$f"
done

在任何形式的数组周围使用双引号,例如"${FILES[@]}",将数组拆分为每个数组元素一个单词。它不会进行任何其他的单词拆分。

使用"${FILES[*]}"也有特殊的含义,但它使用$IFS的第一个字符将数组元素连接起来,生成一个单词,这可能不是您想要的。

使用裸的${array[@]}${array[*]}将展开结果进一步进行单词拆分,因此您将得到在空格(和$IFS中的任何其他字符)上拆分的单词,而不是每个数组元素一个单词。

如果您对此不太清楚并且不想担心单词拆分,则使用C风格的for循环也可以:

for (( i = 0; i < ${#FILES[@]}; i++ ))
do
  echo "${FILES[$i]}"
done

12

这个问题之前已经得到了回答,但那个回答有点简短,并且手册摘录有点晦涩难懂。我想提供一个完整的实例来演示如何实际操作。

如果没有加引号,数组就会扩展为由空格分隔的字符串。

for file in ${FILES[@]}; do

扩展为

for file in 2011-09-04 21.43.02.jpg 2011-09-05 10.23.14.jpg 2011-09-09 12.31.16.jpg 2011-09-11 08.43.12.jpg ; do

但是如果您引用扩展,bash会在每个单元周围添加双引号,因此:

for file in "${FILES[@]}"; do

扩展为

for file in "2011-09-04 21.43.02.jpg" "2011-09-05 10.23.14.jpg" "2011-09-09 12.31.16.jpg" "2011-09-11 08.43.12.jpg" ; do

简单来说,无论何时使用[@]代替[*]并在需要保留空格时引用数组扩展是个好习惯。

更进一步地解释一下,另一个回答中的手册说明了如果未被引用,$*$@的行为是相同的,但是它们在被引用时是不同的。因此,考虑到:

array=(a b c)

然后$*$@都被扩展为

a b c

"$*"会扩展为

"a b c"

"$@"会被扩展为:

"a" "b" "c"

9
如果你的数组长这样: #!/bin/bash
Unix[0]='Debian'
Unix[1]="Red Hat"
Unix[2]='Ubuntu'
Unix[3]='Suse'

for i in $(echo ${Unix[@]});
    do echo $i;
done

你会得到:
Debian
Red
Hat
Ubuntu
Suse

我不知道为什么,但是循环会将空格拆分成独立的项,即使您用引号括起来也是如此。

为了解决这个问题,您可以通过调用索引而不是数组中的元素来解决,这将获取完整的字符串并用引号括起来。一定要用引号括起来!

#!/bin/bash

Unix[0]='Debian'
Unix[1]='Red Hat'
Unix[2]='Ubuntu'
Unix[3]='Suse'

for i in $(echo ${!Unix[@]});
    do echo ${Unix[$i]};
done

然后您将获得:
Debian
Red Hat
Ubuntu
Suse

4

对于那些喜欢使用单行模式设定数组的人,可以使用换行符临时更改 IFS 以避免转义。

OLD_IFS="$IFS"
IFS=$'\n'

array=( $(ls *.jpg) )  #save the hassle to construct filename

IFS="$OLD_IFS"

3
#! /bin/bash

renditions=(
"640x360    80k     60k"
"1280x720   320k    128k"
"1280x720   320k    128k"
)

for z in "${renditions[@]}"; do
    echo "$z"
    
done

输出

640x360 80k 60k

1280x720 320k 128k

1280x720 320k 128k

`


这个答案与已经给出的答案不同/更好吗? - SiKing
是的,正如您所看到的输出,renditions数组中的每个元素都是带有空格的字符串,并且我们在没有引号的情况下循环遍历它${renditions[@]},那么空格将被视为元素分隔符,因此我在${renditions[@]}周围包装双引号,这给了我上面的输出。 - subham prasad

3

虽然这并不是对原问题中引用/转义问题的确切答案,但可能更有用于问题提出者的内容:

unset FILES
for f in 2011-*.jpg; do FILES+=("$f"); done
echo "${FILES[@]}"

当然,表达式必须根据具体要求进行调整(例如,对于所有图片使用*.jpg或仅限某一天的图片使用2001-09-11*.jpg)。


2
逃逸字符起作用了。
#!/bin/bash

FILES=(2011-09-04\ 21.43.02.jpg
2011-09-05\ 10.23.14.jpg
2011-09-09\ 12.31.16.jpg
2011-09-11\ 08.43.12.jpg)

echo ${FILES[0]}
echo ${FILES[1]}
echo ${FILES[2]}
echo ${FILES[3]}

输出:

$ ./test.sh
2011-09-04 21.43.02.jpg
2011-09-05 10.23.14.jpg
2011-09-09 12.31.16.jpg
2011-09-11 08.43.12.jpg

引用字符串也会产生相同的输出。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接