1回のパスで複数の文字列を置換する

1回のパスで複数の文字列を置換する

一般的な Unix ツール (bash、sed、awk、おそらく perl) を使用して、テンプレート ファイル内のプレースホルダー文字列を具体的な値に置き換える方法を探しています。 置き換えは 1 回のパスで行われることが重要です。つまり、すでにスキャン/置き換えたものは、別の置き換えの対象として考慮されてはいけません。 たとえば、次の 2 つの試みは失敗します。

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 つと最も長く一致するものを探し、一致するものごとに置換を実行し、入力のその時点から続行することと同等です (既に読み込まれた入力や実行された置換は、一致として考慮されません)。実際には、詳細は重要ではなく、置換の結果が全体的または部分的に別の置換に考慮されることはないという点だけが重要です。

注記私は正しい一般的な解決策のみを探しています。たとえ可能性が低いように見えても、特定の入力 (入力ファイル、検索と置換のペア) で失敗する解決策を提案しないでください。

答え1

一般的な解決策です。次の bash 関数には2k引数が必要です。各ペアはプレースホルダーと置換で構成されます。文字列を適切に引用符で囲んで関数に渡すのは、あなた次第です。引数の数が奇数の場合、暗黙的に空の引数が追加され、最後のプレースホルダーの出現が効果的に削除されます。

プレースホルダーも置換も NUL 文字を含めることはできませんが、が必要な場合\など、標準の C エスケープを使用できます(したがって、 が必要な場合は と記述する必要があります)。\0NUL\\\

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"
}

\引数では必要に応じて が既にエスケープされていると想定していますが、二重引用符がある場合はエスケープする必要があります。これが、2 番目の printf の 2 番目の引数が行うことです。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

このような方法は、ストリーム内の'sに出現するターゲット文字sed列を1行に1回ずつ置換します。これが、私が想像する最も速い方法です。しかし、私はC言語を書いていません。しかし、これはする必要に応じてヌル区切り文字を確実に処理します。この答えどのように動作するか。これには、特別なシェル文字などが含まれていても問題ありませんが、ASCII ロケール固有、つまり、od同じ行にマルチバイト文字を出力せず、1 行につき 1 つだけ出力します。これが問題になる場合は、 を追加する必要がありますiconv

答え3

解決perl策。不可能だと主張する人もいますが、私は解決策を見つけました。ただし、一般的に単純な一致と置換は不可能であり、NFA のバックトラッキングによって状況が悪化し、予期しない結果になることもあります。

一般的に、そしてこれは言わなければならないことですが、この問題は、置換タプルの順序と長さに応じて異なる結果をもたらします。つまり、

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

関連情報