一次替换多个字符串


11

我正在寻找一种使用通用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。

通常,该解决方案应等效于从左至右扫描输入,以与给定替换字符串之一进行最长匹​​配,并且对于每个匹配,执行替换并从输入中的该点继续进行操作(无已读取的输入,也不应考虑执行替换操作以进行匹配)。实际上,细节并不重要,只是替换的结果从不考虑全部或部分替换。

注意我只是在寻找正确的通用解决方案。请不要提出某些输入(输入文件,搜索和替换对)失败的解决方案,但是看起来不太可能。


我认为它们长于一个字符?为此,您可以使用tr AB BA
2014年

3
坦率地说,如果有人认为您的笔记有点粗鲁,我不会感到惊讶。
彼得2014年

1
当您未提供示例输入或输出时,您如何期望“仅获得正确的解决方案”?
jasonwryan 2014年

1
恐怕您需要按照描述的方式进行操作-从一开始就进行分析并随便替换-即不使用正则表达式。
彼得2014年

2
这是一个公平的问题,但答案是您需要一个状态机解析器,这是rici的答案所提供的(我认为是真正的黑客风格)。换句话说,您低估了任务的复杂性,比如“我想用正则表达式来一般地解析(HT | X)ML”->答案是否定的。您不能(只是)使用sed。您不能(只是)使用awk。AFAIK没有现成的工具可以做到这一点。Sans rici的漏洞利用,您需要编写一些代码。
goldilocks

Answers:


10

好的,一般的解决方案。以下bash函数需要2k参数;每对都包含一个占位符和一个替换符。由您适当地引用字符串以将它们传递到函数中。如果参数数量为奇数,则将添加一个隐式空参数,这将有效删除最后一个占位符。

无论是占位符,也没有更换可能包含NULL字符,但你可以使用标准C \作为-escapes例如\0,如果你需要NULS(因此你需要写的\\,如果你想有一个\)。

它需要标准的构建工具,该工具应该存在于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

我不确定这是否是个玩笑;)
Ambroz Bizjak 2014年

3
@ambrozbizjak:它可以工作,对于大的输入它是快速的,对于小输入是可以接受的快速。它可能不会使用您想到的工具,但它们是标准工具。为什么会开个玩笑?
rici 2014年

4
+1不要开玩笑!:D
goldilocks

那将是POSIX可移植的fn() { tcc ; } <<CODE\n$(gen code)\nCODE\n。我能问一下吗-这是一个了不起的答案,我一读它就立即投票赞成-但我不了解shell阵列发生了什么?这是"${@//\"/\\\"}"做什么的?
mikeserv

@mikeserv:«对于每个带引号的参数(“ $ @”),将所有(//)出现的引号(\“)替换为(/)的反斜杠(\\)和引号(\”) »。请参见bash手册中的参数扩展。
rici 2014年

1
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


+1为什么您说它仅代替“目标字符串的最早出现”?在输出中,看起来好像替换了所有它们。我不是要看它,但是可以在不对值进行硬编码的情况下以这种方式完成?
goldilocks 2014年

@goldilocks-是的-但只有在它们发生时。也许我应该改写。是的-您可以添加一个中间值sed并保存为null或其他内容,然后sed编写该脚本;或将其放在shell函数中,并为其赋予每行一"/$1/""/$2/"
比特的

这似乎并不在其中占位符的情况下工作PLACE1PLACE2PLAPLA总是赢。OP说:“等效于从左至右扫描输入以与给定替换字符串之一进行最长匹配 ”(强调)
rici 2014年

@rici-谢谢。然后,我将不得不执行空定界符。在一瞬间。
mikeserv

@rici-我正要发布另一个版本,该版本可以处理您所描述的内容,但是再次查看它,我认为我不应该这样做。他说,为最长一个给定替换字符串。做到这一点。没有迹象表明一个字符串是另一个字符串的子集,只有替换的值可能是。我也不认为遍历列表是解决问题的有效方法。就我所了解的问题而言,这是一个可行的解决方案。
mikeserv

1

一个perl解决方案。即使有人指出这是不可能的,我还是找到了一个,但总的来说,不可能进行简单的匹配和替换,甚至由于NFA的回溯而变得更糟,结果可能是意料之外的。

通常,必须说,问题产生的结果取决于替换元组的顺序和长度。即:

A B
AA CC

输入AAA结果为BBBCCB

这里的代码:

#!/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

Checkerbunny:

$ echo 'ABBabbaBBbaabAAabbc'|perl script
$ BAAbaabAAabbcBBaaba
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.