本文会通过示例进行介绍文件读取的各种应用场景和代码案例,帮你快速了解shell的各种文件读取用法和学会最佳实践。
按行读取文件
按行读取文件代码示例:
while IFS= read -r line; do
printf '%s\n' "$line"
done < "$file"
读取时进行简单处理
-r 选项用于 read 命令,可以防止反斜杠的解释(通常用作反斜杠换行符号,以便在多行上继续或转义分隔符)。如果不使用此选项,输入中的任何未转义的反斜杠都将被丢弃。在使用 read 命令时,几乎总是应该使用 -r 选项。
最常见的例外是在使用 -e 选项时,该选项使用 Readline 从交互式 shell 中获取行。在这种情况下,制表符补全将添加反斜杠来转义空格等字符,而您不希望它们被字面包含在变量中。但是,这种情况永远不会用于逐行读取任何内容,因此在这种情况下仍应始终使用 -r 选项。
默认情况下,read 命令会修改每一行的内容,即删除所有前导和尾随空格字符(如果存在于 IFS 中,则包括空格和制表符)。如果不需要这样的行为,可以清除 IFS 变量,就像上面的示例一样。如果需要将行首和行尾的空格删除,则可以不清除 IFS 变量。
# Leading/trailing whitespace trimming.
while read -r line; do
printf '%s\n' "$line"
done < "$file"
如果您想要操作每一行中的各个字段,可以向 read 命令提供额外的变量。
# 输入包含三列,通过空格或者tab进行分割
while read -r first_name last_name phone; do
# 打印第二列
printf '%s\n' "$last_name"
done < "$file"
如果使用了特殊的分割符,可以通过IFS进行设置
# 从 /etc/passwd提取用户名
while IFS=: read -r user pass uid gid gecos home shell; do
printf '%s: %s\n' "$user" "$shell"
done < /etc/passwd
对于制表符分隔的文件,请使用 IFS=‘\t\t’ 解决方法在 Bash 中无法工作)。
您不一定需要知道每行输入包含多少个字段。如果提供的变量数量超过字段数,则多余的变量将为空。如果提供的变量数少于字段数,则最后一个变量将获得在前面的变量都被满足后剩余的所有字段。例如,
# Bash
read -r first last junk <<< 'Bob Smith 123 Main Street Elk Grove Iowa 123-555-6789'
# first = "Bob", last = "Smith".
# junk = 123 Main Street Elk Grove Iowa 123-555-6789
有些人使用下划线_ 作为“占位符变量”来忽略字段。它也可以在单个 read 命令中使用多次:
# Bash
read -r _ _ first middle last _ <<< "$record"
# 跳过前两个字段读取后续的.
# 下划线可以忽略多个
需要注意的是,只有在 Bash 中使用 _ 才能保证能够正常工作。许多其他 shell 使用 _ 来表示其他用途,这可能会导致该脚本失效。因此最好选择一个在脚本中没有其他用途的唯一变量,尽管 _ 是 Bash 中的常见约定。
如果期望屏蔽掉#开头的注释
# Bash
while read -r line; do
[[ $line = #* ]] && continue
printf '%s\n' "$line"
done < "$file"
输入块
重定向 < “file 中保存的文件名中读取。如果您希望使用文本路径名而不是变量,也可以这样做。如果您的输入源是脚本的标准输入,则根本不需要进行重定向。
如果您的输入源是一个变量/参数的内容,Bash 可以使用 here string 迭代其行:
while IFS= read -r line; do
printf '%s\n' "$line"
done <<< "$var"
在 Shell 脚本中,Heredocs 和 Herestrings 可以用来方便地将文本传递给命令或者将文本写入文件。
Heredocs 是一种用于将多行文本传递给命令或脚本的方法。使用 Heredocs 可以将文本嵌入到 Shell 脚本中,而无需将文本存储在外部文件中。Heredocs 使用 << 运算符,后跟一个自定义的标识符来标识文本块的开头。然后在接下来的行中输入文本,直到行中包含该标识符,并且不希望将其包含在文本中为止。例如:
cat <<END This is a Heredoc example. It allows you to embed multiple lines of text without having to escape any characters. END
在上面的示例中,<<END 表示 Heredocs 的开始,END 是自定义的标识符。然后在接下来的行中,输入要嵌入的文本,直到再次输入 END 为止。在此示例中,cat 命令将输出 Heredocs 中的文本。
Herestrings 是一种将文本传递给命令或脚本的方法,类似于 Heredocs。但是,它们使用单行文本,而不是多行文本,并且不需要自定义标识符。Herestrings 使用 <<< 运算符,后跟一个字符串来传递文本。例如:
grep foo <<< “This is a line of text containing the word foo.”
在上面的示例中,<<< 运算符将字符串 “This is a line of text containing the word foo.” 传递给 grep 命令。grep 命令将输出包含字符串 foo 的行。
Herestrings 和 Heredocs 都是非常有用的 Shell 编程工具,可以大大简化 Shell 脚本中的文本处理。
在任何 Bourne 类型的 shell 中都可以使用“here document”来执行相同的操作(尽管 read -r 是 POSIX 标准,而不是 Bourne shell)。
while IFS= read -r line; do
printf '%s\n' "$line"
done <<EOF
$var
EOF
除了从普通文件中读取外,还可以从命令中读取:
some command | while IFS= read -r line; do
printf '%s\n' "$line"
done
配合find命令进行定制化处理在日常工作中非常有用
find . -type f -print0 | while IFS= read -r -d '' file; do
mv "$file" "${file// /_}"
done
这个例子从 find 命令中逐个读取文件名,并将文件重命名,将空格替换为下划线。
请注意在 find 命令中使用 -print0,它使用 NUL 字节作为文件名分隔符;以及在 read 命令中使用 -d ”,以指示它将所有文本读入文件变量,直到找到 NUL 字节为止。默认情况下,find 和 read 命令将输入分隔符设置为换行符;但是,由于文件名本身可能包含换行符,此默认行为将在换行符处拆分文件名,并导致循环体失败。此外,还需要将 IFS 设置为空字符串,否则 read 命令仍会删除前导和尾随空格
find 命令是一个非常强大的文件查找工具,可以在指定的目录中递归查找文件和目录。find 命令的 -print 选项用于输出找到的文件和目录的名称,每个名称占一行。
有时候,文件名可能包含空格或其他特殊字符,这会导致一些问题。例如,如果一个文件名包含空格,则默认情况下 find 命令会将文件名分成多个行,这会导致脚本无法
处理该文件名。为了解决这个问题,可以使用 -print0 选项,它使用 NUL 字符作为文件名分隔符,而不是使用换行符。
当使用 -print0 选项时,find 命令会将每个文件名之间用 NUL 字符分隔,而不是用换行符分隔。这样,Shell 脚本就可以正确处理包含空格和其他特殊字符的文件名了。在 Shell 脚本中,可以使用 read 命令的 -d 选项指定 NUL 字符作为分隔符,以读取 find 命令输出的文件名。
例如,下面的命令可以递归查找当前目录下的所有文件和目录,并将它们的名称输出到屏幕上,每个名称之间用 NUL 字符分隔:
find . -print0
如果要将 find 命令的输出传递到另一个命令中,以便进一步处理,可以使用管道和 xargs 命令。xargs 命令可以读取 find 命令的输出,并将其作为参数传递给另一个命令。例如,下面的命令可以查找当前目录下的所有文本文件,并将它们的名称传递给 grep 命令:
find . -type f -name “*.txt” -print0 | xargs -0 grep “pattern”
在这个命令中,find 命令查找当前目录下的所有 .txt 文件,并使用 -print0 选项输出它们的名称。管道操作符将 find 命令的输出传递给 xargs 命令,它使用 -0 选项读取输入,并将每个文件名作为参数传递给 grep 命令。
使用管道将 find 命令的输出发送到 while 循环中会将循环放置在子 Shell 中,这意味着您进行的任何状态更改(更改变量、cd、打开和关闭文件等)都将在循环结束时丢失。为避免这种情况,可以使用进程替换:
while IFS= read -r line; do
printf '%s\n' "$line"
done < <(some command)
非正常文件处理方式
如果文件的最后一行后面还有一些字符(或者换句话说,如果最后一行没有以换行符终止),那么 read 命令将读取该行内容,但会返回 false,同时将未完成的部分行留在 read 变量中。您可以在循环后处理这些行:
while IFS= read -r line; do
printf '%s\n' "$line"
done < "$file"
[[ -n $line ]] && printf %s "$line"
或者
# 这个不工作:
printf 'line 1\ntruncated line 2' | while read -r line; do echo $line; done
# 这个也会异常:
printf 'line 1\ntruncated line 2' | while read -r line; do echo "$line"; done; [[ $line ]] && echo -n "$line"
# 这个处理方式符合预期:
printf 'line 1\ntruncated line 2' | { while read -r line; do echo "$line"; done; [[ $line ]] && echo "$line"; }
第一个示例除了缺少循环后的测试外,还缺少引号。有关原因,请参见“引号或参数”页面的说明。
关于为什么上面的第二个示例不按预期工作的讨论,请参考前面章节说明。
或者,您可以在 while 测试中添加逻辑 OR 操作符:
while IFS= read -r line || [[ -n $line ]]; do
printf '%s\n' "$line"
done < "$file"
printf 'line 1\ntruncated line 2' | while read -r line || [[ -n $line ]]; do echo "$line"; done
如何防止其他命令“吃掉”输入
有些命令会贪婪地读取标准输入中的所有数据。上面的示例没有采取措施来防止这种情况发生。例如,
while read -r line; do
cat > ignoredfile
printf '%s\n' "$line"
done < "$file"
只会打印第一行的内容,其余内容会被写入“ignoredfile”,因为 cat 命令会贪婪地读取所有可用的输入内容。
一种解决方案是使用一个数字文件描述符,而不是标准输入:
# Bash
while IFS= read -r -u 9 line; do
cat > ignoredfile
printf '%s\n' "$line"
done 9< "$file"
# 注意:read -u并非所有shell都支持
while IFS= read -r line <&9; do
cat > ignoredfile
printf '%s\n' "$line"
done 9< "$file"
或者:
exec 9< "$file"
while IFS= read -r line <&9; do
cat > ignoredfile
printf '%s\n' "$line"
done
exec 9<&-
这个示例将等待用户在每次循环迭代时向文件 ignoredfile 中输入内容,而不是贪婪地读取循环输入。
例如,您可能需要这种解决方案,当使用 mencoder 时,如果有用户输入,它将接受输入,但如果没有,它将继续执行而不发出任何警告。其他表现类似的命令包括 ssh 和 ffmpeg。