循环遍历名称中带有空格的文件?

循环遍历名称中带有空格的文件?

我编写了以下脚本来比较两个目录中所有相同文件的输出:

#!/bin/bash

for file in `find . -name "*.csv"`  
do
     echo "file = $file";
     diff $file /some/other/path/$file;
     read char;
done

我知道还有其他方法可以实现这一目标。但奇怪的是,当文件中包含空格时,该脚本会失败。我该如何处理这个问题?

查找的示例输出:

./zQuery - abc - Do Not Prompt for Date.csv

答案1

简短答案(最接近您的答案,但处理空格)

OIFS="$IFS"
IFS=$'\n'
for file in `find . -type f -name "*.csv"`  
do
     echo "file = $file"
     diff "$file" "/some/other/path/$file"
     read line
done
IFS="$OIFS"

更好的答案(还处理文件名中的通配符和换行符)

find . -type f -name "*.csv" -print0 | while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
done

最佳答案(基于吉尔斯的回答

find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' exec-sh {} ';'

或者更好的是,避免sh每个文件运行一个:

find . -type f -name '*.csv' -exec sh -c '
  for file do
    echo "$file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
  done
' exec-sh {} +

长答案

你有三个问题:

  1. 默认情况下,shell 将命令的输出拆分为空格、制表符和换行符
  2. 文件名可以包含通配符,这些字符会被扩展
  3. 如果有一个名称以 结尾的目录怎么办*.csv

1. 仅在换行符上分割

为了弄清楚要设置的内容file,shell 必须获取 的输出find并以某种方式解释它,否则file将只是 的整个输出find

shell 读取默认IFS设置的变量。<space><tab><newline>

然后它查看 输出中的每个字符find。一旦它看到 中的任何字符IFS,它就会认为 标记了文件名的结尾,因此它设置file为到目前为止看到的任何字符并运行循环。然后它从上次停止的地方开始获取下一个文件名,并运行下一个循环等,直到到达输出末尾。

所以它有效地做到了这一点:

for file in "zquery" "-" "abc" ...

要告诉它只在换行符上分割输入,您需要这样做

IFS=$'\n'

在你的命令之前for ... find

这设置IFS为单个换行符,因此它仅在换行符上分割,而不是空格和制表符。

如果您使用shordash代替ksh93, bashor zsh,则需要IFS=$'\n'这样写:

IFS='
'

这可能足以让您的脚本正常工作,但如果您有兴趣正确处理其他一些极端情况,请继续阅读...

2.$file不使用通配符扩展

在循环内部

diff $file /some/other/path/$file

外壳尝试扩展$file(再次!)。

它可以包含空格,但由于我们已经IFS在上面设置了,所以这里不会有问题。

但它也可能包含通配符,例如*?,这会导致不可预测的行为。 (感谢吉尔斯指出这一点。)

要告诉 shell 不要扩展通配符,请将变量放在双引号内,例如

diff "$file" "/some/other/path/$file"

同样的问题也可能会困扰我们

for file in `find . -name "*.csv"`

例如,如果您有这三个文件

file1.csv
file2.csv
*.csv

(极不可能,但仍有可能)

就好像你跑了一样

for file in file1.csv file2.csv *.csv

这将扩展到

for file in file1.csv file2.csv *.csv file1.csv file2.csv

导致file1.csv并被file2.csv处理两次。

相反,我们必须做

find . -name "*.csv" -print | while IFS= read -r file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
done

read从标准输入读取行,根据将行拆分为单词IFS并将它们存储在您指定的变量名称中。

在这里,我们告诉它不要将该行拆分为单词,并将该行存储在$file.

另请注意,read line已更改为read line </dev/tty.

这是因为在循环内部,标准输入来自find管道。

如果我们只是这样做read,它将消耗部分或全部文件名,并且某些文件将被跳过。

/dev/tty是用户运行脚本的终端。请注意,如果脚本通过 cron 运行,这将导致错误,但我认为在这种情况下这并不重要。

那么,如果文件名包含换行符怎么办?

我们可以通过更改-print-print0read -d ''在管道末端使用来处理这个问题:

find . -name "*.csv" -print0 | while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read char </dev/tty
done

这使得find在每个文件名的末尾放置一个空字节。空字节是文件名中唯一不允许的字符,因此这应该处理所有可能的文件名,无论多么奇怪。

为了获取另一侧的文件名,我们使用IFS= read -r -d ''.

在上面使用的地方read,我们使用了默认的行分隔符换行符,但现在find使用 null 作为行分隔符。在 中bash,您不能将参数中的 NUL 字符传递给命令(即使是内置命令),但bash可以理解-d ''为含义NUL 分隔。所以我们使用-d ''makeread使用与 相同的行分隔符find。请注意-d $'\0',顺便说一句, 也可以工作,因为bash不支持 NUL 字节会将其视为空字符串。

为了正确起见,我们还添加了-r,它表示不要专门处理文件名中的反斜杠。例如,没有-r,\<newline>被删除,并\n转换为n.

一种更可移植的编写方式不需要bashzsh或记住上述所有有关空字节的规则(再次感谢 Gilles):

find . -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read char </dev/tty
' exec-sh {} ';'

*3. 跳过名称结尾的目录.csv

find . -name "*.csv"

还将匹配名为 的目录something.csv

为了避免这种情况,请添加-type ffind命令中。

find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' exec-sh {} ';'

作为格伦·杰克曼指出,在这两个示例中,为每个文件执行的命令都在子 shell 中运行,因此如果更改循环内的任何变量,它们将被忘记。

如果您需要设置变量并在循环结束时仍然设置它们,您可以重写它以使用进程替换,如下所示:

i=0
while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
    i=$((i+1))
done < <(find . -type f -name '*.csv' -print0)
echo "$i files processed"

请注意,如果您尝试在命令行中复制并粘贴此内容,read line将会消耗echo "$i files processed",因此该命令将不会运行。

为了避免这种情况,您可以删除结果read line </dev/tty并将结果发送到寻呼机,例如less.


笔记

;我删除了循环内的分号 ( )。如果需要,您可以将它们放回去,但不需要它们。

如今,$(command)比 更常见`command`。这主要是因为它$(command1 $(command2))`command1 \`command2\``.

read char并没有真正读取一个字符。它读取整行,所以我将其更改为read line.

答案2

如果任何文件名包含空格或 shell 通配符,则此脚本将失败\[?*。该find命令每行输出一个文件名。然后`find …`shell 评估命令替换,如下所示:

  1. 执行find命令,获取其输出。
  2. 将输出拆分find为单独的单词。任何空白字符都是单词分隔符。
  3. 对于每个单词,如果它是通配模式,则将其展开到它匹配的文件列表。

例如,假设当前目录中有三个文件,分别称为`foo* bar.csvfoo 1.txtfoo 2.txt

  1. find命令返回./foo* bar.csv
  2. shell 在空格处分割该字符串,生成两个单词:./foo*bar.csv
  3. 由于./foo*包含一个通配元字符,因此它会扩展为匹配文件的列表:./foo 1.txt./foo 2.txt
  4. 因此,for循环将依次执行./foo 1.txt./foo 2.txtbar.csv

您可以通过减少分词和关闭通配符来避免此阶段的大多数问题。要减弱分词效果,请将IFS变量设置为单个换行符;这样, 的输出find将仅在换行符处分割,并且空格将保留。要关闭通配符,请运行set -f.那么只要文件名不包含换行符,这部分代码就会起作用。

IFS='
'
set -f
for file in $(find . -name "*.csv"); do …

(这不是您问题的一部分,但我建议使用$(…)over `…`。它们具有相同的含义,但反引号版本具有奇怪的引用规则。)

下面还有一个问题:diff $file /some/other/path/$file应该是

diff "$file" "/some/other/path/$file"

否则, 的值$file将被拆分为单词,并且单词将被视为全局模式,就像上面的命令 substitutio 一样。如果您必须记住有关 shell 编程的一件事,请记住这一点:$foo始终在变量扩展 ( ) 和命令替换 ( $(bar))周围使用双引号,除非你知道你想分裂。 (在上面,我们知道我们想要将find输出分成几行。)

一种可靠的调用方式find是告诉它为找到的每个文件运行命令:

find . -name '*.csv' -exec sh -c '
  echo "$0"
  diff "$0" "/some/other/path/$0"
' {} ';'

在这种情况下,另一种方法是比较两个目录,尽管您必须显式排除所有“无聊”文件。

diff -r -x '*.txt' -x '*.ods' -x '*.pdf' … . /some/other/path

答案3

我很惊讶没有看到readarray提及。与运算符结合使用时,这变得非常容易<<<

$ touch oneword "two words"

$ readarray -t files <<<"$(ls)"

$ for file in "${files[@]}"; do echo "|$file|"; done
|oneword|
|two words|

使用该<<<"$expansion"构造还允许您将包含换行符的变量拆分为数组,例如:

$ string=$(dmesg)
$ readarray -t lines <<<"$string"
$ echo "${lines[0]}"
[    0.000000] Initializing cgroup subsys cpuset

readarray已经在 Bash 中使用多年了,所以这可能应该是在 Bash 中执行此操作的规范方法。

答案4

Afaik find 有您需要的一切。

find . -okdir diff {} /some/other/path/{} ";"

find 负责保存调用程序。 -okdir 将在差异之前提示您(您确定是/否)。

不涉及 shell,没有通配符、小丑、pi、pa、po。

作为旁注:如果将 find 与 for/while/do/xargs 结合起来,在大多数情况下,您会做错。 :)

相关内容