web-dev-qa-db-ja.com

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つとの最長一致を検索し、一致ごとに置換を実行して、入力のそのポイントから続行することと同等でなければなりません(すでに読み込まれた入力も、実行された置換も一致と見なされます)。実際、詳細は関係ありません。置換の結果が全体または一部が別の置換の対象になることはありません。

[〜#〜] note [〜#〜]正しい一般的なソリューションのみを探しています。特定の入力(入力ファイル、検索および置換ペア)で失敗するソリューションを提案しないでください。

11
Ambroz Bizjak

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

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

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
10
rici

Perlソリューション。不可能だと言った人もいますが、一般的には単純な一致と置換は不可能であり、NFAのバックトラックのために結果が予想外になる可能性があるため、さらに悪化します。

一般に、そしてこれは言わなければならないことですが、問題は置換タプルの順序と長さに依存するさまざまな結果をもたらします。例:

A B
AA CC

入力AAABBBまたは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
1
user55518
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で1行に1バイトずつ発生するため、ターゲット文字列の各出現箇所を常に1回だけ置き換えます。これは私が想像できる最速の方法です。繰り返しますが、Cは記述しません。ただし、これはdoes必要に応じて、ヌル区切り文字を確実に処理します。それがどのように機能するかについては この答え を参照してください。これには、含まれている特殊なシェル文字などの問題はありませんが、isASCIIロケール固有、または、つまり、odは同じ行にマルチバイト文字を出力せず、1行に1つしか実行しません。これが問題である場合は、iconvに追加します。

1
mikeserv