![一次替换多个字符串](https://linux22.com/image/38365/%E4%B8%80%E6%AC%A1%E6%9B%BF%E6%8D%A2%E5%A4%9A%E4%B8%AA%E5%AD%97%E7%AC%A6%E4%B8%B2.png)
我正在寻找一种方法,使用常见的 Unix 工具(bash、sed、awk,也许是 perl)用具体值替换模板文件中的占位符字符串。重要的是,替换是在单遍中完成的,也就是说,已经扫描/替换的内容不得考虑进行另一次替换。例如,这两次尝试都失败了:
echo "AB" | awk '{gsub("A","B");gsub("B","A");print}'
>> AA
echo "AB" | sed 's/A/B/g;s/B/A/g'
>> AA
这种情况下正确的结果当然是BA。
一般来说,该解决方案应该相当于从左到右扫描输入以查找与给定替换字符串之一的最长匹配,并且对于每个匹配,执行替换并从输入中的该点继续(没有已读取的输入或执行的替换均应考虑进行匹配)。实际上,细节并不重要,只是替换的结果永远不会被考虑用于另一次替换,无论是全部还是部分。
笔记我只是在寻找正确的通用解决方案。请不要提出对某些输入(输入文件、搜索和替换对)失败的解决方案,无论它们看起来多么不可能。
答案1
好的,通用解决方案。以下 bash 函数需要2k
参数;每对由一个占位符和一个替换符组成。您可以适当地引用字符串以将它们传递到函数中。如果参数数量为奇数,则会添加隐式空参数,这将有效删除最后一个占位符的出现。
占位符和替换都不能包含 NUL 字符,但您可以使用标准的 C\
转义符,例如\0
如果您需要s (因此如果您想要 a ,则NUL
需要编写)。\\
\
它需要标准构建工具,这些工具应该存在于类 posix 系统(lex 和 cc)上。
replaceholder() {
local dir=$(mktemp -d)
( cd "$dir"
{ printf %s\\n "%option 8bit noyywrap nounput" "%%"
printf '"%s" {fputs("%s", yyout);}\n' "${@//\"/\\\"}"
printf %s\\n "%%" "int main(int argc, char** argv) { return yylex(); }"
} | lex && cc lex.yy.c
) && "$dir"/a.out
rm -fR "$dir"
}
我们假设\
参数中如有必要,已经转义,但我们需要转义双引号(如果存在)。这就是第二个 printf 的第二个参数的作用。由于lex
默认操作是ECHO
,所以我们不需要担心它。
运行示例(对于持怀疑态度的人来说,它只是一个廉价的商用笔记本电脑):
$ time echo AB | replaceholder A B B A
BA
real 0m0.128s
user 0m0.106s
sys 0m0.042s
$ time printf %s\\n AB{0000..9999} | replaceholder A B B A > /dev/null
real 0m0.118s
user 0m0.117s
sys 0m0.043s
对于较大的输入,向 提供优化标志可能会很有用cc
,并且对于当前的 Posix 兼容性,最好使用c99
。更雄心勃勃的实现可能会尝试缓存生成的可执行文件,而不是每次都生成它们,但生成它们并不完全昂贵。
编辑
如果你有TCC,您可以避免创建临时目录的麻烦,并享受更快的编译时间,这将有助于正常大小的输入:
treplaceholder () {
tcc -run <(
{
printf %s\\n "%option 8bit noyywrap nounput" "%%"
printf '"%s" {fputs("%s", yyout);}\n' "${@//\"/\\\"}"
printf %s\\n "%%" "int main(int argc, char** argv) { return yylex(); }"
} | lex -t)
}
$ time printf %s\\n AB{0000..9999} | treplaceholder A B B A > /dev/null
real 0m0.039s
user 0m0.041s
sys 0m0.031s
答案2
printf 'STRING1STRING1\n\nSTRING2STRING1\nSTRING2\n' |
od -A n -t c -v -w1 |
sed 's/ \{1,3\}//;s/\\$/&&/;H;s/.*//;x
/\nS\nT\nR\nI\nN\nG\n1/s//STRING2/
/\nS\nT\nR\nI\nN\nG\n2/s//STRING1/
/\\n/!{x;d};s/\n//g;s/./\\&/g' |
xargs printf %b
###OUTPUT###
STRING2STRING2
STRING1STRING2
STRING1
像这样的东西总是只会替换目标字符串的每次出现一次,因为它们出现在sed
流中,每行一次。这是我能想到的最快的方法。话又说回来,我不写C。但是这个做如果您愿意,可以可靠地处理空分隔符。看这个答案了解它是如何工作的。这对于任何包含的特殊 shell 字符或类似字符都没有问题 - 但它是特定于 ASCII 语言环境,或者换句话说,od
不会在同一行上输出多字节字符,并且只会输出一个。如果这是一个问题,您需要添加iconv
.
答案3
一个perl
办法。即使有人说这是不可能的,我也找到了一个,但一般来说,简单的匹配和替换是不可能的,甚至由于 NFA 的回溯而变得更糟,结果也可能是意外的。
一般来说,必须指出的是,该问题会产生不同的结果,具体取决于替换元组的顺序和长度。 IE:
A B
AA CC
输入AAA
结果为BBB
或CCB
。
这里是代码:
#!/usr/bin/perl
$v='if (0) {} ';
while (($a,$b)=split /\s+/, <DATA>) {
$k.=$a.'|';
$v.='elsif ($& eq \''.$a.'\') {print \''.$b.'\'} ';
}
$k.='.';
$v.='else {print $&;}';
eval "
while (<>) {
\$_ =~ s/($k)/{$v}/geco;
}";
print "\n";
__DATA__
A B
B A
abba baab
baab abbc
abbc aaba
棋盘兔:
$ echo 'ABBabbaBBbaabAAabbc'|perl script
$ BAAbaabAAabbcBBaaba