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 2k
Argumente; 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. \0
wenn 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 lex
Standardaktion 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, od
es 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 perl
Lö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 AAA
ergibt BBB
oder 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