Ersetzen Sie mehrere Zeichenfolgen in einem Durchgang

11

Ich suche nach einer Möglichkeit, Platzhalterzeichenfolgen in einer Vorlagendatei durch konkrete Werte durch gängige Unix-Tools (bash, sed, awk, möglicherweise perl) zu ersetzen. Es ist wichtig, dass der Austausch in einem einzigen Durchgang erfolgt, dh was bereits gescannt / ersetzt wurde, darf bei einem anderen Austausch 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 entsprechen, um eine längste Übereinstimmung mit einer der angegebenen Ersatzzeichenfolgen zu erzielen, und für jede Übereinstimmung eine Ersetzung durchzuführen und von diesem Punkt an in der Eingabe fortzufahren (keine der Bereits gelesene Eingaben oder die durchgeführten Ersetzungen sollten für Übereinstimmungen berücksichtigt werden. Tatsächlich spielen die Details keine Rolle, nur dass die Ergebnisse des Austauschs weder ganz noch teilweise für einen anderen Ersatz berücksichtigt werden.

HINWEIS Ich suche nur nach korrekten generischen Lösungen. Bitte schlagen Sie keine Lösungen vor, die für bestimmte Eingaben (Eingabedateien, Suchen und Ersetzen von Paaren) fehlschlagen, auch wenn dies unwahrscheinlich erscheint.

Ambroz Bizjak
quelle
Ich nehme an, sie sind länger als ein Charakter? Dafür könnten Sie verwenden tr AB BA.
Kevin
3
Und ehrlich gesagt wäre ich nicht überrascht, wenn jemand Ihre Notiz für etwas unhöflich halten würde.
Peterph
1
Wie erwarten Sie, "nur dann korrekte Lösungen zu erhalten", wenn Sie keine Beispieleingabe oder -ausgabe bereitgestellt haben?
Jasonwryan
1
Ich fürchte, Sie müssen es genau so machen, wie Sie es beschreiben - von Anfang an analysieren und im Laufe der Zeit ersetzen - dh nicht durch reguläre Ausdrücke.
Peterph
2
Dies ist eine faire Frage, aber die Antwort ist, dass Sie einen State-Machine-Parser benötigen , wie es Ricis Antwort liefert (im wahren Hacker-Stil, denke ich). Mit anderen Worten, Sie unterschätzen die Komplexität der Aufgabe, unter anderem "Ich möchte (HT | X) ML generisch mit regulären Ausdrücken analysieren" -> Die Antwort lautet NEIN. Sie können sed nicht (nur) verwenden. Sie können awk nicht (nur) verwenden. AFAIK Es gibt kein Tool, das dies sofort erledigt. Ohne Ricis Exploit müssten Sie Code schreiben.
Goldlöckchen

Antworten:

10

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, das das Auftreten des letzten Platzhalters effektiv löscht.

Weder Platzhalter noch Ersetzungen dürfen NUL-Zeichen enthalten, aber Sie können Standard-C- \Landschaften verwenden, z. B. \0wenn Sie NULs benötigen (und folglich müssen Sie schreiben, \\wenn Sie a 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 \bei Bedarf bereits in den Argumenten maskiert ist, aber wir müssen doppelte Anführungszeichen vermeiden, falls vorhanden. Das ist es, was das zweite Argument zum zweiten printf bewirkt. Da die lexStandardaktion ist ECHO, müssen wir uns darüber keine Sorgen machen.

Beispiellauf (mit Timings 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

Für größere Eingaben kann es nützlich sein, ein Optimierungsflag für bereitzustellen cc, und für die aktuelle Posix-Kompatibilität ist es besser, diese zu verwenden c99. Eine noch ehrgeizigere Implementierung könnte versuchen, die generierten ausführbaren Dateien zwischenzuspeichern, anstatt sie jedes Mal zu generieren, aber ihre Generierung ist nicht gerade teuer.

Bearbeiten

Wenn Sie über tcc verfügen , können Sie den Aufwand beim Erstellen eines temporären Verzeichnisses vermeiden und die schnellere Kompilierungszeit genießen, 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
Rici
quelle
Ich bin nicht sicher, ob dies ein Witz ist oder nicht;)
Ambroz Bizjak
3
@ambrozbizjak: Es funktioniert, es ist schnell für große Eingaben und akzeptabel schnell für kleine Eingaben. Möglicherweise werden nicht die Tools verwendet, an die Sie gedacht haben, aber es handelt sich um Standardwerkzeuge. Warum sollte es ein Witz sein?
Rici
4
+1 Weil du kein Witz bist! : D
Goldlöckchen
Das wäre wie POSIX portabel fn() { tcc ; } <<CODE\n$(gen code)\nCODE\n. Kann ich fragen - das ist eine großartige Antwort und ich habe sie sofort nach dem Lesen positiv bewertet -, aber ich verstehe nicht, was mit dem Shell-Array passiert? Was macht "${@//\"/\\\"}"das?
Mikeserv
@mikeserv: «Ersetzen Sie für jedes Argument als Anführungszeichen (" $ @ ") alle (//) Vorkommen eines Anführungszeichens (") durch (/) einen Backslash (\\), gefolgt von einem Anführungszeichen ("). ». Siehe Parametererweiterung im Bash-Handbuch.
Rici
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

So etwas ersetzt jedes Vorkommen Ihrer Zielzeichenfolgen immer nur einmal, da sie sedin Streams mit einem Biss pro Zeile auftreten. Dies ist der schnellste Weg, den ich mir vorstellen kann. Dann wieder, ich schreibe C. nicht , aber diese nicht zuverlässig null delimiters handhaben, wenn Sie es wünschen. In dieser Antwort erfahren Sie, wie es funktioniert. Dies hat keine Probleme mit enthaltenen speziellen Shell-Zeichen oder ähnlichem - aber es ist ASCII-länderspezifisch oder gibt mit anderen Worten odkeine Mehrbyte-Zeichen in derselben Zeile aus und führt nur eines pro Zeichen aus. Wenn dies ein Problem ist, möchten Sie hinzufügen iconv.

mikeserv
quelle
+1 Warum ersetzt es Ihrer Meinung nach nur "das früheste Auftreten Ihrer Zielzeichenfolgen"? In der Ausgabe sieht es so aus, als ob es alle ersetzt. Ich möchte es nicht sehen, aber könnte dies auf diese Weise geschehen, ohne die Werte fest zu codieren?
Goldlöckchen
@goldilocks - Ja - aber nur sobald sie auftreten. Vielleicht sollte ich das umformulieren. Und ja - Sie könnten einfach eine Mitte hinzufügen sedund bis zu einer Null oder so etwas speichern, um dann seddas Skript dieses einen schreiben zu lassen. oder setzen Sie es in eine Shell-Funktion und geben Sie ihm Werte bei einem Biss pro Zeile wie "/$1/"... "/$2/"- vielleicht schreibe ich diese Funktionen auch ...
mikeserv
Dies scheint nicht zu funktionieren, wenn sich die Platzhalter befinden PLACE1, PLACE2und PLA. PLAgewinnt immer. OP sagt: "Entspricht dem Scannen der Eingabe von links nach rechts für eine längste Übereinstimmung mit einer der angegebenen Ersatzzeichenfolgen" (Hervorhebung hinzugefügt)
Rici
@rici - danke. Dann muss ich die Nulltrennzeichen machen. Blitzschnell zurück.
Mikesserv
@rici - Ich wollte gerade eine andere Version veröffentlichen, die das behandelt, was Sie beschreiben, aber ich schaue sie mir noch einmal an und denke nicht, dass ich das sollte. Er sagt am längsten für eine der angegebenen Ersatzsaiten. Das macht das. Es gibt keinen Hinweis darauf, dass eine Zeichenfolge eine Teilmenge einer anderen ist, nur dass der ersetzte Wert sein kann. Ich denke auch nicht, dass das Durchlaufen einer Liste ein gültiger Weg ist, um das Problem zu lösen. Angesichts des Problems, wie ich es verstehe, ist dies eine funktionierende Lösung.
Mikeserv
1

Eine perlLösung. Selbst wenn einige angaben, dass dies nicht möglich ist, habe ich eines gefunden, aber im Allgemeinen ist ein einfaches Abgleichen und Ersetzen nicht möglich, und selbst wenn es aufgrund des Zurückverfolgens einer NFA schlechter wird, kann das Ergebnis unerwartet sein.

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

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

quelle