1. Introducción a los entornos de trabajo UNIX

1.13. El lenguaje de procesado de archivos GAWK

1.13.3. Síntesis condensada de GAWK

GAWK no se limita a ser una herramienta de filtrado de texto estructurado; es un lenguaje de programación completo que cuenta con estructuras de control de flujo, bucles, diversos operadores y funciones integradas para trabajar tanto con cadenas de caracteres como con números. Además, te brinda la posibilidad de controlar el formato de la salida impresa, utilizar estructuras de datos y escribir funciones ad hoc. La combinación inteligente de estos elementos te permitirá crear programas para realizar transformaciones complejas en archivos de texto, como podrás ver en muchos de los ejemplos que se presentan a continuación.

La siguiente lista presenta algunos de los elementos y estructuras sintácticas esenciales del lenguaje de programación gawk. Aunque no vamos a cubrir todos estos elementos en profundidad, esta lista te dará una idea del poder y la flexibilidad de gawk como lenguaje de programación.

  • condicionales
if(condicion1){code1}else

if(condición2){code2}else

{code3}
  • bucles for
for (i in array) {code}; 


for (initialization;condition;

      increment|decrement)
  • bucles while
while(true){code}
  • operadores aritméticos
+, -, *, /, %, =, ++, –, +=, -=, …)
  • operadores boleanos
||, &&
  • operadores relacionales
<, <=, ==, !=,>=, >
  • funciones integradas
length(str); int(num); index (str1, str2); split(str,arr,del); substr(str,pos,len); printf(fmt,args); tolower(str); toupper(str); gsub(regexp, replacement [, target])
  • funciones escritas por el usuario
function FUNNAME (arg1, arg1) {code}
  • estructuras de datos
(hashes o arreglos asociativos): array[string]=value

Evaluemos el potencial de gawk. Los conjuntos de datos genómicos a menudo se distribuyen con archivos separados para cada cromosoma, y generalmente necesitamos efectuar la misma operación en cada uno. Para realizar esta tarea, podemos usar la estructura de bucle for del shell. Si tuviéramos archivos llamados data_chr[chromosome].txt, donde [chromosome] varía de 1 a 22, y quisiéramos ejecutar ./command en ellos, se escribirían los siguientes comandos en el terminal:

$ for i in {1..22}; do ./command data_chr$i.txt; done

El código anterior recorre todos los valores entre 1 y 22, estableciendo la variable de shell $i en cada valor, sucesivamente. La sintaxis para especificar los rangos de números es que {A..B} da un rango entre A y B, donde A y B son valores enteros.

También podemos utilizar for para recorrer las extensiones de archivo. Supongamos que teníamos algunos datos en formato PLINK llamados data.bed, data.bim y data.fam. Podríamos listar estos archivos individualmente ejecutando:

$ for ext in bed bim fam; do ls data.$ext; done

Aquí, la variable $ext (para extensión) se establece sucesivamente en bed, bim y fam. Este ejemplo en particular no es muy útil, ya que podríamos haber escrito ls data.* y visto que existen estos tres archivos. Lo útil es poder renombrar el conjunto de datos base con una sola línea de código en el shell. Si quisiera renombrar estos archivos human_data.bed, human_data.bim y human_data.fam, se podría escribir:

$ for ext in bed bim fam; do mv -i data.$ext human_data.$ext; done

Otra construcción for útil es recorrer grupos de archivos. Podemos hacer lo siguiente para ejecutar ./command en cada archivo con extensión .txt en nuestro directorio actual:

$ for file in *.txt; do ./command $file; done

Ahora, supongamos que tenemos un archivo llamado data.txt y que queremos separar la información correspondiente a cada cromosoma en archivos separados. Podríamos usar un bucle for para recorrer cada número de cromosoma y luego una expresión booleana en gawk para extraer solo las líneas correspondientes a ese cromosoma. Comencemos nuestra tarea con un programa que no funciona del todo y luego lo arreglaremos.

Si la columna 2 de data.txt contiene los números de cromosoma y queremos archivos separados para cada cromosoma, podríamos pensar que lo siguiente funciona (ten en cuenta, nuevamente, el comportamiento predeterminado de gawk para imprimir las líneas que coinciden con la expresión booleana dada):

$ for chr in {1..22}; do awk '$2 == $chr' data.txt > data_chr$chr.txt; done

Esto es casi correcto, pero la expresión dada en gawk es '$2 == $chr' gawk no la entiende, porque no conoce ni sabe nada sobre la variable de shell $chr que se ha definido. En lugar de hacer referencia a una variable en la shell, podemos asignar explícitamente variables para que gawk las use con la opción -v:

$ for chr in {1..22}; do awk -v chr=$chr '$2 == chr' data.txt > data_chr$chr.txt; done

¡No está mal! Podemos proporcionar a gawk tantas opciones -v como queramos para todas las variables que nos importen asignar:

$ for chr in {1..22};
   do awk -v chr=$chr -v threshold=10 '$2 == chr && $4 > threshold' data.txt

   > data_chr$chr.txt; done

Esto separa cada cromosoma en un archivo individual con la condición de que los valores en la columna 4 sean mayores que 10.

¿Qué ocurre si tenemos un archivo que contiene dos columnas con un recuento de «éxitos» y «intentos» para algún proceso, cada uno recolectado de una fuente diferente, y queremos calcular la tasa promedio de éxito entre todas las fuentes? Podemos usar gawk para sumar cada columna y luego imprimir el promedio al final usando una sintaxis especial. Si el archivo data.txt tiene los éxitos y los intentos en las columnas 2 y 3, respectivamente, podríamos hacer esto:

$ gawk 'BEGIN
 {total_success = total_attempts = 0;}

 {total_success += $2; total_attempts += $3}

END

{print "Éxitos:", total_success, "Intentos:", total_attempts, "Tasa:", total_success / total_attempts}' data.txt

El comando gawk ejecuta el código en la sección BEGIN antes de procesar cualquier línea en la entrada y el código en la sección END después de leer la entrada. La sección BEGIN inicializa dos variables a 0, la sección de código principal suma las columnas en cada línea, y la sección END imprime los totales y la tasa.

Supongamos ahora que queremos buscar mediante condicionales aquellos ensambles que contiene el organismo en Xenopus tropicalis:

$ gawk ' BEGIN {
    FS="\t";

    print "assembly_accession\torganism_name\tseq_rel_date\tasm_name\tsubmitter"

    }



{

    if ($2 == "Xenopus tropicalis") {

        print $1 "\t" $2 "\t" $3 "\t" $4 "\t" $5

    }

}

' ensamble.txt
GCA_017527675.1 Phaeodactylum tricornutum  2022-03-24 Phatr3.0   NCBI

GCA_011586775.1 Xenopus tropicalis   2022-04-11 Xenbase_v9.2    NCBI

En el siguiente ejemplo, se quieren filtrar los datos de Xenopus tropicalis que tengan una fecha de lanzamiento inferior a 2015. El ejemplo utilizará un condicional en bash y se creará script que se pueda ejecutar. Salva las próximas órdenes en un fichero de tu terminal.

#!/bin/bash

# Leer la tabla y guardar las líneas que cumplen las condiciones en un nuevo archivo

awk -F'\t' 'NR==1 || ($2 == "Xenopus tropicalis" && $3 > "2015-01-01")' $1 > filtered_table.txt

# Contar el número de líneas en el archivo filtrado

num_lines=$(wc -l < filtered_table.txt)

# Si el número de líneas es mayor que 1 (es decir, si hay entradas que cumplen las condiciones),

# imprimir un mensaje y el contenido del archivo filtrado

if [ $num_lines -gt 1 ]; then

  echo "Se encontraron las siguientes entradas para Xenopus tropicalis publicadas después de 2015-01-01:"

  cat filtered_table.txt

# Si el número de líneas es igual a 1, imprimir un mensaje con el nombre de la entrada

elif [ $num_lines -eq 1 ]; then

  echo "Se encontró la siguiente entrada para Xenopus tropicalis publicada después de 2015-01-01:"

  awk -F'\t' '{print $4}' filtered_table.txt

# Si el número de líneas es 0, imprimir un mensaje de que no se encontraron entradas que cumplan las condiciones

else

  echo "No se encontraron entradas para Xenopus tropicalis publicadas después de 2015-01-01."

fi

Salva el fichero y ejecuta las siguientes órdenes en el terminal:

     $ chmod +x script.sh
     $ ./script.sh ensamble.txt

Este script emplea gawk para leer la tabla y guardar las líneas que cumplen las condiciones en un nuevo archivo llamado filtered_table.txt. Luego, cuenta el número de líneas en ese archivo y utiliza dos condiciones if para imprimir mensajes diferentes dependiendo de si hay entradas que cumplan las condiciones o no. En el primer caso, cuando hay más de una entrada que cumple las condiciones, el script imprime un mensaje que indica que se encontraron entradas y muestra el contenido de la tabla filtrada. En el segundo caso, imprime un mensaje si solo hay una entrada que cumple la condición y la última acción ocurrirá si no hay ninguna entrada en el fichero de partida que cumpla las condiciones demandadas.

La tabla 12 es un listado de las operaciones más comunes y usadas con GAWK.

Tabla 12. Operaciones utilizadas en GAWK.

Variable Descripción
i=0 Asignar un valor
a[i]=0; Guardar un valor en una tabla
i++; Incrementar un contador
print i; Mostrar una variable por pantalla
print NR Mostrar el número de líneas procesadas
print $1,$4 Mostrar la primera y cuarta columnas

Fuente: elaboración propia.

Y en la tabla 13 se muestran ejemplos de instrucciones condicionales con GAWK.

Tabla 13. Instrucciones condicionales con GAWK.

Variable Descripción
if ($1 > 0) print $0;  Si la primera es positiva
if ($1 < 0) print $0;  Si la primera es negativa
if ($1 >= 0) print $0;  Si la primera columna es mayor o igual a cero
if ($1 <= 0) print $0;  Si la primera columna es menor o igual a cero
if ($1 == 0) print $0;  Si la primera columna es igual a cero
if (CONDICION1) && (CONDICION2) Si se cumplen las dos condiciones
if (CONDICION1) || (CONDICION2 Si se cumple alguna de las dos condiciones
if ¡(CONDICION1) Si no se cumple la condición

Fuente: elaboración propia.