替换找到的第一个文件中的字符串

Replace string in the first file it is found

我有一堆这样命名的文件:

chapter1.tex
chapter2.tex
chapter3.tex
...
chapter 10.tex
chapter 11.tex
etc.

我正在尝试使用 sed 在所有文件中查找 AAAAAA 的第一个实例并将其替换为 ZZZZZZ

sed -i "0,/AAAAAA/s//ZZZZZZ/" chapter*.tex

我试过上面的命令,但是有两个问题:

  1. 它在每个文件中查找并替换 AAAAAA 的第一个实例。我只想要所有文件中的第一个实例。
  2. 我怀疑,像许多 Bash 工具一样,它不能正确地按顺序对我的文件进行排序。例如。如果我键入 ls,那么 chapter10.tex 会列在 chapter1.tex 之前。按章节顺序搜索文件很重要。

如何使用 Bash 工具从大量文件中查找和替换第一个实例,因此只替换第一个找到的文件中的第一个实例,同时还尊重文件顺序 (chapter1.tex第一,chapter10.tex第十)?

这是一个基于 bash 循环的解决方案,可以处理 chapter 10.tex 等文件名,即带空格的文件名等:

while IFS= read -r -d '' file; do
   if grep -q 'AAAAAA' "$file"; then
      echo "changing $file"
      sed -i '0,/AAAAAA/s//ZZZZZZ/' "$file"
      break
   fi
done < <(printf '%s[=10=]' chapter*.tex | sort -z -V)

这是假设 sedsort 都来自 gnu utils。


如果您有支持就地编辑的 gnu awk 4+ 版本,即 -i inplace 那么您可以将 grep + sed 替换为单个 awk:

while IFS= read -r -d '' file; do
   awk -i inplace '!n {n=sub(/AAAAAA/, "ZZZZZZ")} 1;
   END {exit !n}' "$file" && break
done < <(printf '%s[=11=]' chapter*.tex | sort -z -V)

#更新

我站在巨人的背上,哈哈

感谢@potong 提供了一个很棒的带有大括号扩展的排序解决方案!这意味着整个事情可以简化为单进程一行:

sed -i '0,/^AAA/{ /^AAA/{ s/AAA/ZZZ/; h; } }; ${ x; /./{x;q;}; x; }' chapter\ {[0-9],[0-9][0-9]}.tex 

#编辑

正如所指出的,下面的原始解决方案将处理和更改每个文件中的第一次出现,并且不会更正文件顺序。 @anubhava 已经提供了一个优秀、优雅的排序解决方案,我不会尝试对其进行改进。

while IFS= read -r -d '' file; do lst+=( "$file" ); done < <(printf '%s[=11=]' chapter*.tex | sort -z -V)

这会按正确的顺序创建一个文件名列表,可以将其传递给 sed 的单个调用来处理它们 en masse.

将其应用于基于 sed 的解决方案的排序,并且只命中 any 文件中的第一次出现 -

sed -i '0,/^AAA/{ /^AAA/{ s/AAA/ZZZ/; h; } }; ${ x; /./{x;q;}; x; }' "${lst[@]}"

这将遍历每个文件并更改它在该文件中找到的第一个匹配项,h保留第一个找到它的行。在每个文件的最后一行,它 ex 更改保持缓冲区的当前行并检查交换后模式缓冲区中是否有任何内容。如果没有,它会将其换回并继续。如果 ,它会将其换回并 q 退出,跳过所有后续文件。

虽然有些复杂,但这不会为每个文件生成进程。


原创


使用双重条件 -

sed -i '0,/AAAAAA/{ /AAAAAA/s/AAAAAA/ZZZZZZ/ }' chapter*.tex

要查看相同的通用逻辑:

$: cat a.tex b.tex
111
AAA
BBB
AAA
222

111
AAA
BBB
AAA
222

$: sed -i '0,/^AAA/{ /^AAA/s/AAA/ZZZ/; }' *.tex
$: cat a.tex b.tex
111
ZZZ
BBB
AAA
222

111
ZZZ
BBB
AAA
222

'0,/^AAA/ 是正确的,因为它的范围是从文件的开头到目标字符串的 第一次 出现。

{ 打开一个块,我们可以在其中使用第二个搜索来确保它只影响目标字符串。

在块内,/^AAA/s/AAA/ZZZ/; 替换 AAA 字符串并忽略它之前的所有记录。 } 关闭区块。之后的所有记录将保持不变。

有了完整的 GNU 工具箱,您就不需要循环了。

printf '%s[=10=]' chapter*.tex    \
| sort -zV                    \
| xargs -0 grep -FlZ 'AAAAAA' \
| head -zn1                   \
| xargs -0r sed -i 's/AAAAAA/ZZZZZZ/'

这可能对你有用(GNU sed 和 grep):

grep -ns 'AAAAAA' chapter{1..9999}.txt | head -1 |
sed -nE 's#([^:]*):([^:]*):.*#sed -i "s/AAAAAA/ZZZZZZ/" #e'

使用 grep 和 bash 的大括号扩展来识别一个可能的匹配文件和行号,并构建一个 sed 脚本来更新该行号处的文件。

N.B。大括号扩展以正确的顺序生成文件名,grep 的 -s 命令行选项会抑制不存在的文件消息。


使用 GNU 并行的替代方案:

grep -sno 'AAAAAA' chapter{1..9999}.txt | head -1 |
parallel --colsep : sed '{2}s/{3}/ZZZZZZ/' {1}