複数の文字列を一度に置き換える

複数の文字列を一度に置き換える

私は一般的な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つと最も長い一致を見つけるために入力を左から右にスキャンするのと同じです。各一致に対して置換を実行し、入力の対応するポイントから続行します(入力を読み取ったり読み取ったりせずに)。実行された置換は一致のために考慮されるべきです。実際、代替結果が全体的または部分的に他の置換について考慮されないことを除いて、詳細は重要ではない。

ノート私は正しい一般的な解決策を探しています。いくら不可能に見えても、特定の入力(入力ファイル、検索、置換のペア)に対して失敗する解決策を提案しないでください。

答え1

いいですね。一般的な解決策です。次のbash関数には2k引数が必要です。各ペアはプレースホルダと代替項目で構成されます。文字列を適切に引用して関数に渡すことができます。引数の数が奇数の場合、暗黙のヌル引数が追加され、最後のプレースホルダの発生を効果的に排除します。

\プレースホルダと代替エントリの両方にNUL文字を含めることはできませんが、たとえば、sが必要な場合は標準のCエスケープを使用できます\0(したがって、aが必要な場合はNULaを作成する必要があります)。\\\

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

sedこれは常にストリームに表示されるターゲット文字列の各項目を1行に1回だけ置き換えます。これが私が考えることができる最速の方法です。そして再びCを書きません。しかし、これはする必要に応じてNull区切り文字を確実に処理できます。バラよりこの回答どのように動作するかをご覧ください。特殊なシェル文字や同様の文字が含まれている場合は問題ありません。はいASCII ロケール専用です。つまり、odマルチバイト文字は同じ行に出力されず、1 つだけ出力されます。これが問題の場合は追加する必要がありますiconv

答え3

一方perl通行。誰かが不可能だと言っても見つけましたが、一般的に単純な一致と交換は不可能です。

一般に、この問題は代替タプルの順序と長さによって異なる結果が生じることに注意してください。つまり:

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

関連情報