我编写了以下脚本,以比较其中包含相同文件的两个导演的输出,例如:

#!/bin/bash

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


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

find的示例输出:

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


评论

我不同意这将是重复的。可接受的答案回答了如何用空格循环文件名;这与“为什么循环遍历find的输出错误做法”无关。我发现了这个问题(不是另一个问题),因为我需要用空格循环遍历文件名,例如:$ LIST_OF_FILES中的文件; ... ... $ LIST_OF_FILES不是find的输出;它只是文件名列表(以换行符分隔)。

@CarloWood-文件名可以包含换行符,因此您的问题很独特:循环遍历可以包含空格但不能包含换行符的文件名列表。我认为您将必须使用IFS技术,以指示中断发生在'\ n'

@ Diagon-哇,我从未意识到文件名不允许包含换行符。我主要(仅)使用linux / UNIX,甚至很少有空格。当然,我一生中从未见过使用换行符:p。他们也应该禁止那个恕我直言。

@CarloWood-文件名以null结尾('\ 0',与“''相同)。其他任何都可以接受。

#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
最好的答案(基于Gilles的答案) )
find . -type f -name '*.csv' -exec sh -c '
  file="
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 {} +
" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty ' exec-sh {} ';'

甚至更好,为避免每个文件运行一个sh
for file in "zquery" "-" "abc" ...


长答案
您有三个问题:

默认情况下,shell在空格,制表符和换行符上拆分命令的输出
文件名可能包含通配符,这些字符会被扩展
如果目录名称以*.csv结尾,该怎么办?

1。仅在换行符上分割
要弄清楚将file设置为什么,外壳程序必须采用find的输出并以某种方式对其进行解释,否则file只是find的整个输出。
外壳读取IFS变量,默认情况下设置为<space><tab><newline>
然后它查看find输出中的每个字符。一旦它看到IFS中的任何字符,就会认为它标记了文件名的结尾,因此它将file设置为到目前为止所看到的任何字符并运行循环。然后,它从中断处开始获取下一个文件名,并运行下一个循环等,直到到达输出末尾。
因此,它可以有效地做到这一点:
IFS=$'\n'

要告诉它仅在换行符上拆分输入,您需要先执行
IFS='
'

,然后再执行for ... find命令。
IFS设置为单个换行符,因此它仅在换行符上进行拆分,而不会
如果您使用的是shdash而不是ksh93bashzsh,则需要这样编写IFS=$'\n'
diff $file /some/other/path/$file

这可能足以使您的脚本正常工作,但是如果您有兴趣适当处理其他一些极端情况,请继续阅读...
2。在不使用通配符的情况下扩展$file
在循环的内部
diff "$file" "/some/other/path/$file"

shell会尝试再次扩展$file
它可以包含空格,但是由于我们已经在上面设置了IFS,所以这里不会有问题。
但是它也可以包含通配符,例如*?,这将导致不可预测的行为。 (感谢Gilles指出了这一点。)
要告诉Shell不要扩展通配符,请将变量放在双引号中,例如
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

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

,从而使file1.csvfile2.csv进行两次处理。
相反,我们必须要做从标准输入中,根据read将行拆分为单词,并将其存储在您指定的变量名中。
在这里,我们告诉它不要将行拆分为单词,并将行存储在IFS中。
还请注意,$file已更改为read line
这是因为在循环内,标准输入是通过管道从read line </dev/tty输入的。
如果我们只是做了find,那将是包含文件名的一部分或全部,某些文件将被跳过。
read是用户从中运行脚本的终端。请注意,如果脚本是通过cron运行的,则会导致错误,但是我认为在这种情况下这并不重要。
然后,如果文件名包含换行符怎么办?
我们可以通过更改来解决该问题。 /dev/tty-print并在管道的末尾使用-print0
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

这使read -d ''在每个文件名的末尾放置一个空字节。空字节是文件名中唯一不允许使用的字符,因此,无论多么奇怪,它都应处理所有可能的文件名。
要获取另一端的文件名,请使用find
在上面我们使用IFS= read -r -d ''的地方,我们使用了换行符的默认行定界符,但是现在,read使用null作为行定界符。在find中,您不能在参数中将NUL字符传递给命令(甚至是内置的),但是bash理解bash的含义是NUL分隔。因此,我们使用-d ''来使-d ''使用与read相同的行定界符。请注意,由于不支持NUL字节的find会将其视为空字符串,因此-d $'bash'也可以正常工作。
为正确起见,我们还添加了-r,它说不要专门处理文件名中的反斜杠。例如,在没有-r的情况下,将\<newline>删除,然后将\n转换为n
一种更可移植的编写方式,不需要bashzsh或记住所有上述有关空字节的规则(再次感谢Gilles):
find . -name '*.csv' -exec sh -c '
  file="
find . -name "*.csv"
" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty ' exec-sh {} ';'

* 3。跳过名称以.csv结尾的目录
find . -type f -name '*.csv' -exec sh -c '
  file="
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"
" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty ' exec-sh {} ';'

也将匹配名为something.csv的目录。
为避免这种情况,请将-type f添加到find命令。
q4312078q
正如glenn jackman指出的那样,在这两个示例中,要为每个文件执行的命令都在子shell中运行,因此,如果在循环中更改任何变量,它们将被忘记。
如果需要设置变量并仍在循环末尾进行设置,您可以将其重写以使用进程替代,如下所示:
q4312078q
请注意,如果您尝试在命令行中复制并粘贴此变量,则read line将消耗echo "$i files processed",这样该命令将不会运行。
要避免这种情况,您可以删除read line </dev/tty并将结果发送到类似less的传呼机。

注释
我删除了循环内的分号(;)。您可以根据需要将它们放回去,但不需要。
这些天,$(command)`command`更常见。这主要是因为写$(command1 $(command2))`command1 \`command2\``更容易。
read char并不真正读取字符。它读取了整行,所以我将其更改为read line

评论


放置在管道中会在创建子外壳时产生问题(例如,命令完成后,循环块中的变量不可见)。对于bash,我将使用输入重定向和进程替换:在读取-r -d $'\ 0'文件时;做...;完成<<(查找... -print0)

–格伦·杰克曼
2011-3-18在1:23

当然可以,或者使用heredoc:阅读时;做;完成<< EOF“ $(find)” EOF。但是,阅读起来并不容易。

– Mikel
2011-3-18的1:41

@glenn jackman:我刚才尝试添加更多解释。我只是变得更好或更糟了吗?

– Mikel
2011-3-18的2:36

您不需要IFS -print0,而只要您将find完全处理即可读取,如下面的解决方案所示。

–用户未知
2011年3月19日在23:10

如果您还使用set -f关闭glob,则您的第一个解决方案将处理除换行符以外的任何字符。

–吉尔斯'所以-不再是邪恶的'
2011年4月4日在19:28

#2 楼

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


执行find命令,获取其输出。
find输出拆分为单独的单词。任何空格字符都是一个单词分隔符。
对于每个单词,如果它是一个通配符模式,请将其扩展到与之匹配的文件列表。

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


find命令返回./foo* bar.csv
shell在该空格处拆分此字符串,产生两个单词:./foo*bar.csv
由于./foo*包含全局元字符,因此已扩展到匹配文件列表:./foo 1.txt./foo 2.txt
因此,for循环与./foo 1.txt./foo 2.txtbar.csv相继执行。

在此阶段,可以通过减少分词和关闭通配来避免大多数问题。要调低分词效果,请将IFS变量设置为单个换行符。这样,find的输出将仅在换行符处分割,并且将保留空格。要关闭水珠,请运行set -f。然后,只要没有文件名包含换行符,这部分代码就可以使用。

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


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

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

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


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

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

find . -name '*.csv' -exec sh -c '
  echo "
diff -r -x '*.txt' -x '*.ods' -x '*.pdf' … . /some/other/path
" diff "q4312078q" "/some/other/path/q4312078q" ' {} ';'


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

q4312078q

评论


我忘记了通配符作为正确引用的另一个原因。谢谢! :-)

– Mikel
2011-3-18在2:34



而不是find -exec sh -c'cmd 1; cmd 2'“;”,应使用find -exec cmd 1 {}“;” -exec cmd 2 {}“;”,因为外壳程序需要屏蔽参数,而find则不需要。在这里的特殊情况下,echo“ $ 0”不必是脚本的一部分,只需在';'之后附加-print。您没有包括要继续进行的问题,但是即使如此也可以通过查找来完成,如下所示。 ;)

–用户未知
2011年3月19日23:25

@userunknown:使用{}作为find -exec中参数的子字符串不是可移植的,这就是需要shell的原因。我不明白您的意思是“外壳需要屏蔽参数”;如果是关于报价,则我的解决方案会正确报价。没错,回声部分可以由-print执行。 -okdir是一个相当新的GNU find扩展,并非在所有地方都可用。我不包括等待进行的时间,因为我认为,如果他愿意的话,极差的UI和asker可以轻松地将读取的内容放入shell片段中。

–吉尔斯'所以-不再是邪恶的'
2011-3-19在23:59

报价是一种掩饰形式,不是吗?我不理解您关于便携式和不便携式的说法。您的示例(自下而上第2个)使用-exec调用sh并使用{}-所以我的示例(在-okdir旁边)的可移植性在哪里?找 。 -name“ * .csv” -exec diff {} / some / other / path / {}“;” -打印

–用户未知
2011年3月20日在1:05

在外壳文学中,“掩盖”不是常用术语,因此,如果您想被理解,就必须解释一下您的意思。我的示例仅在单独的参数中使用{}一次;其他情况(两次使用或作为子字符串使用)则不可移植。 “可移植”意味着它将在所有unix系统上运行; POSIX / Single Unix规范是一个很好的指南。

–吉尔斯'所以-不再是邪恶的'
2011年3月20日,1:15

#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 . -okdir diff {} /some/other/path/{} ";"


查找自己会谨慎地节省程序调用。 -okdir将在差异之前提示您(确定是/否)。

不涉及任何外壳,不包含浮空,小丑,pi,pa,po。

作为旁注:如果将find与for / while / do / xargs结合使用,情况下,您做错了。 :)

评论


感谢你的回答。如果将find与for / while / do / xargs结合使用,为什么做错了?

–阿米尔·阿富汗尼
2011年3月18日14:56

查找已经遍历文件的子集。大多数出现问题的人都可以只使用其中一种动作(-ok(dir)-exec(dir),-delete)和“;”组合使用或+(以后称为并行调用)。这样做的主要原因是,您不必摆弄文件参数,而无需为shell屏蔽它们。没那么重要:您不需要一直在进行新的处理,更少的内存,更快的速度。较短的程序。

–用户未知
2011-3-18在21:05

不是在这里伤透精神,而是进行比较:time find -type f -exec cat“ {}” \;随着时间的推移-type f -print0 | xargs -0-我放猫的东西。处理10000个空文件时,xargs版本的速度提高了11秒。断言在大多数情况下,将find与其他实用程序结合使用是错误的,请当心。 -print0和-0用来通过使用零字节作为项目分隔符而不是空格来处理文件名中的空格。

–乔纳森·科玛(Jonathan Komar)
17年7月5日11:00



@JonathanKomar:您的find / exec突击队在我的系统上花费了11.7 s,包含10.000个文件,xargs版本为9.7 s,时间find -type f -exec cat {} +如我先前的评论中所建议的花费了0.1 s。请注意“做错了”和“您做错了”之间的细微差别,尤其是当装饰有笑脸时。例如,您做错了吗? ;)顺便说一句,文件名中的空格对于上面的命令来说并没有问题,通常可以找到。货运崇拜者?顺便说一句,将find与其他工具结合起来很好,大多数情况下,仅xargs是多余的。

–用户未知
17年7月5日在12:48



@userunknown我解释了我的代码如何处理后代空间(教育未来的观看者),但并不表示您的代码没有。正如您提到的,并行调用的+非常快。我不会说货运狂热程序员,因为以这种方式使用xargs的能力在许多场合都派上用场。我更同意Unix的哲学:做一件事并做好事(单独使用程序或组合使用程序来完成工作)。发现在那儿走的很好。

–乔纳森·科玛(Jonathan Komar)
17年7月6日在7:21



#5 楼

可以完全安全地查找所有文件(包括任何特殊字符)(请参阅文档链接):

exec 9< <( find "$absolute_dir_path" -type f -print0 )
while IFS= read -r -d '' -u 9
do
    file_path="$(readlink -fn -- "$REPLY"; echo x)"
    file_path="${file_path%x}"
    echo "START${file_path}END"
done


评论


感谢您提及-d”。我没有意识到$'\ 0'与''相同,但似乎是。很好的解决方案。

– Mikel
2011年4月4日在11:30

我喜欢find的解耦,还有一段时间,谢谢。

–寒意
13年4月11日在16:32

#6 楼

我很惊讶没有人在这里提到明显的zsh解决方案:

for file (**/*.csv(ND.)) {
  do-something-with $file
}


(D)也包括隐藏文件,(N)避免了如果没有匹配项的错误,(.)限制为常规文件。)

bash4.3及更高版本现在也部分支持它:

shopt -s globstar nullglob dotglob
for file in **/*.csv; do
  [ -f "$file" ] || continue
  [ -L "$file" ] && continue
  do-something-with "$file"
done


#7 楼

如果文件名中不带引号,则它们在命令行上看起来像多个名称。如果您的文件名为“ Hello World.txt”,则差异行将扩展为:

diff Hello World.txt /some/other/path/Hello World.txt


,它看起来像四个文件名。只需在引号周围加上引号即可:

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


评论


这有帮助,但不能解决我的问题。我仍然看到文件被拆分为多个令牌的情况。

–阿米尔·阿富汗尼
2011-3-18在0:37

这个答案是误导的。问题是`find中的for文件。 -name“ * .csv”`命令。如果有一个名为Hello World.csv的文件,则该文件将设置为./Hello,然后设置为World.csv。引用$ file将无济于事。

– G-Man说“恢复莫妮卡”
15年3月4日在19:11

#8 楼

双引号是您的朋友。

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


否则,变量的内容将被单词拆分。

评论


这是误导。问题是`find中的for文件。 -name“ * .csv”`命令。如果有一个名为Hello World.csv的文件,则该文件将设置为./Hello,然后设置为World.csv。引用$ file将无济于事。

– G-Man说“恢复莫妮卡”
15年3月4日在19:11

#9 楼

使用bash4,还可以使用内置的mapfile函数来设置包含每行的数组并在该数组上进行迭代。

$ tree 
.
├── a
│   ├── a 1
│   └── a 2
├── b
│   ├── b 1
│   └── b 2
└── c
    ├── c 1
    └── c 2

3 directories, 6 files
$ mapfile -t files < <(find -type f)
$ for file in "${files[@]}"; do
> echo "file: $file"
> done
file: ./a/a 2
file: ./a/a 1
file: ./b/b 2
file: ./b/b 1
file: ./c/c 2
file: ./c/c 1


#10 楼

可以通过简单的for循环构造来避免值中的空格

for CHECK_STR in `ls -l /root/somedir`
do 
echo "CHECKSTR $CHECK_STR"
done


ls -l root / somedir contains
带空格的我的文件

上述内容的输出
我的文件





为了避免此输出,请采用简单的解决方案(注意双引号)
/>
for CHECK_STR in "`ls -l /root/somedir`"
do 
echo "CHECKSTR $CHECK_STR"
done


输出
带空格的我的文件

在bash上尝试

评论


“遍历文件” –这就是问题所在。您的解决方案将立即输出整个ls -l输出。它实际上等效于echo“ CHECKSTR`ls -l / root / somedir`”。

–manatwork
13年5月13日在7:02