逐行读取文件并将值赋给变量

953

我有一个如下的 .txt 文件:

Marco
Paolo
Antonio

我想逐行读取文件,并为每行分配一个 .txt 行值到一个变量中。假设我的变量是 $name,流程如下:

  • 从文件中读取第一行
  • 分配$name = "Marco"
  • $name执行一些任务
  • 从文件中读取第二行
  • 分配$name = "Paolo"

4
那些问题可以以某种方式合并吗?两个问题都有一些非常好的答案,强调了问题的不同方面,坏答案在评论中有深入的解释,说明了它们的问题所在。目前来看,您无法从这对问题中的一个答案中获得完整的概述,因此将所有内容放在一个地方而不是分散在2页上会很有帮助。 - Egor Hans
10个回答

1706
以下代码按行读取传递给它的文件:
while IFS= read -r line; do
    echo "Text read from file: $line"
done < my_filename.txt

这是从文件中循环读取行的标准形式。解释如下:

  • IFS=(或IFS='')可以防止前导/尾随空格被修剪。
  • -r 可以防止反斜杠转义字符被解释。

或者您可以将其放入bash文件助手脚本中,示例内容如下:

#!/bin/bash
while IFS= read -r line; do
    echo "Text read from file: $line"
done < "$1"

如果将上述内容保存为文件名为readfile的脚本,则可以按以下方式运行:
chmod +x readfile
./readfile filename.txt

如果文件不是标准的POSIX文本文件(即没有以换行符结尾),则可以修改循环以处理尾随的部分行:
while IFS= read -r line || [[ -n "$line" ]]; do
    echo "Text read from file: $line"
done < "$1"

这里,|| [[ -n $line ]] 防止最后一行没有以 \n 结尾而被忽略(因为当遇到 EOF 时,read 返回非零退出码)。
如果循环内的命令也从标准输入读取,则可以将 read 使用的文件描述符更改为其他值(避免使用 standard file descriptors),例如:
while IFS= read -r -u3 line; do
    echo "Text read from file: $line"
done 3< "$1"

(非Bash shell可能不了解read -u3;请使用read <&3代替。)

34
使用这种方法需要注意一个问题,如果 while 循环内部有任何交互式操作(例如从 stdin 读取输入),它将从 $1 中获取输入。你将没有机会手动输入数据。 - carpie
17
请注意 - 一些命令会打破这个循环(也就是说,它们会打断循环)。例如,没有使用-n标志的ssh会有效地使您退出循环。可能有很好的原因解释这种情况,但在我发现此问题导致我的代码失败之前,我花了一段时间才找到它。 - Alex
8
一句话翻译:使用IFS='' read -r line || [[ -n "$line" ]];命令以逐行读取“filename”文件中的内容并输出。 - Jo Jo
12
@OndraŽižka,这是由于ffmpeg占用了stdin所导致的。在你的ffmpeg命令中添加</dev/null可以解决问题,或者使用循环的其他文件描述符。使用“其他文件描述符”的方法看起来像是while IFS='' read -r line <&3 || [[ -n "$line" ]]; do ...; done 3<"$1" - Charles Duffy
12
在关于给脚本文件添加.sh后缀的建议方面,咕噜声,在UNIX系统中,可执行文件通常根本没有扩展名(例如你并不运行ls.elf),因此在脚本文件中既使用bash shebang(#!/bin/bash)和bash专用工具(例如[[ ]]),又添加扩展名以暗示其与POSIX sh兼容是自相矛盾的。 - Charles Duffy
显示剩余12条评论

347

我建议您在使用read命令时使用-r标志,该标志代表:

-r  Do not treat a backslash character in any special way. Consider each
    backslash to be part of the input line.

我引用自 man 1 read

另外一件事是将文件名作为参数传入。

下面是更新的代码:

#!/usr/bin/bash
filename="$1"
while read -r line; do
    name="$line"
    echo "Name read from file - $name"
done < "$filename"

7
去除行首和行尾的空格。 - barfuin
@Thomas 中间的空格会发生什么?提示:不需要的尝试命令执行。 - kmarsh
1
这对我有用,与被接受的答案相比。 - Neurotransmitter
3
@TranslucentCloud,如果这个方法有效而已被接受的答案无效,我怀疑您使用的shell是sh而不是bash。 在接受的答案中使用的扩展测试命令|| [[ -n "$line" ]] 是一种Bash语法。 也就是说,该语法实际上具有相关意义:即使输入文件的最后一行没有换行符,它也会导致循环继续进行。 如果要以符合POSIX标准的方式执行此操作,您需要使用|| [ -n "$line" ],其中使用[而不是[[ - Charles Duffy
3
尽管如此,为了防止修剪空格,仍需要修改以设置IFS =以供read使用。 - Charles Duffy
显示剩余2条评论

161

使用下面的Bash模板,可以让您逐个读取文件中的值并进行处理。

while read name; do
    # Do what you want to $name
done < filename

20
逐行读取文件"filename"中的内容并输出每行的值。命令为:"while read name; do echo ${name}; done < filename"。 - Jo Jo
6
@CalculusKnight,它之所以“起作用”,是因为您没有使用足够有趣的数据进行测试。尝试包含反斜杠或仅包含 * 的行的内容。 - Charles Duffy
8
@Matthias,最终被证明错误的假设是导致漏洞的最大来源之一,无论是安全相关还是其他方面。我曾经见过的最大数据丢失事件是由于某人假设“绝对不会发生”的情况,导致缓冲区溢出将随机内存转储到用于命名文件的缓冲区中,从而使对哪些名称可能出现的假设产生非常,非常不幸的行为。 - Charles Duffy
5
@Matthias,这一点在这里尤其重要,因为在StackOverflow上展示的代码示例旨在作为教学工具,供人们重用模式以应用到自己的工作中! - Charles Duffy
6
我完全不同意“你只应该为你期望的数据编写代码”的说法。未预料到的情况是出现错误和安全漏洞的地方——处理它们是草率代码和健壮代码之间的区别。当然,这种处理并不需要很复杂——它可以只是“以错误退出”,但如果你根本没有任何处理,那么在意外情况下你的行为就是未定义的。 - Charles Duffy
显示剩余8条评论

97
#! /bin/bash
cat filename | while read LINE; do
    echo $LINE
done

8
没有对其他答案有意见,也许它们更复杂,但我赞同这个答案,因为它简单易读,足够满足我的需求。请注意,为了使它起作用,要读取的文本文件必须以空行结尾(即在最后一行后需要按Enter键),否则最后一行将被忽略。至少这就是我的经历。 - Antônio Medeiros
14
滥用cat命令,是吧? - Brian Agnew
5
引文引用出现问题;变量名不应使用大写,因为这些保留给系统使用。 - tripleee
8
@AntonioViniciusMenezesMedei,此外,我见过有人因为认为这些警告根本与他们无关而遭受财务损失;没有学习良好的实践方法;然后在编写管理重要计费数据备份脚本时继续沿用惯常习惯。学会正确的做事方式非常重要。 - Charles Duffy
7
这里的另一个问题是管道开启了一个新的子shell,也就是说,在循环内设置的所有变量在循环结束后无法读取。 - mxmlnkn
显示剩余6条评论

26

使用:

filename=$1
IFS=$'\n'
for next in `cat $filename`; do
    echo "$next read from $filename" 
done
exit 0
如果您已经不同地设置了IFS,那么您将会得到奇怪的结果。

35
这是一种可怕的方法。请不要使用它,除非你想在意识到之前就遇到全局通配符的问题! - gniourf_gniourf
1
这并不可怕,执行没有中断。 - MUY Belgium
16
@MUYBelgium,你尝试过使用只包含单个 * 的文件吗?无论如何,这是一种反模式不要使用 for 循环读取行 - gniourf_gniourf
3
@OndraŽižka,“read”方法是社区共识的最佳实践方法。您在评论中提到的警告适用于循环运行读取标准输入的命令(例如“ffmpeg”)时,可以使用非标准输入FD进行循环或重定向此类命令的输入来轻松解决。相比之下,解决您的“for”循环方法中的通配符错误意味着需要进行(然后需要撤消)全局shell设置更改。 - Charles Duffy
2
@OndraŽižka,此外,您在此处使用的“for”循环方法意味着在循环开始执行之前必须读取所有内容,即使您已经禁用了globbing,如果您正在循环处理千兆字节的数据,则完全无法使用;“while read”循环只需要存储一行数据,因此它可以在生成内容的子进程仍在运行时开始执行(因此可用于流式传输),并且具有有限的内存消耗。 - Charles Duffy
显示剩余5条评论

22

许多人发布的解决方案过于优化。我不认为它是错误的,但我谦虚地认为一个不那么优化的解决方案将更加可取,以便让每个人都能轻松理解这是如何工作的。以下是我的建议:

#!/bin/bash
#
# This program reads lines from a file.
#

end_of_file=0
while [[ $end_of_file == 0 ]]; do
  read -r line
  # the last exit status is the 
  # flag of the end of file
  end_of_file=$?
  echo $line
done < "$1"

12

如果您需要同时处理输入文件和用户输入(或来自标准输入的任何其他内容),请使用以下解决方案:

#!/bin/bash
exec 3<"$1"
while IFS='' read -r -u 3 line || [[ -n "$line" ]]; do
    read -p "> $line (Press Enter to continue)"
done

基于被接受的答案bash-hackers重定向教程

在这里,我们为作为脚本参数传递的文件打开文件描述符3,并告诉read使用此描述符作为输入(-u 3)。 因此,我们将默认的输入描述符(0)附加到终端或其他输入源,可以读取用户输入。


如果您想接受管道输入,请执行exec 3<&0 - Orwellophile

7

为了正确处理错误:

#!/bin/bash

set -Ee    
trap "echo error" EXIT    
test -e ${FILENAME} || exit
while read -r line
do
    echo ${line}
done < ${FILENAME}

请您能否加上一些解释说明? - Tyler Christian
很遗憾,它错过了文件中的最后一行。 - ungalcrys
由于缺乏引用,同时也因为存在通配符,它会对行进行混淆 -- 正如BashPitfalls #14中所描述的那样。 - Charles Duffy

1
使用bash中的IFS(内部字段分隔符)工具,定义用于将行分隔成标记的字符,默认情况下包括//。
步骤1:加载文件数据并插入列表中:
# declaring array list and index iterator
declare -a array=()
i=0

# reading file in row mode, insert each line into array
while IFS= read -r line; do
    array[i]=$line
    let "i++"
    # reading from file path
done < "<yourFullFilePath>"

步骤2:现在迭代并打印输出:

:现在迭代并打印输出:

for line in "${array[@]}"
  do
    echo "$line"
  done

在数组中输出特定索引:访问数组中的变量:

echo "${array[0]}"

你需要引用你的变量,array[i]="$line" - ErikE
array[i++]=$line without the need for let - Jetchisel

-9
以下代码将仅打印文件的内容:
cat $Path/FileName.txt

while read line;
do
echo $line     
done

6
这个答案并没有比现有的答案更有价值,因为存在一个打字错误/漏洞导致无法工作,而且在许多方面出现了问题。 - Konrad Rudolph

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