使用Bash脚本无法将内容追加到文件中

4
这是我的代码:
#!/bin/bash -e
dirs=( * )
for f in "${dirs[@]}"
do
  while IFS= read -r line; do
     case "$line" in
       *disabled\>true* )
          sed -i '1i "$f"' list.txt;;
     esac
  done < "$f/config.xml"
done

我尝试使用echo和printf代替sed,但是文件list.txt始终为空。为什么我不能追加内容到文件中呢?

echo "$f" >> list.txt;;
printf '%s\n' "$f" >> list.txt;;

测试文件夹下的示例config.xml文件:

<?xml version='1.0' encoding='UTF-8'?>
<project>
<disabled>true</disabled>
</project>

目标:如果test/config.xml中有<disabled>true,则将"test"打印到list.txt文件中。


2
运行 bash -x yourscript 命令是否显示追加行(其中 printf 解决方案最为健壮)是否被调用? - Charles Duffy
bash -x 显示 sed 语句被调用,但没有显示 echo。我甚至尝试了 echo "am here"。但它没有被调用 :-( - user1164061
1
嗯,流程控制本身并不会变化。你能否让你在问题中提到的示例可独立使用(即使它自己创建config.xml文件,这样其他人就可以重现你的问题,而不需要大量未显示的内容)? - Charles Duffy
1
或者,如果在case之前无条件地在while read中立即打印出 printf 'line=%q\n' "$line",并找到应该触发但实际上没有的单独行,则可以仅包含line变量的分配以及case语句,并略去for循环和不需要任何输入文件作为再现问题的必要部分。 - Charles Duffy
2
你有没有注意到你正在尝试在单引号内扩展"$f" - Rany Albeg Wein
2个回答

3
要高效地完成你在问题中所尝试的任务,可以使用以下方法(需要使用GNU工具):
grep -FZl 'disabled>true' */*.config.xml | xargs -0 dirname | tac > list.txt

这里假设你想要:
- 只在`list.txt`中记录目录名称(如果你想要相对路径下的文件路径,只需删除`| xargs -0 dirname`)。 - 按照字母顺序反向排序,如果你想要升序排列,只需省略`| tac`部分。 说明: - 通配符`*/*.config.xml`可以高效地返回当前目录的任何子目录中`config.xml`文件的(相对)文件路径。 - `grep -FZl 'disabled>true'`在输入文件中搜索字面量(`-F`)`disabled>true`,仅当找到第一个匹配项时才停止搜索,并打印出包含该字符串的输入文件路径(`-l`),多个路径用`NUL`字符分隔(`-Z`)。 - `xargs -0 dirname`将输入按`NUL`(`0`)分割为参数,并将其传递给`dirname`,结果是这些目录的目录名称,它们的`config.xml`文件包含感兴趣的字符串。 - `tac`反转行。 - `> list.txt`将整体结果写入文件`list.txt`。
关于你尝试中的问题
  • dirs=( * ) 会匹配当前目录下的任何类型的文件系统项目(如果你知道当前目录只包含目录,则这可能不是一个问题);
    dirs=( */ ) 仅限于匹配目录(但注意,匹配结果将包括尾部的 / 符号)。

  • user2021201其答案中所指出的,使用命令 sed -i '1i ...' list.txt 的核心问题在于:如果输入文件是一个空(零字节)文件,则根本不会向 list.txt 中添加任何内容,此处脚本根本没有执行。

    • 这个问题没有简单的、单一的 sed 解决方案;对于逐步附加到文件的操作,应该使用符号>>
  • 此外,如Rany Albeg Wein在问题评论中所说,命令 sed -i '1i "$f"' list.txt 尝试在一个带有单引号的字符串中展开变量 $f ,这是不可行的,实际上只会将字面量 "$f" 写入文件(假定文件是非空的)。
    此外,通过使用 1i(在第1行之前插入),每个新条目将作为第一个行添加,实际上导致 list.txt 中包含匹配目录的反向顺序。

  • 如果在找到匹配字符串后不退出 while 循环,则不仅需要处理文件中剩余的行,而且还可能在搜索字符串出现在多个行的情况下将正在处理的目录多次添加到 list.txt 中。

  • user2021201其答案中所指出的,你的方法效率低下,因为(a)可以使用多级通配符(如 */*.config.xml)替代循环, (b) 因为可以使用 grep 更高效地搜索每个文件的内容,并在找到第一个匹配项后退出(有着略微不同的语义的 grep -m1grep -qgrep -l 命令)。


1
非常感谢您的回复并指出了我代码中的所有问题。 - user1164061
使用xargs并为每行创建一个新进程并不高效。 cut 的优点在于它只创建一个进程,然后通过它进行数据传输。 - user2021201
在这种情况下,xargs仅创建_1_个进程(除非文件路径列表变得非常大,无法适合单个命令行)。 xargs -0 dirname相对于您的cut解决方案的优点是它也适用于多目录组件路径(在OP的情况下不是问题,但拥有更通用的解决方案很好)。 - mklement0
为了验证只创建了一个进程,请尝试使用 printf 'one\0two\0three' | xargs -0 - mklement0
1
是的,你说得对。我只是太习惯使用带有-n1的xargs了,抱歉误解了。 - user2021201

2
首先,需要注意的是您的sed表达式无法处理空文件。我修改了代码并实现了您的目标:
#!/bin/bash -e

for f in */config.xml # process all config.xml files
do
  while IFS= read -r line; do
     case "$line" in
       *disabled\>true* )
          # obtain directory name of the file
          # and append it to list.txt
          dirname "$f" >> list.txt 
     esac
  done < "$f"
done

然而,我更倾向于使用以下方法:

#!/bin/bash -e

for f in */config.xml 
do
  # -m1 - exit at first occurence for performance
  if grep -m1 'disabled>true' "$f" >/dev/null; then
      dirname "$f" >> list.txt
  fi
done

甚至更简单:
grep -l 'disabled>true' */config.xml | cut -d/ -f1 > list.txt

++ 对于优化来说非常好,但请用双引号引用 $f 实例;此外,grep -qgrep -m1 >/dev/null 更简单。 最后,注意原帖的解决方案会导致目录名称以相反的顺序出现在 list.txt 中,虽然我不确定这是否真正符合意图。 - mklement0
我喜欢你的方法。非常简单易懂。 - user1164061

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