Wie funktioniert awk '!a[$0]++'?

Wie funktioniert awk '!a[$0]++'?

Dieser Einzeiler entfernt doppelte Zeilen aus der Texteingabe ohne Vorsortierung.

Zum Beispiel:

$ cat >f
q
w
e
w
r
$ awk '!a[$0]++' <f
q
w
e
r
$ 

Der Originalcode, den ich im Internet gefunden habe, lautete:

awk '!_[$0]++'

Das war für mich umso verwirrender, da ich dachte, _es hätte in awk eine spezielle Bedeutung, wie in Perl, sich aber herausstellte, dass es nur der Name eines Arrays war.

Jetzt verstehe ich die Logik hinter dem Einzeiler: Jede Eingabezeile wird als Schlüssel in einem Hash-Array verwendet, sodass der Hash nach Abschluss eindeutige Zeilen in der Reihenfolge ihres Eintreffens enthält.

Was ich gerne erfahren würde, ist, wie genau diese Notation von awk interpretiert wird. Also was das Ausrufezeichen ( !) bedeutet und die anderen Elemente dieses Codeausschnitts.

Wie funktioniert es?

Antwort1

Hier ist eine "intuitive" Antwort, für eine ausführlichere Erklärung des awk-Mechanismus siehe entweder @Cuonglm's

In diesem Fall kann !a[$0]++das Postinkrement ++für einen Moment beiseite gelegt werden, es ändert den Wert des Ausdrucks nicht. Betrachten Sie also nur !a[$0]. Hier:

a[$0]

verwendet die aktuelle Zeile $0als Schlüssel für das Array aund übernimmt den dort gespeicherten Wert. Wenn dieser bestimmte Schlüssel noch nie zuvor referenziert wurde, a[$0]wird die leere Zeichenfolge ausgewertet.

!a[$0]

Dies !negiert den Wert von vorher. Wenn er leer oder null (falsch) war, haben wir jetzt ein wahres Ergebnis. Wenn er ungleich null (wahr) war, haben wir ein falsches Ergebnis. Wenn der gesamte Ausdruck als wahr ausgewertet wurde, also a[$0]nicht von Anfang an gesetzt war, wird die gesamte Zeile als Standardaktion gedruckt.

Außerdem addiert der Post-Increment-Operator, unabhängig vom alten Wert, eins zu a[$0], sodass der nächste Zugriff auf denselben Wert im Array positiv sein wird und die gesamte Bedingung fehlschlägt.

Antwort2

Hier die Abwicklung:

  • a[$0]: Sehen Sie sich den Wert des Schlüssels $0im assoziativen Array an a. Wenn er nicht vorhanden ist, erstellen Sie ihn automatisch mit einer leeren Zeichenfolge.

  • a[$0]++: Erhöhen Sie den Wert von a[$0], geben Sie den alten Wert als Wert des Ausdrucks zurück. Der ++Operator gibt einen numerischen Wert zurück, wenn also a[$0]anfangs leer war, 0wird zurückgegeben und a[$0]auf erhöht 1.

  • !a[$0]++: negiert den Wert des Ausdrucks. Wenn (ein falscher Wert) a[$0]++zurückgegeben wird , wird der gesamte Ausdruck als „true“ ausgewertet und die Standardaktion ausgeführt . Andernfalls wird keine weitere Aktion ausgeführt, wenn der gesamte Ausdruck als „false“ ausgewertet wird.0awkprint $0

Verweise:

Mit gawkkönnen wir verwendendgawk (oder awk --debugmit neuerer Version)um ein gawkSkript zu debuggen. Erstellen Sie zunächst ein gawkSkript mit dem Namen test.awk:

BEGIN {                                                                         
    a = 0;                                                                      
    !a++;                                                                       
}

Dann renne:

dgawk -f test.awk

oder:

gawk --debug -f test.awk

In der Debugger-Konsole:

$ dgawk -f test.awk
dgawk> trace on
dgawk> watch a
Watchpoint 1: a
dgawk> run
Starting program: 
[     1:0x7fe59154cfe0] Op_rule             : [in_rule = BEGIN] [source_file = test.awk]
[     2:0x7fe59154bf80] Op_push_i           : 0 [PERM|NUMCUR|NUMBER]
[     2:0x7fe59154bf20] Op_store_var        : a [do_reference = FALSE]
[     3:0x7fe59154bf60] Op_push_lhs         : a [do_reference = TRUE]
Stopping in BEGIN ...
Watchpoint 1: a
  Old value: untyped variable
  New value: 0
main() at `test.awk':3
3           !a++;
dgawk> step
[     3:0x7fe59154bfc0] Op_postincrement    : 
[     3:0x7fe59154bf40] Op_not              : 
Watchpoint 1: a
  Old value: 0
  New value: 1
main() at `test.awk':3
3           !a++;
dgawk>

Wie Sie sehen, Op_postincrementwurde es zuvor ausgeführt Op_not.

Für eine klarere Darstellung können Sie statt sioder auch oder verwenden:stepisstep

dgawk> si
[     3:0x7ff061ac1fc0] Op_postincrement    : 
3           !a++;
dgawk> si
[     3:0x7ff061ac1f40] Op_not              : 
Watchpoint 1: a
  Old value: 0
  New value: 1
main() at `test.awk':3
3           !a++;

Antwort3

ah, der allgegenwärtige, aber auch ominöse awk-Duplikatsentferner

awk '!a[$0]++'

Dieses süße Baby ist das Kind der Liebe aus der Kraft und Prägnanz von Awk. Der Höhepunkt der Einzeiler von Awk. Kurz, aber kraftvoll und geheimnisvoll zugleich. Entfernt Duplikate, während die Ordnung erhalten bleibt. Eine Leistung, die von nicht erreicht wird uniqoder sort -ubei der nur benachbarte Duplikate entfernt werden oder die Ordnung durchbrochen werden muss, um Duplikate zu entfernen.

hier ist mein Versuch zu erklären, wie dieser AWK-Einzeiler funktioniert. Ich habe mir Mühe gegeben, die Dinge so zu erklären, dass auch jemand, der kein AWK kennt, folgen kann. Ich hoffe, das ist mir gelungen.

zunächst einige Hintergrundinformationen: awk ist eine Programmiersprache. Dieser Befehl awk '!a[$0]++'ruft den awk-Interpreter/Compiler für den awk-Code auf !a[$0]++. Ähnlich wie python -c 'print("foo")'oder node -e 'console.log("foo")'. awk-Code besteht häufig aus Einzeilern, da awk speziell für die Textfilterung entwickelt wurde.

jetzt etwas Pseudocode. Dieser Einzeiler macht im Wesentlichen Folgendes:

for every line of input
  if i have not seen this line before then
    print line
  take note that i have now seen this line

ich hoffe, Sie erkennen, wie hierdurch Duplikate entfernt werden, während die Ordnung erhalten bleibt.

aber wie passen eine Schleife, ein „if“, ein „print“ und ein Mechanismus zum Speichern und Abrufen von Zeichenfolgen in 8 Zeichen Awk-Code? Die Antwort ist implizit.

die Schleife, das „if“ und das „print“ sind implizit.

zur Erklärung wollen wir uns noch einmal etwas Pseudocode ansehen:

for every line of input
  if line matches condition then
    execute code block

dies ist ein typischer Filter, den Sie wahrscheinlich schon oft in der einen oder anderen Form in Ihren Code geschrieben haben, egal in welcher Sprache. Die Sprache AWK ist so konzipiert, dass das Schreiben dieser Art von Filtern sehr schnell geht.

awk führt die Schleife für uns aus, sodass wir nur den Code innerhalb der Schleife schreiben müssen. Die Syntax von awk lässt außerdem den Boilerplate-Code eines if weg und wir müssen nur die Bedingung und den Codeblock schreiben:

condition { code block }

in awk wird dies als „Regel“ bezeichnet.

wir können entweder die Bedingung oder den Codeblock weglassen (offensichtlich können wir nicht beides weglassen) und awk füllt den fehlenden Teil mit einigen impliziten Elementen auf.

wenn wir die Bedingung weglassen

{ code block }

dann ist es implizit wahr

true { code block }

Das bedeutet, dass der Codeblock für jede Zeile ausgeführt wird

wenn wir den Codeblock weglassen

condition

dann wird die aktuelle Zeile implizit gedruckt

condition { print current line }

Schauen wir uns noch einmal unseren ursprünglichen Awk-Code an

!a[$0]++

Es steht nicht in geschweiften Klammern und ist daher der bedingte Teil einer Regel.

Schreiben wir die implizite Schleife auf und drucken

for every line of input
  if !a[$0]++ then
    print line

Vergleichen Sie mit unserem ursprünglichen Pseudocode

for every line of input                      # implicit by awk
  if i have not seen this line before then   # at least we know the conditional part
    print line                               # implicit by awk
  take note that i have now seen this line   # ???

wir verstehen die Schleife, das „if“ und das „print“. Aber wie funktioniert es, sodass es nur bei doppelten Zeilen als „false“ ausgewertet wird? Und wie werden bereits gesehene Zeilen zur Kenntnis genommen?

Lasst uns dieses Biest auseinandernehmen:

!a[$0]++

Wenn Sie ein wenig C oder Java kennen, sollten Ihnen einige der Symbole bereits bekannt sein. Die Semantik ist identisch oder zumindest ähnlich.

Das Ausrufezeichen ( !) ist ein Negator. Es wertet den Ausdruck als Booleschen Wert aus und das Ergebnis wird negiert. Wenn der Ausdruck als „true“ ausgewertet wird, ist das Endergebnis „false“ und umgekehrt.

a[..]ist ein Array. Ein assoziatives Array. Andere Sprachen nennen es Map oder Dictionary. In awk sind alle Arrays assoziative Arrays. Das ahat keine besondere Bedeutung. Es ist nur ein Name für das Array. Es könnte genauso gut xoder sein eliminatetheduplicate.

$0ist die aktuelle Zeile der Eingabe. Dies ist eine awk-spezifische Variable.

Das Pluszeichen plus ( ++) ist ein Post-Inkrement-Operator. Dieser Operator ist etwas knifflig, da er zwei Dinge tut: Der Wert in der Variablen wird inkrementiert. Aber er „gibt“ auch den ursprünglichen, nicht inkrementierten Wert zur weiteren Verarbeitung zurück.

   !        a[         $0       ]        ++
negator   array   current line      post increment

wie arbeiten sie zusammen?

ungefähr in dieser Reihenfolge:

  1. $0ist die aktuelle Zeile
  2. a[$0]ist der Wert im Array für die aktuelle Zeile
  3. das Post-Increment ( ++) ruft den Wert von ab a[$0], erhöht ihn und speichert ihn wieder in a[$0]; dann „gibt“ es den ursprünglichen Wert an den nächsten Operator in der Zeile „zurück“: den Negator.
  4. der Negator ( !) erhält einen Wert von , der ++der ursprüngliche Wert von war a[$0]; er wird als Boolescher Wert ausgewertet, dann negiert und dann an das implizite if übergeben.
  5. Das if entscheidet dann, ob die Zeile gedruckt wird oder nicht.

das heißt also, ob die Zeile gedruckt wird oder nicht, oder im Kontext dieses awk-Programms: ob die Zeile ein Duplikat ist oder nicht, wird letztendlich durch den Wert in entschieden a[$0].

als Erweiterung: Der Mechanismus, der feststellt, ob diese Zeile bereits gesehen wurde, muss dann ausgeführt werden, wenn ++der erhöhte Wert wieder in gespeichert wird a[$0].

Schauen wir uns unseren Pseudocode noch einmal an

for every line of input
  if i have not seen this line before then   # decided based on value in a[$0]
    print line
  take note that i have now seen this line   # happens by increment from ++

Einige von Ihnen sehen vielleicht schon, wie sich das abspielt, aber wir sind so weit gekommen, lassen Sie uns die letzten Schritte unternehmen und die++

Wir beginnen mit dem in den impliziten

for each line as $0
  if !a[$0]++ then
    print $0

Lassen Sie uns Variablen einführen, um etwas Spielraum zum Arbeiten zu haben

for each line as $0
  tmp = a[$0]++
  if !tmp then
    print $0

jetzt nehmen wir es ++auseinander.

Denken Sie daran, dass dieser Operator zwei Dinge tut: Er erhöht den Wert in der Variablen und gibt den Originalwert zur weiteren Verarbeitung zurück. So ++werden daraus zwei Zeilen:

for each line as $0
  tmp = a[$0]       # get original value
  a[$0] = tmp + 1   # increment value in variable
  if !tmp then
    print $0

oder mit anderen Worten

for each line as $0
  tmp = a[$0]       # query if have seen this line
  a[$0] = tmp + 1   # take note that has seen this line
  if !tmp then
    print $0

vergleichen Sie mit unserem ersten Pseudocode

for every line of input:
  if i have not seen this line before:
    print line
  take note that i have now seen this line

also da haben wir es. Wir haben die Schleife, das „Wenn“, das „Drucken“, die Abfrage und die Notizen. Nur in einer anderen Reihenfolge als der Pseudocode.

komprimiert auf 8 Zeichen

!a[$0]++

möglich aufgrund der impliziten Schleife, des impliziten „if“ und des impliziten „print“ von awks und weil ++sowohl die Abfrage als auch die Notizenerfassung durchgeführt werden.

Es bleibt eine Frage. Was ist der Wert a[$0]für die erste Zeile? Oder für jede Zeile, die noch nicht zuvor gesehen wurde? Die Antwort ist wiederum implizit.

in awk wird jede Variable, die zum ersten Mal verwendet wird, implizit deklariert und mit einem leeren String initialisiert. Außer Arrays. Arrays werden mit einem leeren Array deklariert und initialisiert.

dies ++führt implizite Konvertierungen in Zahlen durch. Die leere Zeichenfolge wird in Null umgewandelt. Andere Zeichenfolgen werden durch einen Best-Effort-Algorithmus in eine Zahl umgewandelt. Wenn die Zeichenfolge nicht als Zahl erkannt wird, wird sie erneut in Null umgewandelt.

dies !führt eine implizite Konvertierung in einen Booleschen Wert durch. Die Zahl Null und die leere Zeichenfolge werden in „false“ konvertiert. Alles andere wird in „true“ konvertiert.

das bedeutet, wenn eine Zeile zum ersten Mal angezeigt wird, a[$0]wird dann auf die leere Zeichenfolge gesetzt. Die leere Zeichenfolge wird durch in Null umgewandelt ++(ebenfalls auf 1 erhöht und wieder in gespeichert a[$0]). Die Null wird durch in „false“ umgewandelt !. Das Ergebnis von !ist „true“, sodass die Zeile gedruckt wird.

der Wert in a[$0]ist jetzt die Zahl 1.

Wenn eine Zeile zum zweiten Mal angezeigt wird, a[$0]ist dies die Zahl 1, die in „true“ umgewandelt wird, und das Ergebnis !ist „false“, sodass es nicht gedruckt wird.

Jedes weitere Vorkommen derselben Zeile erhöht die Zahl. Da alle Zahlen außer Null wahr sind, !ist das Ergebnis immer falsch, sodass die Zeile nie erneut gedruckt wird.

Auf diese Weise werden die Duplikate entfernt.

Kurz zusammengefasst: es zählt, wie oft eine Zeile gesehen wurde. Wenn Null, dann drucken. Wenn eine andere Zahl, dann nicht drucken. Es kann aufgrund vieler impliziter Angaben kurz sein.


Bonus: einige Varianten des Einzeilers und eine superkurze Erklärung seiner Wirkung.

Ersetzen Sie $0(gesamte Zeile) durch $2(zweite Spalte). Dadurch werden Duplikate entfernt, allerdings nur basierend auf der zweiten Spalte.

$ cat input 
x y z
p q r
a y b

$ awk '!a[$2]++' input 
x y z
p q r

Ersetzen Sie !(Negator) durch ==1(gleich eins) und es wird die erste Zeile gedruckt, die ein Duplikat ist

$ cat input 
a
b
c
c
b
b

$ awk 'a[$0]++==1' input 
c
b

Ersetzen durch >0(größer als Null) und Hinzufügen {print NR":"$0}druckt alle doppelten Zeilen mit der Zeilennummer. NRist eine spezielle Awk-Variable, die die Zeilennummer enthält (Datensatznummer im Awk-Jargon).

$ awk 'a[$0]++>0 {print NR":"$0}' input 
4:c
5:b
6:b

ich hoffe, diese Beispiele tragen zum besseren Verständnis der oben erläuterten Konzepte bei.

Antwort4

Ich möchte nur hinzufügen, dass sowohl expr++als auch ++exprnur eine Abkürzung für sind expr=expr+1. Aber

$ awk '!a[$0]++' f # or 
$ awk '!(a[$0]++)' f

druckt alle eindeutigen Werte, da vor der Addition expr++ausgewertet wird , währendexpr

$ awk '!(++a[$0])' f

gibt einfach nichts aus, da als ++exprausgewertet wird expr+1, was in diesem Fall immer einen Wert ungleich Null zurückgibt, und die Negation gibt immer einen Wert von Null zurück.

verwandte Informationen