Bash 逐行读取文本:保住空格、反斜杠和没有换行的最后一条
看见四条内容,循环却只处理三条
读取一份手工编辑的文本清单时,最后一条明明看得见,循环却没有执行它。常见原因是文件末尾没有换行符:read 已经把末尾字符放进变量,但由于遇到文件结束而返回非零。如果循环只依据退出状态决定是否进入正文,这段已经读到的内容就被丢下了。
本文约定每个换行结束一条记录,并把文件末尾非空的残余内容也算作一条。换行本身是分隔符,不属于记录内容;空文件没有记录,单独一个换行则表示一条空记录。先说清这个口径,才能判断行数变化究竟是错误还是不同的统计约定。
三个设置分别保护三种边界
IFS= 让这次读取不按默认空白规则拆分并去掉边缘空白;参数 -r 让反斜杠保持字面含义;逻辑或后面的非空判断负责接住未以换行结束的末尾。它们解决的问题互不替代。尤其不能只补上 -r,就以为前后空格和最后一条也会自动保留。
下面整段交给 Bash 运行,要求 Bash 五及以上和常见 GNU 工具。脚本在当前目录建立专用临时目录,退出时清理。样本依次放入带边缘空格的文字、含反斜杠的路径、空行和没有结尾换行的尾部文字;读取函数把每条内容放进数组,便于逐项检查。
AI生成概念示意图,非真实界面
(
set -eu
work=$(mktemp -d './read-demo.XXXXXX')
trap 'rm -rf -- "$work"' EXIT
printf '%s\n' ' two words ' 'C:\tmp\new' '' > "$work/input.txt"
printf '%s' 'tail' >> "$work/input.txt"
read_lines() {
local line=''
rows=()
while IFS= read -r line || [[ -n "$line" ]]; do
rows+=("$line")
done < "$1"
}
read_lines "$work/input.txt"
[[ ${#rows[@]} -eq 4 ]]
[[ ${rows[0]} == ' two words ' ]]
[[ ${rows[1]} == 'C:\tmp\new' ]]
[[ ${rows[2]} == '' && ${rows[3]} == tail ]]
count=0
for line in "${rows[@]}"; do
count=$((count + 1))
printf '%s=<%s>\n' "$count" "$line"
done
printf 'count=%s\n' "$count"
: > "$work/empty.txt"
printf '\n' > "$work/blank.txt"
printf 'one\n' > "$work/complete.txt"
read_lines "$work/empty.txt"
[[ ${#rows[@]} -eq 0 ]]
read_lines "$work/blank.txt"
[[ ${#rows[@]} -eq 1 && ${rows[0]} == '' ]]
read_lines "$work/complete.txt"
[[ ${#rows[@]} -eq 1 && ${rows[0]} == one ]]
printf 'boundary checks passed\n'
)预期输出四个带编号的内容框:第一框里的两端空格仍在,第二框里的两个反斜杠仍在,第三框为空,第四框是 tail。随后 count 为四,末行显示 boundary checks passed。脚本还验证空文件、只有换行的文件以及正常结尾的单条记录,防止修好末行后又多算一条。
空行为什么不会被非空判断过滤
逻辑或采用短路规则。正常读到换行时,read 已经成功,后面的非空判断不会执行,所以中间的空行照样进入循环。只有 read 返回非零时才查看变量是否还有内容:末尾有残余就再处理一次,完全没有残余便停止。不要把逻辑或误写成逻辑与,否则合法空行会被排除。
演示中的数组用于确认内容,没有先用命令替换把整个文件塞进变量。命令替换会去掉末尾的换行,容易在读取之前就改变样本。输出也统一使用带固定格式的 printf,再把内容作为单独参数传入,避免文字里的百分号被当成格式串。内容框只是便于观察,不是新的文件格式。
让循环在需要保存变量的那一层运行
输入重定向放在 done 后面,让循环直接从文件读取。若改成 cat 文件再通过管道连接 while,Bash 通常会把循环放进子 shell,里面增加的计数或数组元素不会带回外层。lastpipe 在特定设置下可以改变末段执行位置,但小脚本没有必要为读取一个文件依赖这些额外条件。
外面的圆括号只用于隔离本次演示的变量和清理动作;读取循环与后续断言仍处于同一个演示 shell。读取函数也直接调用,没有再包进命令替换,所以它更新的数组能被调用者检查。真实脚本若还要在退出这个 shell 后使用结果,应设计明确的输出渠道。
保留行内容,不等于复制所有字节
该写法移除了换行分隔符,也没有保存每条记录原来是否带结尾换行。处理 Windows 风格的回车换行时,回车会留在变量末尾,应按输入协议决定是否去除。不要不加区别地清理所有空白,因为空格可能正是业务数据的一部分。
Bash 变量不适合承载含零字节的任意二进制内容。本例也没有超时、交互输入或循环内抢读同一输入流的其他命令;这些条件会引入新的失败原因。需要保留原始字节、判断编码或诊断底层读取错误时,应改用适合该任务的文件接口,并单独记录错误。


