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


11

テンプレートファイルのプレースホルダー文字列を、一般的な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文字より長いと思いますか?このために使用できますtr AB BA
Kevin

3
率直に言って、誰かがあなたのメモを少し失礼だと思っても驚かないでしょう。
peterph 2014年

1
サンプルの入力または出力を提供していない場合、「正しい解決策のみを取得する」ことをどのように期待しますか?
jasonwryan 14年

1
私はあなたがそれをあなたがそれを説明しているように正確に行う必要があると思います-最初から解析し、あなたが行くにつれて置き換えます-つまり、正規表現ではありません。
peterph 2014年

2
これは公平な質問ですが、答えはステートマシンパーサーが必要であるということです。これがリチの答えが提供するものです(真のハッカースタイルで)。言い換えれば、「正規表現を使用して(HT | X)MLを一般的に解析したい」というタスクの複雑さを過小評価しています->答えはNOです。sedを(ただ)使用することはできません。awkを(ただ)使用することはできません。私の知る限り、箱から出してこれを行う既存のツールはありません。Sans Riciのエクスプロイトでは、いくつかのコードを記述する必要があります。
goldilocks 2014年

回答:


10

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

プレースホルダーも置換もNUL文字を含むことはできませんが、s が必要な場合\など、標準のC エスケープを使用\0できます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"
}

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

これが冗談かどうかわからない;)
Ambroz Bizjak 14年

3
@ambrozbizjak:機能します。大きな入力の場合は高速で、小さな入力の場合は許容範囲内で高速です。それはあなたが考えていたツールを使用しないかもしれませんが、それらは標準的なツールです。なぜそれは冗談でしょうか?
リチ2014年

4
+1冗談ではないため!:D
goldilocks 2014年

それはのようなPOSIXポータブルですfn() { tcc ; } <<CODE\n$(gen code)\nCODE\n。私は質問してもいいですか-これは素晴らしい答えで、読んだらすぐに賛成しました-しかし、シェル配列に何が起こっているのかわかりませんか?これは何をし"${@//\"/\\\"}"ますか?
mikeserv 2014年

@mikeserv:«引用符付きの引数( "$ @")ごとに、引用符(\ /)が出現するすべての(//)を(/)バックスラッシュ(\\)に置き換え、その後に引用符(\ ")を続けます。 »。bashマニュアルのパラメーター拡張を参照してください。
リチ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ストリーム内で1行に1バイトずつ発生するため、このようなものは常にターゲット文字列の各出現を1回だけ置き換えます。これは私が想像する最も速い方法です。その後、再び、私はCを書いていない。しかし、これはないあなたがそれを望むなら確実にヌル区切り文字を処理します。それがどのように機能するかについては、この回答を参照してください。これには、含まれている特殊なシェル文字などの問題はありませんが、 ASCIIロケール固有であり、つまり、od同じ行にマルチバイト文字を出力せず、1つだけを実行します。これが問題である場合は、追加する必要がありますiconv


+1それが「ターゲット文字列の最初の出現」のみを置き換えると言うのはなぜですか?出力では、それらをすべて置き換えたように見えます。私はそれを見るように求めていませんが、これは値をハードコーディングせずにこの方法で行うことができますか?
goldilocks 2014年

@goldilocks-はい-しかし、それらが発生したときのみ。多分私はそれを言い換える必要があります。そして、ええ-ミドルsedを追加して、nullまたは何かに保存してから、sedこのスクリプトを作成することができます。またはシェルの機能でそれを入れて、それはのような1行に1つの一口で値が与え"/$1/"... "/$2/"-多分私はあまりにもこれらの機能を記述します...
mikeserv

これは、プレースホルダーがPLACE1PLACE2およびの場合は機能しないようですPLAPLAいつも勝つ。OPの説明:「入力を左から右にスキャンして、指定された置換文字列の1つに最も長い一致をスキャンするのと同じ」(強調が追加されています)
rici

@rici-ありがとう。次に、ヌル区切り文字を実行する必要があります。すぐに戻る。
mikeserv 2014年

@rici-私はちょうどあなたが説明するものを処理する別のバージョンを投稿しようとしていましたが、もう一度それを見て、私はそうすべきではないと思います。彼は与えられた置換文字列の1つについて最長と言います。これはそれを行います。ある文字列が別の文字列のサブセットであることを示すものはなく、置き換えられた値がそうである可能性があるだけです。また、リストを反復処理することが問題を解決する有効な方法だとは思いません。私が理解している問題を考えると、これは実用的な解決策です。
mikeserv 2014年

1

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.