1。使用zsh
最简单的解决方案是使用zsh,它是一个非bash的shell,支持通过read -d ""读取空分隔值(从4.2版开始,2004年发布),是唯一可以存储的主流shell变量中的空值。此外,管道的最后一个组件没有在zsh 的子shell 中运行,因此在那里设置的变量不会丢失。我们可以简单地写:
#!/usr/bin/env zsh
find . -print0 |while IFS="" read -r -d "" file; do
echo "$file"
done
使用zsh,我们还可以通过使用setopt globdots(使全局匹配隐藏文件)和**(递归到子目录。这适用于 zsh 的所有版本,甚至是那些早于 4.2 的版本:
#!/usr/bin/env zsh
setopt globdots
for file in **/*; do
echo "$file"
done
2。使用 POSIX shell 和 od
2.1 使用管道
一个通用的、与 POSIX 兼容的迭代空分隔值的解决方案需要以一种不会丢失信息的方式转换输入,并且将空值转换为更易于处理的其他内容。我们可以使用od 转储所有输入字节的八进制值,并使用printf 轻松地将数据转换回来:
#!/usr/bin/env sh
find . -print0 |od -An -vto1 |xargs printf ' %s' \
|sed 's/ 000/@/g' |tr @ '\n' \
|while IFS="" read -r file; do
file=`printf '\134%s' $file`
file=`printf "$file@"`
file="${file%@}"
echo "$file"
done
2.2 使用变量存储中间结果
请注意,while 循环将在子 shell 中运行(至少在 zsh 和原始的非公共域 Korn shell 之外的 shell 中),这意味着在该循环中设置的变量不会在其余代码中可见。如果这是不可接受的,while 循环可以从主 shell 运行,它的输入可以存储在一个变量中:
#!/usr/bin/env sh
VAR=`find . -print0 |od -An -vto1 |xargs printf ' %s' \
|sed 's/ 000/@/g' |tr @ '\n'`
while IFS="" read -r file; do
file=`printf '\134%s' $file`
file=`printf "$file@"`
file="${file%@}"
echo "$file"
done <<EOF
$VAR
EOF
2.3 使用临时文件存储中间结果
如果find 命令的输出很长,脚本将无法将输出存储在变量中,并且可能会崩溃。而且most shells use temporary files to implement heredocs,所以与其使用变量,不如显式写入临时文件,避免使用变量存储中间结果的问题。
#!/usr/bin/env sh
TMPFILE="/tmp/$$_`awk 'BEGIN{srand(); print rand()}'`"
find . -print0 |od -An -vto1 |xargs printf ' %s' \
|sed 's/ 000/@/g' |tr @ '\n' >"$TMPFILE"
while IFS="" read -r file; do
file=`printf '\134%s' $file`
file=`printf "$file@"`
file="${file%@}"
echo "$file"
done <"$TMPFILE"
rm -f "$TMPFILE"
2.4 使用命名管道
我们可以使用命名管道来解决上述两个问题:现在读写可以并行完成,我们不需要将中间结果存储在变量中。但是请注意,这在 Cygwin 中可能不起作用。
#!/usr/bin/env sh
TMPFILE="/tmp/$$_`awk 'BEGIN{srand(); print rand()}'`"
mknod "$TMPFILE" p
{
exec 3>"$TMPFILE"
find . -print0 |od -An -vto1 |xargs printf ' %s' \
|sed 's/ 000/@/g' |tr @ '\n' >&3
} &
while IFS="" read -r file; do
file=`printf '\134%s' $file`
file=`printf "$file@"`
file="${file%@}"
echo "$file"
done <"$TMPFILE"
rm -f "$TMPFILE"
3。修改上述解决方案以使用原始 Bourne shell
上述解决方案应该可以在任何 POSIX shell 中工作,但在原始 Bourne shell 中失败,这是 Solaris 10 和更早版本中的默认 /bin/sh。此 shell 不支持 %-substitution,并且文件名中的尾随换行符需要以另一种方式保留,例如:
#!/usr/bin/env sh
TMPFILE="/tmp/$$_`awk 'BEGIN{srand(); print rand()}'`"
mknod "$TMPFILE" p
{
exec 3>"$TMPFILE"
find . -print0 |od -An -vto1 |xargs printf ' %s' \
|sed 's/ 000/@/g' |tr @ '\n' >&3
} &
while read -r file; do
trailing_nl=""
for char in $file; do
if [ X"$char" = X"012" ]; then
trailing_nl="${trailing_nl}
"
else
trailing_nl=""
fi
done
file=`printf '\134%s' $file`
file=`printf "$file"`
file="$file$trailing_nl"
echo "$file"
done <"$TMPFILE"
rm -f "$TMPFILE"
4。使用 null 以外的分隔符
正如 cmets 所指出的,Haravikk 的答案并不完全正确。这是他的代码的修改版本,可以处理各种奇怪的情况,例如以~:/\/: 开头的路径和文件名中的尾随换行符。请注意,它仅适用于相对路径名;可以通过在绝对路径名前面加上/./ 来完成类似的技巧,但是需要更改read_path() 来处理它。这个方法的灵感来自Rich’s sh (POSIX shell) tricks。
#!/usr/bin/env sh
read_path() {
path=
IFS=
read -r path || return $?
read -r path_next || return 0
if [ X"$path" = X"././" ]; then
path="./"
read -r path_next || return 0
return
fi
path="./$path"
while [ X"$path_next" != X"././" ]; do
path=`printf '%s\n%s' "$path" "$path_next"`
read -r path_next || return 0
done
}
find ././ |sed 's,^\./\./,&\n,' |while read_path; do
echo "$path"
done