Ersetzen Sie mehrere Zeichenfolgen in einem Durchgang

Ersetzen Sie mehrere Zeichenfolgen in einem Durchgang

Ich suche nach einer Möglichkeit, Platzhalterzeichenfolgen in einer Vorlagendatei mit gängigen Unix-Tools (bash, sed, awk, evtl. perl) durch konkrete Werte zu ersetzen. Dabei ist es wichtig, dass die Ersetzung in einem einzigen Durchgang erfolgt, d. h., was bereits gescannt/ersetzt wurde, darf bei einer weiteren Ersetzung nicht berücksichtigt werden. Diese beiden Versuche schlagen beispielsweise fehl:

echo "AB" | awk '{gsub("A","B");gsub("B","A");print}'
>> AA

echo "AB" | sed 's/A/B/g;s/B/A/g'
>> AA

Das richtige Ergebnis ist in diesem Fall natürlich BA.

Im Allgemeinen sollte die Lösung dem Scannen der Eingabe von links nach rechts nach der längsten Übereinstimmung mit einer der angegebenen Ersetzungszeichenfolgen entsprechen. Für jede Übereinstimmung sollte eine Ersetzung durchgeführt und von diesem Punkt an in der Eingabe fortgefahren werden (weder die bereits gelesene Eingabe noch die durchgeführten Ersetzungen sollten für Übereinstimmungen berücksichtigt werden). Tatsächlich sind die Details unerheblich, nur dass die Ergebnisse der Ersetzung niemals für eine weitere Ersetzung berücksichtigt werden, weder ganz noch teilweise.

NOTIZIch suche nur nach korrekten generischen Lösungen. Bitte schlagen Sie keine Lösungen vor, die bei bestimmten Eingaben (Eingabedateien, Suchen- und Ersetzen-Paare) fehlschlagen, so unwahrscheinlich sie auch erscheinen mögen.

Antwort1

OK, eine allgemeine Lösung. Die folgende Bash-Funktion erfordert 2kArgumente; jedes Paar besteht aus einem Platzhalter und einem Ersatz. Es liegt an Ihnen, die Zeichenfolgen entsprechend zu zitieren, um sie an die Funktion zu übergeben. Wenn die Anzahl der Argumente ungerade ist, wird ein implizites leeres Argument hinzugefügt, wodurch Vorkommen des letzten Platzhalters effektiv gelöscht werden.

Weder Platzhalter noch Ersetzungen dürfen NUL-Zeichen enthalten, aber Sie können standardmäßige C- \Escapes verwenden, z. B. \0wenn Sie ein s benötigen NUL(und folglich müssen Sie schreiben, \\wenn Sie ein möchten \).

Es erfordert die Standard-Build-Tools, die auf einem POSIX-ähnlichen System (Lex und CC) vorhanden sein sollten.

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

Wir gehen davon aus, dass dies \in den Argumenten bereits maskiert ist, falls erforderlich, aber wir müssen doppelte Anführungszeichen maskieren, falls vorhanden. Dies ist, was das zweite Argument des zweiten printf tut. Da die lexStandardaktion ist ECHO, müssen wir uns darüber keine Gedanken machen.

Beispiellauf (mit Zeitangaben für Skeptiker; es ist nur ein billiger Standard-Laptop):

$ 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

Bei größeren Eingaben kann es sinnvoll sein, ein Optimierungsflag anzugeben cc, und aus Gründen der aktuellen Posix-Kompatibilität wäre es besser, zu verwenden c99. Eine noch anspruchsvollere Implementierung könnte versuchen, die generierten ausführbaren Dateien zwischenzuspeichern, anstatt sie jedes Mal neu zu generieren, aber ihre Generierung ist nicht gerade teuer.

Bearbeiten

Wenn Sie habentcckönnen Sie sich die Mühe ersparen, ein temporäres Verzeichnis erstellen zu müssen, und sich über die schnellere Kompilierungszeit freuen, die bei Eingaben normaler Größe hilfreich ist:

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

Antwort2

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

So etwas ersetzt jedes Vorkommen Ihrer Zielzeichenfolgen immer nur einmal, wenn sie in sed's im Stream vorkommen, und zwar mit einem Bit pro Zeile. Das ist der schnellste Weg, den ich mir vorstellen kann. Andererseits schreibe ich kein C. Aber dastutNull-Trennzeichen zuverlässig verarbeiten, wenn Sie es wünschen. Siehediese Antwortfür die Funktionsweise. Dies hat keine Probleme mit enthaltenen Shell-Sonderzeichen oder ähnlichem - aber esIstASCII-Gebietsschema-spezifisch, oder anders ausgedrückt, odes werden keine Multibyte-Zeichen in derselben Zeile ausgegeben, sondern nur eines pro Zeile. Wenn dies ein Problem darstellt, sollten Sie hinzufügen iconv.

Antwort3

Eine perlLösung. Auch wenn einige behaupteten, dass es nicht möglich sei, habe ich eine gefunden, aber im Allgemeinen ist ein einfaches Matchen und Ersetzen nicht möglich und es wird sogar noch schlimmer, da das Backtracking eines NFA das Ergebnis unerwartet sein kann.

Im Allgemeinen, und das muss gesagt werden, führt das Problem zu unterschiedlichen Ergebnissen, die von der Reihenfolge und Länge der Ersetzungstupel abhängen. Beispielsweise:

A B
AA CC

und die Eingabe AAAergibt BBBoder CCB.

Hier der Code:

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

verwandte Informationen