假设我有以下文件:
/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
单词替换为中的一行? 中的每一行只能使用一次。word1
file.txt
list.txt
list.txt
例如,用 中的第一行word1
替换,用 中的第二行替换,依此类推,一直到 100。/etc/dir1/file.txt
/root/list.txt
word1
/etc/dir2/file.txt
/root/list.txt
sed
由于这不是我的强项,我非常感谢任何帮助和援助。
答案1
您可以sed
在循环中执行此操作如果的线条list.txt
很乖巧。
我如何使用(或类似的东西) 将每个 中的
sed
单词替换为 中的一行? 中的每一行只能使用一次。word1
file.txt
list.txt
list.txt
Ubuntu 有GNU sed, 哪个让一切变得简单仅替换文件中模式的第一次出现,然后停止。要对每个输入文件使用单独的替换字符串,可以使用循环。以下代码是只是由于它非常复杂,我建议将其制作成脚本并运行。有三个主要注意事项:
- 模式
word1
(我猜你可能会改成别的)不能包含/
,除非你在命令中使用了不同的分隔符sed
。它也不能包含sed
特殊对待的字符,例如正则表达式元字符(\
、*
、.
等等),除非这是你的意图。 - 中的行也
list.txt
不能包含/
大多数特殊字符。 - 您的
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
这就是全部了。如果你感兴趣的话,下面是它的工作原理:
mapfile
是Bash 内置命令读取行数组。 我用它读自list.txt
MAPFILE
。我没有指定数组的名称,因此使用默认名称。- Bash 提供替代的(C 风格)
for
循环, 哪个有用当想要循环从或到通过以下方式获得的值时参数扩展, 自从括号扩展在 Bash 中不会扩展诸如 之类的东西{1..$var}
。我使用它从 1 循环到数组的长度MAPFILE
。 sed -i
替换原始文件。除非您提供备份后缀,否则您将丢失旧版本。.bak
如果您不想保留旧版本,可以从命令中删除,但我建议您考虑保留它。在删除它之前,请至少对其进行测试。- 使用 GNU sed,
0,/word1/ s//REPLACEMENT/
仅在文件开头到第一个匹配项word1
(0,/word1/
) 的范围内进行操作,将与相同模式匹配的文本替换为REPLACEMENT
(s//*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
。 - 我将每个文件重命名
.bak
为mv
.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: ...
笔记.代入上面的例子:
更改
sed
为sed -i
实际替换字符串或用于sed -i.bak
进行替换并留下备份文件。$HOME
根据情况删除,描述成问题。