¿Cómo funciona awk '!a[$0]++'?

¿Cómo funciona awk '!a[$0]++'?

Este resumen elimina líneas duplicadas de la entrada de texto sin realizar una clasificación previa.

Por ejemplo:

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

El código original que encontré en Internet decía:

awk '!_[$0]++'

Esto fue aún más desconcertante para mí ya que supuse _que tenía un significado especial en awk, como en Perl, pero resultó ser solo el nombre de una matriz.

Ahora entiendo la lógica detrás de la frase: cada línea de entrada se utiliza como clave en una matriz hash, por lo que, al finalizar, el hash contiene líneas únicas en el orden de llegada.

Lo que me gustaría saber es cómo awk interpreta exactamente esta notación. Por ejemplo, qué significa el signo de explosión ( !) y los demás elementos de este fragmento de código.

¿Como funciona?

Respuesta1

Aquí hay una respuesta "intuitiva". Para obtener una explicación más detallada del mecanismo de awk, consulte @Cuonglm

En este caso, !a[$0]++el post-incremento ++se puede dejar de lado por un momento, no cambia el valor de la expresión. Entonces, mira solo !a[$0]. Aquí:

a[$0]

usa la línea actual $0como clave de la matriz a, tomando el valor almacenado allí. Si nunca antes se hizo referencia a esta clave en particular, a[$0]se evalúa como una cadena vacía.

!a[$0]

El !niega el valor de antes. Si estaba vacío o era cero (falso), ahora tenemos un resultado verdadero. Si fue distinto de cero (verdadero), tenemos un resultado falso. Si toda la expresión se evaluó como verdadera, lo que significa que a[$0]no estaba configurada desde el principio, la línea completa se imprime como acción predeterminada.

Además, independientemente del valor anterior, el operador post-incremento agrega uno a a[$0], por lo que la próxima vez que se acceda al mismo valor en la matriz, será positivo y toda la condición fallará.

Respuesta2

Aquí está el procesamiento:

  • a[$0]: mira el valor de la clave $0, en una matriz asociativa a. Si no existe, créelo automáticamente con una cadena vacía.

  • a[$0]++: incrementa el valor de a[$0], devuelve el valor anterior como valor de expresión. El ++operador devuelve un valor numérico, por lo que si a[$0]estaba vacío al principio, 0se devuelve y a[$0]se incrementa a 1.

  • !a[$0]++: niega el valor de la expresión. Si a[$0]++se devuelve 0(un valor falso), toda la expresión se evalúa como verdadera y hace que awkse realice la acción predeterminada print $0. De lo contrario, si toda la expresión se evalúa como falsa, no se realizan más acciones.

Referencias:

Con gawk, podemos usardgawk (o awk --debugcon una versión más reciente)para depurar un gawkscript. Primero, cree un gawkscript, llamado test.awk:

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

Entonces corre:

dgawk -f test.awk

o:

gawk --debug -f test.awk

En la consola del depurador:

$ 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>

Puedes ver que Op_postincrementfue ejecutado antes Op_not.

También puedes usar sio stepien lugar de so steppara ver más claramente:

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++;

Respuesta3

Ah, el omnipresente pero también siniestro eliminador de duplicados awk.

awk '!a[$0]++'

Este dulce bebé es el hijo amado del poder y la concisión de Awk. el pináculo de awk one liners. Corto pero poderoso y arcano a la vez. elimina duplicados manteniendo el orden. una hazaña no lograda por uniqo sort -uque elimina solo duplicados adyacentes o tiene que romper el orden para eliminar duplicados.

Aquí está mi intento de explicar cómo funciona esta frase extraña. Me esforcé en explicar las cosas para que alguien que no sepa nada de awk pueda seguirlo. Espero haber podido hacerlo.

Primero, algunos antecedentes: awk es un lenguaje de programación. este comando awk '!a[$0]++'invoca el intérprete/compilador de awk en el código de awk !a[$0]++. similar a python -c 'print("foo")'o node -e 'console.log("foo")'. El código de awk suele ser de una sola línea porque awk fue diseñado específicamente para ser conciso para el filtrado de texto.

ahora un pseudocódigo. Lo que hace este delineador es básicamente lo siguiente:

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

Espero que puedas ver cómo esto elimina duplicados manteniendo el orden.

pero ¿cómo caben un bucle, un if, una impresión y un mecanismo para almacenar y recuperar cadenas en 8 caracteres de código awk? la respuesta es implícita.

el bucle, el if y la impresión están implícitos.

Para explicarlo, examinemos nuevamente algún pseudocódigo:

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

Este es un filtro típico que probablemente haya escrito mucho de una forma u otra en su código en cualquier idioma. El lenguaje awk está diseñado para que escribir este tipo de filtros sea muy breve.

awk hace el ciclo por nosotros, por lo que solo necesitamos escribir el código dentro del ciclo. la sintaxis de awk omite aún más el texto estándar de un if y solo necesitamos escribir la condición y el bloque de código:

condition { code block }

En awk esto se llama "regla".

podemos omitir la condición o el bloque de código (obviamente no podemos omitir ambos) y awk completará la parte que falta con algunos implícitos.

si omitimos la condición

{ code block }

entonces será implícitamente verdadero

true { code block }

lo que significa que el bloque de código se ejecutará para cada línea

si omitimos el bloque de código

condition

entonces será implícita imprimir la línea actual

condition { print current line }

echemos un vistazo a nuestro código awk original nuevamente

!a[$0]++

no se encuentra entre llaves, por lo que es la parte condicional de una regla.

escribamos el bucle implícito y si e imprimamos

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

comparar con nuestro pseudocódigo original

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   # ???

entendemos el bucle, el if y la impresión. pero ¿cómo funciona para que se evalúe como falso solo en líneas duplicadas? ¿Y cómo toma nota de las líneas ya vistas?

desmantelemos esta bestia:

!a[$0]++

Si conoce algo de C o Java, ya debería conocer algunos de los símbolos. la semántica es idéntica o al menos similar.

el signo de exclamación ( !) es un negativo. evalúa la expresión como booleana y cualquiera que sea el resultado, se niega. si la expresión se evalúa como verdadera, el resultado final es falso y viceversa.

a[..]es una matriz. una matriz asociativa. otros idiomas lo llaman mapa o diccionario. en awk todas las matrices son matrices asociativas. el ano tiene ningún significado especial. es sólo un nombre para la matriz. también podría ser xo eliminatetheduplicate.

$0es la línea actual de la entrada. esta es una variable específica de awk.

el plus plus ( ++) es un operador de incremento posterior. Este operador es un poco complicado porque hace dos cosas: se incrementa el valor de la variable. pero también "devuelve" el valor original, no incrementado, para su posterior procesamiento.

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

¿Como trabajan juntos?

aproximadamente en este orden:

  1. $0es la linea actual
  2. a[$0]es el valor en la matriz para la línea actual
  3. el incremento posterior ( ++) obtiene el valor de a[$0]; lo incrementa y lo almacena nuevamente en a[$0]; luego "devuelve" el valor original al siguiente operador de la línea: el negativo.
  4. el negador ( !) obtiene un valor del ++cual era el valor original a[$0]; se evalúa como booleano, luego se niega y luego se pasa al if implícito.
  5. el if luego decide si imprimir la línea o no.

eso significa si la línea se imprime o no, o en el contexto de este programa awk: si la línea es un duplicado o no, lo decide en última instancia el valor en a[$0].

por extensión: el mecanismo que toma nota si esta línea ya se ha visto debe ocurrir cuando ++se almacena el valor incrementado nuevamente en a[$0].

echemos un vistazo a nuestro pseudocódigo nuevamente

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 ++

Es posible que algunos de ustedes ya hayan visto cómo se desarrolla esto, pero hemos llegado hasta aquí. Demos los últimos pasos y desarmemoremos los++

comenzamos con el código awk incrustado en los implícitos

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

introduzcamos variables para tener algo de espacio para trabajar

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

ahora desarmamos ++.

recuerde que este operador hace dos cosas: incrementar el valor en la variable y devolver el valor original para su posterior procesamiento. entonces el ++se convierte en dos líneas:

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

o en otras palabras

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

comparar con nuestro primer pseudocódigo

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

Así que ahí lo tenemos. tenemos el bucle, el if, la impresión, la consulta y la toma de notas. solo que en un orden diferente al del pseudocódigo.

condensado a 8 caracteres

!a[$0]++

posible debido al bucle implícito de awks, si implícito, impresión implícita y porque ++realiza tanto la consulta como la toma de notas.

Queda una pregunta. ¿Cuál es el valor de a[$0]la primera línea? ¿O por alguna línea que no se haya visto antes? la respuesta vuelve a ser implícita.

en awk, cualquier variable que se use por primera vez se declara implícitamente y se inicializa en una cadena vacía. excepto matrices. Las matrices se declaran e inicializan en una matriz vacía.

Esto ++implica conversiones implícitas a números. la cadena vacía se convierte a cero. otras cadenas se convertirán en un número mediante algún algoritmo de mejor esfuerzo. si la cadena no se reconoce como un número, se convierte nuevamente a cero.

La !conversión implícita a booleana. el número cero y la cadena vacía se convierte en falso. cualquier otra cosa se convierte en verdadera.

eso significa que cuando se ve una línea por primera vez, a[$0]se establece en la cadena vacía. la cadena vacía se convierte a cero ++(también se incrementa a 1 y se almacena nuevamente en a[$0]). el cero se convierte en falso por !. el resultado !es verdadero, por lo que la línea se imprime.

el valor en a[$0]es ahora el número 1.

Si se ve una línea la segunda vez, entonces a[$0]el número 1 se convierte en verdadero y el resultado !es falso, por lo que no se imprime.

cualquier encuentro posterior de la misma línea aumenta el número. Dado que todos los números excepto cero son verdaderos, el resultado !siempre será falso, por lo que la línea nunca se volverá a imprimir.

así es como se eliminan los duplicados.

TL;DR: cuenta la frecuencia con la que se ha visto una línea. si es cero, imprima. si hay otro número, entonces no se imprime. puede ser breve debido a muchas implicaciones.


Bonificación: algunas variantes del one liner y una explicación súper breve de lo que hace.

reemplazar $0(línea completa) con $2(segunda columna) eliminará los duplicados, pero solo en función de la segunda columna

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

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

reemplace !(negador) con ==1(igual a uno) e imprimirá la primera línea que es un duplicado

$ cat input 
a
b
c
c
b
b

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

reemplazar con >0(mayor que cero) y agregar {print NR":"$0}imprimirá todas las líneas duplicadas con el número de línea. NRes una variable awk especial que contiene el número de línea (número de registro en la jerga awk).

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

Espero que estos ejemplos ayuden a comprender mejor los conceptos explicados anteriormente.

Respuesta4

Solo quiero agregar que ambos expr++y ++exprson solo una abreviatura de expr=expr+1. Pero

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

imprimirá todos los valores únicos ya que expr++se evaluará exprantes de la suma, mientras que

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

simplemente no imprimirá nada ya que ++exprse evaluará como expr+1, que siempre devolverá un valor distinto de cero en este caso, y la negación siempre devolverá un valor cero.

información relacionada