直接调用和 shell 变量中的 bufferin 的 find 输出不同

直接调用和 shell 变量中的 bufferin 的 find 输出不同

我在使用命令时遇到了一些奇怪的行为,find但我找不到解释。

我有一个 .txt 文件,每行有 1 个文件名,我正在使用 find 命令在数据库中递归搜索该文件。当我使用这样的命令时:

for filename in `cat filelist.csv`; do
find /location*/time*/ -name *${filename}*txt
done

我得到每行 1 个输出的预期输出。但是,当我使用相同的命令但将输出设置为变量时(我最终需要这样做):

for filename in `cat filelist.csv`; do
out=`find /location*/time*/ -name *${filename}*txt`
echo ${out}
done

find 命令似乎在同一行上打印文件夹中的所有匹配文件。我有两个问题:

  1. 是什么导致了这种行为?
  2. 如何将find每个匹配文件(即使文件夹中有许多匹配文件)作为变量输出到新行?

干杯!

答案1

发生这种情况只是因为当 shell 扩展变量时,任何换行符都会被“折叠”并更改为空格。因此,如果您的out变量包含换行符,${out}请将所有这些换行符更改为空格。但"${out}"保留换行符。

答案2

如果您filelist.csv包含文件的完全匹配项,您可以使用类似find ... -print0 | grep -z -F -f filelist.csv | xargs -0r...的内容,但您似乎想要匹配该文件中列出的部分文件名(文件名之前的任何字符和附加的“.txt”)。为此,最简单的方法是使用正则表达式。

您可以使用流程替代在 读取时将部分文件名转换filelist.csv为适当的正则表达式。filelist.csvgrep

顺便说一句,除非您使用 sed 的-i选项(不要针对此特定任务执行此操作),否则此转换不是永久性的,它不会影响原始 filelist.csv 文件,只会影响输入的文本流grep -f

或者,您可以将 的输出通过管道传输find . -name '*.txt'grep。这样,grep 看到的输入已经过滤了以 结尾的文件名.txt,因此sed不需要修改正则表达式。

无论如何,尝试这样的事情:

首先,这个实验的一些设置:

$ cat filelist.csv 
test
foo

$ touch test test.txt foo foo.txt footest footest.txt

$ ls -l
total 4
-rw-r--r-- 1 cas cas 10 Sep  8 04:01 filelist.csv
-rw-r--r-- 1 cas cas  0 Sep  8 04:01 foo
-rw-r--r-- 1 cas cas  0 Sep  8 04:01 footest
-rw-r--r-- 1 cas cas  0 Sep  8 04:01 footest.txt
-rw-r--r-- 1 cas cas  0 Sep  8 04:01 foo.txt
-rw-r--r-- 1 cas cas  0 Sep  8 04:01 test
-rw-r--r-- 1 cas cas  0 Sep  8 04:01 test.txt

然后使用 bash 内置函数mapfile填充一个用outfind 和 grep 的输出调用的数组:

$ mapfile -d '' out < \
    <(find . -type f -print0 |
        grep -z -f <(sed -e 's/^\(.*\)/.*\1\.txt$/' filelist.csv)

或者:

$ mapfile -d '' out < \
    <(find . -type f -name '*.txt' -print0 |
        grep -z -f filelist.csv )

结果:

$ declare -p out
declare -a out=([0]="./foo.txt" [1]="./footest.txt" [2]="./test.txt")

$ ls -l "${out[@]}"
-rw-r--r-- 1 cas cas 0 Sep  8 04:01 ./footest.txt
-rw-r--r-- 1 cas cas 0 Sep  8 04:01 ./foo.txt
-rw-r--r-- 1 cas cas 0 Sep  8 04:01 ./test.txt

请注意数组out仅包含foo.txtfootest.txt、 和test.txt,但是不是 footestfootest

$out顺便说一句,您可以使用以下内容迭代文件名:

for f in "${out[@]}"; do
  echo "$f"
  do-something-else-with "$f"
done

或者迭代数组的索引 (0, 1, 2) 而不是值 - 有时这更有用,例如,当您有两个或多个具有相同索引的数组并且您想以某种方式一起使用时,或者当您需要将索引用于其他目的时:

for i in "$!{out[@]}"; do
   echo "${out[$i]}"
done 

记住:

  1. 当您不希望 shell 对变量进行分词或展开 glob 或对变量中的 shell 元字符(如或 )进行操作时,请用双引号引用变量(即 type "$var",而不仅仅是) 。这是$var;&几乎总是。经验法则:如果您不确切知道为什么需要在任何特定情况下使用变量而不用双引号引起来,那么就用双引号引起来。没有引用$out或者$filename是您最初问题的直接原因。

  2. 永远不要假设文件名中不会有烦人的字符,例如空格和换行符 - 这些对于 unix 中的文件名来说是完全有效的字符,因此您的脚本必须处理它们。事实上,仅有的不能出现在路径/文件名中的字符是 NUL。

  3. 始终使用 NUL 作为任意或未知文件名之间的分隔符。这是唯一可以使用的分隔符任何文件名。

  4. 有很多例外,但是:大多数时候,当您希望变量保存多个值时,应该使用数组,而不是空格分隔的字符串或类似的“伪造/模拟数组”方法。特别是当这些值是文件名或其他内容时,其中分隔符是其中一个值中的有效字符。

答案3

txt要查找名称以 结尾并包含 中 行的任何内容的所有文件filelist.csv,在zshshell 中您可以执行以下操作:

print -rC1 -- **/*(${(j[|])~${(fb)"$(<filelist.csv)"}}*})*txt(ND)

或者一次分解一步:

csv_contents=$(<filelist.csv)
non_empty_lines_of_csv=(${(f)csv_contents})
lines_with_wildcards_excaped=(${(b)non_empty_lines_of_csv})
ored_patterns=${(j[|])lines_with_wildcards_excaped}
filename_pattern="*($ored_patterns)*txt"

print -rC1 -- **/$~filename_pattern(ND)

如果filelist.csv包含:

???
foo bar
baz

这最终会扩展一个递归全局,例如:

**/*(\?\?\?|foo bar|baz)*txt(ND)

至于你的问题:

for filename in `cat filelist.csv`; do
out=`find /location*/time*/ -name *${filename}*txt`
echo ${out}
done
  • for var in `cmd`不循环 的输出行cmd`cmd`获取 的输出cmd,删除尾随的换行符,然后对其执行 split+glob (仅在 zsh 中拆分),因此对 的字符进行拆分$IFS(默认为空格、制表符和换行符),然后在生成的单词中展开通配符。因此,如果是cmdoutput a* b*,它不会以varbeing循环一次a* b*,它会循环当前目录中以.开头的文件名a,然后循环以.开头的文件名b
  • 在 中-name *${filename}*.txt,由于这些*${filename}没有被引用,我们再次进行 shell 通配符操作。因此,如果${filename}abc*abc*txtshell 将将 扩展至匹配文件列表。如果xabcytxt当前目录中有一个文件被调用,则该文件将变为-name xabcytxt.另外,即使您将其更正为-name "*${filename}*txt",如果$filename*,也会-name '***txt'返回find所有以 结尾的文件名txt,而不仅仅是那些也包含*.
  • 在 中echo ${out},再次$out不加引号意味着 split+glob (zsh 中除外)。还echo应该避免输出任意数据,因为在不同实现的各种极端情况下它不会执行您想要的操作。

也可以看看:

相关内容