翻译自http://mywiki.wooledge.org/BashFAQ/001

不要使用for. 要使用while循环和read来实现.

while IFS= read -r line; do
    printf '%s\n' "$line"
done < "$file"

read后面的-r选项可以阻止\转义, 如果不用-r, 单独的\会被忽略. 使用read的时候, 几乎一定要跟着-r.

看一下例子吧,先是用-r

% cat 01.sh
file="01.sh"
while IFS= read -r line; do
    printf '%s\n' "$line"
done < "$file"

% sh 01.sh
file="01.sh"
while IFS= read -r line; do
    printf '%s\n' "$line"
done < "$file"

把-r去掉看一下

% cat 01.sh
file="01.sh"
while IFS= read line; do
    printf '%s\n' "$line"
done < "$file"

% sh 01.sh
file="01.sh"
while IFS= read line; do
    printf '%sn' "$line"
done < "$file"

IFS= 是为了避免把前后的空格去掉, 如果你就是想把前后空格去掉, 就不要用IFS= 了.

IFS是一个很有意思的东西,再多一些了解之后,会记录一下.

line是一个变量名, 随便你叫什么,可以用任何在shell中合法的变量名

重定向符号 < “$file” , 告诉while循环从file这个文件中读取内容. 也可以不用变量, 就直接用一个字符串. 像 < 01.sh.

如果数据源就是标准输入,就不用任何重定向了. (ctrl+D)结束.

如果输入源是变量或者参数中的内容,bash可以用 «< 遍历数据 (原文中把它叫做here string)

while IFS= read -r line; do
  printf '%s\n' "$line"
done <<< "$var"

也可以用 « (原文中把它叫做here document)

while IFS= read -r line; do
  printf '%s\n' "$line"
done <<EOF
$var
EOF

如果想把#开头的过滤掉, 可以在循环中直接跳过, 如下

# Bash
while read -r line; do
  [[ $line = \#* ]] && continue
  printf '%s\n' "$line"
done < "$file"

如果想对每一列单独处理, 可以在read后用多个变量

# Input file has 3 columns separated by white space (space or tab characters only).
while read -r first_name last_name phone; do
  # Only print the last name (second column)
  printf '%s\n' "$last_name"
done < "$file"

如果分隔符不是空白符, 可以设置IFS, 如下:

# Extract the username and its shell from /etc/passwd:
while IFS=: read -r user pass uid gid gecos home shell; do
  printf '%s: %s\n' "$user" "$shell"
done < /etc/passwd

如果文件是用tag做分隔的, 可以设置IFS=$’\t’, 不过注意了, 多个连着的tab会被当成一个(Ksh93/Zsh中可以用IFS=$’\t\t’, 但Bash中没用)

如果你给的变量数多于这行中的列数, 多出来的变量就是空, 如果少于列数,最后多出来的所有的数据会写到最后一个变量中

read -r first last junk <<< 'Bob Smith 123 Main Street Elk Grove Iowa 123-555-6789'

# first will contain "Bob", and last will contain "Smith".
# junk holds everything else.

也可以使用点位符忽略我们不需要的值

read -r _ _ first middle last _ <<< "$record"

# We skip the first two fields, then read the next three.
# Remember, the final _ can absorb any number of fields.
# It doesn't need to be repeated there.

再次注意, bash中用_肯定是没问题的, 但其他一些shell中, 可能有其它含义,有可能会使脚本完全不能用, 所以最好选一个不会在脚本的其它地方用到的变量替代,以防万一.

也可以把一个命令的输出做为read的输入:

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里面用到了print0, 是说一个null作为文件名的分隔符; read用了-d选项,也是说用null做分隔符. 默认情况下,它们都是用\n做分隔符的,但文件名本身就有\n时,脚本就出错了. IFS也要设置为空字符串,避免文件名前后有空白符的情况.

我的文件最后一行没有最后的换行符!

最后一行不是以\n结尾的话,read会读取之后返回false,所以就跳出了while循环,循环里面是不能输出这最后一行的. 可以这样处理:

# Emulate cat
while IFS= read -r line; do
  printf '%s\n' "$line"
done < "$file"
[[ -n $line ]] && printf %s "$line"

再看下面这段代码:

# This does not work:
printf 'line 1\ntruncated line 2' | while read -r line; do echo $line; done

# This does not work either in bash, but work in zsh:
printf 'line 1\ntruncated line 2' | while read -r line; do echo "$line"; done; [[ $line ]] && echo -n "$line"

# This works:
printf 'line 1\ntruncated line 2' | { while read -r line; do echo "$line"; done; [[ $line ]] && echo "$line"; }

第一段显然不会输出最后一行, 但奇怪的是第二行也不会!, 因为while循环是在一个subshell里面的, subshell里面的变量的生命周期只在subshell里面; 第三段就{}强制把while和后面的判断放在一个subshell里面,就OK了.

注:zsh中,第二行种写法是会输出的。

也可以用下面这样(我觉得挺有意思的)

printf 'line 1\ntruncated line 2' | while read -r line || [[ -n $line ]]; do echo "$line"; done