透过Bash循环读取以空值分隔的字符串

71

我想遍历一个文件列表,而不用关心文件名可能包含的任何字符,因此我使用了以空字符为分隔符的列表。代码会更好地解释这些。

# Set IFS to the null character to hopefully change the for..in
# delimiter from the space character (sadly does not appear to work).
IFS=$'\0'

# Get null delimited list of files
filelist="`find /some/path -type f -print0`"

# Iterate through list of files
for file in $filelist ; do
    # Arbitrary operations on $file here
done
以下代码可以从文件中读取内容,但我需要从一个包含文本的变量中读取内容。
while read -d $'\0' line ; do
    # Code here
done < /path/to/inputfile

8
我认为在Bash变量中存储空字符是不可能的。至少我从未发现过这样的做法…… - Gordon Davisson
1
确认,bash:警告:命令替换:忽略输入中的空字节。这是因为bash旨在用于posix派生环境,在这些环境中,环境变量在以空字符结尾的缓冲区中内部存储,并且bash变量是(在我检查过的每种情况下)主机环境变量。 - memtha
你可能能够在bash变量中存储一个空字符,但是你无法将其取出,因此没有办法判断。第一个例子证明了分配不可显示的字符是有效的(正如我们所知道的那样),例如八进制制表符:test=$'a\011b';echo ${#test} ="${test}"= 的结果为 3 =a b=。然后尝试一个八进制0:test=$'a\0b';echo ${#test} ="${test}"= 的结果为 1 =a=;这报告了$test的零终止字符串长度为1,但'b'和另一个零仍然可以存储到变量中,我们不知道。 - db-inf
5个回答

119
首选的方法是使用进程替换。
while IFS= read -r -d $'\0' file; do
    # Arbitrary operations on "$file" here
done < <(find /some/path -type f -print0)

如果你非常想以类似的方式解析bash变量,只要列表没有以NUL结尾,就可以这样做。
以下是一个包含制表符分隔字符串的bash变量示例。
$ var=$(echo -ne "foo\tbar\tbaz\t"); 
$ while IFS= read -r -d $'\t' line ; do \
    echo "#$line#"; \
  done <<<"$var"
#foo#
#bar#
#baz#

5
既然设置了-d标志,那么IFS有什么用途? - thisirs
15
IFS 设置为空字符串,将会保留前导和尾随的空白字符。 - toxalot
4
完全同意@joanpau的看法。我不知道2011年的情况如何,但是在2016年使用bash4时,这种方法不起作用。您可以轻松验证,如果在while循环之前分配var=$(find . -print0),则会失败。进程替换确实有效,但变量不行。即使您像这样即兴构建变量 var=$(echo -e "some\0text"),也无法将 sometext 分开。对于变量,您需要像这样进行技巧处理:https://dev59.com/eGw15IYBdhLWcg3wfr9y - George Vasiliou
3
这并不是将IFS设置为空字符串。它是取消设置IFS,导致它使用默认值($' \t\n') - jeremysprofile
6
@jeremysprofile 实际上,IFS=禁用 单词分割功能。它不会unset IFS 那样将 IFS 设置回其默认值。 - SiegeX
显示剩余6条评论

5
将它们通过管道传递到 xargs -0files="$( find ./ -iname 'file*' -print0 | xargs -0 )" xargs 手册:
-0, --null
    Input items are terminated by a null character instead of
    by whitespace, and the quotes and backslash are not
    special (every character is taken literally).

1
您的回答可以通过添加额外的支持信息来改进。请编辑以添加更多详细信息,例如引用或文档,以便他人确认您的答案是否正确。您可以在帮助中心中找到有关撰写良好答案的更多信息。 - Community

2
使用env -0以零字节的形式输出赋值结果。
env -0 | while IFS='' read -d '' line ; do
    var=${line%%=*}
    value=${line#*=}
    echo "Variable '$var' has the value '$value'"
done

1

从可读性和可维护性的角度来看,Bash 函数可能更加清晰:

以下是一个使用 ffmpegMOV 文件转换为 MP4 的示例(适用于包含空格和特殊字符的文件):

#!/usr/bin/env bash

do_convert () { 
  new_file="${1/.mov/.mp4}"
  ffmpeg -i "$1" "$new_file" && rm "$1" 
}

export -f do_convert  # needed to make the function visible inside xargs

find . -iname '*.mov' -print0 | xargs -0 -I {} bash -c 'do_convert "{}"' _ {}

这与 OP 的问题无关,但如果您的输入是由 find 生成的,则无需通过 xargs -0 进行管道传输,因为 find 完全能够处理文件名中的非 ASCII 字符和空格。如果您不关心可读性和可维护性,则可以将上面的命令简化为:

find . -type f -iname "*.mov" -exec bash -c 'ffmpeg -i "${1}" "${1%.*}.mp4" && rm "${1}"' _ {} \;

正如此答案所指出的那样,它无法处理带有空字符的任意字符串。然而,它确实回答了一个不同但常见的问题,即在不使用“-print0 | xargs -0”形式和不使用IFS=覆盖的情况下执行文件操作。 - Groboclown

-6

我尝试使用上面的bash示例,最终放弃了,并使用Python,第一次就成功了。对我来说,问题在shell外部变得更简单了。我知道这可能与bash解决方案无关,但我仍然在这里发布它,以防其他人想要另一种选择。

import sh
import path
files = path.Path(".").files()
for x in files:
    sh.cp("--reflink=always", x, "UUU00::%s"%(x.basename(),))
    sh.cp("--reflink=always", x, "UUU01::%s"%(x.basename(),))

之前遇到了一个类似的问题,是在 sh(不是 bash)脚本中。于是决定用 Perl 重写整个该死的脚本。 - Axel Beckert

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