按顺序用列表中的文本替换多个文件中的文本

按顺序用列表中的文本替换多个文件中的文本

假设我有以下文件:

/etc/dir1/file.txt
/etc/dir2/file.txt
/etc/dir3/file.txt

... 一直到dir100(100 个目录),每个目录都有file.txt

我在 中有以下文本文件/root/list.txt。在 中list.txt,我有 100 行,每行都有不同的文本字符串。

每个 中file.txt都有一个文本字符串word1

我如何使用(或类似的东西) 将每个 中的sed单词替换为中的一行? 中的每一行只能使用一次。word1file.txtlist.txtlist.txt

例如,用 中的第一行word1替换,用 中的第二行替换,依此类推,一直到 100。/etc/dir1/file.txt/root/list.txtword1 /etc/dir2/file.txt/root/list.txt

sed由于这不是我的强项,我非常感谢任何帮助和援助。

答案1

您可以sed在循环中执行此操作如果的线条list.txt很乖巧。

我如何使用(或类似的东西) 将每个 中的sed单词替换为 中的一行? 中的每一行只能使用一次。word1file.txtlist.txtlist.txt

Ubuntu 有GNU sed, 哪个让一切变得简单仅替换文件中模式的第一次出现,然后停止。要对每个输入文件使用单独的替换字符串,可以使用循环。以下代码是只是由于它非常复杂,我建议将其制作成脚本并运行。有三个主要注意事项:

  1. 模式word1(我猜你可能会改成别的)不能包含/,除非你在命令中使用了不同的分隔符sed。它也不能包含sed特殊对待的字符,例如正则表达式元字符(\*.等等),除非这是你的意图。
  2. 中的行也list.txt不能包含/大多数特殊字符。
  3. 您的dir1、、dir2...在/etc,您的list.txt/root,但我已经编写了脚本来假设这些目录(以及该文件)在当前目录而是。我这样做是因为存储在这些位置的文件通常很重要,并且我假设您在实际使用之前会想要测试此脚本(并且可能进行自己的修改)。您可以更改脚本以使用您提供的位置或您需要的任何其他位置。

我把 #2 加粗了,因为我认为它可能会给你带来麻烦,具体取决于可能list.txt包含的内容。既然你已经收到警告,下面是脚本:

#!/bin/bash

mapfile -t <list.txt

for ((i=1; i<=${#MAPFILE[@]}; ++i))
do sed -i.bak "0,/word1/ s//${MAPFILE[i-1]}/" "dir$i/file.txt"
done

这就是全部了。如果你感兴趣的话,下面是它的工作原理:

  • mapfileBash 内置命令读取行数组。 我用它读自 list.txtMAPFILE。我没有指定数组的名称,因此使用默认名称。
  • Bash 提供替代的(C 风格)for循环, 哪个有用当想要循环从或到通过以下方式获得的值时参数扩展, 自从括号扩展在 Bash 中不会扩展诸如 之类的东西{1..$var}。我使用它从 1 循环到数组的长度MAPFILE
  • sed -i替换原始文件。除非您提供备份后缀,否则您将丢失旧版本。.bak如果您不想保留旧版本,可以从命令中删除,但我建议您考虑保留它。在删除它之前,请至少对其进行测试。
  • 使用 GNU sed,0,/word1/ s//REPLACEMENT/仅在文件开头到第一个匹配项word1( 0,/word1/) 的范围内进行操作,将与相同模式匹配的文本替换为REPLACEMENTs//*REPLACEMENT*/)。也就是说,它只替换输入中的第一个匹配项。无论输入的其余部分是否与模式匹配,它都不会改变。
  • Bash 数组的索引以 开头0,但文件的名称以 开头1,我决定循环变量$i应该从 开始。幸运的是,这很容易处理,因为 Bash 数组接受算术表达式作为索引。${MAPFILE[i-1]}扩展到元素我 - 1(那就是来自的行数组的第个list.txt元素) 。

替换使用随意的从文件读取的文本,考虑替代方案sed

如果你不能遵守警告 #2——也就是说, 中的行list.txt可以是任何内容——那么我不知道用 来做到这一点的好方法sed。但有很多替代方案。Bash 实际上可以自己做到这一点,不需要任何外部命令,我将展示一个几乎纯 Bash 的解决方案。(也许会发布更多答案来展示使用awk或其他实用程序。)

在 中sed,模式是正则表达式。但是方法处理模式作为一个整体。 也可以看看此 FOLDOC 条目man 7 glob*。因此,包括、?[]-- 以及其他一些特殊字符(如\--)在 中具有特殊含义word1(但通常与 中的含义不同sed)。如果word1是字面意思word1或任何其他没有通配符的文本,则没有问题。否则,您必须对其进行相应的修改。此方法消除了警告 #2,但是不是警告 #1——也不是#3,但你可以轻松地自己处理。

#!/bin/bash

pattern='word1' # text to search for
suffix='.bak'   # suffix to append for backup files

mapfile -t <list.txt

for ((i=1; i<=${#MAPFILE[@]}; ++i)); do
    name="dir$i/file.txt"
    mv "$name" "$name$suffix" || exit  # quit with an error if we can't rename

    {
        while read -r; do  # output up to and including the replacement
            case "$REPLY" in
            *"$pattern"*)
                printf '%s\n' "${REPLY/$pattern/${MAPFILE[i-1]}}"
                break ;;
            *)
                printf '%s\n' "$REPLY" ;;
            esac
        done

        while read -r; do  # output the rest
            printf '%s\n' "$REPLY"
        done

    } <"$name$suffix" >"$name"
done

如果你(仍然)感兴趣,下面是方法作品:

  • 和之前一样,我将其读list.txt入一个数组。我也可以将每个文件读file.txt入一个数组,但据我所知,这些文件可能非常大,因此我改为一次读取一行read -r
  • 我将每个文件重命名.bakmv.mv是此脚本使用的唯一外部实用程序。我没有费心--在路径前传递,因为在这种情况下,路径不可能以 开头-。如果移动操作失败,mv将输出错误并|| exit终止脚本,以防止意外数据丢失。运行此脚本可能丢失的唯一数据是预先存在的.bak文件中的数据。
  • 读取输入和写入输出的两个循环是组合在一起{ }整个组都有其输入输出重定向到使用该.bak文件作为输入,并使用与原始文件同名的文件作为输出(<"$name$suffix" >"$name")。
  • 每个循环可能会读取多行,而修改它们的唯一方法read -r是删除换行符在末端(printf稍后会放回\n)。在 Bash 中,read -r没有变量名会读取一行$REPLY,并且不会删除前导和尾随空格;它相当于IFS= read -r REPLY
  • 第一个循环读取,直到出现一行,该行由任意字符或无字符 ( *) 组成,后面跟着word1(" $pattern" ),后面再跟着任意字符或无字符 ( *)。当它找到这样的一行时,它会打印它,但会"$pattern"list.txt( ) 开始的第 1${MAPFILE[i-1]}行,然后中断循环。该行之前的所有行都直接打印。
  • 第二个循环逐字打印所有剩余的行。

通过使用两个组合在一起的循环,我实现了与sed上面详述的方法相同的基本逻辑——首先处理直到(包括但不超过)第一个匹配的文本,以便替换匹配,然后根本不搜索后续文本,只是复制。但是,与该sed方法不同,${MAPFILE[i-1]}扩展为中的特殊字符不被视为命令的一部分。

例如,观察一个替换字符串试图通过关闭内部参数扩展来制造麻烦,并且注射额外的替换不会成功:

$ s=foobarbaz t=bar u='}$s$s$s'; echo "${s/$t/${u}}"
foo}$s$s$sbaz

答案2

让我们创建测试环境放置在用户$HOME目录中。

  • 首先作为单个命令执行下一行:

    path="${HOME}/etc/dir"; for i in {1..100}; do mkdir -p "$path$i" ; echo -e "$path$i/file.txt:\nline1 some text here\nline2 word1 some text here word1\nline3 word1 some text here" > "$path$i/file.txt"; done
    

    这将创建一百个目录 - ~/etc/dir{1..100}。每个目录中还将创建一个名为 的文件,file.txt其中包含以下字符串word1几次:

    $ cat ~/etc/dir{1..100}/file.txt
    /home/<user>/etc/dir1/file.txt:
    line1 some text here
    line2 word1 some text here word1
    line3 word1 some text here
    /home/<user>/etc/dir2 file.txt: 
    ...
    
  • 然后执行这一行:

    path="${HOME}/root" && mkdir "$path"; for i in {1..100}; do echo '*{string line ['"$i"']}*' >> "$path/list.txt"; done
    

    这将创建一个名为 的目录~/root。在该目录中还将创建一个名为 的文件list.txt,其中包含一百行:

    $ cat ~/root/list.txt
    *{string line [1]}*
    *{string line [2]}*
    ...
    

让我们解决任务。根据具体情况,创建进入上面的步骤,因为字符串word1出现多次,我们有几种情况。示例解决方案:

  • word1要仅替换每个中第一次出现的file.txt,请执行此行:

    i=""; while read line; do i=$((i+1)); sed "0,/word1/ s|\word1|${line}|1" "$HOME/etc/dir$i/file.txt"; done < "$HOME/root/list.txt"
    

    输出应为:

    /home/<user>/etc/dir1/file.txt:
    line1 some text here
    line2 *{string line [1]}* some text here word1
    line3 word1 some text here
    /home/<user>/etc/dir2 file.txt:
    ...
    
  • 要仅替换word1每行中第一次出现的file.txt,请执行此行:

    i=""; while read line; do i=$((i+1)); sed "s|\word1|${line}|1" "$HOME/etc/dir$i/file.txt"; done < "$HOME/root/list.txt"
    

    输出应为:

    /home/<user>/etc/dir1/file.txt:
    line1 some text here
    line2 *{string line [1]}* some text here word1
    line3 *{string line [1]}* some text here
    /home/<user>/etc/dir2 file.txt:
    ...
    
  • 要替换word1每个中出现的所有内容file.txt,请执行以下行:

    i=""; while read line; do i=$((i+1)); sed "s|\word1|${line}|g" "$HOME/etc/dir$i/file.txt"; done < "$HOME/root/list.txt"
    

    输出应为:

    /home/<user>/etc/dir1/file.txt:
    line1 some text here
    line2 *{string line [1]}* some text here *{string line [1]}*
    line3 *{string line [1]}* some text here
    /home/<user>/etc/dir2 file.txt:
    ...
    

笔记.代入上面的例子:

  • 更改sedsed -i实际替换字符串或用于sed -i.bak进行替换并留下备份文件。

  • $HOME根据情况删除,描述成问题。

相关内容