在Bash中从文本文件创建一个数组

120

一个脚本获取URL地址,解析需要的字段,并将其输出重定向到文件file.txt中。每次找到一个字段时,输出会保存在新行上。

file.txt

A Cat
A Dog
A Mouse 
etc... 

我希望从file.txt中读取数据,在一个新的脚本中将每一行作为一个字符串变量放入数组中。目前我已经尝试了:

#!/bin/bash

filename=file.txt
declare -a myArray
myArray=(`cat "$filename"`)

for (( i = 0 ; i < 9 ; i++))
do
  echo "Element [$i]: ${myArray[$i]}"
done
当我运行这个脚本时,空格导致单词被分开,而不是得到期望的输出。

期望的输出

Element [0]: A Cat 
Element [1]: A Dog 
etc... 

我最终得到了这个:

实际输出

Element [0]: A 
Element [1]: Cat 
Element [2]: A
Element [3]: Dog 
etc... 

我应该如何调整下面的循环,以使每行字符串与数组中的每个变量一一对应?

如何调整下面的循环,以保证每行完整的字符串都能与数组中的每一个变量一一对应?


5
这就是Bash FAQ 001 的内容。同时,也包括Bash FAQ 005中数组主题的此部分 - Etan Reisner
1
我会将这个链接视为 https://dev59.com/B2gu5IYBdhLWcg3wWVyF 的重复,但那里的被接受答案很糟糕。 - Charles Duffy
Etan,非常感谢您如此快速准确的回复!我曾尝试在论坛中搜索我的问题,但没有想到要在stackoverflow上查找常见问题解答。mapfile命令完全满足了我的需求!再次感谢 :)请参阅第2.1节中的答案。 - TheDailyToast
2
(由于我们这里有一个更好的被接受的答案,所以请将链接设置为相反方向。) - Charles Duffy
7个回答

150
使用 mapfile 命令:
mapfile -t myArray < file.txt

错误在于使用for循环--遍历文件的行的惯用方法是:
while IFS= read -r line; do echo ">>$line<<"; done < file.txt

更多详细信息请参见BashFAQ/005


7
由于这被推广为规范的问答,您还可以包括链接中提到的内容:while IFS= read -r; do lines+=("$REPLY"); done <file - fedorqui
11
bash 4.x之前的版本中不存在mapfile命令。 - ericslaw
16
Bash 4已经推出了大约5年时间。请升级。 - glenn jackman
10
尽管 bash 4 于 2009 年发布,但 @ericslaw 的评论仍然具有参考价值,因为许多机器仍然配备 bash 3.x (并且只要 bash 在 GPLv3 下发布,它们就不会升级)。如果你关心可移植性,这是一个重要的事情需要注意。 - De Novo
18
问题不在于开发人员无法安装升级版本,而是开发人员应该意识到,使用 mapfile 的脚本需要额外的步骤才能在许多机器上正常运行。@ericslaw macs 在可预见的未来将继续使用 bash 3.2.57。更近期的版本使用了一项许可证,要求苹果公司分享或允许他们不想分享或允许的内容。 - De Novo
显示剩余4条评论

43

mapfilereadarray(这两者是同义词)在Bash版本4及以上可用。如果您使用较旧的Bash版本,则可以使用循环将文件读入数组中:

arr=()
while IFS= read -r line; do
  arr+=("$line")
done < file

如果文件的最后一行是不完整的(缺少换行符),你可以使用以下替代方法:

arr=()
while IFS= read -r line || [[ "$line" ]]; do
  arr+=("$line")
done < file

相关:


我发现我必须在 IFS= read -r line || [[ "$line" ]] 周围加上括号才能使其正常工作。否则,它的表现非常好! - Tatiana Racheva
1
@TatianaRacheva:难道不是在 do 前面缺少了分号吗? - codeforester

11

您也可以这样做:

oldIFS="$IFS"
IFS=$'\n' arr=($(<file))
IFS="$oldIFS"
echo "${arr[1]}" # It will print `A Dog`.

注意:

文件名扩展仍然会发生。例如,如果有一行包含文字*,它将扩展到当前文件夹中的所有文件。因此,只有在您的文件不涉及此类情况时才使用它。


有没有办法在仅临时设置IFS的情况下(使其在此命令后恢复其原始值),同时仍保留对arr的赋值? - Hugues
1
请注意,文件名扩展仍然会发生;例如:IFS=$'\n' arr=($(echo 'a 1'; echo '*'; echo 'b 2')); printf "%s\n" "${arr[@]}" - Hugues
@Hugues:是的,文件名扩展仍然会发生。我会添加这一信息。谢谢。 - Jahid
抱歉,我不同意。IFS=... command 不会改变当前 shell 中的 IFS。然而,IFS=... other_variable=...(没有任何命令)会同时改变当前 shell 中的 IFSother_variable - Hugues
@Hugues:你又是对的,抱歉...我用保存和重置的方式解决了它。 - Jahid
1
谢谢!这个方法可行;不幸的是,没有比arr=符号更简单的方式,因为我喜欢它(相对于mapfile/readarray)。 - Hugues

11

使用mapfile或read -a

在编写代码时,请始终使用 shellcheck 进行检查。它通常会给出正确的答案。在这种情况下,SC2207 涵盖了将一个空格分隔或换行分隔的值读入数组中的方法。

不要这样做

array=( $(mycommand) )

按换行符分隔值的文件

mapfile -t array < <(mycommand)

用空格分隔的值的文件

IFS=" " read -r -a array <<< "$(mycommand)"

shellcheck页面将为您解释为什么这被认为是最佳做法。

5

您可以直接从文件中读取每一行,并将其赋值给一个数组。

#!/bin/bash
i=0
while read line 
do
        arr[$i]="$line"
        i=$((i+1))
done < file.txt

1
你如何访问数组? - hola

0

请确保将内部文件分隔符(IFS)变量设置为 $'\n',以便它不会将每个单词放入新的数组条目中。

#!/bin/bash

# move all 2020 - 2022 movies to /backup/movies
# put list into file 1 line per dir

# dirs are  "movie name (year)/"
ls | egrep 202[0-2]  > 2020_movies.txt

OLDIFS=${IFS}
  
IFS=$'\n'    #fix separator

declare -a MOVIES  # array for dir names

MOVIES=( $( cat "${1}" ) )  // load into array 

for M in ${MOVIES[@]} ; do
        echo "[${M}]"
        if [ -d "${M}" ] ; then  # if dir name

                mv -v "$M" /backup/movies/
        fi

done

IFS=${OLDIFS}  # restore standard separators
               # not essential as IFS reverts when script ends

#END

0

这个答案建议使用

mapfile -t myArray < file.txt

我为mapfile制作了一个shim,如果你想在bash < 4.x上使用mapfile,它可以派上用场。如果你的bash版本>= 4.x,则使用现有的mapfile命令。

目前,只有选项-d-t可用。但对于上述命令来说应该足够了。我只在macOS上进行了测试。在macOS Sierra 10.12.6上,系统bash是3.2.57(1)-release。因此,这个shim可能会很有用。你也可以使用homebrew更新你的bash,自己构建bash等。

它使用this technique在一个调用堆栈中设置变量。


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