1 Presentación

Este tutorial es desarrollado por Pablo Vinuesa, CCG-UNAM (twitter: @pvinmex) a partir de material previo, presentado en diversos cursos y talleres, como Taller 3 - Análisis comparativo de genomas microbianos: Pangenómica y filoinformática de los Talleres Internacionales de Bioinformática - TIB2019, celebrados en el Centro de Ciencias Genómicas de la Universidad Nacional Autónoma de México, del 29 de julio al 2 de agosto de 2019.

Las versión actual (v.2022-09-26) contiene extensiones significativas con respecto a cursos anteriores, así como adaptaciones para el Taller de Ciencias Genómicas: de Moléculas a Ecosistemas, que impartimos investigadores del CCG-UNAM para de la Facultad de Ciencias - UNAM, así como para el curso de introducción a Linux que imparto en la Licenciatura en Ciencias Genómicas - LCG de la UNAM.

Si éste es tu primer contacto con Linux, te recomiendo que leas primero esta presentación sobre introducción al biocómputo en sistemas Linux - PDF.

1.1 Objetivos

Este documento sirve tanto como un tutorial completo de iniciación a la programación en \(AWK\) y \(Bash\) para bioinformática para los que no han tenido contacto previo con un sistema UNIX o GNU/Linux, como una referencia para usuarios avanzados.

Como muestra el índice, el tutorial avanza desde los aspectos más elementales de conexión a un servidor, navegación del sistema, propiedades de archivos y tuberías de filtrado de texto, a programación avanzada en AWK y BASH. Contiene muchísimos ejemplos prácticos, desde programas simples pero siempre útiles para ejecutar en la consola de comandos (1liners), a scripts avanzados, aptos para el trabajo de investigación en el área como fasta_toolkit.awk, extract_CDSs_from_GenBank.awk y run_phylip.sh. Éstos hacen uso de funciones, banderas, estructuras de control de flujo, estructuras de datos complejas, menús de usuario, y muchos otros atributos propios de programas profesionales. Estos ejemplos te servirán para aprender a programar herramientas bioinformáticas robustas y modulares, siguiendo mejores prácticas. El tutorial te enseña el uso práctico de todos los elementos sintácticos implementados en dichos scripts, los cuales puedes descargar del repositorio GitHub de este curso. Estos programas se describen en detalle en este documento.

1.2 Licencia y términos de uso

Este material didáctico lo distribuyo públicamente a través de este repositorio GitHub intro2linux bajo la Licencia No Comercial Creative Commons 4.0

Creative Commons Licence
This work is licensed under a Creative Commons Attribution-NonCommercial 4.0

GNU General Public License v3
El código se distribuye bajo la licencia GNU General Public License v3

1.3 ¿Cómo correr los ejemplos de este manual?

Idealmente debes tener acceso local o remoto a una máquina UNIX o GNU/Linux. Si tienes una máquina Windows, otra opción es instalar mobaXterm home edition, como se explica en el esta sección README del repositorio

1.4 ¿Cómo obtener la última versión de este documento?

Tienes básicamente 2 opciones:

  1. clonar el repositorio intro2linux y correr con frecuencia el comando git pull, como se explica en el REAEME del repositorio
  2. navegar y/o descargar directamente el archivo html del manual

La primera es la más conveniente, ya que no sólo descargas el archivo html del curso, sino todos los scripts y datos asociados para que puedas seguir cada ejemplo.

2 El Proyecto de Software Libre GNU

al mundo de cómputo y software libre del proyecto GNU. Antes de empezar este tutorial, sugiero que te informes sobre lo que es el proyecto GNU, iniciado por Richard Stallman (rms) en 1983 y desarrollado por una enorme comunidad de programadores, y su relación con Linux, para formar el sistema de cómputo libre GNU/Linux.

Así lo explica rms en su artículo Linux y el sistema GNU, del que cito los primeros dos párrafos abajo:

Muchos usuarios de ordenadores ejecutan a diario, sin saberlo, una versión modificada del sistema GNU. Debido a un peculiar giro de los acontecimientos, a la versión de GNU ampliamente utilizada hoy en día se la llama a menudo «Linux», y muchos de quienes la usan no se dan cuenta de que básicamente se trata del sistema GNU, desarrollado por el proyecto GNU.

Efectivamente existe un Linux, y estas personas lo usan, pero constituye solo una parte del sistema que utilizan. Linux es el núcleo: el programa del sistema que se encarga de asignar los recursos de la máquina a los demás programas que el usuario ejecuta. El núcleo es una parte esencial de un sistema operativo, pero inútil por sí mismo, sólo puede funcionar en el marco de un sistema operativo completo. Linux se utiliza normalmente en combinación con el sistema operativo GNU: el sistema completo es básicamente GNU al que se le ha añadido Linux, es decir, GNU/Linux. Todas las distribuciones denominadas «Linux» son en realidad distribuciones GNU/Linux.

Richard M. Stallman

2.1 La controversia sobre el nombre: ¿GNU/Linux o sólo Linux?

Hay que aclarar que destacados miembros de la comunidad de desarrolladores de software libre esgrimen diversos argumentos por los que consideran que el sistema no se debe llamar GNU/Linux, favoreciendo el nombre de Linux.

Sirva de ejemplo uno de los comentario de Linus Torvalds:

Umm, this discussion has gone on quite long enough, thank you very much. It doesn’t really matter what people call Linux, as long as credit is given where credit is due (on both sides). Personally, I’ll very much continue to call it “Linux”, …

The GNU people tried calling it GNU/Linux, and that’s ok. It’s certainly no worse a name than “Linux Pro” or “Red Hat Linux” or “Slackware Linux” …

Lignux is just a punny name—I think Linux/GNU or GNU/Linux is a bit more “professional” …

Linus Torvalds

Es importante notar que muchas de las distribuciones de Linux, incluyendo Ubuntu, no siguen estrictamente el principio de software libre de GNU. Ubuntu, por ejemplo, incluye el repositorio \(restricted\), que contiene paquetes soportados por los desarrolladores de Ubuntu debido a su importancia, pero que no está disponible bajo ningún tipo de licencia libre. Ejemplos notables son los controladores propietarios de algunas tarjetas gráficas, como los de ATI y NVIDIA. Ello ha permitido que Ubuntu sea una de las distribuciones de Linux con mayor compatibilidad de hardware y facilidad de instalación. Por ello recomiendo y uso esta distro.

2.2 Nota de agradecimiento y postura personal

En lo personal, soy pragmático y agnóstico en lo que respecta a la controversia. Estoy muy agradecido a toda la comunidad por haber desarrollado, integrado y puesto a nuestra disposición esta maravilla de entorno de cómputo libre: kernel, sistema operativo y software. Por ello gustosamente doy crédito al proyecto GNU y al el núcleo o kernel, desarrollado originalmente por Linus Torvalds y conocido actualmente como núcleo Linux. Desde el año 2004 realizo todo mi trabajo en este entorno.

Como académico coincido plenamente con la visión de rms en que los centros de educación pública deberían de usar el entorno GNU/Linux, idealmente desde la secundaria. En diversos lugares, notablemente en India, este mensaje de rms ha calado hondo. El estado de Kerala fue pionero: desde 2006 toda la educación pública de Kerala se hace en en el entorno GNU/Linux, habiendo eliminado Windows y software privativo de las escuelas estatales. Otros estados como Karnataka, Gujarat, Assam, West Bengal siguieron rápidamente el ejemplo, incorporando software libre y código fuente abierto - OSS´ como una clave de sus sistemas educativos. Es una tendencia respaldada actualmente por el gobierno Indio, como pueden leer aquí: Adoption of Free and Open Source Software in India. El resultado es contundente - la India es actualmente una potencia mundial en cómputo y matemáticas.

Mi modesta pero activa manera de contribuir a la promoción del uso de software libre y código fuente abierto en México y Latinoamérica es divulgando el uso del ambiente de cómputo GNU/Linux a alumnos universitarios mediante cursos en diversos países de habla hispana, y promoviendo su implementación en la universidad, particularmente en la LCG-UNAM. Me referiré a este ambiente de cómputo con ambos nombres (GNU/Linux y Linux), para expresar mi neutralidad al respecto de la controversia arriba mencionada, aunque tiendo a preferir el uso de GNU/Linux para remarcar el crédito a todos los componentes de esta extraordinaria “simbiosis”.

2.3 GNU - Documentación y recursos GNU

Todo software de calidad debe de estar bien documentado. Sin duda la documentación del software GNU es extraordinaria. No dejes de visitar al menos estos recursos:

2.3.1 Referencias adicionales

Una vez que domines los comandos básicos que se presentarán en este documento, recomiendo revisar tutoriales más detallados y completos como los siguientes:


3 Primer contacto con un sistema Linux - navegación del sistema

Si no has trabajado previamente con la consola de comandos, debes empezar en esta sección que te guiará paso a paso para aprender rápidamente los fundamentos del \(Shell\).

3.1 Conexión a un servidor y exploración de sus características básicas

Estas prácticas están diseñadas para correr en un servidor remoto, pero puedes hacerlo también en una sesión local, es decir, en tu máquina. Sólo tienes que poner en un directorio los archivos con los que vamos a trabajar, los cuales puedes descargar del directorio intro2linux/data del sitio GitHub.

3.1.1 ssh establecer sesion remota encriptada (segura) via ssh al servidor con número dado de IP

ssh -l $USER IP
  • Nota: para poder desplegar el ambiente gráfico en el servidor remoto debemos hacer el login con:
ssh -X $USER@IP

Al establecer una sesión remota vía \(ssh\), la inicias en tu \(HOME\)

3.1.2 hostname muestra el nombre del host (la máquina a la que estoy conectado) y la IP

hostname
hostname -i
## alisio
## 127.0.1.1

3.1.3 uname muestra información sobre el sistema y detalles del kernel o núcleo

uname
uname -a
## Linux
## Linux alisio 5.15.0-48-generic #54~20.04.1-Ubuntu SMP Thu Sep 1 16:17:26 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

3.1.4 lsb_release muestra información sobre la distribución instalada

lsb_release -a
## No LSB modules are available.
## Distributor ID:  Ubuntu
## Description: Ubuntu 20.04.5 LTS
## Release: 20.04
## Codename:    focal

3.1.5 df reporta el uso de disco del sistema

df -h

Filesystem            Size  Used Avail Use% Mounted on
/dev/mapper/VolGroup00-LogVol00
                       40G  2,6G   35G   7% /
tmpfs                 174G  3,1G  171G   2% /dev/shm
/dev/sda3             190M   90M   91M  50% /boot
/dev/mapper/VolGroup00-LogVol05
                     1012G  913G   49G  95% /home
/dev/mapper/VolGroup00-LogVol03
                       30G   23M   28G   1% /tmp
/dev/mapper/VolGroup00-LogVol02
                       59G   16G   41G  28% /usr
/dev/mapper/VolGroup00-LogVol04
                       79G  9,1G   66G  13% /var
/dev/mapper/VolGroup01-LogVol00
                      739G  518G  184G  74% /export/apps
/dev/mapper/VolGroup01-LogVol01
                       17T   16T  731G  96% /export/space1
buluc-cluster:/export/space2/users
                       17T   17T  461G  98% /export/space2/users
bonampak-cluster:/space29/data
                       12T   11T  938G  92% /export/space29/data

3.1.6 free reporta la memoria del sistema y kernel

free -g # -g indica la memoria en gibibytes

             total       used       free     shared    buffers     cached
Mem:           346        253         93          0          2        197
-/+ buffers/cache:         53        293
Swap:          351         16        335

3.1.7 top o htop muestran los procesos en ejecución y los recursos que consumen

htop # sales con q o CTRL-c

3.1.8 w mustra quien está logeado y qué está haciendo

w

 11:41:49 up 679 days, 20:42, 20 users,  load average: 3,26, 3,91, 3,79
USER     TTY      FROM              LOGIN@   IDLE   JCPU   PCPU WHAT
ati      pts/0    132.248.220.117  27Feb20 35:33m 34:36m  0.50s sshd: ati [priv] 
ati      tty1     :0               21Jan19 679days  2:30m  0.40s pam: gdm-password
ati      pts/3    :0.0             21Jan19 325days  0.71s 57.47s gnome-terminal
ajhernan pts/13   132.248.220.117  06Sep20 33days  0.33s  0.33s -bash
ati      pts/22   132.248.220.117  27Feb20 11:03m 15.24s  0.50s sshd: ati [priv] 
jwong    pts/27   :pts/20:S.0      30Jan19 668days  0.06s  0.06s /bin/bash
ati      pts/31   132.248.220.117  23Apr20  2days  3.71s  3.71s -bash
ehernans pts/36   pukuj            09:57    5:55   1.10s  1.00s python3
cdomingu pts/40   :pts/91:S.0      17Jul20 134days  0.15s  0.15s /bin/bash
cdomingu pts/41   :pts/91:S.1      17Jul20 134days  0.19s  0.19s /bin/bash
cdomingu pts/53   :pts/91:S.2      17Jul20 134days  0.20s  0.20s /bin/bash
cdomingu pts/57   :pts/91:S.3      17Jul20 134days  0.17s  0.17s /bin/bash
rolayo   pts/73   mosh             24Aug19 467days  0.18s  0.18s -bash
cdomingu pts/76   :pts/91:S.4      17Jul20 86days  0.36s  0.36s /bin/bash
ati      pts/82   132.248.220.117  05Oct20 11days  1.14s  1.14s -bash
cdomingu pts/91   :pts/83:S.0      17Jul20 86days  5:14   5:14  screen -r 10767.pts-13.tepeu
fcastane pts/132  :pts/133:S.0     20Aug20 100days  0.06s  0.03s SCREEN -S K12A
aescobed pts/167  pukuj            Sat12    1:54m  0.51s  0.51s -bash
vinuesa  pts/176  akbal.ccg.unam.m 10:50    1.00s  0.44s  0.14s w
rbucio   pts/179  pukuj            Sat18   42:29   2:41   0.28s sshd: rbucio [priv]

3.2 Exploración del sistema de archivos

El sistema de archivos de GNU/Linux tiene una estructura jerárquica de directorios y subdirectorios que penden del directorio raíz \(/\). Esta estructura puede ser visualizada convenientemente con el comando \(tree\). (Nota: \(tree\) tal vez no esté instalado en tu sistema).

3.2.1 Imprime la estructura de directorios de tu home u otro directorio

tree -d $HOME/cursos/

/home/vinuesa/cursos/
└── intro2linux
    ├── bin
    ├── data
    ├── docs
    │ └── data
    ├── intro2linux
    │ ├── sesion1
    │ └── sesion1_linux
    ├── participantes_Taller_CG_FC
    ├── pics
    └── tutorials
        └── awk
  • Nota: según el directorio que indiques, la salida puede ser copiosa. Usar con criterio. Puedes limitar el número de nivels (subdirectorios) a mostrar con -L # (Level number)
tree -d -L 2 $HOME/cursos/

/home/vinuesa/cursos/
└── intro2linux
    ├── bin
    ├── data
    ├── docs
    ├── intro2linux
    ├── participantes_Taller_CG_FC
    ├── pics
    └── tutorials

3.2.2 pwd imprime la ruta absoluta del directorio actual

# dónde me encuentro en el sistema?
pwd
## /home/vinuesa/cursos/intro2linux

3.2.3 ls lista contenidos del directorio

# Qué contiene el directorio actual?
#  => Nota: muchos comandos los preced con '| head' para listar sólo las primeras (10) líneas 
#           de la salida del primer comando, con el fin de que ésta no sea demasiado larga. 
#     Veremos el uso de pipes '|' en detalle más adelante. 
#           Por ahora ignóralo y teclea simplemente ls
ls | head
## align_seqs_with_clustal_or_muscle.sh
## aoa.awk
## assembly_summary.txt.gz
## awkvars.out
## bash_script_template_with_getopts.sh
## bin
## blast-imager.pl
## clases_avance.txt
## compute_DNA_seq_stats.awk
## consensus_tre.nwk
# mostrar todos (-a all) los archivos, incluidos los ocultos, y sus propiedades
ls -al | head -20
## total 102140
## drwxr-xr-x 12 vinuesa vinuesa    20480 sep  7 14:10 .
## drwxr-xr-x  4 vinuesa vinuesa     4096 nov 11  2020 ..
## -rwxr-xr-x  1 vinuesa vinuesa     2135 oct 14  2020 align_seqs_with_clustal_or_muscle.sh
## -rw-rw-r--  1 vinuesa vinuesa      244 dic 27  2020 aoa.awk
## -rw-r--r--  1 vinuesa vinuesa  6780296 oct 20  2020 assembly_summary.txt.gz
## -rw-rw-r--  1 vinuesa vinuesa      895 dic 21  2020 awkvars.out
## -rw-r--r--  1 vinuesa vinuesa     5505 nov  7  2020 bash_script_template_with_getopts.sh
## drwxrwxr-x  2 vinuesa vinuesa     4096 nov 16  2020 bin
## -rwxr-xr-x  1 vinuesa vinuesa     5406 oct 14  2020 blast-imager.pl
## -rw-rw-r--  1 vinuesa vinuesa      933 nov  4  2020 clases_avance.txt
## -rwxr-xr-x  1 vinuesa vinuesa     1245 dic 19  2020 compute_DNA_seq_stats.awk
## -rw-rw-r--  1 vinuesa vinuesa     1335 nov 12  2020 consensus_tre.nwk
## -rw-rw-r--  1 vinuesa vinuesa     3715 dic  1  2020 count_genome_features_for_taxon.awk
## -rw-rw-r--  1 vinuesa vinuesa     3460 nov  7  2020 count_genome_features_for_taxon_V2.awk
## drwxrwxr-x  2 vinuesa vinuesa     4096 dic 29  2020 data
## -rw-rw-r--  1 vinuesa vinuesa       43 nov  4  2020 dna_string.txt
## drwxr-xr-x  2 vinuesa vinuesa     4096 nov 26  2020 docs
## -rwxr-xr-x  1 vinuesa vinuesa    21118 ene  1  2021 extract_CDSs_from_GenBank.awk
## -rwxr-xr-x  1 vinuesa vinuesa     1699 nov 28  2020 extract_DNA_string_from_genbank.awk

3.2.3.1 Veamos el contenido del directorio raíz /

  • corramos un \(ls\) sin argumentos
ls / | head
## bin
## boot
## cdrom
## dev
## etc
## home
## lib
## lib32
## lib64
## libx32
  • mucha más información obtenemos con el formato largo de ls: \(ls -l\)
ls -l / | head
## total 128
## lrwxrwxrwx   1 root root     7 oct  4  2020 bin -> usr/bin
## drwxr-xr-x   4 root root  4096 sep 23 17:12 boot
## drwxr-xr-x   2 root root  4096 oct  4  2020 cdrom
## drwxr-xr-x  21 root root  4840 sep 26 17:07 dev
## drwxr-xr-x 163 root root 12288 sep 26 17:31 etc
## drwxr-xr-x   5 root root  4096 abr 15  2020 home
## lrwxrwxrwx   1 root root     7 oct  4  2020 lib -> usr/lib
## lrwxrwxrwx   1 root root     9 oct  4  2020 lib32 -> usr/lib32
## lrwxrwxrwx   1 root root     9 oct  4  2020 lib64 -> usr/lib64

3.2.3.2 Veamos las primeras 5 entradas y últimas 5 del directorio /bin

ls /usr/local/bin | head
## alimask
## append_sff
## asap
## bam_coverage_windows.pl
## baseml
## basemlg
## bdf2gdfont.pl
## blastall
## blastdb_aliastool
## blastdbcheck
# idem, pero con detalles de permisos etc
ls -l /usr/local/bin | head 
## total 172288
## -rwxr-xr-x 1 root root   464848 ene 22  2021 alimask
## -rwxr-xr-x 1 root root    27376 dic  8  2020 append_sff
## -rwxr-xr-x 1 root root   144000 may  7 14:41 asap
## -r-xr-xr-x 1 root root     2941 nov  6  2021 bam_coverage_windows.pl
## lrwxrwxrwx 1 root root       47 feb 18  2021 baseml -> /home/vinuesa/soft_download/paml4.9j/bin/baseml
## lrwxrwxrwx 1 root root       48 feb 18  2021 basemlg -> /home/vinuesa/soft_download/paml4.9j/bin/basemlg
## -r-xr-xr-x 1 root root     4689 jun 19 21:24 bdf2gdfont.pl
## lrwxrwxrwx 1 root root       53 dic 31  2020 blastall -> /home/vinuesa/soft_download/blast-2.2.26/bin/blastall
## lrwxrwxrwx 1 root root       68 jul 16  2021 blastdb_aliastool -> /home/vinuesa/soft_download/ncbi-blast-2.12.0+/bin/blastdb_aliastool
# idem, pero ordenando los archivos por fechas de modificacion (-t), listando los mas recientes al final (-r), es decir, en orden reverso
ls -ltr /usr/local/bin | tail
## -r-xr-xr-x 1 root root     4495 jun 19 21:47 bp_seqpart
## -r-xr-xr-x 1 root root     9709 jun 19 21:49 bp_flanks
## -r-xr-xr-x 1 root root    16202 jun 19 21:50 lwp-request
## -r-xr-xr-x 1 root root     2413 jun 19 21:50 lwp-mirror
## -r-xr-xr-x 1 root root     2711 jun 19 21:50 lwp-dump
## -r-xr-xr-x 1 root root    10292 jun 19 21:50 lwp-download
## -r-xr-xr-x 1 root root    10803 jun 19 21:50 pod2text
## -r-xr-xr-x 1 root root    15034 jun 19 21:50 pod2man
## -r-xr-xr-x 1 root root    22155 jun 19 21:50 make_method
## -rwxr-xr-x 1 root root  2232640 ago 15 20:29 raxml-ng-mpi

3.2.4 Expansión de caracteres con * y ?

# lista los archivos en /bin que empiezan por las letras b y c
ls /bin/b* | head -5
ls /bin/c* | head -5
## /bin/b2
## /bin/b2sum
## /bin/backtranambig
## /bin/backtranseq
## /bin/bamToBed
## /bin/c++
## /bin/c89
## /bin/c89-gcc
## /bin/c99
## /bin/c99-gcc
# lista los archivos en /bin que empiezan por la letra c seguida de uno o dos caracteres más
ls /bin/c? | head -5
ls /bin/c?? | head -5
## /bin/cc
## /bin/cp
## /bin/c++
## /bin/c89
## /bin/c99
## /bin/cai
## /bin/cal

3.2.5 Opciones más comunes e importantes del comando \(ls\)

Es fundamental que domines estas opciones básicas de \(ls\)

  • -a list all (lista archivo ocultos tipo .directorio o .archivo)
  • -l long format (muestra permisos y otros atributos como usuario, grupo, tamaño del archivo y fecha de modificación)
  • -t time sort (ordena por tiempo)
  • -r reverse sort
  • -R list subdirectories recursively
  • -S size sort (ordena por tamaño)
  • -h human readable (indica Kb, Mb, Gb …)

3.2.6 Ejemplos de opciones del comando \(ls\)

Recuerda que podemos indicar múltiples opciones usando un solo guión y que el orden de las opciones no importa.

Es decir ls -la == ls -l -a == ls -a -l

Prueba los siguientes comandos y aprende bien estas opciones básicas de \(ls\)

  • ls -a
  • ls -lt
  • ls -ltr
  • ls -altrh
  • ls -lS
  • ls -lh

3.3 Permisos (modo de un archivo)

Los sistemas UNIX y GNU/Linux, al contrario que los basados en MS-DOS, están diseñados desde un inicio para operar en modo multiusuario y multitarea (multitasking). Por ello el sistema operativo debe asegurar la privacidad de los archivos y directorios de cada usuario del sistema.

3.3.1 Usuario, grupo y resto del mundo (User, Group, Others …) y permisos \(rwx\)

En sistemas UNIX y GNU/Linux cada archivo y directorio tiene unos permisos determinados de lectura=\(r\) escritura=\(w\) y ejecución=\(x\) para el usuario, grupo y resto del mundo, asignados en ese orden (UGO).

Un archivo regular, escrito por el usuario tiene los siguientes permisos por defecto, como muestra el comando \(ls -l\)

ls -l | grep odp
-rw-r--r-- 1 vinuesa vinuesa 2993606 sep 30 10:52 intro_biocomputo_Linux_LCG.odp

Veamos lo que quiere decir. Para ello necesitamos separar la cadena de caracteres en los siguientes componentes

   U   G   O       usuario grupo
1  2   3   4  5    6       7
-|rw-|r--|r-- 1 vinuesa vinuesa

donde:

1. la posición 1 (-) indica que se trata de un archivo regular. Un directorio se indica con "d" y una liga simbólica con "l"
2. El grupo 2,3 y 4 de caraceres indican el "modo" del archivo (permisos) para el usuario (U), grupo (G) y otros (resto del mundo O), 
   separados por "|" para facilitar su visualización.
   En este caso el usuario tiene permisos de lectura (r) y escritura (w) sobre el archivo que no es ejecutable (-)
   El grupo y el resto del mundo sólo pueden leer el archivo, pero no modificarlo.
   

Un directorio generado por el usuario con el comando estándar \(mkdir\) tiene los siguientes permisos por defecto, como muestra el comando \(ls -l\)

ls -l | grep intro
drwxr-xr-x 4 vinuesa vinuesa    4096 sep 29 22:56 intro2linux
  • Ejercicio: lista los permisos (modo) de este directorio para U|G|O

3.3.2 Tabla de atributos de los permisos

La siguiente tabla resume los atributos que tienen los permisos \(r\), \(w\), \(x\) sobre archivos regulares y directorios:

Atributo Archivos Directorios
r abrir y leer listar contenidos si tiene +x
w editar pero no renombrar/borrar (atributo dir) permite generar archivos en dir, si tiene +x
x permite ejecutra archivo (programa) si +r permite entrar al directorio

3.3.3 chmod - cambiar el modo (permisos) de un archivo o directorio

Hay dos maneras de hacerlo:

  1. Usando notación simbólica para U|G|O y todos (a)
Símbolo Significado
u usuario, el dueño del archivo o directorio
g dueño del gruop
o otros (resto del mundo)
a todos (all); combinación de u,g,o
  • Ejemplos

\(chmod\ notación\ archivo|dir\)

Notación Significado
u+x da permiso de ejecución a usuario
u-x revoca permiso de ejecución a usuario
o-r otros (resto del mundo) no puede leer
+x equivale a a+x
o-rw quitar a otros permisos de rw
u+x,go=-rx asignar +x a U, revocar a O permisos de rx

\(chmod\ a+rx\ script.sh\) hace el archivo script.sh leíble y ejecutable para todos

  1. Usando representación octal

Los sistemas de numeración \(octal\) (base 8) y su primo el \(hexadecimal\) (base 16) se usan frecuentemente para expresar números en computadoras.

Los humanos usamos el sistama \(decimal\) ya que (la mayoría) tenemos 10 dedos. Las computadoras en cambio “nacieron con un solo dedo”, por lo que cuentan usando el sistema \(binario\) (base 2) usando sólo 1s y 0s. Por tanto en binario, contamos así: 0,1, 10,11, 100,101, 110,111 …

En \(octal\), contamos así: 0,1,2,3,4,5,6,7, 10,11,12,13,14,15,16,17, 20,21 …

Usando una cadena de tres dígitos octales, podemos de manera muy conveniente definir el modo de un archivo para U|G|O acorde a la siguiente tabla

octal binario modo del archivo
0 000
1 001 –x
2 010 -w-
3 011 -wx
4 100 r–
5 101 r-x
6 110 rw-
7 111 rwx

De modo que combinando los octales

READ = 4

WRITE = 2

EXECUTE = 1

con las posiciones U|G|O, define los modos:

USER GROUP OTHERS MODE
r w x r w x r w x UGO
4 2 0 0 0 0 0 0 0 600
4 2 1 4 0 1 4 0 1 755
  • Ejemplos
    • \(chmod\ 755\ script.sh\) otorga a “script.sh” modo de: -rwxr-xr-x
    • \(chmod\ 700\ script.sh\) otorga a “script.sh” modo de: -rwx——
    • \(chmod\ 644\ script.sh\) otorga a “script.sh” modo de: -rw-r–r–

3.3.4 file nos indica las propiedades de un archivo

# mostrar las características de los archivos con file
file assembly_summary.txt.gz
file linux_commands.tab
file intro2linux
## assembly_summary.txt.gz: gzip compressed data, was "assembly_summary.txt", last modified: Mon Jul 22 00:26:11 2019, from Unix, original size modulo 2^32 48497020
## linux_commands.tab: symbolic link to /home/vinuesa/Cursos/TIB/TIB19-T3/sesion1_intro2linux/linux_basic_commands.tab
## intro2linux: directory

3.4 Moviéndonos por el sistema de archivos: comando cd

Para movernos por el sistema de archivos de un sistema Linux podemos hacer uso de la ruta absoluta o la ruta relativa.

3.4.1 La ruta absoluta

Define la ruta a cualquier directorio o archivo en el sistema partiendo del directorio raíz \(/\), como indica el comando \(pwd\)

3.4.1.1 de nuevo, ¿dónde estoy?: imprime directorio actual con pwd

pwd 
## /home/vinuesa/cursos/intro2linux

3.4.1.2 entra a un directorio cualquiera usando la ruta absoluta

# indicamos la ruta absoluta o "path" al directorio al que queremos entrar
cd /home/vinuesa/cursos/intro2linux/data

3.4.2 La ruta relativa

Se define en relación al directorio actual en el que estás.

3.4.2.1 entra a un subdirectorio bajo en directorio actual usando la ruta relativa

cd directorio

3.4.2.2 sube un nivel de directorio usando RUTA RELATIVA: cd ..

cd ..

3.4.2.3 sube dos niveles en la jerarquía de directorios usando RUTA RELATIVA: cd ../..

cd ../..
  • ¿dónde estoy?
pwd 
## /home/vinuesa/cursos/intro2linux

3.4.2.4 regresa al directorio en el que estabas justo antes con cd -

cd -
pwd
## /home/vinuesa/Cursos/intro_biocomp4genomics_TFC
## /home/vinuesa/Cursos/intro_biocomp4genomics_TFC

3.4.2.5 regresa a tu home desde cualquier punto del sistema de archivos con cd

cd $HOME

# que es equivalente a:
cd ~ 

# o también a 
cd
  • cd cambiar directorios con rutas absolutas (/ruta/completa/al/dir) y relativas ../../
# a dónde nos lleva este comando?
cd /
pwd
## /
  • cambia de nuevo a tu home
cd -
pwd
## /home/vinuesa/Cursos/intro_biocomp4genomics_TFC
## /home/vinuesa/Cursos/intro_biocomp4genomics_TFC
  • sube al directorio home/ usando la ruta relativa
cd ../
  • y lista los contenidos
ls | head
## align_seqs_with_clustal_or_muscle.sh
## aoa.awk
## assembly_summary.txt.gz
## awkvars.out
## bash_script_template_with_getopts.sh
## bin
## blast-imager.pl
## clases_avance.txt
## compute_DNA_seq_stats.awk
## consensus_tre.nwk

3.5 Generación de directorios: comando mkdir

# vamos a $HOME y generamos el directorio intro2linux
cd
if [ -d intro2linux ]; then 
    echo "found dir intro2linux"
else 
    mkdir intro2linux
fi
## found dir intro2linux
  • comprueba los permisos del nuevo directorio
ls -ld intro2linux
## drwxr-xr-x 4 vinuesa vinuesa 4096 sep 29  2020 intro2linux
  • generemos un subdirectorio por debajo del que acabamos de crear:
mkdir -p intro2linux/sesion1 && cd intro2linux/sesion1
## /home/vinuesa/intro2linux/sesion1
  • cambiamos a /home/vinuesa e intenta crear estos mismos directorios ahí
cd /home/vinuesa && mkdir -p intro2linux/sesion1

No puedes escribir en mi directorio, porque no te he otorgado permiso para ello ;)

3.6 Qué es exactamente un \(comando\) en GNU/Linux?

Un comando de GNU/Linux puede ser una de cuatro cosas:

  1. un programa ejecutable, como los que hemos estado usando, tales como \(cut\), \(sort\), \(grep\) … que están guardados en directorios del sistame tales como \(/bin\), \(/usr/bin\) … Estos son programas compilados, escritos en lenguajes como \(C\) o \(C++\). Pero un comando también puede ser un script escrito en un lenguaje interpretado como \(Bash\), \(Perl\), \(Phython\) o \(R\), posiblemente escrito por el propio usuario. Estos frecuentemente se ubican en el directorio $HOME/bin

  2. puede ser un comando del shell, un “\(shell\ builtin\)”, como \(cd\), \(cp\)

  3. una función de shell. Estos son miniprogramas de shell exportadas al ambiente (environment). Veremos más adelante cómo se delcaran funciones.

  4. un alias, es decir un nombre de nuestra elección para otro comando.

3.6.1 Genera tu propio comando con un alias

El shell provee el comando integrado (“builtin”) \(alias\) que es muy útil para simplificar la llamada a comandos complejos que repetimos con frecuencia.

La sintaxis es la siguiente alias NOMBRE=“COMANDO”

Es importante asegurarse que el nombre del alias no existe previamente en el sistema. Antes de generarlo, tecleo \(tep\) en la consola para asegurarme que no existe un comando con dicho nombre.

vinuesa@alisio:~$ tep
-bash: tep: command not found

Ahora puedo generar el alias \(tep\) para conectarme al servidor tepeu:

# Nota: uso asteriscos para no hacer público el NIS completo del server
alias tep="ssh -X vinuesa@tepeu.*.*.*"

Noten que el uso de comillas dobles encerrando el comando para el cual vamos a generar un alias

Una vez generado, puedo teclear \(tep\), que me permite conectarme al servidor.

vinuesa@alisio:~$ tep
vinuesa@132.*.*.*'s password:

3.6.1.1 El archivo $HOME/.bash_aliases

Si quieres guardar una lista personal de aliases de manera permanente, puedes escribirlos en el archivo \(\$HOME/.bash\_aliases\), uno de los archivos de configuración del ambiente que los usuarios avanzados de GNU/Linux guardan en su \(\$HOME\)

Cada vez que inicias tu máquina o que estableces una sesión remota, se lee este archivo en memoria, quedando los aliases como parte del ambiente (\(environment\)).

Van unas líneas de mi archivo \(\$HOME/.bash\_aliases\) como ejemplos (los asteriscos los pongo para no hacer públicas las IPs)

# ssh aliases
alias puma="ssh -X vinuesa@132.*.*.*"            # desktop
alias ix="ssh -X vinuesa@132.*.*.*"              # ixchel ubuntu Linux server
alias tep="ssh -X vinuesa@132.*.*.*"             # tepeu Linux server

# fetch (sftp) data from servers
alias fyax="echo 'sftp vinuesa@*.*.unam.mx:'"
alias fku="echo 'sftp vinuesa@*.*.unam.mx:'"

# aliases for Java utils
alias figtree="java -Xms128m -Xmx512m -jar /home/vinuesa/soft_download/FigTree_v1.4.4/lib/figtree.jar"
alias seaview="/home/vinuesa/soft_download/seaview/seaview"

...

El archivo \(\$HOME/.bash_aliases\) es leído por los archivos de configuración \(\$HOME/.bashrc\) cuando iniciamos nuestra máquina local, o \(\$HOME/.bash\_profile\) cuando establecemos una sesión remota vía \(ssh\). Hablaremos más de estos archivo en la sección de administración básica de un sistema Linux.

Como ejercicio te sugiero que busques y leas estos archivos. Recuerda que tienes que usar \(ls\ -a\) para poderlos ver, ya que el nombre de archivo está precedido por un . para ocultarlos a una llamada de \(ls\) estándar.

3.6.2 Identificación del tipo de comando con type

Podemos identificar a cuál de las cuatro categorías arriba listadas pertenece un comando particular. Veamos unos ejemplos:

type grep
type alias
## grep is /usr/bin/grep
## alias is a shell builtin

3.6.3 Muestra la ruta del comando con which

Recuerda, podemos tener diferentes versiones de un mismo comando en diferentes directorios del sistema. El comando \(which\) nos permite identificar la ruta absoluta del que primero ve el sistema en el \(PATH\)

which blastn
which perl
which trunc_seq.pl
which transpose_pangenome_matrix.sh
## /usr/local/bin/blastn
## /usr/bin/perl
## /home/vinuesa/bin/trunc_seq.pl
## /home/vinuesa/bin/transpose_pangenome_matrix.sh

3.6.4 Manual de cada comando: man command

La mayoría de los programas ejecutables del sistema proveen un formato estandarizado de documentación que se despliega en la consola usando la línea de comandos

# mira las opciones de cut y sort en la manpage
man cut | head -30
## CUT(1)                                                                                    User Commands                                                                                    CUT(1)
## 
## NAME
##        cut - remove sections from each line of files
## 
## SYNOPSIS
##        cut OPTION... [FILE]...
## 
## DESCRIPTION
##        Print selected parts of lines from each FILE to standard output.
## 
##        With no FILE, or when FILE is -, read standard input.
## 
##        Mandatory arguments to long options are mandatory for short options too.
## 
##        -b, --bytes=LIST
##               select only these bytes
## 
##        -c, --characters=LIST
##               select only these characters
## 
##        -d, --delimiter=DELIM
##               use DELIM instead of TAB for field delimiter
## 
##        -f, --fields=LIST
##               select only these fields;  also print any line that contains no delimiter character, unless the -s option is specified
## 
##        -n     (ignored)
## 
##        --complement

3.6.5 Ayuda de cada comando integrado en el Shell: help shell_builtin para shell builtins

# mira las opciones de cut y sort en la manpage
help cd
## cd: cd [-L|[-P [-e]] [-@]] [dir]
##     Change the shell working directory.
##     
##     Change the current directory to DIR.  The default DIR is the value of the
##     HOME shell variable.
##     
##     The variable CDPATH defines the search path for the directory containing
##     DIR.  Alternative directory names in CDPATH are separated by a colon (:).
##     A null directory name is the same as the current directory.  If DIR begins
##     with a slash (/), then CDPATH is not used.
##     
##     If the directory is not found, and the shell option `cdable_vars' is set,
##     the word is assumed to be  a variable name.  If that variable has a value,
##     its value is used for DIR.
##     
##     Options:
##       -L force symbolic links to be followed: resolve symbolic
##          links in DIR after processing instances of `..'
##       -P use the physical directory structure without following
##          symbolic links: resolve symbolic links in DIR before
##          processing instances of `..'
##       -e if the -P option is supplied, and the current working
##          directory cannot be determined successfully, exit with
##          a non-zero status
##       -@ on systems that support it, present a file with extended
##          attributes as a directory containing the file attributes
##     
##     The default is to follow symbolic links, as if `-L' were specified.
##     `..' is processed by removing the immediately previous pathname component
##     back to a slash or the beginning of DIR.
##     
##     Exit Status:
##     Returns 0 if the directory is changed, and if $PWD is set successfully when
##     -P is used; non-zero otherwise.

3.6.6 Despliega información básica de uso del comando: command –help

Muchos programas del sistema soportan la opción \(--help\) que despliega la descripción de la sintaxis del comando y las opciones. Esta suele ser una descripción más corta que la que nos dan las páginas de \(man\) para el comando

# mira las opciones de cut y sort en la manpage
mkdir --help
## Usage: mkdir [OPTION]... DIRECTORY...
## Create the DIRECTORY(ies), if they do not already exist.
## 
## Mandatory arguments to long options are mandatory for short options too.
##   -m, --mode=MODE   set file mode (as in chmod), not a=rwx - umask
##   -p, --parents     no error if existing, make parent directories as needed
##   -v, --verbose     print a message for each created directory
##   -Z                   set SELinux security context of each created directory
##                          to the default type
##       --context[=CTX]  like -Z, or if CTX is specified then set the SELinux
##                          or SMACK security context to CTX
##       --help     display this help and exit
##       --version  output version information and exit
## 
## GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
## Full documentation at: <https://www.gnu.org/software/coreutils/mkdir>
## or available locally via: info '(coreutils) mkdir invocation'

3.6.7 Encuentra comandos adecuados para un tipo de acción: apropos

Por ejemplo, quiero saber qué distribución de Linux corre mi máquina, pero no recuerdo el comando.

# encuentra comandos que tienen que ver con la palabra clave distribution
apropos distribution
## Alien::Base::Authoring (3pm) - Authoring an Alien distribution using Alien::Base
## Bio::Tools::RandomDistFunctions (3pm) - A set of routines useful for generating random data in different distributions
## hmmsim (1)           - collect profile score distributions on random sequences
## Linux::Distribution (3pm) - Perl extension to detect on which Linux distribution we are running.
## Math::GSL::CDF (3pm) - Cumulative Distribution Functions
## Math::GSL::Randist (3pm) - Probability Distributions
## Module::Build::Bundling (3pm) - How to bundle Module::Build with a distribution
## Module::Build::Notes (3pm) - Create persistent distribution configuration modules
## Module::CPANTS::Analyse (3pm) - Generate Kwalitee ratings for a distribution
## Module::CPANTS::Kwalitee::Distros (3pm) - Information retrieved from the various Linux and other distributions
## Package::DeprecationManager (3pm) - Manage deprecation warnings for your distribution
## Parse::Distname (3pm) - parse a distribution name
## Test2::Suite (3pm)   - Distribution with a rich set of tools built upon the Test2 framework.
## Test::CPAN::Meta::JSON (3pm) - Validate a META.json file within a CPAN distribution.
## Test::File::ShareDir::Object::Dist (3pm) - Object Oriented ShareDir creation for distributions
## Test::Kwalitee (3pm) - Test the Kwalitee of a distribution before you release it
## Test::Pod::Coverage (3pm) - Check for pod coverage in your distribution.
## bdftopcf (1)         - convert X font from Bitmap Distribution Format to Portable Compiled Format
## CPAN::DistnameInfo (3pm) - Extract distribution name and version from a distribution filename
## CPAN::Meta (3perl)   - the distribution metadata for a CPAN dist
## CPAN::Meta::Converter (3perl) - Convert CPAN distribution metadata structures
## CPAN::Meta::Feature (3perl) - an optional feature provided by a CPAN distribution
## CPAN::Meta::Prereqs (3perl) - a set of distribution prerequisites by phase and type
## CPAN::Meta::Spec (3perl) - specification for CPAN distribution metadata
## CPAN::Meta::Validator (3perl) - validate CPAN distribution metadata structures
## debian-distro-info (1) - provides information about Debian's distributions
## distro-info (1)      - provides information about the distributions' releases
## dpkg-vendor (1)      - queries information about distribution vendors
## gsl-randist (1)      - generate random samples from various distributions
## iptables-extensions (8) - list of extensions in the standard iptables distribution
## lsb_release (1)      - print distribution-specific information
## perlnewmod (1)       - preparing a new module for distribution
## perlutil (1)         - utilities packaged with the Perl distribution
## pfscale (1)          - fit parameters of an extreme-value distribution to a profile score list
## Test::CPAN::Changes (3pm) - Validation of the Changes file in a CPAN distribution
## ubuntu-distro-info (1) - provides information about Ubuntu's distributions
  • veo que es lsb_release. Consultemos su \(--help\)
# primero consulto su ayuda 
lsb_release --help
## Usage: lsb_release [options]
## 
## Options:
##   -h, --help         show this help message and exit
##   -v, --version      show LSB modules this system supports
##   -i, --id           show distributor ID
##   -d, --description  show description of this distribution
##   -r, --release      show release number of this distribution
##   -c, --codename     show code name of this distribution
##   -a, --all          show all of the above information
##   -s, --short        show requested information in short format
  • ahora ejecuto el comando con la opción deseada
lsb_release -a
## No LSB modules are available.
## Distributor ID:  Ubuntu
## Description: Ubuntu 20.04.5 LTS
## Release: 20.04
## Codename:    focal

3.6.8 Imprime desripciones de una línea de un comando con: whatis

Si no recuerdas o sabes lo que hace un comando particular, puedes obtener una respuesta rápida con \(whatis\ command\)

whatis cron
## cron (8)             - daemon to execute scheduled commands (Vixie Cron)
## cron (3tcl)          - Tool for automating the period callback of commands

4 Trabajando con archivos: copiar, mover, renombrar y borrar archivos

# cambia a tu home, y luego a intro2linux/sesion1
cd && cd intro2linux/sesion1
## /home/vinuesa/intro2linux/sesion1

4.1 copia de archivo simple: cp /path/to/file .

  • copia el archivo /home/vinuesa/intro2linux/data/linux_basic_commands.tab al directorio actual
cp /home/vinuesa/intro2linux/data/linux_basic_commands.tab . # <<< vean el punto, significa, dir actual
  • otra manera, usando rutas absolutas y la variable de ambiente $HOME
cp /home/vinuesa/intro2linux/data/linux_basic_commands.tab $HOME/intro2linux/sesion1/
  • otra variante, usando rutas absolutas y la tilde \(~\) como abreviación de $HOME
cp /home/vinuesa/intro2linux/data/linux_basic_commands.tab ~/intro2linux/sesion1/
  • copia un archivo a otro directorio renombrando la copia
cp ~/tmp/working_with_linux_commands.html ~/cursos/docs/index.html

4.2 copiar un directorio: cp -r dir . [-r recursively, el directorio y su contenido]

  • copiar el directorio /home/vinuesa/intro2linux/data/ a tu dir actual
# Noten el punto '.' y cp -r (recursively), necesario para copiar directorios completos
cp -r /home/vinuesa/intro2linux/data/ . 
  • NOTA: podemos hacer uso de las combinaciones de ruta absoluta y/o relativas que más nos convengan.

4.3 Eliminar un directorio: rm -r[f] [recursively -r and force -f]

mkdir borrame

cp linux_basic_commands.tab borrame

ls borrame

rm -rf borrame
## linux_basic_commands.tab

Prueba ahora este comando

rm data

qué pasa?

¿Cómo tengo que borrar un directorio? rm -r[f] directorio

rm -rf data
  • NOTA: en algunos sistemas la llamada a \(rm\ -r\ directorio\) no permite borrar \(directorio\) directamente, imprimiendo un mensaje de error. Par forzar el borrado, si el mensaje de advertencia, usamos \(rm\ -rf\ directorio\)

4.4 scp copia de archivos entre máquinas vía Internet

Para llevar/copiar un archivo de tu máquina local al servidor, usamos el comando \(scp\)

Sintaxis: scp ARCHIVO_LOCAL :/home/USUARIO/ruta/al/dir/destino/

  • Ejercicio con \(scp\)
    1. descarga el archivo linux_basic_commands.tab del repositorio GitHub a tu máquina
    2. cópialo a tu \(\$HOME\) en el servidor
# asegúrate de estar en el directorio que contiene el archvo descargado:
ls linux_basic_commands.tab

scp linux_basic_commands.tab USUARIO@ip.maquina:/home/USUARIO/ruta/al/dir/destino/
  • NOTAS sobre scp:
  1. Si queremos copiar un directorio podemos usar la sintaxis scp -r dir :/home/USUARIO/ruta/al/dir/destino/
  2. Usa scp -r dir destino sólo si \(dir\) contiene pocos archivos pequeños.
  3. Si necesitas copiar archivos grandes o directorios con muchos archivos, comprímelos primero con \(gzip\) y \(tar\) (ver más adelante)

4.5 sftp descarga de archivos de una máquina remota a tu máquina local (laptop o desktop)

Para traer/copiar un archivo del servidor al directorio actual en la máquina local, usamos el comando \(sftp\)

Sintaxis: sftp :/home/USUARIO/ruta/al/dir/destino/

# asegúrate de estar en el directorio de tu máquina local donde quieres depositar el archivo a descargar del servidor:

cd $HOME/ruta/dir/destino

# ahora establecemos una sesión de ftp segura (sftp) a la máquina remoata
sftp USUARIO@ip.maquina:/home/USUARIO/ruta/al/dir/destino/

# podemos hacer un ls para buscar el/los archivo(s) que queremos descargar
ls *tab

# con la orden get, recuperamos los archivos deseados
get linux_basic_commands.tab
get *tab
get -r directorio

# cerrar la sesión sftp al serivdor
quit

# ésto nos regresa a $HOME/ruta/dir/destino
ls 
  • Ejercicio con \(sftp\)
    1. genera el directorio tmp_sftp en tu máquina y métete en él
    2. copia de tepeu el archivo de tu elección a tmp_sftp meditante \(sftp\)

4.6 Generación de ligas simbólicas a archivos: comando ln -s /ruta/al/archivo/fuente nombre_liga

Hacer ligas simbólicas en vez de copiar cada archivo es muy importante, ya que permite ahorrar mucho espacio en disco al evitar la multiplicación de copias fisicas en el disco duro del mismo archivo en el $HOME de uno o más usuarios.

Sintaxis básica:

  • ln -s /path/to/file . # que genera una liga simbólica a /path/to/file, llamada file, en el directorio de trabajo
  • ln -s /path/to/file nombre_nuevo # que genera una liga simbólica a /path/to/file, llamada nombre_nuevo, en el directorio de trabajo
  • ln -s /path/to/dir_with_many_files/*fastq.gz . # que genera una liga simbólica en el directorio actual para cada archivo fastq.gz ubicado en /path/to/dir_with_many_files
hostn=$(hostname)
if [ "$hostn" == "alisio" ]; then
  ln -s /home/vinuesa/Cursos/TIB/TIB19-T3/sesion1_intro2linux/linux_basic_commands.tab comandos_de_linux.tab
elif [ "$hostn" == "puma" ]; then
  ln -s /home/vinuesa/Cursos/TIB19-T3/sesion1_intro2linux/linux_basic_commands.tab comandos_de_linux.tab
elif [ "$hostn" == "tepeu" ]; then 
   ln -s /home/vinuesa/intro2linux/data/linux_basic_commands.tab comandos_de_linux.tab 
   ln -s /home/vinuesa/intro2linux/data/assembly_summary.txt.gz .
fi

# confirmamos que se generaron las ligas
ls -l | head
## total 102112
## -rwxr-xr-x 1 vinuesa vinuesa     2135 oct 14  2020 align_seqs_with_clustal_or_muscle.sh
## -rw-rw-r-- 1 vinuesa vinuesa      244 dic 27  2020 aoa.awk
## -rw-r--r-- 1 vinuesa vinuesa  6780296 oct 20  2020 assembly_summary.txt.gz
## -rw-rw-r-- 1 vinuesa vinuesa      895 dic 21  2020 awkvars.out
## -rw-r--r-- 1 vinuesa vinuesa     5505 nov  7  2020 bash_script_template_with_getopts.sh
## drwxrwxr-x 2 vinuesa vinuesa     4096 nov 16  2020 bin
## -rwxr-xr-x 1 vinuesa vinuesa     5406 oct 14  2020 blast-imager.pl
## -rw-rw-r-- 1 vinuesa vinuesa      933 nov  4  2020 clases_avance.txt
## lrwxrwxrwx 1 vinuesa vinuesa       78 sep 26 22:11 comandos_de_linux.tab -> /home/vinuesa/Cursos/TIB/TIB19-T3/sesion1_intro2linux/linux_basic_commands.tab
  • renombramos la liga (o cualquier archivo o directorio)
  mv comandos_de_linux.tab linux_commands.tab
  • podemos elegir el nombre del apuntador que nos convenga
  ln -s nombre_largo_de_archivo.tgz arch_tmp.tgz
  • podemos generar ligas simbólicas a todos los archivos de un directorio (por ej. lecturas fastq comprimidas de un secuenciador Illumina) en el directorio de trabajo (actual) con un comando como éste:
  ln -s /export/data3/illumina_reads/run_stenos_2020-10-14/*fastq.gz .

4.7 Nombres de archivos y directorios y uso de comillas sencillas y dobles

Los usuarios de Windows suelen escribir nombres de archivo con espacios: ‘mi archivo de secuencias.fasta’. Como subrayamos en el tutoral de introducción a GNU/Linux, en el \(Shell\) los espacios separan argumentos. Por ello lo mejor es usar guiones bajos para separar las partes del nombre de un archivo: ‘recA_Bradyrhizobium.fna’. Recuerda también que en el \(Shell\), la puntuación se considera, es decir ‘recA_Bradyrhizobium.fna’ y ‘recA_bradyrhizobium.fna’ son dos archivos distintos.

En las versiones modernas de GNU/Linux, los nombres de archivos con espacios son automáticamente mostrados entre comillas sencillas: ‘mi archivo de secuencias.fasta’. Así el \(Shell\) considera el nombre del archivo como un solo argumento.

4.7.1 Uso de comillas sencillas(\('\)): interpretación literal y escape de caracteres reservados para el SHELL

  • Para renombrar o copiar un archivo que contiene espacies, debes escaparlo con comillar sencillas (\('\))

En el siguiente ejemplo las comillas sencillas sireven para interpretar literalmente los espacios como caracteres, por lo que el nombre de archivo es visto por el comando \(mv\) como un solo argumento

mv 'mi archivo de secuencias.fasta' recA_Bradyrhizobium.fna

En ciertas ocasiones usarás comillas sencillas (\('\)) para la ejecución de algunos programas desde la línea de comandos como por ejemplo al pasar opciones complejas a sed, perl, awk … Estos son lenguajes interpretados, cuyo uso básico veremos más adelante

  • Ejemplo de llamada del comando \(rename\) para eliminar espacios en nombres de archivos y directorios
# rename es un script de Perl que permite renombrar archivos de manera muy eficiente 
#   haciendo uso de expresiones regulares y expansión de nombres de archivo
# El siguiente ejemplo renombra todos los archivos docx del directorio, 
#   cabmiando todas las instancias (globalmente) de espacios por guiones bajos
rename 's/ /_/g' *.docx
  • Ejemplo de llamada a \(sed\)
# con esta sentencia el editor de flujo sed cambiará todas las instancias de Windos por GNU-Linux
#  que aparecen en el archivo txt
sed 's/Windows/GNU-Linux/g' archivo.txt # cambia Windows por GNU-Linux globalmente ;)

4.7.2 Uso de comillas dobles (\("\)) para interpolación de variables

Se requieren comillas dobles (\("\)) siempre que queramos interpolar una variable (obtener su valor) contenida dentro de una cadena de texto, como muestra el siguiente ejemplo

# echo imprime el contenido de la cadena acotada entre comillas dobles, interpolando las variables que tenga
echo "soy el usuario $USER en la máquina $HOSTNAME"
## soy el usuario vinuesa en la máquina alisio

Si usáramos comillas sencillas, las variables no se interpolan y se imprime literalmente el nombre de las mismas

echo 'soy el usuario $USER en la máquina $HOSTNAME'
## soy el usuario $USER en la máquina $HOSTNAME

5 Trabajando con archivos: visualización, generación y edición de archivos de texto plano (codificación ASCII)

Linux ofrece una amplia gama de programas para visualizar y editar archivos, todos con opciones poderosas y muy útiles. La elección de uno de ellos depende de lo que necesitamos o queremos.

5.1 Visualización de contenidos de archivos: comandos head, tail, cat, less, more

Los comandos \(head\), \(tail\), \(cat\), \(less\) y \(more\) son algunos de los disponibles para visualizar el contenido de un archivo ASCII.

5.1.1 Usa head y tail para desplegar la cabecera y cola de archivos

head linux_commands.tab 
## IEEE Std 1003.1-2008 utilities Name  Category    Description     First appeared
## admin    SCCS    Create and administer SCCS files    PWB UNIX
## alias    Misc    Define or display aliases   
## ar   Misc    Create and maintain library archives    Version 1 AT&T UNIX
## asa  Text processing     Interpret carriage-control characters   System V
## at   Process management  Execute commands at a later time    Version 7 AT&T UNIX
## awk  Text processing     Pattern scanning and processing language    Version 7 AT&T UNIX
## basename     Filesystem  Return non-directory portion of a pathname; see also dirname    Version 7 AT&T UNIX
## batch    Process management  Schedule commands to be executed in a batch queue   
## bc   Misc    Arbitrary-precision arithmetic language     Version 6 AT&T UNIX
tail linux_commands.tab
## val  SCCS    Validate SCCS files     System III
## vi   Text processing     Screen-oriented (visual) display editor     1BSD
## wait     Process management  Await process completion    Version 4 AT&T UNIX
## wc   Text processing     Line, word and byte or character count  Version 1 AT&T UNIX
## what     SCCS    Identify SCCS files     PWB UNIX
## who  System administration   Display who is on the system    Version 1 AT&T UNIX
## write    Misc    Write to another user's terminal    Version 1 AT&T UNIX
## xargs    Shell programming   Construct argument lists and invoke utility     PWB UNIX
## yacc     C programming   Yet another compiler compiler   PWB UNIX
## zcat     Text processing     Expand and concatenate data     4.3BSD
  • Por defecto, \(head\) y \(tail\) despliegan 10 líneas. Pero podemos pasarle como opción el número de líneas deseado con esta sintaxis:

tail -n -íntegro archivo o simplemente con: tail -íntegro archivo

como se muestra en esto ejemplos:

# le podemos indicar el numero de lineas a desplegar
head -3 linux_commands.tab
## IEEE Std 1003.1-2008 utilities Name  Category    Description     First appeared
## admin    SCCS    Create and administer SCCS files    PWB UNIX
## alias    Misc    Define or display aliases   
tail -1 linux_commands.tab
## zcat     Text processing     Expand and concatenate data     4.3BSD
  • \(tail\) tiene otras dos opciones que uso frecuentemente:
    • tail -n +íntegro archivo # para imprimir a partir de la fila número -n +íntegro (permite eliminar primeras líneas)
    • tail -f archivo # opción follow, que permite ver cómo crece la cola de un archivo a medida que se va escribiendo.

Ejemplo de tail -n +íntegro archivo

# imprime a partir de la línea 2, es decir, elimina cabecera del archivo
tail -n +2 linux_commands.tab | head -2
## admin    SCCS    Create and administer SCCS files    PWB UNIX
## alias    Misc    Define or display aliases   

5.1.2 cat despliega uno o más archivos, concatenándolos si son varios

cat linux_commands.tab | head
## IEEE Std 1003.1-2008 utilities Name  Category    Description     First appeared
## admin    SCCS    Create and administer SCCS files    PWB UNIX
## alias    Misc    Define or display aliases   
## ar   Misc    Create and maintain library archives    Version 1 AT&T UNIX
## asa  Text processing     Interpret carriage-control characters   System V
## at   Process management  Execute commands at a later time    Version 7 AT&T UNIX
## awk  Text processing     Pattern scanning and processing language    Version 7 AT&T UNIX
## basename     Filesystem  Return non-directory portion of a pathname; see also dirname    Version 7 AT&T UNIX
## batch    Process management  Schedule commands to be executed in a batch queue   
## bc   Misc    Arbitrary-precision arithmetic language     Version 6 AT&T UNIX

cat -n nos permite añadir números de línea a los archivos desplegados

cat -n linux_commands.tab | head
##      1   IEEE Std 1003.1-2008 utilities Name     Category    Description     First appeared
##      2   admin   SCCS    Create and administer SCCS files    PWB UNIX
##      3   alias   Misc    Define or display aliases   
##      4   ar  Misc    Create and maintain library archives    Version 1 AT&T UNIX
##      5   asa     Text processing     Interpret carriage-control characters   System V
##      6   at  Process management  Execute commands at a later time    Version 7 AT&T UNIX
##      7   awk     Text processing     Pattern scanning and processing language    Version 7 AT&T UNIX
##      8   basename    Filesystem  Return non-directory portion of a pathname; see also dirname    Version 7 AT&T UNIX
##      9   batch   Process management  Schedule commands to be executed in a batch queue   
##     10   bc  Misc    Arbitrary-precision arithmetic language     Version 6 AT&T UNIX

5.1.2.1 cat también permite generar contenidos copiados con el ratón o tecleando

A veces queremos copiar o teclear algún texto corto a un archivo. Esto podemos hacerlo abriendo un editor de textos como \(vim\) o \(gedit\), pero lo más rápido es hacerlo con \(cat\) usando esta sintaxis:

cat > archivo.txt
AQUI PEGO O TECLEO EL TEXTO
Va otra línea
y otra

CTRL-d
  • Ejercicio:
    1. trata de generar el archivo borrame.txt haciendo uso del comando \(cat\) como se indica arriba y que contenga algún texto de tu elección.
    2. despliega el contenido de dicho archivo con \(cat\)
    3. despliega el archivo sin su primera línea usando \(tail\ -n\ +2\)
    4. borra el archivo

5.1.3 el paginador less despliega archivos página a página

El paginador \(less\) es el más poderoso (otra alternativa ess \(more\), pero extrañamente: \(less\) is more!)

  • sintaxis: less archivo
  • opciones más comunes
    • con la barra espaciadora o avance de página puedes ir pasando páginas
    • con inicio o fin vas a dichas páginas
    • / te permite buscar un texto: prueba \(/grep\)
    • -L nos permitiría navegar horizontalmente archivos con líneas largas
    • con \(q\) sales (quit)

Ejemplo 1:

less linux_commands.tab
/grep
q

Nota: con \(q\) salimos del paginador \(less\)

Ejemplo 2:

# less -L archivo nos permitiría navegar horizontalmente archivos con líneas largas, como tablas grandes
less -L linux_commands.tab 

\(less\) tiene muchas más opciones: explóralas con less –help

5.1.4 zcat y zless permiten visualizar el contenido de archivos de texto con compresión gzip (gnuzip)

Los archivos usados en genómica son frecuentemente grandes y/o numerosos, por lo que es una muy buena práctica mantenerlos comprimidos para ocupar el mínimo espacio posible en disco. los comandos \(zcat\) y \(zless\) permiten visualizar su contenido sin tenerlos que descomprimir. Veremos más comandos, como \(zgrep\) que pueden trabajar sobre archivos de texto comprimidos.

  • \(zcat\) desplegará todo el archivo, pudiéndolo pasar a otras herramientas de filtrado
zcat assembly_summary.txt.gz | head -5 | cut -f1-5
## #   See ftp://ftp.ncbi.nlm.nih.gov/genomes/README_assembly_summary.txt for a description of the columns in this file.
## # assembly_accession bioproject  biosample   wgs_master  refseq_category
## GCF_000010525.1  PRJNA224116 SAMD00060925        representative genome
## GCF_000007365.1  PRJNA224116 SAMN02604269        representative genome
## GCF_000007725.1  PRJNA224116 SAMN02604289        representative genome
  • usa \(zless\) para navegar el archivo, página a página o usando las múltiples opciones de \(less|zless\)
zless assembly_summary.txt.gz | head -5 | cut -f1-5

5.2 Edición de archivos con los editores vim o [g|n]edit

vim (vi improved) es un poderoso editor programable presente en todos los sistemas GNU/Linux. La principal característica tanto de Vim como de Vi consiste en que disponen de diferentes modos (normal, visual, insert, command-line, select, and ex) entre los que se alterna para realizar ciertas operaciones, lo que los diferencia de la mayoría de editores comunes, que tienen un solo modo (insert), en el que se introducen las órdenes mediante combinaciones de teclas (o interfaces gráficas).

Vim Se controla por completo mediante el teclado desde un terminal, por lo que puede usarse sin problemas a través de conexiones remotas ya que no carga el sistema al no desplegar un entorno gráfico.

Es muy recomendable aprender a usar Vim, pero no tenemos tiempo de hacerlo en el Taller, por lo que les recomiendo este tutorial de uso de Vim en español, o directamente en su terminal tecleando el comando

 vimtutor
 
 # para salir de vim, 
 
 <ESC> # para estar seguros que estamos en modo ex 
 :q

En el taller usaremos generalmente el editor con ambiente gráfico gedit, de uso muy sencillo.

# noten el uso de & al final de la sentencia para enviar el proceso al fondo
# para evitar que bloquee la terminal
gedit linux_commands.tab &

5.3 Edición de archivos con el editor de flujo sed (stream editor)

\(sed\) (stream editor) es un editor de flujo, una potente herramienta de tratamiento de texto para el sistema operativo UNIX que acepta como entrada un archivo, lo lee y modifica línea a línea de acuerdo a un script, mostrando el resultado por salida estándar (normalmente en pantalla, a menos que se realice una redirección). Sed permite manipular flujos de datos, como por ejemplo cortar líneas, buscar y reemplazar texto (con soporte de expresiones regulares ), entre otras cosas. Posee muchas características de \(ed\) y \(ex\).

Pueden consultar esta página para aprender lo básico de expresiones regulares

La sintaxis general de la orden \(sed\) es:

$ sed [-n] [-e'script'] [-f archivo] archivo1 archivo2 ...

donde:

-n indica que se suprima la salida estándar.
-e indica que se ejecute el script que viene a continuación. Si no se emplea la opción -f se puede omitir -e.
-f indica que las órdenes se tomarán de un archivo

5.3.1 Ejemplos de uso básico de sed: sustituciones s///

  • Sustituciones \(s///\) de palabras \(s/esto/aquello/\) o caracteres en archivos de texto
$ sed 's/Windows/Linux/g' archivo

Noten en el ejemplo anterior el uso del modificador \(///g\), que indica hacer las sustituciones de manera global, es decir en cada instancia de la ocurrencia de “/ésto//” en la línea. Si no se usa g, sólo se sustituye la primera instancia.

Ejemplos de uso básico de \(sed\)

  • imprime los directorios de \(\$PATH\), uno por línea
echo "# >>> sin modificador global <<<"
echo $PATH | sed 's/:/\n/'  
echo && echo "# >>> con modificador global <<<"
echo $PATH | sed 's/:/\n/g' # con modificador global
## # >>> sin modificador global <<<
## /home/vinuesa/.local/bin
## /home/vinuesa/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/vinuesa/edirect:/home/vinuesa/soft_download/get_homologues-x86_64-20190805:/home/vinuesa/soft_download/sratoolkit.2.10.5-ubuntu64/bin:/home/vinuesa/soft_download/Fiji.app:/home/vinuesa/soft_download/circos/current/bin:/usr/local/genome/bin:/home/vinuesa/soft_download/cgview_comparison_tool/scripts:/usr/lib/rstudio/bin/quarto/bin:/usr/lib/rstudio/bin/postback
## 
## # >>> con modificador global <<<
## /home/vinuesa/.local/bin
## /home/vinuesa/bin
## /usr/local/sbin
## /usr/local/bin
## /usr/sbin
## /usr/bin
## /sbin
## /bin
## /usr/games
## /usr/local/games
## /snap/bin
## /home/vinuesa/edirect
## /home/vinuesa/soft_download/get_homologues-x86_64-20190805
## /home/vinuesa/soft_download/sratoolkit.2.10.5-ubuntu64/bin
## /home/vinuesa/soft_download/Fiji.app
## /home/vinuesa/soft_download/circos/current/bin
## /usr/local/genome/bin
## /home/vinuesa/soft_download/cgview_comparison_tool/scripts
## /usr/lib/rstudio/bin/quarto/bin
## /usr/lib/rstudio/bin/postback
  • cambia espacios sencillos por guiones bajos
head -1 linux_commands.tab                 # usamos head -1 para ver la primera línea, que modificaremos con sed
head -1 linux_commands.tab | sed 's/ /_/'  # se sustituye sólo la primera instancia de espacio en blanco!
head -1 linux_commands.tab | sed 's/ /_/g' # ahora globalmente
## IEEE Std 1003.1-2008 utilities Name  Category    Description     First appeared
## IEEE_Std 1003.1-2008 utilities Name  Category    Description     First appeared
## IEEE_Std_1003.1-2008_utilities_Name_ Category_   Description_    First_appeared

5.3.2 Ejemplos de uso básico de sed: cambio de fuente: y///

  • Cambia todas las minúsculas a mayúsculas de archivo:
head -1 linux_commands.tab | sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/'
## IEEE STD 1003.1-2008 UTILITIES NAME  CATEGORY    DESCRIPTION     FIRST APPEARED
  • Podemos combinar diversas acciones de sustitución, separándolas así: s///; s///
head -1 linux_commands.tab | sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/; s/ /_/g'
## IEEE_STD_1003.1-2008_UTILITIES_NAME_ CATEGORY_   DESCRIPTION_    FIRST_APPEARED

5.3.3 Ejemplos de uso básico de sed: borrado de líneas: ‘d’

  • Borra la 1ª línea de archivo:
$ sed '1d' archivo

Ejemplo:

head -5 linux_commands.tab
echo '------------------------------------------------'
sed '1d' linux_commands.tab | head -4
## IEEE Std 1003.1-2008 utilities Name  Category    Description     First appeared
## admin    SCCS    Create and administer SCCS files    PWB UNIX
## alias    Misc    Define or display aliases   
## ar   Misc    Create and maintain library archives    Version 1 AT&T UNIX
## asa  Text processing     Interpret carriage-control characters   System V
## ------------------------------------------------
## admin    SCCS    Create and administer SCCS files    PWB UNIX
## alias    Misc    Define or display aliases   
## ar   Misc    Create and maintain library archives    Version 1 AT&T UNIX
## asa  Text processing     Interpret carriage-control characters   System V
  • Elimina las líneas en blanco. Nótese el uso de expresiones regulares, donde:
    • // delimitan la expresión regular. Noten que hay que escaparla entre ‘comillas sencillas’.
    • ^ indica el inicio de la línea
    • $ indica el término de la línea
$ sed '/^$/d' archivo
  • Genera una lista numerada de los nombres de campos o cabeceras del archivo linux_commands.tab
    • // delimitan la expresión regular. Noten que hay que escaparla entre comillas sencillas.
    • \t representa al tabulador
    • \n representa el salto de línea
    • //g la g indica que se reemplacen todas las instancias
head -1 linux_commands.tab | sed 's/\t/\n/g' | cat -n
##      1   IEEE Std 1003.1-2008 utilities Name 
##      2   Category 
##      3   Description 
##      4   First appeared

5.3.4 Tutorial extenso de \(sed\)

Esta fue una presentación minimalista de \(sed\), que no le hace justicia. Les recomiendo mucho el siguiente tutorial para profundizar:

grymoire’s sed tutorial


6 Salida estándar o standard output STDOUT

Como se explicó en el tutoral de introducción a GNU/Linux, la salida estándar \(STDOUT\) de un comando o programa generalmente está ligada a la consola, es decir, vemos en la pantalla la salida del mismo.

6.1 Imprimir texto a STDOUT con echo

Para imprimir texto simple a la consola o archivo usa el comando \(echo\)

echo introduzca nombre de usuario:
## introduzca nombre de usuario:
  • \(echo\), como su nombre indica, simplemente imprime los argumentos que se le pasan. Por ello colapsa multiples espacios, ya que los toma como separadores de argumentos
echo vea    como     se    ignoran      múltiples espacios, colapsándolos a uno!
## vea como se ignoran múltiples espacios, colapsándolos a uno!
  • Usa comillas sencillas o dobles si quieres preservar espaciado
echo '$USER: usa comillas sencillas para interpetación literal    de caracteres, incluídos espacios'
## $USER: usa comillas sencillas para interpetación literal    de caracteres, incluídos espacios
  • Usa comillas dobles si quieres preservar espaciado y además interpolar variables. Nota el uso de **diagonal invertida ** para escapar el símbolo de pesos y evitar que se interpolen variables
echo "Hola $USER: usa comillas dobles para interpetación literal    de caracteres e interpolación de \$VARIABLES"
## Hola vinuesa: usa comillas dobles para interpetación literal    de caracteres e interpolación de $VARIABLES

6.1.1 Opciones más comunes de \(echo\)

Siempre puedes consultar las opciones de echo con el comando \(man\ echo\). Pero te muestro algunas de las más comunes:

  • imprimir texto separado por tabuladores usando la opción -e y **
echo -e "texto\tseparado\tpor\ttabuladores"
## texto    separado    por tabuladores
  • imprimir texto sin salto de línea al final usando la opción -n
echo -ne "este texto no lleva salto de línea al final,"; echo " pero esta línea sí!"
## este texto no lleva salto de línea al final, pero esta línea sí!

6.2 Redireccionado de STDOUT a un archivo con el comando >

Para redireccionar el STDOUT de un comando a archivo, necesitamos redirigir el flujo a dicho archivo con el comando \(>\).

echo -e "L1\nL2\L3" > tmp1.txt  # \n es el salto de línea; nota que si escribes sólo \, se imprime literalmente!
  • Qué tipo de archivo genera el comando \(>\) ?
file tmp1.txt
echo '-------------------'
ls -l tmp1.txt
## tmp1.txt: ASCII text
## -------------------
## -rw-rw-r-- 1 vinuesa vinuesa 9 sep 26 22:11 tmp1.txt
  • podemos desplegar el archivo con \(cat\) u otros comandos como el paginador \(less\)
cat tmp1.txt
## L1
## L2\L3

6.2.1 Ejemplo de “vaciado” o desctrucción accidental de un archivo con >

Es importante entender que cada vez que se abre un nuevo descriptor de archivo con > se genera en primer lugar un archivo vacío, el cual es llenado después con la salida del comando. Eso se demuestra fácilmente con el siguiente ejemplo:

echo -e "L1\nL2\L3" > tmp1.txt
cat  tmp1.txt
## L1
## L2\L3

Ahora veamos qué pasa si accidentalmente leemos y escribimos al mismo archivo

cat  tmp1.txt > tmp1.txt

En este caso, como el comando \(cat\) primero genera el archivo tmp1.txt vacío, antes de leerlo. En el siguiente paso lee tmp1.txt, que es un archivo vacío, como demuestran los siguientes comandos

cat  tmp1.txt
ls -l tmp1.txt
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 tmp1.txt

6.3 ¿Cómo indicar a un comando o programa que lea de un archivo y escriba a otro? - combinación de < y >

Muchas veces un programa debe leer un archivo para procesarlo e imprimir el resultado a un archivo. Esto es muy fácil en GNU/Linux, combinando < con >.

Para ello usa la sintaxis: programa < archivo_a_leer > archivo_a_escribir

Pero recuerden, no podemos leer y escribir al mismo archivo simultáneamente, como mostramos arriba.

echo -e "texto arbitrario\nmás texto\ny otra línea" > texto_arbitrario.txt
cat < texto_arbitrario.txt > borrame.txt

echo "imprimiendo texto_arbitrario.txt:" && cat texto_arbitrario.txt
echo '-----------------------------------------'
echo "imprimiendo borrame.txt:"; cat borrame.txt

echo
echo "borrando textos arbitrarios ..."
rm texto_arbitrario.txt borrame.txt
## imprimiendo texto_arbitrario.txt:
## texto arbitrario
## más texto
## y otra línea
## -----------------------------------------
## imprimiendo borrame.txt:
## texto arbitrario
## más texto
## y otra línea
## 
## borrando textos arbitrarios ...

6.3.1 Ejemplo de alineamiento generación de alineamiento múltiple con el alineador \(muscle\)

\(muscle\) es un programa para hacer alineamientos múltiples. Por tanto no es una herramienta estándar de UNIX o GNU/Linux. Es un programa bioinformático que debermos descargar e instalar en nuestro sistema. Está instalado en tepeu. Lo usaremos aquí como ejemplo de llamadas del tipo programa < archivo_a_leer > archivo_a_escribir. Pasaremos la opción \(-clw\) para escribir el alineamiento resultante en en formato clustal, más conveniente para su visualización en una terminal.

Nota: si quieres aprender más sobre alineamientos múltiples y alineadores, puedes consultar el tutorial sobre alineamientos múltiples que preparé para los Talleres Internacionales de Bioinformática - 2019 (TIB19).

# Noten el uso de los descriptores de archivo 1> y 2> /dev/null, para eliminar los mensajes de progreso de muscle
# Explicaremos descriptores de archivo en la siguiente sección
# podían haber escrito simplemente:
# muscle -clw < recA_Byuanmingense.fna > recA_Byuanmingense_muscle.aln
muscle -clw < recA_Byuanmingense.fna 1> recA_Byuanmingense_muscle.aln 2> /dev/null

Ahora veamos el primer bloque del alineamiento:

head -36 recA_Byuanmingense_muscle.aln
## MUSCLE (3.8) multiple sequence alignment
## 
## 
## EU574297.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## AY591565.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574319.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574318.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574294.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574293.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574291.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574290.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574289.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574288.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574287.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574286.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574283.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574279.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574249.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574255.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGGTCGCTC
## EU574254.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGGTCGCTC
## EU574253.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGGTCGCTC
## EU574250.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGGTCGCTC
## EU574248.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGGTCGCTC
## EU574292.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574282.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574296.1      ATGAAGCTCGGCAAGAACGATCGCTCCATGGACATCGAGGCGGTCTCCTCCGGCTCGCTC
## AY591573.1      ATGAAGCTCGGCAAGAACGACCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574295.1      ATGAAGCTCGGCAAGAACGATCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574285.1      ATGAAGCTCGGCAAGAACGATCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574284.1      ATGAAGCTCGGCAAGAACGATCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574281.1      ATGAAGCTCGGCAAGAACGATCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574280.1      ATGAAGCTCGGCAAGAACGATCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574278.1      ATGAAGCTCGGCAAGAACGATCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## EU574277.1      ATGAAGCTCGGCAAGAACGATCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
## AY591566.1      ATGAAGCTCGGCAAGAACGATCGCTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTC
##                 ******************** *********************** ******** ******

6.4 Concatenación o pegado de salidas de varios comandos a un solo archivo con >>

  • escribimos unas líneas a tmp1.txt
echo -e "L1\nL2\L3" > tmp1.txt
cat  tmp1.txt
## L1
## L2\L3
  • si no usamos un nombre de archivo de salida diferente, se sobreescribe tmp1.txt
echo -e "L4\nL5\L6" > tmp1.txt
cat  tmp1.txt
## L4
## L5\L6
  • podemos concatenar o pegar secuencialmene la salida de varios comando a un mismo archivo con >>
echo -e "L1\nL2\L3" > tmp1.txt
echo -e "L4\nL5\L6" >> tmp1.txt
cat  tmp1.txt

# limpiamos
rm tmp1.txt
## L1
## L2\L3
## L4
## L5\L6

7 Descriptores de archivo: 0 == STDIN, 1 == STDOUT, 2 == STDERR

En GNU/Linux cada proceso típicamente inicia abriendo tres descriptores de archivo: 0 == STDIN, 1 == STDOUT, 2 == STDERR. Es responsabilidad del programador adherirse a estas convenciones, es decir, indicarle a los programas que escribe que imprima los resultados del programa a STDOUT y los posibles mensajes de error a STDOUT.

Copia y ejecuta los siguientes comandos en tu consola, para que lo entiendas. Le estamos pidiendo a

$ ls no_existo.txt > salida_no_existo.txt
ls: cannot access 'no_existo.txt': No such file or directory

Lo que vemos arriba es el mensaje de error del comando \(ls\) impreso a \(STDOUT\), ya que no lo hemos redirigido a un archivo.

$ ls -l salida_no_existo.txt # 0 bytes
-rw-rw-r-- 1 vinuesa vinuesa 0 oct 18 20:54 salida_no_existo.txt

7.1 Uso de 1>, 2> y &> para redirgir \(STDOUT\), \(STDERR\) o ambos a un archivo

Esta llamada a \(ls\) escribe su STDOUT al archivo stdout_no_existo.txt y su STDERR stderr_no_existo.txt.

ls no_existo.txt 1> stdout_no_existo.txt 2> stderr_no_existo.txt
ls -l stdout_no_existo.txt 
ls -l stderr_no_existo.txt
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 stdout_no_existo.txt
## -rw-rw-r-- 1 vinuesa vinuesa 61 sep 26 22:11 stderr_no_existo.txt

Cuando escribamos \(scripts\) u otros programas, debemos manejar adecuadamente los descriptores de archivos para imprimir resultados y mensajes de error a sus archivos correspondientes. Si los queremos unir en uno solo, usaremos la sintaxis &>

ls no_existo.txt &> stdout_y_stderr_no_existo.txt
ls -l stdout_y_stderr_no_existo.txt

# limpiemos la basura
rm *existo.txt
## -rw-rw-r-- 1 vinuesa vinuesa 61 sep 26 22:11 stdout_y_stderr_no_existo.txt

7.2 Eliminar mensajes impresos por un programa a STDERR con 2> /dev/null

A veces no queremos ver los mensajes que imprime un protrama a STDOUT o STDERR, por ejemplo al correr un pipeline que llama a diversas aplicaciones, algunas de las cuales pueden imprimir copiosos mensajes de progreso del proceso a STDOUT.

# no imprime nada, ya que el mensaje a STDERR lo mandamos al "cubo de la basura"
ls no_existo.txt 2> /dev/null
  • Veamos otros ejemplos muy ilustrativos.
head -2 recA_Bradyrhizobium_vinuesa.fna
## >EU574327.1 Bradyrhizobium liaoningense strain ViHaR5 recombination protein A (recA) gene, partial cds
## ATGAAGCTCGGCAAGAACGACCGGTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTCGGGCTCGACA
head -2 recA_Bradyrhizobium_vinuesa.fna 1> /dev/null
head -2 recA_Bradyrhizobium_vinuesa.fna 2> /dev/null
## >EU574327.1 Bradyrhizobium liaoningense strain ViHaR5 recombination protein A (recA) gene, partial cds
## ATGAAGCTCGGCAAGAACGACCGGTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTCGGGCTCGACA
head -2 recA_Bradyrhizobium_vinuesa.fna &> /dev/null
  • Revisemos el ejemplo de alineamiento múltiple con \(muscle\) que mostramos en la sección anterior usando:
    • Una llamada estándar a \(muscle\), que imprime muchas líneas de mensaje de progreso a STDERR
    # llamada estándar a muscle, que imprime a STDERR líneas documentando el progreso del programa ...
    muscle -clw < recA_Byuanmingense.fna > recA_Byuanmingense_muscle.aln
    • una llamada enviando STDERR a /dev/null para eliminar los mensajes
    # con uso de los descriptores de archivo 1> y 2> /dev/null, para eliminar los mensajes de progreso
    muscle -clw < recA_Byuanmingense.fna 1> recA_Byuanmingense_muscle.aln 2> /dev/null

Nota: si quieres aprender más sobre alineamientos múltiples y alineadores, puedes consultar mi tutorial sobre alineamientos múltiples


8 Trabajando con archivos: creación de tarros con tar y compresión gzip

Vimos en la sección anterior cómo leer archivos de texto comprimidos con compresión GNUzip, que convencionalmente llevan la extensión \(.gz\). En ésta veremos cómo comprimir y descomprimir archivos y directorios.

8.1 Compresión gnuzip de archivos ASCII: comandos gzip y gunzip

El comando \(gzip\) se usa principalmente para comprimir archivos con caracteres ASCII, y les agrega la extensión \(.gz\) automáticamente. Para descomprimir archivos \(*.gz\) usamos el comando \(gunzip\)

  • sintaxis \(gzip\): gzip file o también gzip *.gbk
  • sintaxis \(gunzip\): gunzip file.gz

8.2 Generación de ‘tarros’ comprimidos y extracción de archivos de ellos con el comando tar

Cuando necesitamos agrupar muchos archivos y comprimirlos, o comprimir un directorio y sus contenidos, el comando ideal es \(tar\) (tape archive) que puede combinarse con gzip usando pipes, o más convenientemente aún, usando la opción -z, como se muestra abajo.

La convención es añadir la extensión tgz, o tar.gz a los tarros comprimidos con \(gzip\).

  1. Generación de tarros comprimidos
  • sintaxis \(tar\) para múltiples archivos (genbank, por ejemplo): tar -czf tarro_de_archivos_genbank_comprimidos.tgz *.gbk
  • sintaxis \(tar\) para archivos con compresión bz2: tar -cjvf tarro_de_archivos_genbank_comprimidos.tar.bz2 *.gbk
  • sintaxis \(tar\) para un directorio: tar -czvf tarro_de_directorio.tgz mi_directorio
  1. Extracción de tarros comprimidos
  • sintaxis para extraer archivos de un \(tar.gz\): tar -xzvf tarro_de_directorio.tgz
  1. Listar contenidos de tarros comprimidos con la opción -t
  • tar -tzvf tarro_de_directorio.tgz

Resumen de las opciones básicas de \(tar\):

  • -c create: esta opción es obligatoria si se quiere crear un tarro
  • -x extract: esta opción es obligatoria si se quiere extraer archivos de un tarro
  • -z use gzip compression. La compresión gnuzip estándar, con archivos terminados en \(gz\)
  • -j use bz2 compression. Esta compresión es todavía más compacta, y los archivos resultantes clásicamente llevan la extensión \(.bz2\)
  • -v verbose command output, es decir, va imprimiendo a STDOUT los archivos que va procesando
  • -f file; esta opción es obligatoria y tiene que ir al final del comando
  • -t List the contents of an archive.

Es muy importante dominar \(tar\) y \(gzip\) y usarlos para: - ocupar el mínimo espacio posible en disco, un recurso siembre limitado en un ambiente multiusuario - comprimir archivos y directorios antes de moverlos entre máquinas mediante \(scp\) o \(sftp\) - poder desempacar y descomprimir distribuciones de código, las cuales casi invariablemente se distribuyen como tarros comprimidos.

Ejemplos:

  • generación de un tarro comprimido
ls -l *fna                   # listamos los archivos *fna a empacar y comprimir
echo '----------------------------'
tar -czf fna_files.tgz *fna  # generamos un tarro comprimido con los archivos *fna
ls -l fna_files.tgz          # comprobamos que se generó el archivo
echo '----------------------------'
ls *fna                      # se empaca una copia de los archivos *fna, los cuales siguen existiendo sin comprimir
echo '----------------------------'
rm *fna                      # por ello los eliminamos con rm
## -rw-rw-r-- 1 vinuesa vinuesa    114 dic 21  2020 mini_CDS.fna
## -rw-r--r-- 1 vinuesa vinuesa  77803 sep 29  2020 recA_Bradyrhizobium_vinuesa.fna
## -rw-r--r-- 1 vinuesa vinuesa  17736 sep  7 14:10 recA_Byuanmingense.fna
## -rw-rw-r-- 1 vinuesa vinuesa 164638 sep  7 14:10 Salmonella_enterica_33676_pIncAC_CDSs.fna
## ----------------------------
## -rw-rw-r-- 1 vinuesa vinuesa 48437 sep 26 22:11 fna_files.tgz
## ----------------------------
## mini_CDS.fna
## recA_Bradyrhizobium_vinuesa.fna
## recA_Byuanmingense.fna
## Salmonella_enterica_33676_pIncAC_CDSs.fna
## ----------------------------
  • listar contenidos de un tarro comprimido
tar -tzf fna_files.tgz      # la opción -t permite listar el contenido de un tarro
## mini_CDS.fna
## recA_Bradyrhizobium_vinuesa.fna
## recA_Byuanmingense.fna
## Salmonella_enterica_33676_pIncAC_CDSs.fna
  • desempacado de un tarro comprimido
tar -xzf fna_files.tgz        # desempacamos y descomprimimos
ls -l *fna                    # vemos los *fna desempacados
echo '----------------------------'
ls -l fna_files.tgz           # vemos que sigue existiendo el *tgz
echo '----------------------------'
rm fna_files.tgz              # lo eliminamos
## -rw-rw-r-- 1 vinuesa vinuesa    114 dic 21  2020 mini_CDS.fna
## -rw-r--r-- 1 vinuesa vinuesa  77803 sep 29  2020 recA_Bradyrhizobium_vinuesa.fna
## -rw-r--r-- 1 vinuesa vinuesa  17736 sep  7 14:10 recA_Byuanmingense.fna
## -rw-rw-r-- 1 vinuesa vinuesa 164638 sep  7 14:10 Salmonella_enterica_33676_pIncAC_CDSs.fna
## ----------------------------
## -rw-rw-r-- 1 vinuesa vinuesa 48437 sep 26 22:11 fna_files.tgz
## ----------------------------
  • generación de archivos con compresión gnuzip con \(gzip\)
ls -l *fna                    # listamos archivos *fna 
gzip *fna                     # comprimimos *fna
echo '----------------------------'
ls -l *fna*                    # vemos que ya no existen *fna, solo los *fna.gz, con compresión
## -rw-rw-r-- 1 vinuesa vinuesa    114 dic 21  2020 mini_CDS.fna
## -rw-r--r-- 1 vinuesa vinuesa  77803 sep 29  2020 recA_Bradyrhizobium_vinuesa.fna
## -rw-r--r-- 1 vinuesa vinuesa  17736 sep  7 14:10 recA_Byuanmingense.fna
## -rw-rw-r-- 1 vinuesa vinuesa 164638 sep  7 14:10 Salmonella_enterica_33676_pIncAC_CDSs.fna
## ----------------------------
## -rw-rw-r-- 1 vinuesa vinuesa   104 dic 21  2020 mini_CDS.fna.gz
## -rw-r--r-- 1 vinuesa vinuesa  3936 sep 29  2020 recA_Bradyrhizobium_vinuesa.fna.gz
## -rw-r--r-- 1 vinuesa vinuesa   882 sep  7 14:10 recA_Byuanmingense.fna.gz
## -rw-rw-r-- 1 vinuesa vinuesa 42881 sep  7 14:10 Salmonella_enterica_33676_pIncAC_CDSs.fna.gz
  • descompresión de archivos *.gz con \(gunzip\)
ls -l *fna*                  # listamos archivos *fna* (*fna.gz)
echo '----------------------------'
gunzip *fna.gz               # descomprimimos los *fna.gz
echo '----------------------------'
ls -l *fna*                   # vemos que ya no existen los archivos *fna.gz, solo los descomprimidos (*fna)
## -rw-rw-r-- 1 vinuesa vinuesa   104 dic 21  2020 mini_CDS.fna.gz
## -rw-r--r-- 1 vinuesa vinuesa  3936 sep 29  2020 recA_Bradyrhizobium_vinuesa.fna.gz
## -rw-r--r-- 1 vinuesa vinuesa   882 sep  7 14:10 recA_Byuanmingense.fna.gz
## -rw-rw-r-- 1 vinuesa vinuesa 42881 sep  7 14:10 Salmonella_enterica_33676_pIncAC_CDSs.fna.gz
## ----------------------------
## ----------------------------
## -rw-rw-r-- 1 vinuesa vinuesa    114 dic 21  2020 mini_CDS.fna
## -rw-r--r-- 1 vinuesa vinuesa  77803 sep 29  2020 recA_Bradyrhizobium_vinuesa.fna
## -rw-r--r-- 1 vinuesa vinuesa  17736 sep  7 14:10 recA_Byuanmingense.fna
## -rw-rw-r-- 1 vinuesa vinuesa 164638 sep  7 14:10 Salmonella_enterica_33676_pIncAC_CDSs.fna

9 Trabajando con archivos - buscando archivos en el sistema con los comandos [ml]ocate y find

Otra habilidad importante es saber encontrar archivos desde la línea de comandos. En sistemas GNU/Linux hay dos herramientas fundamentales para ello: \(locate\) y \(find\).

9.1 \(locate\)

Los sistemas UNIX y GNU/Linux mantienen una base de datos de ¡todos los archivos del sistema!. En Ubuntu la base de datos está en \(/etc/updatedb.conf\) y se basa en el paquete mlocate que viene instalado por defecto. Como puede leerse en la documentación, \(mlocate\) lee una o más bases de datos generadas por \(updatedb\). En distribuciones recientes de Ubuntu el sistema operativo está programado mediante cron para correr diariamente \(updatedb\) con el fin de actualizar la base de datos de archivos.

Es muy fácil encontrar archivos en la base de datos usando una cadena o una expresión regular estándar (las cuales presentaremos en la sección de \(AWK\)).

9.1.1 Opciónes básicas más usadas de \(mlocate\)

La sintaxis es muy sencilla:

locate [OPTION]… PATTERN…

Donde las opciones que más uso son:

  • -c, –count cuenta las instancias en vez de imprimirlas
  • -i búsqueda insensible a la fuente (mayúscula o minúsculas)
  • -r regexp (expresión regular) -L, –follow para seguir a las ligas simbólicas

9.1.2 Ejemplos de uso de \(mlocate\)

  • ¿cuántos archivos con extensión ‘.odp’ existen en mis sistema de archivos?

locate -c -r ‘.odp$’

1159
  • ¿Dónde está la presentación odp en la que estoy trabajando para mi proyecto de metalostasis y virulencia? locate -r ‘.odp$’ | grep -i metallostasis
/home/vinuesa/Projects/Stenotrophomonas_metallostasis/metallostasis/metallostasis_and_virulence_in_Stenotrophomonas.odp

Como ven, con acordarse de algún detalle del archivo que quieres localizar, combinando \(locate\) con \(grep\) es muy fácil y rápido localizarlo en el sistema.

9.2 El poderoso comando \(find\)

>locate sólo localiza archivos en base a su nombre. El comando find es mucho más poderoso, ya que busca en un determinado direcotorio ( y sus subdirectorios) por archivos en base a una serie de atributos del mismo, como temaño o tiempo de modifiación.

Find tiene muchísimas opciones, que puedes consultar con man find o en el GNU findutils manual en línea, con ejemplos muy interesantes.

Aquí sólo mostraré algunas de las opciones más comunes en el trabajo cotidiano en la terminal y en \(scripts\),

En su invocación más simple, \(find\) produce una lista del directorio indicado, como por ejemplo

find ~/bin | wc -l

7712

El poder y belleza de \(find\) radica en que puede usarse para identificar archivos por que cumplen ciertos criterios (atributos), aplicando \(tests\), \(acciones\) y \(opciones\).

9.2.1 \(find\) - tests

  • Cuenta los directorios y subdirecotorios en ~/bin find ~/bin -type d | wc -l
1316
  • Cuenta los archivos en ~/bin y sus y subdirecotorios find ~/bin -type f | wc -l
6300

Siguen unas tablas con algunos de los tests más importantes

  • Tests por atributos (una pequeña selección)
caracter descripción
-cmin [+-]n encuentra archivos o directorios cuyo contenido o atributos fueron modificados exactamente \(n\) minutos antes. Podemos usar \(-n\) o \(+n\) para especificar < ó > de \(n\) minutos
-mmin [+-]n encuentra archivos o directorios cuyo contenido fueron modificados exactamente \(n\) minutos antes. Podemos usar \(-n\) o \(+n\) para especificar < ó > de \(n\) minutos
-ctime [+-]n encuentra archivos o directorios cuyo contenido o atributos fueron modificados exactamente \(n\) días antes. Podemos usar \(-n\) o \(+n\) para especificar < ó > de \(n\) días
-perm modo encuentra archivos o directorios con cierto modo de permisos en codificación octal, como 777, 755, 644
-name encuentra archivos o directorios con el nombre ‘nombre.ext’; noten el uso de comillas sencillas para que el Shell no expanda los asteriscos
-empty encuentra archivos o directorios vacíos (0 bytes)
-size [+-]n encuentra archivos o directorios de un cierto tamaño
-type c encuentra archivos o directorios de tipo d, f … ver siguiente tabla
-user USER encuentra archivos o directorios que pertenecen al usuario USER
  • Tipos de archivo
Tipo de archivo Descripción
b ispositivos orientados a bloques
c dispositivos orientados a caracteres
d directorio
f archivo regular
l liga simbólica
  • Tamaños de archivo
caracter unidad de tamaño
b bloques de 512-bytes (por defecto, si no se especifican unidades)
c bytes
w palabras de 2-bytes
k Kilobytes (unidad de 1024 bytes)
M Megabytes (uniades de 1,048,576 bytes; 1024^2)
G Gigabytes (uniades de 1,073,471,824 bytes; 1024^3)

Podemos buscar archivos por tipo, nombre, tamaño y fecha de modificación. El siguiente ejemplo encuentra \(scripts\) de Bash con extensión \(*sh\) en el directorio ~/bin, de al menos 25 Kilobites de tamaño, modificados en las últimas 96 horas

find ~/bin -type f -name '*.sh' -size +25k -mtime -96
/home/vinuesa/bin/git/get_phylomarkers/run_get_phylomarkers_pipeline.sh

9.2.2 \(find\) - operadores

Para describir y combinar relaciones lógicas entre los tests arriba mencionados, usaremos los operadores Booleanos listados en la siguiente tabla

Operador Descripción
-and encuentra archivos que satisfacen ambas condiciones a izq. y derecha de -and
-or encuentra archivos que satisfacen una de las condiciones a izq. y derecha de -or
-not encuentra archivos que no satisfacen la condición a la derecha de -not (o -!)
( ) agrupa tests y operadores for definir expresiones más complejas

Ejemplo: quiero encontrar archivos regulares en el directorio ~/bin que no tienen el modo octal 0755 estándar para scripts ejecutables.

find ~/bin -type f -name '*.*' -and -not -perm 0755
/home/vinuesa/bin/perl_code_Teide_May10/collapse2haplotypes.pl
/home/vinuesa/bin/run_entropy_saturation_test.sh

# confirmamos
ls -l /home/vinuesa/bin/perl_code_Teide_May10/collapse2haplotypes.pl
-rwxrwxrwx 1 vinuesa vinuesa 966 oct 16  2011 /home/vinuesa/bin/perl_code_Teide_May10/collapse2haplotypes.pl

ls -l /home/vinuesa/bin/run_entropy_saturation_test.sh
-rwxr--r-- 1 vinuesa vinuesa 4298 jul 20 18:55 /home/vinuesa/bin/run_entropy_saturation_test.sh

9.2.3 \(find\ -exec\) o \(find\ -xargs\) y acciones definidas por el usuario

Podemos combinar la versatilidad de \(find\) con la ejecución de comandos de \(Shell\) o \(scripts\) de nuestra elección, lo cual es muy poderoso.

Generemos primero 100 directorios, cada uno con 26 archivos, bajo el directorio “borrame”

mkdir -p borrame/dir-{00{1..9},0{10..99},100}
touch borrame/dir-{00{1..9},0{10..99},100}/arch-{A..Z}
  • Cuenta todos los archivos de nombre ‘arch-A’
find borrame/ -type f -name 'arch-A' | wc -l
## 100
  • borra todos los archivos de nombre ‘arch-A’ y verifica
find borrame/ -type f -name 'arch-A' -delete
find borrame/ -type f -name 'arch-A' | wc -l
## 0
  • lista en formato largo los primeros 5 archivos arch-B haciendo uso de -exec ls -l
find borrame/ -type f -name arch-B -exec ls -l '{}' + | head -5
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 borrame/dir-001/arch-B
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 borrame/dir-002/arch-B
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 borrame/dir-003/arch-B
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 borrame/dir-004/arch-B
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 borrame/dir-005/arch-B
  • lista en formato largo los primeros 5 archivos arch-B haciendo uso de xargs ls -l
find borrame/ -type f -name arch-B | xargs ls -l | head -5
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 borrame/dir-001/arch-B
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 borrame/dir-002/arch-B
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 borrame/dir-003/arch-B
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 borrame/dir-004/arch-B
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 borrame/dir-005/arch-B

9.2.4 \(find\) combinado con expansión de caracteres

find borrame/ -maxdepth 1 -type d -name 'dir-001' -or -name 'dir-01[1-3]'
## borrame/dir-012
## borrame/dir-011
## borrame/dir-013
## borrame/dir-001
find borrame/ -maxdepth 1 -type d -name 'dir-0?1'
## borrame/dir-061
## borrame/dir-091
## borrame/dir-051
## borrame/dir-031
## borrame/dir-071
## borrame/dir-041
## borrame/dir-011
## borrame/dir-021
## borrame/dir-081
## borrame/dir-001
  • borremos el directorio de prácticas
rm -rf borrame

-¿qué hace este comando?

find borrame/ -type f -name 'arch-*' | xargs ls -l | tail -3

-¿qué hace este comando?

find borrame/ -type d -name 'dir-??9' | xargs ls -l | tail -3

-¿qué hace este comando?

find borrame/ -maxdepth 1 -type d -name 'dir-00*'

10 ¿Cuánto espacio queda en disco? - comandos df y du

En cualquier sistema de cómputo es crítico saber cuánto espacio de disco está ocupado y cuánto queda libre. Las utilidades \(df\) y \(du\) nos ayudan en estas tareas.

10.1 La utilidad \(df\) (disc free)

Si se invoca directamente sin argumentos, reporta el espacio total disponible en el sistema y en cada partición o dispositivo montados en el sistema de archivos.

df        
## Filesystem      1K-blocks      Used Available Use% Mounted on
## udev             16364740         0  16364740   0% /dev
## tmpfs             3280648      2196   3278452   1% /run
## /dev/nvme0n1p3  465816208  80226228 361854328  19% /
## tmpfs            16403228      5072  16398156   1% /dev/shm
## tmpfs                5120         4      5116   1% /run/lock
## tmpfs            16403228         0  16403228   0% /sys/fs/cgroup
## /dev/loop0            128       128         0 100% /snap/bare/5
## /dev/loop1         145792    145792         0 100% /snap/chromium/2105
## /dev/loop2          64768     64768         0 100% /snap/core20/1623
## /dev/loop4          56960     56960         0 100% /snap/core18/2566
## /dev/loop10        446848    446848         0 100% /snap/kde-frameworks-5-96-qt-5-15-5-core20/7
## /dev/loop7          83328     83328         0 100% /snap/gtk-common-themes/1534
## /dev/loop8         192256    192256         0 100% /snap/okular/115
## /dev/loop3         145664    145664         0 100% /snap/chromium/2082
## /dev/loop5         166784    166784         0 100% /snap/gnome-3-28-1804/145
## /dev/loop6          48128     48128         0 100% /snap/snapd/16292
## /dev/loop9          56960     56960         0 100% /snap/core18/2560
## /dev/loop17        331392    331392         0 100% /snap/kde-frameworks-5-qt-5-15-core20/14
## /dev/loop20         93952     93952         0 100% /snap/gtk-common-themes/1535
## /dev/loop11         47104     47104         0 100% /snap/snap-store/592
## /dev/loop13        224256    224256         0 100% /snap/gnome-3-34-1804/77
## /dev/loop18        354688    354688         0 100% /snap/gnome-3-38-2004/115
## /dev/loop19        224256    224256         0 100% /snap/gnome-3-34-1804/72
## /dev/loop14         47104     47104         0 100% /snap/snap-store/599
## /dev/loop16        168832    168832         0 100% /snap/gnome-3-28-1804/161
## /dev/loop12         63488     63488         0 100% /snap/core20/1611
## /dev/loop15        153856    153856         0 100% /snap/okular/109
## /dev/loop23         49152     49152         0 100% /snap/snapd/16778
## /dev/loop22        106880    106880         0 100% /snap/standard-notes/278
## /dev/loop21        410496    410496         0 100% /snap/gnome-3-38-2004/112
## /dev/nvme0n1p1    1994928      5356   1989572   1% /boot/efi
## /dev/sda1      1921724608 891404800 932627752  49% /home
## tmpfs             3280644        20   3280624   1% /run/user/125
## tmpfs             3280644        88   3280556   1% /run/user/1000
## /dev/loop25        106880    106880         0 100% /snap/standard-notes/279

Si le pasamos como argumento un directorio o partición, nos da la información correspondiente al espacio disponible.

df /
## Filesystem     1K-blocks     Used Available Use% Mounted on
## /dev/nvme0n1p3 465816208 80226228 361854328  19% /

10.1.1 Opciones más comunes de df

Como cualquier comando, le podemos pasar opciones para adecuar mejor la salida a nuestra necesidad.

Va una selección de entradas de la página \(man\) para \(df\) que uso con más frecuencia

-a, --all
              include pseudo, duplicate, inaccessible file systems

-B, --block-size=SIZE
              scale  sizes by SIZE before printing them; e.g., '-BM' prints sizes in units of 1,048,576 bytes; see SIZE format be‐
              low

-h, --human-readable
              print sizes in powers of 1024 (e.g., 1023M)

-i, --inodes
              list inode information instead of block usage

--total
              elide all entries insignificant to available space, and produce a grand total              

y algunos ejemplos de su efecto sobre la salida

df --total -BM
echo
echo
df -h /home
## Filesystem     1M-blocks    Used Available Use% Mounted on
## udev              15982M      0M    15982M   0% /dev
## tmpfs              3204M      3M     3202M   1% /run
## /dev/nvme0n1p3   454899M  78346M   353374M  19% /
## tmpfs             16019M      5M    16014M   1% /dev/shm
## tmpfs                 5M      1M        5M   1% /run/lock
## tmpfs             16019M      0M    16019M   0% /sys/fs/cgroup
## /dev/loop0            1M      1M        0M 100% /snap/bare/5
## /dev/loop1          143M    143M        0M 100% /snap/chromium/2105
## /dev/loop2           64M     64M        0M 100% /snap/core20/1623
## /dev/loop4           56M     56M        0M 100% /snap/core18/2566
## /dev/loop10         437M    437M        0M 100% /snap/kde-frameworks-5-96-qt-5-15-5-core20/7
## /dev/loop7           82M     82M        0M 100% /snap/gtk-common-themes/1534
## /dev/loop8          188M    188M        0M 100% /snap/okular/115
## /dev/loop3          143M    143M        0M 100% /snap/chromium/2082
## /dev/loop5          163M    163M        0M 100% /snap/gnome-3-28-1804/145
## /dev/loop6           47M     47M        0M 100% /snap/snapd/16292
## /dev/loop9           56M     56M        0M 100% /snap/core18/2560
## /dev/loop17         324M    324M        0M 100% /snap/kde-frameworks-5-qt-5-15-core20/14
## /dev/loop20          92M     92M        0M 100% /snap/gtk-common-themes/1535
## /dev/loop11          46M     46M        0M 100% /snap/snap-store/592
## /dev/loop13         219M    219M        0M 100% /snap/gnome-3-34-1804/77
## /dev/loop18         347M    347M        0M 100% /snap/gnome-3-38-2004/115
## /dev/loop19         219M    219M        0M 100% /snap/gnome-3-34-1804/72
## /dev/loop14          46M     46M        0M 100% /snap/snap-store/599
## /dev/loop16         165M    165M        0M 100% /snap/gnome-3-28-1804/161
## /dev/loop12          62M     62M        0M 100% /snap/core20/1611
## /dev/loop15         151M    151M        0M 100% /snap/okular/109
## /dev/loop23          48M     48M        0M 100% /snap/snapd/16778
## /dev/loop22         105M    105M        0M 100% /snap/standard-notes/278
## /dev/loop21         401M    401M        0M 100% /snap/gnome-3-38-2004/112
## /dev/nvme0n1p1     1949M      6M     1943M   1% /boot/efi
## /dev/sda1       1876685M 870513M   910770M  49% /home
## tmpfs              3204M      1M     3204M   1% /run/user/125
## tmpfs              3204M      1M     3204M   1% /run/user/1000
## /dev/loop25         105M    105M        0M 100% /snap/standard-notes/279
## total           2394868M 952572M  1323714M  42% -
## 
## 
## Filesystem      Size  Used Avail Use% Mounted on
## /dev/sda1       1.8T  851G  890G  49% /home

10.2 La utilidad \(du\) (disc usage)

\(du\) despliega información de uso de disco por archivo, incluyendo directorios y subdirectorios debajo del actual, por lo que si se corre sin argumentos desde un directorio con muchos sudirectorios y archivos, imprime una lista generalmente demasiado larga como para ser de utilidad.

10.2.1 Opciones clave de \(du\) - du -shc

  • La opción clave es -s o –summarize, que nos da resúmenes de un directorio particular.

  • La opción \(-c\) nos da un conteo total

  • \(-h\) lo hace en el amigable formato human readable

  • imprime resumen de uso de disco del directorio actual

du -s
du -sh
## 273124   .
## 267M .
  • imprime resumen de uso de disco por archivos en /usr/local/ y /usr/local/*
du -sh /usr/local
du -shc /usr/local/*
## 5.4G /usr/local
## 169M /usr/local/bin
## 4.0K /usr/local/etc
## 32K  /usr/local/ete3
## 4.0K /usr/local/games
## 41M  /usr/local/genome
## 472K /usr/local/include
## 5.0G /usr/local/lib
## 0    /usr/local/man
## 4.0K /usr/local/sbin
## 187M /usr/local/share
## 4.0K /usr/local/src
## 5.4G total

Noten que el uso de un asterisco en \(/usr/local/*\) hace que se listen los archivos y/o directorios que hay debajo de de \(/usr/local\), imprimiendo los detalles de uso de disco de los mismos.

Cuando corremos un \(du\) sobre un directorio con muchas entradas, el comando puede tardar varios segundos o minutos en terminar. Además, si encuentra archivos o directorios que el usuario no puede leer por no tener los permisos adecuados, imprimirá mensajes de error. ¿Recuerdas cómo evitar que éstos llenen la pantalla?

  • Prueba a correr este comando
du -sh $HOME/
du: cannot read directory '/home/vinuesa/.cache/dconf': Permission denied
du: cannot read directory '/home/vinuesa/.cpan/build/Alien-Libxml2-0.09-0/_alien/build_n0Y4': Permission denied
du: cannot read directory '/home/vinuesa/.cpan/build/Alien-Libxml2-0.09-0/_alien/extract_njRo': Permission denied
...
  • Y ahora éste, que envía los mensajes de error a \(/dev/null\). Noten el uso del comando \(time\) para que contabilice el tiempo invertido. En mi máquina son 32 segundos!!!
time du -shc $HOME 2> /dev/null
408G    /home/vinuesa

real    0m32.409s
user    0m1.840s
sys 0m8.524s

11 Tuberias (pipelines) de comandos para filtrado de texto conectados mediante “pipes” |

UNIX y GNU/Linux ofrecen una gran cantidad de herramientas para todo tipo de funciones o labores, cada una generalmente con muchas opciones. En bioinformática y genómica, los archivos de texto plano (ASCII) son los más comunes. Por ello es muy útil dominar al menos algunas de las herramientas de filtrado de texto que ofrece el entorno GNU/Linux. Como ejemplo, trabajaremos con el archivo assembly_summary.txt.gz, que contiene los datos de ensambles genómicos de la división RefSeq de GenBank. Lo descargué y comprimí en Julio de 2019 con los siguientes comandos:

# descargamos con el comando wget
wget -c ftp://ftp.ncbi.nlm.nih.gov/genomes/refseq/bacteria/assembly_summary.txt

# compresión gnu-zip del archivo
gzip assembly_summary.txt
# con zcat mandamos el flujo de datos descomprimidos a less -L, 
#  que nos permite navegar esta tabla de muchas columnas horizontal y verticalmente
zcat assembly_summary.txt.gz | less -L

Veamos en detalle el uso del comando | y de algunas herramientas de filtrado trabajando sobre el archivo assembly_summary.txt

11.1 El comando | (pipe:)

Como explicamos en el tutoral de introducción y detallamos en la sección de STDOUT, el resultado de un comando ejecutado en la consola se despliega en la pantalla. Es decir, la salida estándar o STDOUT de un comando está asociada por defecto a la terminal o pantalla. Hemos podido comprobar también que cada comando hace por lo general una sola función, pero con muchas opciones. Las herramientas de GNU/Linux siguen fielmente este principio de modularidad, fundamental en la ingeniería de código.

Lo genial del diseño de los sistemas operativos UNIX y GNU/Linux, es que las diferentes utilerías pueden combinarse según se requiera, lo cual confiere una gran versatilidad al usuario para diseñar soluciones específicas para esencialmente cualquier tarea o problema. Para ello se conecta la salida estándar de un comando (STDOUT) con la entrada estándar (STDIN) del siguiente mediante el comando | o pipe, siguiendo esta sintaxis:

comando1 [-opcion1 …] | comando2 [-opcion1 …] | comando3 [-opcion1 …] …

como se muestra en los siguientes ejemplos básicos, que ilustran el principio:

  • cuenta el número de archivos en el directorio \(/bin\)
ls /bin | wc -l
## 3424
  • filtra los subdirectorios en tu \(\$HOME\) que inician con ‘D’
ls -d $HOME/*  | grep '/D'
## /home/vinuesa/DBs
## /home/vinuesa/Dell_XPS_13_9330_Nov30.pdf
## /home/vinuesa/Descargas
## /home/vinuesa/Desktop
## /home/vinuesa/Docker_dev
## /home/vinuesa/Documentos
## /home/vinuesa/Documents
## /home/vinuesa/Downloads
## /home/vinuesa/Dropbox

La selección juiciosa de secuencias de comandos y sus opciones permite hacer operaciones muy específicas y de complejidad arbitraria.

Una secuencia de comandos típica tendría una estructura como la siguiente:

grep ‘patrón’ ARCHIVO.tsv | cut -f 1,3-5,9-11 | sort -dk1

Veamos ahora las opciones más comunes de los comandos grep, cut, sort, wc y uniq, en ese orden.

11.2 \(grep\) Filtra las lineas de un archivo que contienen (o no) caracteres o expresiones regulares

Como muestra el ejemplo genérico anterior, el comando \(grep\) frecuentemente es el primero en un \(pipeline\), ya que permite seleccionar las entradas de interés al filtrar las líneas del archivo o STDIN que contienen un patrón o término particular que las identifica.

El ‘patrón’ se pasa a \(grep\) como argumento (o en un archivo, si son muchos) y puede ser una cadena literal de caracteres (entre comillas sencillas si contiene espacios o caracteres que pueden reservados del \(Shell\) como por ejemplo el ‘>’) o una expresión regular, según la sintaxis:

SYNOPSIS
       grep [OPTION...] PATTERNS [FILE...]
       grep [OPTION...] -e PATTERNS ... [FILE...]
       grep [OPTION...] -f PATTERN_FILE ... [FILE...]

La invocación más sencilla de \(grep\) sería la siguiente:

grep ‘palabra’ texto.txt imprime sólo las líneas del archivo texto que contienen la cadena literal de caracteres ‘palabra’

Veremos el uso de expresiones regulares posteriormente, cuando les presente \(awk\).

\(grep\) es una herramienta sumamente poderosa, con una gran cantidad de opciones. Listo abajo algunas de las más usadas, agrupadas por el tipo de control que ejercen.

Recomiendo revisar el manual con \(man\ grep\) para que vayas ampliando tu conocimiento de las mismas.

11.2.1 \(grep\) - control de sintaxis del patrón

  • -E hace uso de expresiones regulares extendidas grep -E ‘^XXX|YYY|zzz$’ FILE
  • -P hace uso de expresiones regulares compatiblesc on Perl (PCREs).

Veamos la tabla que vamos a filtrar con \(grep\)

cat mini_tabla.tsv
## #assembly_accession  organism_name   seq_rel_date    asm_name    submitter
## GCF_004343645.1  Klebsiella grimontii    2019/03/11  ASM434364v1 Aarhus University
## GCF_901563825.1  Klebsiella grimontii    2019/05/29  SB3355_SG266_Ko4    Institut Pasteur
## GCF_003086675.1  Stenotrophomonas sp. ZAC14D2_NAIMI4_7   2018/05/03  ASM308667v1 CCG-UNAM
## GCF_003086855.1  Stenotrophomonas sp. YAU14A_MKIMI4_1    2018/05/03  ASM308685v1 CCG-UNAM
## GCF_000534095.1  Klebsiella aerogenes UCI 47 2014/02/03  Ente_aero_UCI_47_V1 Broad Institute
## GCF_000006765.1  Pseudomonas aeruginosa PAO1 2006/07/07  ASM676v1    PathoGenesis Corporation
## GCF_000017205.1  Pseudomonas aeruginosa PA7  2007/07/05  ASM1720v1   J. Craig Venter Institute
## GCF_000072485.1  Stenotrophomonas maltophilia K279a  2008/06/10  ASM7248v1   Wellcome Trust Sanger Institute
## GCF_000284595.1  Stenotrophomonas maltophilia D457   2012/04/11  ASM28459v1  University of Valencia
grep -E 'UNAM$|Valencia$' mini_tabla.tsv
## GCF_003086675.1  Stenotrophomonas sp. ZAC14D2_NAIMI4_7   2018/05/03  ASM308667v1 CCG-UNAM
## GCF_003086855.1  Stenotrophomonas sp. YAU14A_MKIMI4_1    2018/05/03  ASM308685v1 CCG-UNAM
## GCF_000284595.1  Stenotrophomonas maltophilia D457   2012/04/11  ASM28459v1  University of Valencia

11.2.2 \(grep\) - control del modo de los apareamientos (matching control)

  • -f FILE obtiene los patrones (uno por línea) del archivo FILE
  • -i ignora mayúsculas y minúsculas
  • -v invierte el match o coincidencia, es decir, imprime las líneas que NO contienen al ‘patrón’
  • -w usa el ‘patrón’ como palabra completa
grep 'Institute$' mini_tabla.tsv | grep -iv 'pseudo'
## GCF_000534095.1  Klebsiella aerogenes UCI 47 2014/02/03  Ente_aero_UCI_47_V1 Broad Institute
## GCF_000072485.1  Stenotrophomonas maltophilia K279a  2008/06/10  ASM7248v1   Wellcome Trust Sanger Institute

11.2.3 \(grep\) - control de la salida

  • -c cuenta el número de líneas que contienen ‘patrón’ en vez de imprimir las líneas
  • -l imprime los nombres de archivo que contienen ‘patrón’, no sus líneas
grep 'Institute$' mini_tabla.tsv | grep -civ 'pseudo'
## 2
grep -li pseudo *.tsv *.txt
## mini_tabla.tsv
## Salmonella_enterica_33676_pIncAC.tsv
## prokaryotes.txt
## TODO.txt

11.2.4 \(grep\) - control del contexto de la línea que aparea con el patrón (Context Line Control)

  • -A NUM imprime también las NUM líneas que siguen (after) al match
  • -B NUM imprime también las NUM líneas que anteceden (before) al match
grep 'Institute$' mini_tabla.tsv | grep -A 1 'Pseudo'
## GCF_000017205.1  Pseudomonas aeruginosa PA7  2007/07/05  ASM1720v1   J. Craig Venter Institute
## GCF_000072485.1  Stenotrophomonas maltophilia K279a  2008/06/10  ASM7248v1   Wellcome Trust Sanger Institute
grep 'Institute$' mini_tabla.tsv | grep -B 1 'Pseudo'
## GCF_000534095.1  Klebsiella aerogenes UCI 47 2014/02/03  Ente_aero_UCI_47_V1 Broad Institute
## GCF_000017205.1  Pseudomonas aeruginosa PA7  2007/07/05  ASM1720v1   J. Craig Venter Institute

11.3 \(cut\) corta las líneas por delimitadores de campo (-d) imprimiendo sólo los campos (-f) deseados

Si trabajamos con tablas, después de extraer las líneas que no interesan usando \(grep\), frecuentemente querremos concentrarnos en algunos campos de la tabla. Para ello la herramienta ideal es \(cut\), que corta campos de líneas de texto/tablas por elimitadores de campo específicos (TAB por defecto), extrayendo los campos indicados (-f), como muestra el siguiente ejemplo genérico

grep ‘patrón’ ARCHIVO.tsv | cut -f 1,3-5

11.3.1 Opciones de \(cut\)

  • -d DELIMITADOR_DE_CAMPO, que por defeto es el tabulador (). Para cambiarlo a un espacio usamos cut -d’ ’
  • -f NUM,NUM-NUM indica los números de campo a extraer, separados por comas (1,3) o indicando rangos de campos (3-5)
grep 'Institute$' mini_tabla.tsv | cut -f1-2,5
## GCF_000534095.1  Klebsiella aerogenes UCI 47 Broad Institute
## GCF_000017205.1  Pseudomonas aeruginosa PA7  J. Craig Venter Institute
## GCF_000072485.1  Stenotrophomonas maltophilia K279a  Wellcome Trust Sanger Institute

11.4 \(sort\) ordena las salidas atendiendo a diversos criterios

Una vez seleccionadas las filas del archivo con \(grep\) y corados los campos de interés con \(cut\), frecuentemente querremos usar \(sort\) para ordenar la salida atendiendo a algún criterio.

\(sort\) también tiene muchas opciones. Listo abajo sólo algunas de las más usadas. Debes revisar el manual con \(man\ sort\) para que vayas ampliando tu conocimiento de las mismas.

11.4.1 Opciones frecuentemente usadas de \(sort\)

  • -u da una salida no redundante, es decir, una sola instancia de elementos repetidos
  • -n hace ordenamiento numérico
  • -d hace ordenamiento tipo diccionario
  • -r da la salida en orden inverso
  • -k<#> ordena la salida por la columna número #
  • idiomas frecuentes
    • sort -u
    • sort -dk2
    • sort -nrk2
grep Steno mini_tabla.tsv 
## GCF_003086675.1  Stenotrophomonas sp. ZAC14D2_NAIMI4_7   2018/05/03  ASM308667v1 CCG-UNAM
## GCF_003086855.1  Stenotrophomonas sp. YAU14A_MKIMI4_1    2018/05/03  ASM308685v1 CCG-UNAM
## GCF_000072485.1  Stenotrophomonas maltophilia K279a  2008/06/10  ASM7248v1   Wellcome Trust Sanger Institute
## GCF_000284595.1  Stenotrophomonas maltophilia D457   2012/04/11  ASM28459v1  University of Valencia
grep Steno mini_tabla.tsv | sort
## GCF_000072485.1  Stenotrophomonas maltophilia K279a  2008/06/10  ASM7248v1   Wellcome Trust Sanger Institute
## GCF_000284595.1  Stenotrophomonas maltophilia D457   2012/04/11  ASM28459v1  University of Valencia
## GCF_003086675.1  Stenotrophomonas sp. ZAC14D2_NAIMI4_7   2018/05/03  ASM308667v1 CCG-UNAM
## GCF_003086855.1  Stenotrophomonas sp. YAU14A_MKIMI4_1    2018/05/03  ASM308685v1 CCG-UNAM
grep Steno mini_tabla.tsv | sort -rk4
## GCF_003086675.1  Stenotrophomonas sp. ZAC14D2_NAIMI4_7   2018/05/03  ASM308667v1 CCG-UNAM
## GCF_003086855.1  Stenotrophomonas sp. YAU14A_MKIMI4_1    2018/05/03  ASM308685v1 CCG-UNAM
## GCF_000072485.1  Stenotrophomonas maltophilia K279a  2008/06/10  ASM7248v1   Wellcome Trust Sanger Institute
## GCF_000284595.1  Stenotrophomonas maltophilia D457   2012/04/11  ASM28459v1  University of Valencia

11.5 \(wc\) cuenta líneas, palabras y caracteres

\(wc\) es un comando muy sencillo que cuenta líneas, palabras y caracteres de un archivo o del que recibió. Tiene sólo tres opciones, para imprimir sólo alguna de esta información.

El comando \(wcq\) suele ir al final de un pipeline para generar un resumen numérico.

11.5.1 Opciones de \(wc\)

  • -l cuenta líneas
  • -w cuena palabras
  • -c cuenta caracteres
echo -e "Pseudo\nSteno\nEsch\nSteno\nKleb\nSalmo\nKleb"  | wc 
echo -e "Pseudo\nSteno\nEsch\nSteno\nKleb\nSalmo\nKleb" | wc -l
##       7       7      40
## 7

11.6 \(uniq\) - reporta u omite líneas repetidas

SYNOPSIS
       uniq [OPTION]... [INPUT [OUTPUT]]

\(uniq\) filtra líneas adyacentes iguales en INPUT (o ), escribiendo a OUTPUT (o ).

Por ello, para usar \(uniq\) efectivamente, necesitamos que \(sort\) ordene previamente las entradas.

11.6.1 Opciones de \(uniq\)

\(uniq\) tiene también una gama de opciones que controlan cómo contar las instancias, pero la opción \(-c\) es posiblemente la más usada en pipelines, donde se usa al final para generar estadísticas de resumen.

  • -c cuenta las instancias únicas, añadiendo la cuenta al inicio de las líneas repetidas
  • -d imprime sólo líneas duplicadas
  • -D imprime todas la líneas duplicadas
  • -f N ignora duplicados en los primeros N campos del archivo
  • -i ignora mayúsculas y minúsculas en el cómputo de líneas iguales
  • -u imprime sólo líneas únicas
echo -e "Pseudo\nSteno\nEsch\nSteno\nKleb\nSalmo\nKleb\nKleb" | nl
##      1   Pseudo
##      2   Steno
##      3   Esch
##      4   Steno
##      5   Kleb
##      6   Salmo
##      7   Kleb
##      8   Kleb
  • aquí nos falta ordenar
echo -e "Pseudo\nSteno\nEsch\nSteno\nKleb\nSalmo\nKleb\nKleb" | uniq | nl
##      1   Pseudo
##      2   Steno
##      3   Esch
##      4   Steno
##      5   Kleb
##      6   Salmo
##      7   Kleb
  • una vez ordenada la salida, \(uniq\) puede trabajar correctamente
echo -e "Pseudo\nSteno\nEsch\nSteno\nKleb\nSalmo\nKleb\nKleb" | sort | uniq | nl
##      1   Esch
##      2   Kleb
##      3   Pseudo
##      4   Salmo
##      5   Steno
  • \(uniq\) sabe contar
echo -e "Pseudo\nSteno\nEsch\nSteno\nKleb\nSalmo\nKleb\nKleb" | sort | uniq -c | nl
##      1         1 Esch
##      2         3 Kleb
##      3         1 Pseudo
##      4         1 Salmo
##      5         2 Steno
  • Aquí sacamos sólo las líneas repetidas
echo -e "Pseudo\nSteno\nEsch\nSteno\nKleb\nSalmo\nKleb\nKleb" | sort | uniq -D
## Kleb
## Kleb
## Kleb
## Steno
## Steno

11.7 Ejemplos de herramientas de filtrado de texto: pipelines con grep, cut, sort, uniq, wc en acción

Veamos ahora los comandos más usados en tuberías de filtrado de texto en acción, haciendo uso de sólo algunas de las opciones más frecuentes listados en la sección anterior.

  • ¿cuántas líneas tiene el archivo assembly_summary.txt.gz?
# ¿cuántas líneas tiene el archivo assembly_summary.txt.gz?
zcat assembly_summary.txt.gz | wc
zcat assembly_summary.txt.gz | wc -l
##  161297 3788695 48497020
## 161297
  • la columna assembly_level (#12) indica el estado del ensamble. ¿Cuáles son los niveles de la variable categórica assembly_level (valores únicos de la misma?
# la columna assembly_level (#12) indica el estado del ensamble. ¿Cuáles son los niveles de la variable categórica assembly_level (valores únicos de la misma?
zcat assembly_summary.txt.gz | grep -v "^#" | cut -f 12 | sort -u
## Chromosome
## Complete Genome
## Contig
## Scaffold
  • ¿cuántos genomas hay por nivel de la variable categórica assembly_level?
# ¿cuántos genomas hay por nivel de la variable categórica assembly_level?
#   noten que en este ejemplo usamos tail -n +2 para evitar la primera línea
#   sed '2q' hubiese funcionado igualmente y es el comando más corto que conozco para ello
zcat assembly_summary.txt.gz | tail -n +2 | cut -f 12 | sort | uniq -c
##       1 assembly_level
##    2018 Chromosome
##   13983 Complete Genome
##   82755 Contig
##   62539 Scaffold
  • asocia cada nombre de columna de la cabecera con el número de la columna correspondiente
# asocia cada nombre de columna de la cabecera con el número de la columna correspondiente
zcat assembly_summary.txt.gz | sed 2q | tr '\t' '\n' | nl
##      1   #   See ftp://ftp.ncbi.nlm.nih.gov/genomes/README_assembly_summary.txt for a description of the columns in this file.
##      2   # assembly_accession
##      3   bioproject
##      4   biosample
##      5   wgs_master
##      6   refseq_category
##      7   taxid
##      8   species_taxid
##      9   organism_name
##     10   infraspecific_name
##     11   isolate
##     12   version_status
##     13   assembly_level
##     14   release_type
##     15   genome_rep
##     16   seq_rel_date
##     17   asm_name
##     18   submitter
##     19   gbrs_paired_asm
##     20   paired_asm_comp
##     21   ftp_path
##     22   excluded_from_refseq
##     23   relation_to_type_material
  • genera una estadística del número de genomas por especie (columna # 8), y muestra sólo las 10 especies con más genomas secuenciados!
# genera una estadística del número de genomas por especie (columna # 8), y muestra sólo las 10 especies con más genomas secuenciados!
zcat assembly_summary.txt.gz | grep -v "^#" | cut -f8 | sort | uniq -c | sort -nrk1 | head -10
##   14089 Escherichia coli
##    8039 Streptococcus pneumoniae
##    6398 Klebsiella pneumoniae
##    5924 Staphylococcus aureus
##    4556 Mycobacterium tuberculosis
##    4358 Pseudomonas aeruginosa
##    3164 Acinetobacter baumannii
##    2789 Listeria monocytogenes
##    2173 Salmonella enterica subsp. enterica serovar Typhi
##    1792 Clostridioides difficile
  • ¿Cuántos genomas completos hay del género Acinetobacter?
# ¿Cuántos genomas completos hay del género Acinetobacter?
zcat assembly_summary.txt.gz | grep Acinetobacter | grep Complete | wc -l

# también puedes usar zgrep para evitar la llamada primero a zcat
zgrep Acinetobacter assembly_summary.txt.gz | grep Complete | wc -l
## 220
## 220
# Ojo: recuerda que GNU/Linux es sensible a mayúsculas y minúsculas: prueba este comando para comprobarlo
zgrep acinetobacter assembly_summary.txt.gz | grep Complete | wc -l # no encuentra nada
# grep -i lo hace insensible a la fuente
zgrep -i acinetobacter assembly_summary.txt.gz | grep Complete | wc -l
## 220
  • filtra y cuenta las lineas que contienen Acinetobacter o Stenotrophomonas
# filtra y cuenta las lineas que contienen Acinetobacter o Stenotrophomonas
zgrep -E 'Acinetobacter|Stenotrophomonas' assembly_summary.txt.gz | wc -l
## 5170
  • Cuenta los genomas de Acinetobacter, Pseudomonas y Klebsiella (por género) y presenta una lista ordenada por número decreciente de genomas
# Cuenta los genomas de Acinetobacter, Pseudomonas y Klebsiella (por género) y presenta una lista ordenada por número decreciente de genomas
zgrep -E 'Acinetobacter|Pseudomonas|Klebsiella' assembly_summary.txt.gz | cut -f 8 | cut -d' ' -f1 |sort -d | uniq -c |sort -nrk1
##    8951 Pseudomonas
##    8515 Klebsiella
##    4747 Acinetobacter
##       7 [Pseudomonas]
##       1 Candidatus
  • NOTA: vean quen tenemos entradas en la tabla de ‘[Pseudomonas]’. Deberíamos de estudiar esos genomas con más detalle para tomar una decisión informada sobre qué hacer con ellos: eliminarlos o incluirlos en la tabla de resultados con el resto de las Pseudomonas. Hay un genoma que contiene el término Candidatus, que se usa para genomas reconstituidos de estudios metagenómicos, es decir, para los cuales no existe un aislamiento en cultivo. Pueden filtrar y revisar esas entradas de la tabla con zgrep ‘\[Pseudomonas\]’ assembly_summary.txt.gz | less -L y tomar la decisión. Van dos soluciones de código para cada caso:
    • incluirlas con las otras Pseudomonas y eliminar la entrada de Candidatus
zgrep -E 'Acinetobacter|Pseudomonas|Klebsiella' assembly_summary.txt.gz | cut -f8 | cut -d' ' -f1 | sed 's/\[//; s/\]//' | grep -v 'Candidatus' | sort -d | uniq -c | sort -nrk1
##    8958 Pseudomonas
##    8515 Klebsiella
##    4747 Acinetobacter
  • descartar las entradas de [Pseudomonas] y Candidatus
zgrep -E 'Acinetobacter|Pseudomonas|Klebsiella' assembly_summary.txt.gz | cut -f8 | cut -d' ' -f1 | grep -Ev '\[|Candidat' | sort -d | uniq -c | sort -nrk1
##    8951 Pseudomonas
##    8515 Klebsiella
##    4747 Acinetobacter
  • Cuenta los genomas de Acinetobacter, Pseudomonas y Klebsiella (por género), con salida ordenada alfabéticamente por género
# filtra las lineas que contienen Filesystem o Text processing y ordénalas alfabéticamente según las entradas de la segunda columna
# eliminando las entradas de Candidatus y [Pseudomonas]
zgrep -E 'Acinetobacter|Pseudomonas|Klebsiella' assembly_summary.txt.gz | cut -f 8 | cut -d' ' -f1 | grep -Ev '^\[|^Cand' | sort | uniq -c | sort -dk2
##    4747 Acinetobacter
##    8515 Klebsiella
##    8951 Pseudomonas

Veremos la gran utilidad y versatilidad de combinaciones de estos comandos para el procesamiento de archivos de secuencias en un ejercicio integrativo posterior.


12 El lenguaje de procesamiento de patrones AWK y su sucesor gawk

AWK es un lenguaje de programación diseñado para procesar datos de texto, ya sean ficheros o flujos de datos. El nombre AWK deriva de las iniciales de los apellidos de sus autores: Alfred Aho, Peter Weinberger, y Brian Kernighan. \(awk\), cuando está escrito todo en minúsculas, hace referencia al programa de UNIX que interpreta programas escritos en el lenguaje de programación AWK. Es decir, AWK es un lenguaje interpretado por el intérprete de comandos \(awk\).

AWK fue creado como un reemplazo a los algoritmos escritos en C para análisis de texto. Fue una de las primeras herramientas en aparecer en UNIX (en la versión 3). Ganó popularidad rápidamente por la gran funcionalidad permitía añadir a las tuberías de comandos de UNIX. Por ello se considera como una de las utilidades necesarias (core) de este sistema operativo.

\(awk\) fue portado originalmente al proyecto GNU por Paul Rubin en 1986, llamándolo \(gawk\). Desde entonces \(gawk\) ha ganado mucha funcionalidad adicional, siendo actualmente Arnold Robins el principal mantenedor del código y de su extraordinaria documentación, la cual te recomiendo consultes asiduamente cuando estés aprendiendo AWK

En sistemas modernos de GNU/Linux la llamada al intérprete de comandos \(awk\) está ligada a \(gawk\), como podemos ver en las siguientes salidas de una máquina que corre ubuntu 20.04.1.

awk --version | head -2
## GNU Awk 5.0.1, API: 2.0 (GNU MPFR 4.0.2, GNU MP 6.2.0)
## Copyright (C) 1989, 1991-2019 Free Software Foundation.
gawk --version | head -2
## GNU Awk 5.0.1, API: 2.0 (GNU MPFR 4.0.2, GNU MP 6.2.0)
## Copyright (C) 1989, 1991-2019 Free Software Foundation.

Por tanto, las llamadas que veremos a \(awk\) de los ejemplos que siguen en realidad ejecutan \(gawk\).

\(gawk\), cuya versión más reciente es la 5.1, es la variante de los “nuevos awks o \(nawk\)” derivados del viejo \(AWK\) con más funcionalidad integrada, como pueden consultar en la excelente guía del usuario de GAWK.

12.1 Conceptos fundamentales sobre la estructura, variables internas y funcionamiento de los programas AWK

Debido a su densa notación, lenguajes como \(awk\) son frecuentemente usados para escribir programas de una línea o (“one-liners”), como veremos seguidamente.

En general, al intérprete de comandos \(awk\) se le pasan dos piezas de datos:

  • un fichero de órdenes o programa
  • uno o más archivos de entrada.

Un fichero de órdenes (que puede ser un fichero real, o puede ser incluido en la invocación de \(awk\) desde la línea de comandos) contiene una serie de sentencias que le indican a \(awk\) cómo procesar el fichero de entrada. Es decir contiene el programa escrito en sintaxis AWK.

  • $ awk ‘programa_AWK’ archivo1 archivo2 …
  • $ awk -f ‘archivo_con_código_AWK’ archivo1 archivo2 …

El fichero primario de entrada es normalmente texto estructurado con un formato particular, por defecto archivos con campos separados por espacios o tabuladores (tablas).

12.1.1 \(FILENAME\)

Si se especifican varios archivos, éstos se procesan en el orden en el que se le pasan a \(awk\). El nombre del archivo que está siendo procesado por \(awk\) queda guardado en la variable interna FILENAME.

12.1.2 Registros y las variables asociadas \(RS\), \(FNR\) y \(NR\)

\(awk\) procesa los archivos en unidades conocidas como registros (records), que se procesan acorde a las órdenes del programa, registro por registro.

Por defecto, los registros se separan con saltos de línea (RS=“\n”). Es decir, por defecto \(awk\) modela tablas, en las que cada fila corresponde a un registro.

  • estructura de un archivo con estructura tabular, donde cada fila corresponde a un registro
cat mini_tabla.tsv
## #assembly_accession  organism_name   seq_rel_date    asm_name    submitter
## GCF_004343645.1  Klebsiella grimontii    2019/03/11  ASM434364v1 Aarhus University
## GCF_901563825.1  Klebsiella grimontii    2019/05/29  SB3355_SG266_Ko4    Institut Pasteur
## GCF_003086675.1  Stenotrophomonas sp. ZAC14D2_NAIMI4_7   2018/05/03  ASM308667v1 CCG-UNAM
## GCF_003086855.1  Stenotrophomonas sp. YAU14A_MKIMI4_1    2018/05/03  ASM308685v1 CCG-UNAM
## GCF_000534095.1  Klebsiella aerogenes UCI 47 2014/02/03  Ente_aero_UCI_47_V1 Broad Institute
## GCF_000006765.1  Pseudomonas aeruginosa PAO1 2006/07/07  ASM676v1    PathoGenesis Corporation
## GCF_000017205.1  Pseudomonas aeruginosa PA7  2007/07/05  ASM1720v1   J. Craig Venter Institute
## GCF_000072485.1  Stenotrophomonas maltophilia K279a  2008/06/10  ASM7248v1   Wellcome Trust Sanger Institute
## GCF_000284595.1  Stenotrophomonas maltophilia D457   2012/04/11  ASM28459v1  University of Valencia
  • El separador de registros, guardado en la variable interna \(RS\) (Record Separator) se puede modificar asignándole un valor que puede ser una expresión regular. Ello le permite al programador modelar con gran precisión y versatilidad la estructura de los registros.

Así por ejemplo, si nuestros datos están contenidos en un archivo de secuencias multifasta, puede ser muy conveniente definir el separador de registros de la siguiente manera: RS=“>”, ya que cada nuevo registro (secuencia en este caso) inicia con el símbolo ‘>’

  • La variable interna FNR cuenta el número de registros del archivo en procesamiento. Esta variable se reinicia a 0 cuando \(awk\) va a procesar un nuevo archivo.
  • La variable interna NR cuenta el número de registros que ha procesado \(awk\) de manera acumulativa (nunca se reinicia si lee un nuevo archivo)

Veamos un primer ejemplo de código AWK que ilustra lo arriba descrito en relación a las variables \(FILENAME\), \(RS\), \(FNR\) y \(NR\).

  • Visualicemos primero la estructura y contenido de los dos archivos que le vamos a pasar a \(awk\)
echo " >>> contenido del archivo seq.list <<<"
cat seq.list 
echo
echo ">>> contenido del archivo mini_fasta.fst <<<"
cat mini_fasta.fst
##  >>> contenido del archivo seq.list <<<
## >especie5
## >especie3
## >especie4
## 
## >>> contenido del archivo mini_fasta.fst <<<
## >especie12
## ATACCGACCATTAC
## TTAGGAACCCAGGC
## >especie2
## CCAGTAGTCGAGGC
## AGCAGCTTCCATAT
## >especie3
## CCAGGGCCCATATT
## ATACCGACCATTAC
## >especie4
## GCATAATCCACCAT
## GCACGAATGCAGAC
## >especie5
## GGGGTACCCATTTA
## CCCCCCCTTTTTAT
  • Sigue el mini programa de AWK (‘encerrado en comillas sencillas’) que simplemente procesa los dos archivos que le pasamos como argumentos al final de programa, imprimiendo el contenido de las variables FILENAME, FNR, NR que \(awk\) automáticamente pone a disposición del programador
awk  '{print FILENAME, FNR, NR}' seq.list mini_fasta.fst
## seq.list 1 1
## seq.list 2 2
## seq.list 3 3
## mini_fasta.fst 1 4
## mini_fasta.fst 2 5
## mini_fasta.fst 3 6
## mini_fasta.fst 4 7
## mini_fasta.fst 5 8
## mini_fasta.fst 6 9
## mini_fasta.fst 7 10
## mini_fasta.fst 8 11
## mini_fasta.fst 9 12
## mini_fasta.fst 10 13
## mini_fasta.fst 11 14
## mini_fasta.fst 12 15
## mini_fasta.fst 13 16
## mini_fasta.fst 14 17
## mini_fasta.fst 15 18

¿Qué está haciendo el programa? Dado que por defecto RS=“\n”, \(awk\) está leyendo cada uno de los dos archivos secuencialmente, línea por línea, imprimiendo los valores actualizados de las variables FILENAME, FNR, NR.

12.1.3 Los campos de un registro y las variables \(FS\), \(NF\), $0 y $1 … $n

Por defecto \(awk\) procesa automáticamente cada registro (acorde al valor asignado a \(RS\)), separándolo en campos delimitados por espacios, tabuladores o saltos de línea, que \(gawk\) define internamente como \(FS="\ "\).

  • Cada campo del registro en procesamiento es guardado en las variables internas $1 … $n

  • El contenido del registro completo está guardado en $0 y el número total de sus campos en NF

Noten que las variables $0, S1 … $n son las únicas en AWK que siempre van precedidas del símbolo $.

  • Define exactamente qué está haciendo el programa, es decir, interpreta o explica con precisión la salida observada
    • parseo estándar de un registro y sus campos en \(gawk\)
echo -e  " campo1  campo2\tcampo3,-#%...  " | awk '{print NF, "["$1"]"}'
echo -e  " campo1  campo2\tcampo3,-#%...  " | awk '{print NF, "["$3"]", "["$1"]"}'
echo -e  " campo1  campo2\tcampo3,-#%...  " | awk '{print NF, $NF}'
echo -e  " campo1  campo2\tcampo3,-#%...  " | awk '{print NF, "["$0"]"}'
## 3 [campo1]
## 3 [campo3,-#%...] [campo1]
## 3 campo3,-#%...
## 3 [ campo1  campo2   campo3,-#%...  ]

Noten los siguientes puntos clave sobre el manejo que \(awk\) hace de los espacios en blanco:

  1. Múltiples espacios en blanco (incluyendo tabuladores) separando campos de texto se ignoran, colapsándose, por lo que \(NF\) vale 3, es decir, \(awk\) ve tres campos con la configuración por defecto de las variables \(RS\) y \(FS\).
  2. Por lo explicado en 1, las variables $1 … $n sólo llevan el valor del campo delimitado por los espacios, como muestran las salidas de las líneas 1 a 3.
  3. En cambio la variable $0, que lleva un registro completo, imprime los separadores de campo del mismo, como muestra la línea 4 de la salida mostrada arriba.
  4. Finalmente aclarar que \(print\) imprime los argumentos que se le pasan separados por comas, dejando por defecto un espacio sencillo entre ellos (OFS=” “). Las variables se escriben sin comillas, los caracteres literales entre comillas dobles. Si omitimos las comas entre argumentos, se concatenan, como muestra la salida de {print NF,”[“$1”]“}.

12.1.4 Ejemplos genéricos de estructura de programas AWK

  • Un programa AWK típico consiste en una serie de líneas, cada una de la forma:

/patrón/ { acción }, donde:

  • la acción por defecto es imprimir {print}. Como explicamos en el ejemplo anterior, \(print\) imprime los argumentos que se le pasan separados por comas, dejando por defecto un espacio sencillo entre ellos (OFS=” “). Las variables se escriben sin comillas, los caracteres literales entre comillas dobles. Si omitimos las comas entre argumentos, se concatenan, como muestra la salida de ‘{print NF, “[”$1”]”}’
  • patrón es una expresión regular, como explicaremos con mayor detalle en una sección posterior
  • acción es una orden o programa, que puede ser todo un script te AWK de cientos o miles de líneas

\(awk\) lee línea por línea el fichero de entrada. Cuando encuentra una línea que coincide con el patrón, ejecuta la(s) orden(es) o programa indicadas en acción.

  • Para llamar a \(awk\) desde la línea de comandos, usaríamos una sintaxis de este tipo:

awk ‘CODIGO AWK’ ARCHIVO_A_PROCESAR

  • para usarlo en una tubería de \(Shell\), conecta el \(STDOUT\) de un programa al \(STDIN\) de \(awk\) mediante |:

programaX | awk ‘CODIGO AWK’ > output_file.txt

Vuelve a examinar la estructura del archivo mini_tabla.tsv y define con precisión qué están haciendo los siguientes programas, acorde a la salida que imprimen 1.

awk  '/UNAM/' mini_tabla.tsv
## GCF_003086675.1  Stenotrophomonas sp. ZAC14D2_NAIMI4_7   2018/05/03  ASM308667v1 CCG-UNAM
## GCF_003086855.1  Stenotrophomonas sp. YAU14A_MKIMI4_1    2018/05/03  ASM308685v1 CCG-UNAM
awk  'NR == 2' mini_tabla.tsv
## GCF_004343645.1  Klebsiella grimontii    2019/03/11  ASM434364v1 Aarhus University
awk  'NF > 9' mini_tabla.tsv
## GCF_000017205.1  Pseudomonas aeruginosa PA7  2007/07/05  ASM1720v1   J. Craig Venter Institute
## GCF_000072485.1  Stenotrophomonas maltophilia K279a  2008/06/10  ASM7248v1   Wellcome Trust Sanger Institute

12.1.5 Formas alternativas del código AWK y uso de bloques BEGIN{}, END{}

Además del ejemplo genérico /patrón/ { acción } presentado en la sección anterior, a \(gawk\) se le pueden pasar las siguientes estructuras de código alternativo para controlar el comportamiento del intérprete de comandos:

  • BEGIN { acción } Ejecuta las órdenes de acción al comienzo de la ejecución, antes de que los datos comiencen a ser procesados. Aquí inicializamos variables globales como RS, FS, OFS …
  • END { acción } Similar a la forma previa, pero ejecuta las órdenes de acción después de que todos los datos sean procesados, por ejemplo para imprimir estadísticas de resumen después de haber analizado los campos de cada registro.
  • /patrón/ Imprime las líneas que contienen al patrón.
  • { acción } Ejecuta acción por cada línea en la entrada.

Cada una de estas formas pueden ser incluidas varias veces en un archivo o \(script\) de \(AWK\). El \(script\) es procesado de manera progresiva, línea por línea, de izquierda a derecha. Entonces, si hubiera dos declaraciones \(BEGIN\), sus contenidos serán ejecutados en orden de aparición. Las declaraciones \(BEGIN\) y \(END\) no necesitan estar en forma ordenada.

12.2 Inicialización de variables en bloque BEGIN{} para modelar adecuadamente la estructura de un registro

Como indicábamos anteriormente, \(AWK\) permite al programador modelar con gran precisión y versatilidad la estructura de los registros mediante la asignación de los valores más apropiados a las variables \(RS\), y \(FS\). Esto se hace generalmente dentro de un bloque de inicialización BEGIN{}, como se muestra en los ejemplos que siguen.

Revisemos nuevamente algunos de los ejemplos usados anteriormente con los valores por defecto de \(RS\), \(FS\) y \(OFS\).

12.2.1 Análisis de archivos con campos separados por tabuladores (tsv)

  1. Mejorando el modelado de la estructura de los registros del archivo mini_tabla.tsv
head -5 mini_tabla.tsv
## #assembly_accession  organism_name   seq_rel_date    asm_name    submitter
## GCF_004343645.1  Klebsiella grimontii    2019/03/11  ASM434364v1 Aarhus University
## GCF_901563825.1  Klebsiella grimontii    2019/05/29  SB3355_SG266_Ko4    Institut Pasteur
## GCF_003086675.1  Stenotrophomonas sp. ZAC14D2_NAIMI4_7   2018/05/03  ASM308667v1 CCG-UNAM
## GCF_003086855.1  Stenotrophomonas sp. YAU14A_MKIMI4_1    2018/05/03  ASM308685v1 CCG-UNAM
  • En base a la salida anterior, ¿cuál piensas que era la intención del programador y qué piensas está mal en el código que sigue?
awk '{print $1, $2, $4, $NF}' mini_tabla.tsv
## #assembly_accession organism_name asm_name submitter
## GCF_004343645.1 Klebsiella 2019/03/11 University
## GCF_901563825.1 Klebsiella 2019/05/29 Pasteur
## GCF_003086675.1 Stenotrophomonas ZAC14D2_NAIMI4_7 CCG-UNAM
## GCF_003086855.1 Stenotrophomonas YAU14A_MKIMI4_1 CCG-UNAM
## GCF_000534095.1 Klebsiella UCI Institute
## GCF_000006765.1 Pseudomonas PAO1 Corporation
## GCF_000017205.1 Pseudomonas PA7 Institute
## GCF_000072485.1 Stenotrophomonas K279a Institute
## GCF_000284595.1 Stenotrophomonas D457 Valencia

Obviamente debemos cambiar el valor por defecto de FS=[[:space:]]+, regexp que se ajusta a uno o más espacios, tabuladores y saltos de línea a esta otra FS=“ qué sólo empareja con tabuladores.

awk 'BEGIN{FS="\t"} {print $1, $2, $4, $NF}' mini_tabla.tsv
## #assembly_accession organism_name asm_name submitter
## GCF_004343645.1 Klebsiella grimontii ASM434364v1 Aarhus University
## GCF_901563825.1 Klebsiella grimontii SB3355_SG266_Ko4 Institut Pasteur
## GCF_003086675.1 Stenotrophomonas sp. ZAC14D2_NAIMI4_7 ASM308667v1 CCG-UNAM
## GCF_003086855.1 Stenotrophomonas sp. YAU14A_MKIMI4_1 ASM308685v1 CCG-UNAM
## GCF_000534095.1 Klebsiella aerogenes UCI 47 Ente_aero_UCI_47_V1 Broad Institute
## GCF_000006765.1 Pseudomonas aeruginosa PAO1 ASM676v1 PathoGenesis Corporation
## GCF_000017205.1 Pseudomonas aeruginosa PA7 ASM1720v1 J. Craig Venter Institute
## GCF_000072485.1 Stenotrophomonas maltophilia K279a ASM7248v1 Wellcome Trust Sanger Institute
## GCF_000284595.1 Stenotrophomonas maltophilia D457 ASM28459v1 University of Valencia

La salida del programa anterior la podemos mejorar indicando que el separador de campo de la salida \(OFS\) sea también un tabulador, para respetar así la estructura original del archivo de entrada.

awk 'BEGIN{FS="\t"; OFS=FS} {print $1, $2, $4, $NF}' mini_tabla.tsv
## #assembly_accession  organism_name   asm_name    submitter
## GCF_004343645.1  Klebsiella grimontii    ASM434364v1 Aarhus University
## GCF_901563825.1  Klebsiella grimontii    SB3355_SG266_Ko4    Institut Pasteur
## GCF_003086675.1  Stenotrophomonas sp. ZAC14D2_NAIMI4_7   ASM308667v1 CCG-UNAM
## GCF_003086855.1  Stenotrophomonas sp. YAU14A_MKIMI4_1    ASM308685v1 CCG-UNAM
## GCF_000534095.1  Klebsiella aerogenes UCI 47 Ente_aero_UCI_47_V1 Broad Institute
## GCF_000006765.1  Pseudomonas aeruginosa PAO1 ASM676v1    PathoGenesis Corporation
## GCF_000017205.1  Pseudomonas aeruginosa PA7  ASM1720v1   J. Craig Venter Institute
## GCF_000072485.1  Stenotrophomonas maltophilia K279a  ASM7248v1   Wellcome Trust Sanger Institute
## GCF_000284595.1  Stenotrophomonas maltophilia D457   ASM28459v1  University of Valencia

Excelente, ya hemos modelado adecuadamente un archivo tabular. Ahora nuestro código AWK responderá adecuadamente a nuestras intenciones, es decir, la sintaxis permite modelar la estructura de los datos para que se ajuste a la semántica del programa.

awk 'BEGIN{FS="\t"; OFS=FS} /Pseudomonas/ {print $1, $2, $4, $NF}' mini_tabla.tsv
## GCF_000006765.1  Pseudomonas aeruginosa PAO1 ASM676v1    PathoGenesis Corporation
## GCF_000017205.1  Pseudomonas aeruginosa PA7  ASM1720v1   J. Craig Venter Institute

12.2.2 Análisis de archivos FASTA - registros delimitados por ‘>’

Veamos ahora cómo modelar de manera más adecuada un archivo FASTA, para lo cual volveremos a trabajar con mini_fasta.fst. Recordemos su estructura:

cat  mini_fasta.fst
## >especie12
## ATACCGACCATTAC
## TTAGGAACCCAGGC
## >especie2
## CCAGTAGTCGAGGC
## AGCAGCTTCCATAT
## >especie3
## CCAGGGCCCATATT
## ATACCGACCATTAC
## >especie4
## GCATAATCCACCAT
## GCACGAATGCAGAC
## >especie5
## GGGGTACCCATTTA
## CCCCCCCTTTTTAT
  • ¿qué intención crees que tenía el programador al escribir la siguiente línea de código y qué problema tiene el programa que impide obtener el resultado esperado?
awk '/seq3/' mini_fasta.fst

En base a la estructura de un archivo FASTA, ¿cómo piensas que debería de modelarse en AWK?

El cambio más obvio e importante sería indicarle a la variable de separador de registro RS que éstos vienen delimitados por un ‘>’.

awk 'BEGIN{RS=">"} /seq3/' mini_fasta.fst
  • Compara e interpreta las diferencias de las salidas de los programas que siguen:
    • esta es la salida con los valores por defecto de \(RS\) y \(FS\)
awk ' {print NR, NF, $1, $0}' mini_fasta.fst
## 1 1 >especie12 >especie12
## 2 1 ATACCGACCATTAC ATACCGACCATTAC
## 3 1 TTAGGAACCCAGGC TTAGGAACCCAGGC
## 4 1 >especie2 >especie2
## 5 1 CCAGTAGTCGAGGC CCAGTAGTCGAGGC
## 6 1 AGCAGCTTCCATAT AGCAGCTTCCATAT
## 7 1 >especie3 >especie3
## 8 1 CCAGGGCCCATATT CCAGGGCCCATATT
## 9 1 ATACCGACCATTAC ATACCGACCATTAC
## 10 1 >especie4 >especie4
## 11 1 GCATAATCCACCAT GCATAATCCACCAT
## 12 1 GCACGAATGCAGAC GCACGAATGCAGAC
## 13 1 >especie5 >especie5
## 14 1 GGGGTACCCATTTA GGGGTACCCATTTA
## 15 1 CCCCCCCTTTTTAT CCCCCCCTTTTTAT
  • esta es la salida con \(RS=">"\), imprimiendo campo 1
awk 'BEGIN{RS=">"} {print NR, NF, $1}' mini_fasta.fst
## 1 0 
## 2 3 especie12
## 3 3 especie2
## 4 3 especie3
## 5 3 especie4
## 6 3 especie5
  • esta es la salida con \(RS=">"\), imprimiendo campos 1 y 2
awk 'BEGIN{RS=">"} {print NR, NF, $1, $2}' mini_fasta.fst
## 1 0  
## 2 3 especie12 ATACCGACCATTAC
## 3 3 especie2 CCAGTAGTCGAGGC
## 4 3 especie3 CCAGGGCCCATATT
## 5 3 especie4 GCATAATCCACCAT
## 6 3 especie5 GGGGTACCCATTTA
  • esta es la salida con \(RS=">"\), imprimiendo campos 1, 2 y 3
awk 'BEGIN{RS=">"} {print NR, NF, $1, $2, $3}' mini_fasta.fst
## 1 0   
## 2 3 especie12 ATACCGACCATTAC TTAGGAACCCAGGC
## 3 3 especie2 CCAGTAGTCGAGGC AGCAGCTTCCATAT
## 4 3 especie3 CCAGGGCCCATATT ATACCGACCATTAC
## 5 3 especie4 GCATAATCCACCAT GCACGAATGCAGAC
## 6 3 especie5 GGGGTACCCATTTA CCCCCCCTTTTTAT
  • esta es la salida con \(RS=">"\), imprimiendo el registro completo
awk 'BEGIN{RS=">"} {print NR, NF, $0}' mini_fasta.fst
## 1 0 
## 2 3 especie12
## ATACCGACCATTAC
## TTAGGAACCCAGGC
## 
## 3 3 especie2
## CCAGTAGTCGAGGC
## AGCAGCTTCCATAT
## 
## 4 3 especie3
## CCAGGGCCCATATT
## ATACCGACCATTAC
## 
## 5 3 especie4
## GCATAATCCACCAT
## GCACGAATGCAGAC
## 
## 6 3 especie5
## GGGGTACCCATTTA
## CCCCCCCTTTTTAT
  • esta es la salida con \(RS=">"\), imprimiendo el registro completo para registros mayores al primero (\(NR>1\))
awk 'BEGIN{RS=">"} NR > 1 {print NR, NF, $0}' mini_fasta.fst
## 2 3 especie12
## ATACCGACCATTAC
## TTAGGAACCCAGGC
## 
## 3 3 especie2
## CCAGTAGTCGAGGC
## AGCAGCTTCCATAT
## 
## 4 3 especie3
## CCAGGGCCCATATT
## ATACCGACCATTAC
## 
## 5 3 especie4
## GCATAATCCACCAT
## GCACGAATGCAGAC
## 
## 6 3 especie5
## GGGGTACCCATTTA
## CCCCCCCTTTTTAT

Si quieres dominar \(awk\), es crítico que sepas manejar las variables \(NR\), \(FS\), \(OFS\) adecuadamente. Los ejemplos precedentes, si los estudias con cuidado, deben darte un buen nivel de comprensión de su comportamiento por defecto y cómo manipularlas para modelar adecuadamente la estructura de los registros (datos).

Una vez revisado en detalle este aspecto clave, podemos pasar a aprender los elementos fundamentales del lenguaje AWK.

12.3 Sintaxis condensada de AWK

AWK no es sólo una herramienta de filtrado de texto estructurado. Es un lenguaje de programación completo, con estructuras de control de flujo, bucles, diversos operadores y funciones integradas para trabajar con cadenas de caracteres o con números, controlar el formato de la salida impresa, estructuras de datos y posibilidad de escribir funciones ad hoc. La combinación juiciosa de estos elementos permit escribir programas para realizar complejas transformaciones de archivos de texto, como veremos en múltiples ejemplos.

12.3.1 Condicionales, bucles, operadores boleanos y relacionales, funciones integradas y definidad por el usuario, arreglos

La siguiente lista presenta algunos de los elementos y estructuras sintácticas fundamentales del lenguaje de programación \(gawk\)

  • 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.

Esta lista no es exhaustiva en absoluto, pero contiene muchos de los elementos del lenguaje más frecuentemente usados. En los ejemplos que siguen veremos implementaciones prácticas de la mayoría de éstos, explicando su sintaxis a medida que aparezcan.

12.3.2 Resumen de variables internas más usadas:

La mayoría de las variables internas listadas seguidamente ya las explicamos y vimos en acción en ejemplos precedentes. Las presento abajo en un solo bloque para facilitar su localización y repaso.

$0       guarda el valor del registro (por defecto fila) actual en memoria de un archivo de entrada
$1-$n    guarda los contenidos de los campos de una fila
ARGC     variable que guarda el número de argumentos (+1) pasados al script desde la línea de comandos, después del bloque de código
         NOTA: ARGC contiene siempre un valor más que argumentos pasados al script debido a  que implícitamente define ARGV[0]=awk 
ARGV     arreglo que guarda los argumentos pasados al script desde la línea de comandos, después del bloque de código. ARGV[0] contiene awk
FILENAME nombre del archivo de entrada actualmente en procesamiento
FS       (Field Separator) separador de campos (por defecto SPACE or TAB)
NF       (Number of fields) número de campos delimitados por FS en un registro
NR       (Number or Records) guarda el número de campos delimitados por FS en registro o fila actual
OFS      (Output Field Separator) separador de campo de la salida (SPACE por defecto)
ORS      (Output Return Separator) separador de registro de la salida (\n por defecto)

12.4 Expresiones regulares (regexps) - una breve introducción

Dado que \(AWK\) es un lenguaje especializado en el procesamiento de archivos de texto en base a patrones, es imprescindible presentar una sección sobre expresiones regulares.

Una expresión regular (regex o regexp) define uno o varios conjuntos de cadenas de caracteres usando una notación específica:

  • Una cadena literal de caracteres es una regex que define a una sola cadena: a sí misma.

  • Una expresión regular más compleja, usando la notación adecuada, que incluye letras, números (caracteres ordinarios) y una gran cantidad de caracteres adicionales (caracteres especiales o metacaracteres), puede definir a un conjunto más amplio, pero específico, de cadenas de caracteres.

  • Los caracteres ordinarios usados en *regexes son:

    • _, A-Z, a-z, 0-9
  • Los metacaracteres usados en regexes son:

    • .*[]^${}+?|()

Las regexes se utilizan para encontrar patrones específicos en archivos de texto. Programas como \(ed\), \(vim\), \(grep\), \(sed\), \(awk\), \(perl\), entre otros muchos, hacen uso de regexes para buscar dichos patrones en archivos de texto. Cuando los encuentra, se dice que la regexp encontró un match o concordancia.

Hay diversas implementaciones del lenguaje de expresiones regulares. Aquí sólo veremos las regexes del estándar POSIX que usan la mayoría de los programas de GNU/Linux.

Además, hay dos motores de búsqueda de patrones mediante expresiones regulares:

  • el motor Básico de Expresiones Regulares (BRE) por sus siglas en inglés, usado por ejemplo por \(sed\) y \(grep\)
  • el motor Extendido de Expresiones Regulares (ERE), usado por ejemplo por \(gawk\) y \(perl\)

12.4.1 Delimitadores de expresiones regulares // y búsqueda de coincidencias

Los delimitadores estándar son /regex/.

En \(gawk\) buscamos coincidencias de la regexp en líneas de un archivo o campos de una línea con sintaxis de este estilo:

  • awk ‘$1 == “cadena” { print $2 }’ archivo # imprime campo dos si campo uno es igual a cadena
  • awk ‘$1 ~ “/regex/” { print $2 }’ archivo # imprime campo dos si campo uno concuerda con regex
  • awk ‘$1 !~ “/regex/” { print $2 }’ archivo # imprime campo dos si campo uno NO concuerda con regex
  • awk ‘/cadena/ { print $0 }’ archivo # imprime toda la línea si concuerda con cadena
  • awk ‘/cadena1/ && !/cadena2/ { print $0 }’ archivo # imprime toda la línea si concuerda con cadena1 y no con cadena2

12.4.2 Notación de caracteres especiales más frecuentemente usados

Notación Significado Motor BRE/ERE
\ escapa el significado del metacaracter, interpretación literal AMBAS
. cualquier caracter sencillo salvo NULL AMBAS
* machea cualquier cantidad de veces (o cero) el caracter precedente AMBAS
^ machea la regexp al inicio de la línea o cadena de caracteres AMBAS
$ machea la regexp al final de la línea o cadena de caracteres AMBAS
[123] [A-Z] machea cualquiera de los caracteres incluidos o rango indicado AMBAS
{n,m} expresión de intervalo: machea n instancias, o de n a m instancias ERE
+ machea una o más instancias de la regexp precedente ERE
? machea cero o una instancia de la regexp precedente ERE
| machea regexp especificada antes o después de | (esto|aquello) ERE
() busca un match al grupo de regexes incluidas: (esto|aquello) ERE

12.4.3 Notación POSIX estándar de clases de caracteres BRE frecuentemente usados

Notación Significado Motor BRE/ERE
[[:alnum:]] alfanuméricos [a-zA-Z0-9_] AMBAS
[[:alpha:]] carcteres alfabéticos [a-zA-Z] AMBAS
[[:space:]] espacios (’ ’, tabuladores, salto de línea) AMBAS
[[:blank:]] machea espacios y tabuladores AMBAS
[[:upper:]] machea [A-Z] AMBAS
[[:lower:]] machea [a-z] AMBAS
[[:digit:]] machea [0-9] AMBAS

12.4.4 Notación POSIX de clases de caracteres ERE frecuentemente usados

Notación Significado Motor BRE/ERE
\w caracteres alfanuméricos [a-zA-Z0-9_] ERE
\W caracteres NO alfanuméricos [^[:alnum:]_] ERE
\s machea espacios y tabuladores ERE
\d machea [0-9] ERE
\< machea el inicio de una palabra ERE
\> machea el final de una palabra ERE

12.4.5 Un ejemplo de expresión regular compleja: validación de un correo electrónico

Este es un ejemplo relativamente complejo que integra la mayoría de los elementos del lenguaje de expresiones regulares descrito arriba. El \(awk\) recibe una cadena de caracteres en su STDIN y revisa si tiene la estructura esperada de un correo electrónico, imprimiéndolo si dicha cadena es modelada por la regexp

echo "usuario@entidad.unam.mx" | awk '/^([a-zA-Z0-9_\-\.\+]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$/'
## usuario@entidad.unam.mx

12.4.6 Manipulación de cadenas de caracteres con las funciones sub(), gsub(), substr(), match(), split() y expresiones regulares

\(AWK\) provee de diversas funciones para la manipulación de cadenas de caracteres. Para empezar, les voy a mostrar tres funciones muy sencillas de usar que se implementan con frecuencia en código AWK.

  • match() match(cadena, regexp [, array])
  • index() index(in_str1, find_str2)
  • sub() /regexp/, “reemplazo”, [, diana]
  • gsub() /regexp/, “reemplazo”, [, diana]
  • substr() cadena, inicio, [, longitud]
  • split() split(string, array [, fieldsep [, seps ] ])

\(sub()\) busca la instancia más larga en la cadena diana de la \(regexp\), sustituyéndola por la cadena reemplazo. La cadena diana así modificada pasa a ser el nuevo valor de diana.

Es importante resaltar que la cadena diana por defecto es \(\$0\)“. A veces es conveniente guardar \(\$0\) en una variable.

\(sub()\) sólo reemplaza la primera instancia de \(regexp\), mientras que \(gsub()\) realiza reemplazos globales (tipo: s/foo/bar/g)

  • \(sub()\), dos ejemplos
echo "CCATCCATGCCGTGCTAA" | awk 'sub(/CCATCC/, "")'
## ATGCCGTGCTAA
awk 'BEGIN { str = "esta es una cadena de caracteres: cadena"; print str; sub(/cadena/, "secuencia", str); print str }'
## esta es una cadena de caracteres: cadena
## esta es una secuencia de caracteres: cadena
  • \(gsub()\)
awk 'BEGIN { str = "esta es una cadena de caracteres: cadena"; print str; gsub(/cadena/, "secuencia", str); print str }'
## esta es una cadena de caracteres: cadena
## esta es una secuencia de caracteres: secuencia

Es importante notar que \(sub()\) y \(gsub()\) regresan como valor el número de sustituciones realizadas, como muestra el siguiente ejemplo:

awk 'BEGIN { str = "esta es una cadena de caracteres: cadena"; print str; print(sub(/cadena/, "secuencia", str)) }'
awk 'BEGIN { str = "esta es una cadena de caracteres: cadena"; print str; print(gsub(/cadena/, "secuencia", str)) }'
## esta es una cadena de caracteres: cadena
## 1
## esta es una cadena de caracteres: cadena
## 2

Ahora veamos unos ejemplos de la función \(substr()\), que regresa una subcadena de la cadena a modificar: - \(substr()\)

awk 'BEGIN { str = "esta es una cadena de caracteres: cadena"; print str; print(substr(str, 1, 11))}'
awk 'BEGIN { str = "esta es una cadena de caracteres: cadena"; fin = substr(str, 12); print fin}'
## esta es una cadena de caracteres: cadena
## esta es una
##  cadena de caracteres: cadena

Noten que \(substr()\) regresa la subcadena correspondiente. Usaremos substr() en la función translate_dna() y extract_sequence_by_coordinates() implementadas en los scripts avanzados fasta_toolkit.awk y extract_CDSs_from_GenBank.awk que veremos al final de la sección de awk.

  • \(index()\) imprime el índice en el que empieza la subcadena (ATG) en la cadena principal (seq en este caso)
awk 'BEGIN { seq = "CCATCCATGCCGTGCTAA"; idx = index(seq, "ATG"); print idx}'
## 7
  • \(index()\) y \(substr()\) con frecuencia se usan juntos
echo "CCATCCATGCCGTGCTAA" | awk '{ idx = index($0, "ATG"); gene = substr($0, idx); print idx, "\n"$0, "\n      " gene }'
## 7 
## CCATCCATGCCGTGCTAA 
##       ATGCCGTGCTAA

Notas:

  1. en gawk no podemos usar una constante como regexp en la función index(), es decir index($0, /ATG/) evalúa como error fatal.
  2. vean que \(\$0\) no se modifica con la acción de \(substr()\), función que regresa la cadena modificada, que debemos guardar en una variable. En cambio \(sub()\) o \(gsub()\) sí modifican \(\$0\), como vimos en el primer ejemplo de esta sección.
  • Veamos match() en acción, usando una regexp para capturar en paréntesis las coordenadas de un CDS como se representan en un archivo GenBank, como hace el script extract_CDSs_from_GenBank.awk.
echo "     CDS           295..861" | \
  awk 'END { 
             m = match($0, /^\s{4,}CDS\s{4,}([[:digit:]]+)\.\.([[:digit:]]+)/, c_arr) 
             s=c_arr[1]; e=c_arr[2] 
             print("m="m, "\nstart=" s, "\nend="e) 
      }'
## m=1 
## start=295 
## end=861

Noten que los dígitos son capturados en los paréntesis ([[:digit:]]+)\.\.([[:digit:]]+) y los matches guardados en el arreglo c_arr. Veremos arreglos de awk más adelante. por ahora baste saber que podemos recuperar los valores o elementos del arreglo indexando numéricamente s=c_arr[1]; e=c_arr[2]

  • Veamos ahora sub(), gsub() y split() en acción, para capturar las coordenadas de un CDS como se representan en un archivo GenBank, como hace el script extract_CDSs_from_GenBank.awk.
echo '     CDS        complement(3633769..3634107)' | \
  awk '{
        coord = $0
        sub(/^\s{4,}CDS\s{4,}complement\(/, "", coord)
        gsub(/[\)]/, "", coord)
        m = split(coord, c_arr, /\.\./) 
        start = c_arr[1]; end = c_arr[2] 
        print("m="m, "\nstart="start, "\nend="end)
  }'
## m=2 
## start=3633769 
## end=3634107

En este ejemplo la línea guardada en coord se va procesando sucesivamente por sub(), gsub() y split(), para finalmente imprimir las coordenadas de inicio y fin del CDS. En la función split() cada campo delimitado por ‘..’, es guardado en el arreglo c_arr. La variable m guarda el número de campos.

12.4.7 Otras funciones para la manipulación de cadenas de caracteres

\(AWK\) implementa muchas más funciones para manipulación de cadenas de caracteres como las siguientes

  • asort(source [, dest [, how ] ])
  • asorti(source [, dest [, how ] ])
  • gensub(regexp, replacement, how [, target])
  • index(in, find)
  • length([string])
  • tolower(string)
  • toupper(string)

Veremos el uso de varias de ellas en ejemplos subsiguientes, notablemente en el script avanzado extract_CDSs_from_GenBank.awk que hace uso extensivo de ellas en combinación con regexps para parsear archivos GenBank. Consulten la guía de usuario de AWK para los detalles y más ejemplos.

12.5 Ejemplos integrativos básicos para entender cómo funciona \(AWK\)

En esta sección aprenderemos algunos idiomas de \(AWK\) muy útiles, que muestran cómo implementar conjuntamente algunas de las variables y elementos de sintaxis listados arriba. Con ellos podrán apreciar mejor la lógica, simpleza y utilidad de \(AWK\).

12.5.1 Determinar la longitud de una cadena (de oligonucleótido de DNA) con length()

  • Fíjate en las diferencias y entiende porqué los dos comandos que muestro seguidamente no dan el mismo resultado
# Fíjate en las diferencias y entiende qué pasa;
echo 'atattGAATTCTAGCACATACTAACGGACC' | wc -c
echo 'atattGAATTCTAGCACATACTAACGGACC' | awk 'END{print "oligo",  $0, "tiene", length($0), "nt de longitud"}'
## 31
## oligo atattGAATTCTAGCACATACTAACGGACC tiene 30 nt de longitud
  • Si no lo has descubierto aún, tal vez te ayude la siguiente salida
# Compara con la salida del bloque anterior
echo    'atattGAATTCTAGCACATACTAACGGACC' | wc -c
echo -n 'atattGAATTCTAGCACATACTAACGGACC' | wc -c
## 31
## 30

De este ejemplo debemos aprender que:

  • \(wc\) cuenta los espacios y saltos de línea como caracteres, incluyendo el \("\n"\) que \(echo\) imprime por defecto al final de las líneas.
  • \(awk\), en modo estándar de parseo de registros, usa también el \("RS=\n"\) para delimitarlos, pero no los incluye en la definición de los mismos.
  • Esto es similar al tratamiento que hace el separador de campos (por defecto: FS = ” “) de los espacios, tabuladores y saltos de línea, los cuales elimina de los campos capturados en $1 … $n, como vimos en ejemplos anteriores.

12.5.2 Eliminar filas en blanco de un archivo

  • Generemos primero un archivo temporal con secuencias en formato FASTA y espacios entre ellas
# genera un archivo temporal con secuencias en formato FASTA y espacios entre ellas
echo -e ">seq1\nATGCATGC\n\n>seq2\nTATTACCAG\n\n>seq3\nATTATTGGC\n\n" > sequences.tmp

# despliega el archivo numerando las líneas
cat -n sequences.tmp
##      1   >seq1
##      2   ATGCATGC
##      3   
##      4   >seq2
##      5   TATTACCAG
##      6   
##      7   >seq3
##      8   ATTATTGGC
##      9   
##     10   
  • ahora usa \(awk\) para imprimir sólo las líneas que contengan al menos 1 campo NO vacío
awk 'NF > 0' sequences.tmp | nl
##      1   >seq1
##      2   ATGCATGC
##      3   >seq2
##      4   TATTACCAG
##      5   >seq3
##      6   ATTATTGGC

12.5.3 Imprime sólo ciertas líneas del archivo

  • imprime las 6 primeras líneas
awk 'NR <= 6' sequences.tmp | cat -n
##      1   >seq1
##      2   ATGCATGC
##      3   
##      4   >seq2
##      5   TATTACCAG
##      6   
  • imprime las primeras 2 líneas y a partir de la sexta y eliminando registros vacíos
awk 'NF > 0 && (NR <= 2 || NR >= 6)' sequences.tmp
rm sequences.tmp
## >seq1
## ATGCATGC
## >seq3
## ATTATTGGC

Noten el uso de paréntesis para agrupar las dos expresiones ( || ) en ‘NF > 0 && (NR <= 2 || NR >= 6)’. Así se obliga a que de deben cumplir ambas condiciones (a izquierda y derecha del &&).

12.5.4 filtra la salida de \(ls\ -l\) para ver sólo los permisos asociados a archivos y directorios selectos

Es importante que entiendas las diferencias de salida de ambos comandos, es decir, el efecto del bloque END{} y la diferencia de imprimir NF y $NF

ls -ld working_with* *.awk *.sh docs pics | awk 'END{print NF}' 
echo '-----------------------------------'
ls -ld working_with* *.awk *.sh docs pics | awk '{print $1, $NF}' 
## 9
## -----------------------------------
## -rwxr-xr-x align_seqs_with_clustal_or_muscle.sh
## -rw-rw-r-- aoa.awk
## -rw-r--r-- bash_script_template_with_getopts.sh
## -rwxr-xr-x compute_DNA_seq_stats.awk
## -rw-rw-r-- count_genome_features_for_taxon.awk
## -rw-rw-r-- count_genome_features_for_taxon_V2.awk
## drwxr-xr-x docs
## -rwxr-xr-x extract_CDSs_from_GenBank.awk
## -rwxr-xr-x extract_DNA_string_from_genbank.awk
## -rwxr-xr-x extract_sequence_strings_by_coords.awk
## -rwxr-xr-x fasta_toolkit.awk
## -rwxr-xr-x filter_fasta_sequences.awk
## -rw-rw-r-- get_sequences_from_list.awk
## -rwxrwxr-x hist.awk
## -rwxr-xr-x parse_blastp_NCBI-WEB_multiquery_text_output.awk
## drwxrwxr-x pics
## -rwxr-xr-x print_ISFinder_stats.awk
## -rw-rw-r-- print_vars_and_params.awk
## -rwxr-xr-x reverse_complement_FASTA.awk
## -rwxr-xr-x run_phylip.sh
## -rw-rw-r-- showargs.awk
## -rw-rw-r-- translate_codons.awk
## -rwxr-xr-x translate_dna.awk
## -rwxr-xr-x translate_fasta.awk
## -rwxr-xr-x transpose_matrix.awk
## -rw-rw-r-- working_with_linux_commands.html
## -rw-rw-r-- working_with_linux_commands.log
## -rw-r--r-- working_with_linux_commands.Rmd
## -rw-rw-r-- working_with_linux_commands.tex

12.5.5 filtrado de archivos con AWK - procesadores en /proc/cpuinfo

  • Cuenta el número de procesadores de tu sistema:
# el archivo /proc/cpuinfo contiene la información sobre las cpus del sistema, incluyendo los cores/procesadores contenidos en la unidad de procesamiento central
head -5 /proc/cpuinfo 
## processor    : 0
## vendor_id    : GenuineIntel
## cpu family   : 6
## model        : 158
## model name   : Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz

Recordemos la sintaxis general de código AWK: /patrón/ { acción }

# usamos el patrón '/^processor/, seguido de la acción {cuenta instancias} y terminamos con un bloque END{} que imprime el valor de la variable n
# este código de AWK se lo pasamos directamente al intérprete de comandos awk como un una cadena entre comillas, seguido del archivo a procesar
# awk 'CODIGO AWK' ARCHIVO_A_PROCESAR
#       PATRON    ACCION END{s}
awk '/^processor/ {n++} END{ print "This computer has", n, "processors"}' /proc/cpuinfo 
## This computer has 12 processors
  • Imprime líneas con 12 o menos caracteres de entre las primeras 20 líneas del archivo /proc/cpuinfo
# con head -20 filtramos las primeras 20 líneas, las cuales pasamos a awk con |
# recuerda: la acción por defecto de awk es imprimir, en este caso las líneas que satisfagan la condición
head -20 /proc/cpuinfo | awk 'length <= 12'
## model        : 158
## core id      : 0
## apicid       : 0
## fpu      : yes
## wp       : yes
  • Imprime líneas con 30 o más caracteres de entre las primeras 20 líneas del archivo /proc/cpuinfo
head -20 /proc/cpuinfo | awk 'length >= 30'
## model name   : Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
## flags        : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp md_clear flush_l1d arch_capabilities
  • Imprime líneas que contienen información sobre tamaño del caché
#     PATRON   acción por defecto: imprimir
awk '/cache size/'  /proc/cpuinfo 
## cache size   : 12288 KB
## cache size   : 12288 KB
## cache size   : 12288 KB
## cache size   : 12288 KB
## cache size   : 12288 KB
## cache size   : 12288 KB
## cache size   : 12288 KB
## cache size   : 12288 KB
## cache size   : 12288 KB
## cache size   : 12288 KB
## cache size   : 12288 KB
## cache size   : 12288 KB
  • Suma el tamaño de cache total del caché de los procesadores
# como ven en la salida anterior, el campo 4 contiene el tamaño de caché de cada procesador
#     PATRON       ACCION    END{}
awk '/cache size/ {size+=$4} END{print "total chache size: ", size, "Kb, ", size/1024, "Mb"}'  /proc/cpuinfo
## total chache size:  147456 Kb,  144 Mb

12.5.6 filtrado de archivos con AWK - suma de tamaños de archivos en Mb

  • Suma el tamaño en Mb de los archivos working_with_linux_commands.* en el directorio actual
# Noten que puedo partir líneas de comandos muy largas usando \ antes de introducir un salto de línea ;)
# sumamos el valor del campo #5 de la salida de ls -l a la variable s (s+=$5);
# hacemos algo de aritmética, para convertir los bytes a Mega-bytes (Mb=s/1024^2)
# usamos además printf "formatting-string", var1, var2 ... para imprimir las variables con formato: string = %s, número flotante=%.2f
ls -l working_with_linux_commands.* | \
 awk 's+=$5; END{Mb=s/1024^2; s1="total size of working_with_linux_commands.* files is:"; s2="Mb"; printf "%s %.2f %s\n", s1, Mb, s2}'
## -rw-rw-r-- 1 vinuesa vinuesa 2063490 sep  7 14:10 working_with_linux_commands.html
## -rw-rw-r-- 1 vinuesa vinuesa   68106 nov 15  2020 working_with_linux_commands.log
## -rw-r--r-- 1 vinuesa vinuesa  436391 sep 26 22:11 working_with_linux_commands.Rmd
## -rw-rw-r-- 1 vinuesa vinuesa  499749 nov 15  2020 working_with_linux_commands.tex
## total size of working_with_linux_commands.* files is: 2.93 Mb
  • comparen y entiendan la diferencia del comando anterior con el del siguiente:
ls -l working_with_linux_commands.* | \
 awk '{s+=$5}; END{Mb=s/1024^2; s1="total size of working_with_linux_commands.* files is:"; s2="Mb"; printf "%s %.2f %s\n", s1, Mb, s2}'
## total size of working_with_linux_commands.* files is: 2.93 Mb

12.5.7 uso de función de autoincremento \(++\) para contar ocurrencias de un patrón

  • Cuenta las entradas para el género Stenotrophomonas en la tabla assembly_summary.txt.gz
# aquí usamos la expresión regular /\sStenotrophomonas/ para filtrar las líneas desadas, 
#  y contamos las occurrencias con la variable c que autoincrementamos con el operador ++
zcat assembly_summary.txt.gz | \
 awk '/\sStenotrophomonas\s/ {c++} END{printf "%s %i %s\n", "Hay", c, "genomas de Stenotrophomonas"}'
## Hay 422 genomas de Stenotrophomonas

12.5.8 Transforma la salida del comando \(uniq\ -c\) en un formato de campos separados por comas (archivo \(csv\)) con cabecera

Vamos a trabajar con el archivo assembly_summary.txt.gz usando herramientas de filtrado que ya conocemos, pero la salida de la tubería la vamos a editar con \(AWK\) para generar un formato tabular

  • veamos primero la salida a modificar con \(AWK\)
# Primero les muestro la salida que quiero transformar con AWK a formato tabular, 
#  generada por herramientas estándar de filtrado que culminando con un sort | uniq -c 

zcat assembly_summary.txt.gz | grep Stenotrophomonas | cut -f8 | cut -d ' ' -f1,2 | \
sort -d | uniq -c | sort -nrk1
##     335 Stenotrophomonas maltophilia
##      55 Stenotrophomonas sp.
##       8 Stenotrophomonas rhizophila
##       4 Stenotrophomonas pavanii
##       4 Stenotrophomonas indicatrix
##       3 Stenotrophomonas acidaminiphila
##       2 Stenotrophomonas panacihumi
##       2 Stenotrophomonas nitritireducens
##       1 Stenotrophomonas terrae
##       1 Stenotrophomonas pictorum
##       1 Stenotrophomonas lactitubi
##       1 Stenotrophomonas koreensis
##       1 Stenotrophomonas humi
##       1 Stenotrophomonas ginsengisoli
##       1 Stenotrophomonas daejeonensis
##       1 Stenotrophomonas chelatiphaga
##       1 Stenotrophomonas bentonitica
  • y ahora filtramos la salida del comando anterior con \(AWK\), y ordenamos por número decreciente de genomas
# Con la llamada a BEGIN{print "Genus\tspecies\tcount"; OFS=","} imprimimos en primer lugar la cabecera 
#   e indicamos que queremos que el Output Field Separator sea ',', y no espacios simples
# Luego le indicamos el orden en que queremos imprimir los campos {print $2 "_" $3,$1}
# Noten que uso $2 "_" $3 para que esos dos campos queden unidos por un guion bajo

zcat assembly_summary.txt.gz | grep Stenotrophomonas | cut -f8 | cut -d ' ' -f1,2 | \
sort -d | uniq -c | sort -nrk1 | \
awk 'BEGIN{print "species,count"; OFS=","} {print $2 "_" $3,$1}' 
## species,count
## Stenotrophomonas_maltophilia,335
## Stenotrophomonas_sp.,55
## Stenotrophomonas_rhizophila,8
## Stenotrophomonas_pavanii,4
## Stenotrophomonas_indicatrix,4
## Stenotrophomonas_acidaminiphila,3
## Stenotrophomonas_panacihumi,2
## Stenotrophomonas_nitritireducens,2
## Stenotrophomonas_terrae,1
## Stenotrophomonas_pictorum,1
## Stenotrophomonas_lactitubi,1
## Stenotrophomonas_koreensis,1
## Stenotrophomonas_humi,1
## Stenotrophomonas_ginsengisoli,1
## Stenotrophomonas_daejeonensis,1
## Stenotrophomonas_chelatiphaga,1
## Stenotrophomonas_bentonitica,1

Para acabar de redondear esta sección, veamos unos ejemplos sencillos de uso de \(awk\) para trabajar con secuencias en archivos multifasta y así reforzar lo aprendido antes de pasar a secciones más avanzadas.

12.5.9 Trabajando con archivo FASTA: cuenta el número de secuencias en recA_Bradyrhizobium_vinuesa.fna

awk '/^>/ {s++} END{print FILENAME, "has", s, "sequences"}' recA_Bradyrhizobium_vinuesa.fna
## recA_Bradyrhizobium_vinuesa.fna has 125 sequences
  • lo mismo, pero con un bucle for (ver sección de programación \(Bash\) más abajo) para contar las secuencias en cada archvo *fna del directorio
for f in *fna; do awk '/^>/ {s++} END{print FILENAME, "has", s, "sequences"}' $f; done
## mini_CDS.fna has 2 sequences
## recA_Bradyrhizobium_vinuesa.fna has 125 sequences
## recA_Byuanmingense.fna has 32 sequences
## Salmonella_enterica_33676_pIncAC_CDSs.fna has 216 sequences

12.5.10 Trabajando con archivo FASTA: ¿cuántos nucleótidos hay en recA_Bradyrhizobium_vinuesa.fna?

Para este ejercicio necesitamos eliminar las líneas que contienen las cabeceras FASTA.

  • Noten que usamos la sintaxis ‘/(^>|^$)/’ para agrupar dos regexes con el fin de saltarnos las líneas que inician con ‘>’ y las que están en blanco. Veamos el efecto de la regex imprimiendo sólo las primeras 15 líneas:
awk '! /(^>|^$)/' recA_Bradyrhizobium_vinuesa.fna | head -15
## ATGAAGCTCGGCAAGAACGACCGGTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTCGGGCTCGACA
## TCGCGCTCGGCATCGGCGGCCTGCCCAAGGGGCGTATCGTCGAGATCTACGGGCCGGAATCCTCGGGCAA
## GACCACGCTGGCGCTGCATACGGTGGCGGAAGCGCAGAAGAAGGGCGGCATCTGCGCCTTCATCGACGCC
## GAGCACGCGCTCGACCCGGTCTATGCCCGCAAGCTCGGCGTCAACATCGACGAGCTCCTGATCTCGCAGC
## CCGACACCGGCGAGCAGGCGCTGGAGATCTGCGACACGCTGGTGCGCTCGGGCGCTGTCGATGTGCTGGT
## GATCGACTCGGTTGCGGCGCTGGTGCCGAAGGCCGAGCTCGAAGGCGAGATGGGCGATGCGCTGCCAGGC
## TTGCAGGCCCGTCTGATGAGCCAGGCGCTGCGCAAGCTGACGGCCTCCATCAACAAGTCCAACACCATGG
## TGATCTTCATCAACCAGATC
## ATGAAGCTCGGCAAGAACGACCGGTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTCGGGCTCGACA
## TCGCGCTCGGCATCGGCGGCCTGCCCAAGGGGCGTATCGTCGAGATCTACGGGCCGGAATCCTCGGGCAA
## GACCACGCTGGCGCTGCATACGGTGGCGGAAGCGCAGAAGAAGGGCGGCATCTGCGCCTTCATCGACGCC
## GAGCACGCGCTCGACCCGGTCTATGCCCGCAAGCTCGGCGTCAACATCGACGAGCTCCTGATCTCGCAGC
## CCGACACCGGCGAGCAGGCGCTGGAGATCTGCGACACGCTGGTGCGCTCGGGCGCTGTCGATGTGCTGGT
## GATCGATTCGGTTGCGGCGCTGGTGCCGAAGGCCGAGCTCGAAGGCGAGATGGGCGATGCGCTGCCGGGC
## TTGCAGGCCCGTCTGATGAGCCAGGCGCTGCGCAAGCTGACGGCCTCCATCAACAAGTCCAACACCATGG
  • podríamos estar tentados de parsarle esta salida a \(wc\ -c\) para contar los caracteres
awk '! /(^>|^$)/' recA_Bradyrhizobium_vinuesa.fna | wc -c
## 64795

Pero este resultado no es correcto, ya que \(wc\ -c\) contabiliza también los saltos de línea.

  • Esto lo podemos demostrar con el siguiente comando \(tr\ --delete\ '\n'\) que elimina los saltos de línea al final de cada línea
awk '! /(^>|^$)/' recA_Bradyrhizobium_vinuesa.fna | tr --delete '\n$' | wc -c
## 63795
  • o concatenando directamente en \(awk\) las líneas, teniendo cuidado de usar \(printf\) indicándole que imprima la cadena resultante SIN salto de línea al final. Noten que ‘{s=s$1}’ concatena cada línea a la variable s, la cual tenemos que imprimir al final, en un bloque END{}, pero usando ‘printf “%s”, s’ para que no imprima el salto de línea
awk '! /(^>|^$)/ {s=s$1} END{printf "%s", s}' recA_Bradyrhizobium_vinuesa.fna | wc -c
## 63795
  • si usamos una llamada a \(print\), que imprime un salto de línea, nuestra cuenta fallaría por un residuo: contaríamos erróneamente el salto de línea que \(print\) introduce al final, como demuestra el siguiente ejemplo:
awk '! /(^>|^$)/ {s=s$1} END{print s}' recA_Bradyrhizobium_vinuesa.fna | wc -c
## 63796
  • Con \(awk\) obtenemos fácilmente la solución, si tenernos que preocupar de saltos de línea o líneas en blanco, con el siguiente código que hace uso de la función \(length()\)
awk '! /(^>|^$)/ {l+=length($1)} END{print FILENAME, "has", l, "nts or", l/1000, "kb"}' recA_Bradyrhizobium_vinuesa.fna
## recA_Bradyrhizobium_vinuesa.fna has 63795 nts or 63.795 kb

Notas: 1. También pudimos haber usado l += length($0), ya que cada registro (línea en este caso) tiene un solo campo: la cadena de nucleótidos correspondiente. 2. El código \(awk\) del ejemplo de arriba no cuenta los saltos de línea ni líneas en blanco, ya que la función \(length()\) sólo contabiliza caracteres

12.6 Arreglos asociativos (hashes) en \(awk\) - una estructura de datos muy poderosa y versátil

Los arreglos son un tipo de variable que permite guardar a uno o varios elementos. En AWK se pueden construir arreglos de arreglos de profundidad arbitraria, como mostraremos más adelante.

Al arreglo (o \(array\)), como a cualquier otra variable, la nombramos con un nombre corto pero informativo, que no puede iniciar con un dígito.

Accedemos a los valores almacenados en un arreglo mediante un índice: arreglo[índice]

El índice puede ser numérico o una cadena de caracteres. Es decir, en \(awk\) todos los arreglos son asociativos!. PUedes imaginarlos como una tabla no rígida de asociaciones llave-valor.

  • Sintaxis de asignación, recuperación y eliminación de valores de un arreglo de \(awk\)
# 1. asignación
nombre_arreglo[índice] = valor

# 2. recuperación de un valor particular
print nombre_arreglo[índice]

# 3. comprobar la existencia de un índice o llave en un arreglo
if (índice in nombre_arreglo) {haz algo con índice}

# 4. extraer todos los valores del arreglo
for (i in nombre_arreglo) print "índice ", i , " guarda valor ", nombre_arreglo[índice]

# 5. eliminar un elemento del arreglo (el par llave-valor)
delete arreglo[índice]

# 6. vaciar un arreglo completamente
delete arreglo

# 7. llenar un arreglo con la función split()
split(cadena, arreglo [,separador_de_campo [, seps]])

En la mayoría de los lenguajes los arreglos tienen índices numéricos, los cuales definen la posición en el arreglo en el que se guardó el elemento correspondiente. Es decir, en ese caso se trata de listar ordenadas posicionalmente en el orden en el que los elementos fueron agregados a la lista.

Los arreglos asociativos o hashes establecen una asociación entre los \(índices\) y los \(elementos\) del arreglo. Es decir, para cada elemento del arreglo se mantienen un par de valores: el índice el elemento y su valor. Es importante notar que bajo este esquema, los elementos del arreglo no se guardan en ningún orden predeterminado, es decir, los arreglos asociativos son arreglos desordenados. Por ello, si bien podemos usar índices numéricos, éstos no definen un orden en una lista ordenada. Son simples etiquetas. No obstante, si se asignan índices numéricos consecutivos a los valores que se guardan en un arreglo, se pueden recuperar en dicho orden al correr un bucle iterativo.

Veamos unos ejemplos de estos conceptos y del uso de las funciones de manipulación de arreglos en \(awk\)

12.6.1 Uso de la funciones básicas de manipulación de \(arreglos\) en \(awk\): split(), delete()

La función \(split()\) divide una cadena en unidades separadas por el separador de campo, guardándolas en arreglo y las cadenas separadoras en el arreglo seps, acorde a la siguiente sintaxis:

  • split(cadena, arreglo [,separador_de_campo [, seps]])

Veamos un ejemplo que implementa \(split()\) y otras funciones básicas de manipulación de arreglos, como asignaciones, eliminación de valores e impresión de todos los pares llave-valor guardados en un arreglo.

# 1. llenado del hash features con nombres de los campos de la tabla assembly_summary.txt.gz,
#     que quedarán indexados por números consecutivos, que corresponden a su número de columna
#     usando la función split(), aplicada al segundo registro de la tabla (la cabecerea) 
#      features[1] = "assembly_accession"; features[2] = "bioproject"; ...
zcat assembly_summary.txt.gz | awk 'NR==2 { split( $0, features, "\t", seps ) } END { print features[8]; delete features[8]; print "ahora features[8] está vacío: [",features[8],"]\n\n"; for (f in features) print f, features[f];  features[8] = "taxon"; print "\nahora features[8] fue reasignado: ", features[8]}'
## organism_name
## ahora features[8] está vacío: [  ]
## 
## 
## 1 # assembly_accession
## 2 bioproject
## 3 biosample
## 4 wgs_master
## 5 refseq_category
## 6 taxid
## 7 species_taxid
## 8 
## 9 infraspecific_name
## 10 isolate
## 11 version_status
## 12 assembly_level
## 13 release_type
## 14 genome_rep
## 15 seq_rel_date
## 16 asm_name
## 17 submitter
## 18 gbrs_paired_asm
## 19 paired_asm_comp
## 20 ftp_path
## 21 excluded_from_refseq
## 22 relation_to_type_material
## 
## ahora features[8] fue reasignado:  taxon

12.6.2 Modelado de archivos FASTA con \(hashes\) de \(awk\)

  • Primero vamos a desplegar el archivo FASTA mini_fasta.fst con el que vamos a trabajar:
# despliega el archivo mini_fasta.fst
cat mini_fasta.fst
## >especie12
## ATACCGACCATTAC
## TTAGGAACCCAGGC
## >especie2
## CCAGTAGTCGAGGC
## AGCAGCTTCCATAT
## >especie3
## CCAGGGCCCATATT
## ATACCGACCATTAC
## >especie4
## GCATAATCCACCAT
## GCACGAATGCAGAC
## >especie5
## GGGGTACCCATTTA
## CCCCCCCTTTTTAT
  • El siguiente código lee el archivo mini_fasta.fst en un \(hash\) o \(arreglo\ asociativo\) llamado seqs haciendo uso de condicionales e imprime un archivo en formato “FASTAB”
# Si la línea inicia con un '>', inicializamos la variable s="" y guardamos $1 en h (header)
#  después de leer la línea de cabecera (header), sabemos que lo que sigue son líneas de secuencia, hasta el siguiente '>'
#  Por ello concatenamos estas líneas y asignamos la cadena resultante a la variable s. 
#  Cuando awk encuentra la siguiente línea que empieza con '>', inicializa s="" y captura la nueva cabecera en h
#  Finalmente, después de haber leído el archivo completo, abrimos un bloque END{}, desde el cual corremos un
#   blucle for para iterar sobre las llaves o índices del hash, que guardamos en h, 
#   imprimiendo h y el valor asociado con h, "\t", seqs[h], generando lo que yo llamo un archivo "FASTAB"
 awk 'BEGIN{OFS="\t"} {if(/^>/){s=""; h=$1}else{s=s$1}; seqs[h]=s;} END{for (h in seqs) print h, seqs[h]} ' mini_fasta.fst >  mini_fasta.fastab
 cat mini_fasta.fastab
## >especie4    GCATAATCCACCATGCACGAATGCAGAC
## >especie5    GGGGTACCCATTTACCCCCCCTTTTTAT
## >especie12   ATACCGACCATTACTTAGGAACCCAGGC
## >especie2    CCAGTAGTCGAGGCAGCAGCTTCCATAT
## >especie3    CCAGGGCCCATATTATACCGACCATTAC
  • La que sigue es una variante del código de arriba, pero modelando la estructura de registros FASTA en un bloque BEGIN{} con RS=“>”; FS=“\n” y usando bucle for (inicialización_var; condición; autoincremento_var)
#    BEGIN{ INICIALIZACIÓN }         PATRON                 { PROGRAMA }                           END { BLOQUE FINAL PARA IMPRIMIR RESULTADOS}
awk 'BEGIN{RS=">"; FS="\n"; OFS="\t"} NR > 1 {s=""; h=$1; for (i=2; i<=NF; i++) s=s$i; seqs[h]=s;} END{for (h in seqs) print h, seqs[h]} ' mini_fasta.fst
## especie2 CCAGTAGTCGAGGCAGCAGCTTCCATAT
## especie3 CCAGGGCCCATATTATACCGACCATTAC
## especie4 GCATAATCCACCATGCACGAATGCAGAC
## especie5 GGGGTACCCATTTACCCCCCCTTTTTAT
## especie12    ATACCGACCATTACTTAGGAACCCAGGC
  • Comentarios y preguntas sobre el bloque anterior:
    • ¿Qué pasaría si no incializamos s=“” al inicio de la lectura de cada registro?
    • ¿Cómo modificarías el código mostrado arriba para que imprima un FASTA canónico?
    • Noten que el archivo FASTAB resultante lo podemos filtrar por ejemplo con \(grep\), si queremos sacar líneas específicas, como muestra el siguiente código
# Filtrar con grep el archivo FASTAB para sacar las secuencias 1,3 y 5
grep -E 'especie1|especie3|especie5'  mini_fasta.fastab > selected_seqs.fastab
  • El siguiente código convierte un archivo en formato “FASTAB” a un FASTA canónico
# como el archivo que se lee es un "FASTAB", inicializamos el FS="\t"
# dado que queremos imprimir los campos header y secuencia en líneas diferentes, inicializamos OFS="\n"
awk 'BEGIN{FS="\t"; OFS="\n"} {print $1,$2}'  selected_seqs.fastab > selected_seqs.fas
cat selected_seqs.fas

rm selected_seqs.fa*
## >especie5
## GGGGTACCCATTTACCCCCCCTTTTTAT
## >especie12
## ATACCGACCATTACTTAGGAACCCAGGC
## >especie3
## CCAGGGCCCATATTATACCGACCATTAC

12.6.3 \(hashes\) y conteo de instancias de cadenas específicas usadas como llaves

Sigue otro ejemplo de la versátil estructura de datos conocida como \(arreglo\ asociativo\) o \(hash\), la cual es muy útil para contar las instancias de ocurrencia de cadenas específicas al usarlas como llave del \(hash\) y usando el operador ++, como muestra el siguiente código genérico:

  • h[llave]++

Veamos un ejemplo:

  • Cuenta el número de genomas en cada nivel de ensamble (campo #12 assembly_level) para un taxon particular, pasado al programa como argumento desde la línea de comando
    • Noten que tienen que pasar el TAXON al \(script\) como una variable con la sintaxis awk ‘programa’ var=VALOR para que no interprete al argumento como un nombre de archivo
    • usamos el \(hash\) al para contar las instancias de la variable categórica assembly_level con el código al[$12]++
zcat assembly_summary.txt.gz | awk 'BEGIN{ FS=OFS="\t"; print "taxon\tassembly_level\tn_genomes" } $8 ~ tax { al[$12]++ } END{ for(l in al) if (l != "") print tax, l, al[l] }' tax=Pseudomonas
## taxon    assembly_level  n_genomes
## Pseudomonas  Contig  5840
## Pseudomonas  Complete Genome 503
## Pseudomonas  Scaffold    2496
## Pseudomonas  Chromosome  120

Noten que el resultado anterior no sale ordenado ni por la llave ni por el valor.

12.6.4 Ordenamiento de la salida de un \(hash\) de \(gawk\) por llave o valor manipulando la variable de ambiente PROCINFO[“sorted_in”]

Como ya mencionamos, internamente los \(hashes\) de \(gawk\) se procesan arbitrariamente por ser arreglos asociativos.

Pero en \(gawk\) podemos manejar la variable de ambiente PROCINFO[“sorted_in”] para pasarle valores o instrucciones predeterminadas de ordenamiento de la salida como @ind_str_asc, @ind_num_asc, @val_str_asc o @val_num_asc, entre otros, como podrás consultar en el \(gawk\) manual

Para ello tenemos que asignarle estas funciones internas de ordenamiento a \(PROCINFO["sorted\_in"]\) dentro de un bloque \(BEGIN{}\), como muestran los ejemplos que siguen:

  1. ordena la salida por llave con PROCINFO[“sorted_in”] = “@ind_str_asc
zcat assembly_summary.txt.gz | awk 'BEGIN{FS=OFS="\t"; print "taxon\tassembly_level\tn_genomes"; PROCINFO["sorted_in"] = "@ind_str_asc"} $8 ~ tax {al[$12]++} END{ for(l in al) if (l != "") print tax, l, al[l] }' tax=Pseudomonas
## taxon    assembly_level  n_genomes
## Pseudomonas  Chromosome  120
## Pseudomonas  Complete Genome 503
## Pseudomonas  Contig  5840
## Pseudomonas  Scaffold    2496
  1. ordena la salida por valor con PROCINFO[“sorted_in”] = “@val_num_asc
zcat assembly_summary.txt.gz | awk 'BEGIN{FS=OFS="\t"; print "taxon\tassembly_level\tn_genomes"; PROCINFO["sorted_in"] = "@val_num_asc"} $8 ~ tax {al[$12]++} END{ for(l in al) if (l != "") print tax, l, al[l] }' tax=Pseudomonas
## taxon    assembly_level  n_genomes
## Pseudomonas  Chromosome  120
## Pseudomonas  Complete Genome 503
## Pseudomonas  Scaffold    2496
## Pseudomonas  Contig  5840

Cabe señalar que según las características de la llave, ésta puede que se imprima de manera ordenada, como muestra el siguiente ejemplo:

  • Cuenta el número de genomas liberados por año para un taxon indicado en la línea de comando
    • Asignamos el ‘TAXON’ a la variable \(tax\) para poderla pasar al \(script\) como un argumento, usando la sintaxis awk ‘programa’ tax=TAXON
    • Además hacemos uso de la función interna gsub() que permite hacer sustituciones globales con la sintaxis: gsub(/regex/, reemplazo)
#                                  | INICIALIZACIÓN de FS e impresión de cabecera |-PATRÓN-|-------------  ACCIÓN --------------|-------- BLOQUE END PARA PROCESAMIENTO FINAL--------------------------------|  pasamos var=VALOR
zcat assembly_summary.txt.gz | awk 'BEGIN{FS="\t"; print "taxon\trel_y\tn_genomes"} $8 ~ tax {gsub(/\/.*$/, ""); count_y[$15]++} END{for (y in count_y) if (y > 0) printf  "%s\t%d\t%d\n", tax, y, count_y[y]}' tax=Pseudomonas
## taxon    rel_y   n_genomes
## Pseudomonas  2003    1
## Pseudomonas  2005    3
## Pseudomonas  2006    6
## Pseudomonas  2007    4
## Pseudomonas  2008    4
## Pseudomonas  2009    6
## Pseudomonas  2010    15
## Pseudomonas  2011    31
## Pseudomonas  2012    94
## Pseudomonas  2013    274
## Pseudomonas  2014    612
## Pseudomonas  2015    1218
## Pseudomonas  2016    1111
## Pseudomonas  2017    861
## Pseudomonas  2018    3833
## Pseudomonas  2019    873
  • Comentarios y preguntas sobre el bloque anterior
    • noten que las secuencias se imprimen ordenadas por contar con un dígito en el índice o llave del \(hash\). No sería así si fueran cadenas de caracteres alfabéticos
    • ¿qué hace $8 ~ tax {gsub(//.*$/, ““) … } ?
    • ¿para qué usamos el código if (y > 0) printf … en el bloque END{}?

12.6.5 Juntar/fusionar dos tablas que comparten un campo común (llave primaria) usando un \(hash\) o tabla asociativa (inner-join)

Como hemos venido discutiendo, en el campo de la bioinformática y de bases de datos en general, las relaciones entre las variables asociadas a un registro se modelan frecuentemente usando tablas. Siguiendo un principio básico del modelo relacional, se usan distintas tablas para guardar variables relacionadas entre ellas, lo cual permite evitar la excesiva redundancia que existiría si tuvieramos todos los datos en una sola tabla enorme. Para poder relacionar valores de distintas tablas, éstas contienen típicamente una llave común o llave primaria que las asocia inequívocamente con un registro particular.

En resumen, columnas particulares de las diferentes tablas pueden relacionarse a través de la llave primaria. Esto se modela fácilmente con \(hashes\) indexándolos con las llaves primarias de las tablas. Cabe mencionar que \(gawk\) maneja también arreglos multidimensionales, por lo que se podrían usar para indexarlos con llaves primarias y secundarias, pero no veremos aquí estos casos más complejos.

Hay varias maneras de fusionar dos o más tablas. Un inner-join of fusión interna mantiene a todos los elementos de las dos tablas que comparten una llave común y es el tipo de fusión más sencilla. Veremos un full-join en la siguiente sección.

Veamos cómo hacer un inner-join en el siguiente ejemplo, en el que juntamos dos tablas con información no redundante sobre ensambles de genomas, pero que comparten el número de accesión de los mismos como llave primaria. La primera tabla contiene la información taxonómica y la segunda información relativa al ensamble en sí.

echo ">>> mini_tabla_parte1.tsv <<<"
cat mini_tabla_parte1.tsv
echo
echo ">>> mini_tabla_parte2.tsv <<<"
cat mini_tabla_parte2.tsv
## >>> mini_tabla_parte1.tsv <<<
## assembly_accession   organism_name
## GCF_003086675.1  Stenotrophomonas sp. ZAC14D2_NAIMI4_7
## GCF_003086855.1  Stenotrophomonas sp. YAU14A_MKIMI4_1
## GCF_000072485.1  Stenotrophomonas maltophilia K279a
## GCF_000284595.1  Stenotrophomonas maltophilia D457
## GCF_000482265.1  Escherichia coli str. K-12 substr. MG1655
## 
## >>> mini_tabla_parte2.tsv <<<
## assembly_accession   seq_rel_date    asm_name    submitter
## GCF_004343645.1  2019/03/11  ASM434364v1 Aarhus University
## GCF_901563825.1  2019/05/29  SB3355_SG266_Ko4    Institut Pasteur
## GCF_003086675.1  2018/05/03  ASM308667v1 CCG-UNAM
## GCF_003086855.1  2018/05/03  ASM308685v1 CCG-UNAM
## GCF_000534095.1  2014/02/03  Ente_aero_UCI_47_V1 Broad Institute
## GCF_000006765.1  2006/07/07  ASM676v1    PathoGenesis Corporation
## GCF_000017205.1  2007/07/05  ASM1720v1   J. Craig Venter Institute
## GCF_000072485.1  2008/06/10  ASM7248v1   Wellcome Trust Sanger Institute
## GCF_000284595.1  2012/04/11  ASM28459v1  University of Valencia
  • Comentarios
    • noten que mini_tabla_parte1.tsv contiene sólo a 4 de las secuencias listadas en mini_tabla_parte2.tsv, que contiene 9 registros
    • la tabla mini_tabla_parte1.tsv contiene un registro (el de E. coli K-12) que no tiene su contraparte en mini_tabla_parte2.tsv
    • la llave primaria es la columna assembly_accession

En el código que se muestra abajo hacemos lo siguiente:

  1. inicializamos variables FS y OFS a “\t” para leer e imprimir campos separados por tabuladores
  2. cuando leemos el primer archivo, es decir, cuando NR==FNR, llenamos el arreglo \(a\), indexado por el campo #1 del primer archivo (assembly_accession) y asignando como valor la línea completa
  3. el bloque de ACCION-2 se ejecuta al leer tabla2, cuando NR!=FNR; si su campo #1, que contiene la llave primaria (assembly_accession), existe en el arreglo a, se ejecuta la acción de imprimir el valor de a[$1] (de la tabla1) seguido de los campos $2 a $4 de la tabla 2, evitando imprimir el campo común entre ambas tablas $1
#       INICIALIZACION      PATRON    ACCION-1         CONDICION-1         ACCION-2                 tabla1                tabla2
awk 'BEGIN { FS=OFS="\t" } NR==FNR { a[$1]=$0; next } { if ($1 in a) { print a[$1], $2, $3, $4 } }' mini_tabla_parte1.tsv mini_tabla_parte2.tsv

# la que sigue es una notación equivalente, algo más corta
# awk 'BEGIN { FS=OFS="\t" } NR==FNR { a[$1]=$0; next } ($1 in a) { print a[$1], $2, $3, $4 }' mini_tabla_parte1.tsv mini_tabla_parte2.tsv
## assembly_accession   organism_name   seq_rel_date    asm_name    submitter
## GCF_003086675.1  Stenotrophomonas sp. ZAC14D2_NAIMI4_7   2018/05/03  ASM308667v1 CCG-UNAM
## GCF_003086855.1  Stenotrophomonas sp. YAU14A_MKIMI4_1    2018/05/03  ASM308685v1 CCG-UNAM
## GCF_000072485.1  Stenotrophomonas maltophilia K279a  2008/06/10  ASM7248v1   Wellcome Trust Sanger Institute
## GCF_000284595.1  Stenotrophomonas maltophilia D457   2012/04/11  ASM28459v1  University of Valencia
  • Comentarios y preguntas
    • leemos y guardamos las llaves de la primera tabla en el bloque de ACCION-1, mientras FNR=NR
    • el bloque de ACCION-2 se ejecuta cuando leemos el segundo archivo, imprimiendo sólo las líneas que contienen a[$1], donde $1 representa a las llaves capturadas al leer el primer archivo
    • noten que la llave GCF_000482265.1 de la tabla1 no está en la 2, por lo que no se imprime
    • Como vemos en la salida de arriba la característica fundamental de un inner join es que las filas que no tienen correspondencias en ambas tablas no se imprimen


Para entender mejor cómo funciona el bloque anterior, podemos descomponerlo en sus partes fundamentales:

awk 'BEGIN { FS=OFS="\t" } NR==FNR { a[$1]=$0; next } { if ($1 in a) print $1, "\t", a[$1] }' mini_tabla_parte1.tsv mini_tabla_parte2.tsv
## assembly_accession           assembly_accession  organism_name
## GCF_003086675.1          GCF_003086675.1 Stenotrophomonas sp. ZAC14D2_NAIMI4_7
## GCF_003086855.1          GCF_003086855.1 Stenotrophomonas sp. YAU14A_MKIMI4_1
## GCF_000072485.1          GCF_000072485.1 Stenotrophomonas maltophilia K279a
## GCF_000284595.1          GCF_000284595.1 Stenotrophomonas maltophilia D457

Queda claro que sólo se imprimen los registros de ambas tablas que comparten la llave común.

12.6.6 Fusión completa de dos tablas mediante un outer full-join

Para generar una tabla que tenga todas las entradas de las tablas fuente, se usa un outer full-join o fusión completa externa de las dos tablas.

El siguiente ejemplo muestra cómo hacer una fusión-completa de las dos tablas:

awk 'BEGIN { FS=OFS="\t" } NR >1 && NR==FNR {a[$1]=$2 ;k[$1];next} FNR > 1 && NR > FNR {b[$1]=$0; k[$1]} END{for(x in k) if (x in a) { print a[x], b[x] } else {print x, "NOT IN mini_tabla_parte1.tsv", b[x]} }' mini_tabla_parte1.tsv mini_tabla_parte2.tsv
## GCF_000017205.1  NOT IN mini_tabla_parte1.tsv    GCF_000017205.1 2007/07/05  ASM1720v1   J. Craig Venter Institute
## GCF_000006765.1  NOT IN mini_tabla_parte1.tsv    GCF_000006765.1 2006/07/07  ASM676v1    PathoGenesis Corporation
## Escherichia coli str. K-12 substr. MG1655    
## Stenotrophomonas maltophilia K279a   GCF_000072485.1 2008/06/10  ASM7248v1   Wellcome Trust Sanger Institute
## GCF_901563825.1  NOT IN mini_tabla_parte1.tsv    GCF_901563825.1 2019/05/29  SB3355_SG266_Ko4    Institut Pasteur
## Stenotrophomonas sp. ZAC14D2_NAIMI4_7    GCF_003086675.1 2018/05/03  ASM308667v1 CCG-UNAM
## Stenotrophomonas maltophilia D457    GCF_000284595.1 2012/04/11  ASM28459v1  University of Valencia
## Stenotrophomonas sp. YAU14A_MKIMI4_1 GCF_003086855.1 2018/05/03  ASM308685v1 CCG-UNAM
## GCF_000534095.1  NOT IN mini_tabla_parte1.tsv    GCF_000534095.1 2014/02/03  Ente_aero_UCI_47_V1 Broad Institute
## GCF_004343645.1  NOT IN mini_tabla_parte1.tsv    GCF_004343645.1 2019/03/11  ASM434364v1 Aarhus University
  • Comentarios y preguntas
    • vean cómo en este full-join, se imprimen todos los registros de ambas tablas
    • ¿qué hacen NR > 1 y FNR > 1?
    • ¿qué hace el condicional if-else en el bloque END{}?
  • Reto de programación
    • modifica el código mostrado arriba para escribir un outer left-join, es decir, una fusión que contenga a todas las entradas de la primera tabla
    • escribe también el código para hacer un right-join, es decir, una fusión que contenga a todos los registros de la segunda tabla

12.6.7 Uso de \(hashes\) para concatenar secuencias en dos o más archivos FASTA

Dados unos archivos de secuencias en formato FASTA, queremos conatenar las secuencias para generar una supermatriz o alineamiento concatenado. Cada uno contiene la secuencia de un gen distinto, para un conjunto de organismos identificados por la etiqueta “especieX”.

Idealmente queremos que en la supermatriz tengamos sólo a las secuencias de los organismos presentes en todos los archivos. Exploremos ahora su contenido:

echo ">>> mini_fasta.fst <<< "
cat mini_fasta.fst
echo
echo ">>> mini_fasta2.fst <<< "
cat mini_fasta2.fst
echo
echo ">>> mini_fasta3.fst <<< "
cat mini_fasta3.fst
## >>> mini_fasta.fst <<< 
## >especie12
## ATACCGACCATTAC
## TTAGGAACCCAGGC
## >especie2
## CCAGTAGTCGAGGC
## AGCAGCTTCCATAT
## >especie3
## CCAGGGCCCATATT
## ATACCGACCATTAC
## >especie4
## GCATAATCCACCAT
## GCACGAATGCAGAC
## >especie5
## GGGGTACCCATTTA
## CCCCCCCTTTTTAT
## 
## >>> mini_fasta2.fst <<< 
## >especie2
## ttacaccaggc
## aattccaccaa
## >especie5
## taaacacacca
## ccaccaggtat
## >especie3
## ccttccngcac
## ttcatccaagc
## >especie4
## ttcggccatta
## ccattaacaat
## >especie1
## ataccaggaca
## ctcaggcacct
## 
## >>> mini_fasta3.fst <<< 
## >especie6
## GGCATACCCATTTA
## GGCCCCCTTTTTAT
## >especie3
## ATGGCACCATCGAC
## TTAGGAACCCATTC
## >especie1
## TTAACAGTCGAGGC
## AGCAGCTTCGCACC
## >especie2
## TGACGGCCCATATT
## ATACCGACCATTAC
## >especie5
## TCATCATCCACCAT
## GCACGAATGCAGAC
## >especie7
## CAAACCCTCATTTA
## GGCCACTACTTTAT
## >especie4
## TTTTTACCCATTTA
## CCCCCCCTTTTTAT
  • Noten lo siguiente de la estructura de los archivos FASTA mostrados arriba:
    • que las secuencias vienen en distinto orden y sus etiquetas no están presentes en todos los archivos (por ejemplo el archivo 1 tiene la especie12)
    • que el archivo mini_fasta3.fst tiene 7 secuencias, mientras que los demás sólo 5

12.6.7.1 Un primer intento de concatenar las secuencias de los tres archivos - versión 1 (fallida)

En esta versión seguimos la “inspiración” de los ejemplos anteriores de fusión de tablas, haciendo uso de FNR == NR.

#     INICIALIZACION                     PATRON-1                               ACCION-1                        PATRON-2                        ACCION-1                               END{ IMPRIME¨}
awk 'BEGIN{RS=">"; FS="\n"; OFS="\n"} NR > 1 && FNR == NR { s=""; h=$1; for (i=2; i<=NF; i++) s=s$i; seqs[h]=s;} NR > FNR {s=""; h=$1; for (i=2; i<=NF; i++) s=s$i; seqs[h]=seqs[h]s; } END{ for (h in seqs) if(h){ print ">"h, seqs[h] } } ' mini_fasta.fst mini_fasta2.fst mini_fasta3.fst 
## >especie2
## CCAGTAGTCGAGGCAGCAGCTTCCATATttacaccaggcaattccaccaaTGACGGCCCATATTATACCGACCATTAC
## >especie3
## CCAGGGCCCATATTATACCGACCATTACccttccngcacttcatccaagcATGGCACCATCGACTTAGGAACCCATTC
## >especie4
## GCATAATCCACCATGCACGAATGCAGACttcggccattaccattaacaatTTTTTACCCATTTACCCCCCCTTTTTAT
## >especie5
## GGGGTACCCATTTACCCCCCCTTTTTATtaaacacaccaccaccaggtatTCATCATCCACCATGCACGAATGCAGAC
## >especie6
## GGCATACCCATTTAGGCCCCCTTTTTAT
## >especie7
## CAAACCCTCATTTAGGCCACTACTTTAT
## >especie12
## ATACCGACCATTACTTAGGAACCCAGGC
## >especie1
## ataccaggacactcaggcacctTTAACAGTCGAGGCAGCAGCTTCGCACC
  • Comentarios y preguntas
    • ¿qué ha salido mal?
    • ¿porqué ha salido mal?
    • ¿qué condición adicional deberíamos de imponer para imprimir el FASTA concatenado?

12.6.7.2 Una solución más general para concatenar múltiples archivos FASTA - versión 2

Necesitamos otra aproximación. La siguiente es una solución general y robusta para resolver el problema de concatenar secuencias comunes almacenadas en distintos archivos, típico de un análisis filogenómico, en la que no importa el orden en el que se lean los archivos, si las secuencias están ordenadas o no, y si faltan o sobran secuencias en los diferentes archivos.

  • Estrategia
    • contar las instancias en las que encontramos cada identificador id de secuencias, guardándolas en el \(hash\) \(k\) con el código k[$1]++
    • imprimir en el bloque END{} sólo aquellas llaves para las que su cuenta en \(k\) sea igual al número de archivos leídos, almacenados en ARGC-1. (Nota: \(ARGC\) guarda el número de argumentos pasados al \(script\) + 1, como se explicará en la siguiente sección)
awk 'BEGIN{RS=">"; FS="\n"; OFS="\n"} NR > 1 {s=""; h=$1; k[$1]++; for (i=2; i<=NF; i++) s=s$i; seqs[h]=seqs[h]s;} END{ for (h in seqs) if(k[h] == ARGC-1) { print ">"h, seqs[h] } }' mini_fasta*fst
## >especie2
## ttacaccaggcaattccaccaaTGACGGCCCATATTATACCGACCATTACCCAGTAGTCGAGGCAGCAGCTTCCATAT
## >especie3
## ccttccngcacttcatccaagcATGGCACCATCGACTTAGGAACCCATTCCCAGGGCCCATATTATACCGACCATTAC
## >especie4
## ttcggccattaccattaacaatTTTTTACCCATTTACCCCCCCTTTTTATGCATAATCCACCATGCACGAATGCAGAC
## >especie5
## taaacacaccaccaccaggtatTCATCATCCACCATGCACGAATGCAGACGGGGTACCCATTTACCCCCCCTTTTTAT
  • comentarios y preguntas
    • noten que contamos las instancias de cada identificador de secuencia como llave del hash \(k\) con el código k[$1]++ de todos los archivos recibidos como argumentos
    • ¿qué hace el código seqs[h]=seqs[h]s?
    • ¿qué hace el código if(k[h] == ARGC-1) { print “>”h, seqs[h] }?
    • noten que podemos pasar múltiples archivos al script usando expansión de caracteres con awk ‘cógigo’ mini_fasta*fst, lo cual es ideal
    • noten también que en este caso las secuencias se concatenan en el orden de expansión de las cadenas mini_fasta*fst, que pueden visualizar con el comando ls mini_fasta*fst
ls mini_fasta*fst
## mini_fasta2.fst
## mini_fasta3.fst
## mini_fasta.fst

Si quieren concatenar en un los archivos en un cierto orden, deben pasarlos al \(script\) in dicho orden:

awk 'BEGIN{RS=">"; FS="\n"; OFS="\n"} NR > 1 {s=""; h=$1; k[$1]++; for (i=2; i<=NF; i++) s=s$i; seqs[h]=seqs[h]s;} END{ for (h in seqs) if(k[h] == ARGC-1) { print ">"h, seqs[h] } }' mini_fasta3.fst mini_fasta2.fst
## >especie2
## TGACGGCCCATATTATACCGACCATTACttacaccaggcaattccaccaa
## >especie3
## ATGGCACCATCGACTTAGGAACCCATTCccttccngcacttcatccaagc
## >especie4
## TTTTTACCCATTTACCCCCCCTTTTTATttcggccattaccattaacaat
## >especie5
## TCATCATCCACCATGCACGAATGCAGACtaaacacaccaccaccaggtat
## >especie1
## TTAACAGTCGAGGCAGCAGCTTCGCACCataccaggacactcaggcacct

En la sección que sigue, aprenderemos a escribir \(scripts\) de \(awk\) y los detalles sobre la variable \(ARGC\) que lleva la cuenta del número de parámetros pasados al \(script\) empleada en el código anterior.

12.7 Estructuras de datos complejas

Uno de los atributos que hacen muy poderoso a AWK como lenguaje es su implementación de estructuras complejas de datos. AWK POSIX estándar implementa los arreglos multidimensionales y GAWK implmententa verderos arreglos de arreglos

12.7.1 Arreglos multidimensionales

En los arreglos multidimensionales de AWK estándar cada elemento es identificado por una secuencia de índices, los cuales son concatenados como una cadena usando el separador “\034”, que codifica para un caracter no imprimible y guardado en la variable SUBSEP

Así por ejemplo, el arreglo multidimensional amd amd[1,2]=“valor12” tiene los índices 1 y 2, y podemos accesar su valor con amd[1,2] o amd[1 SUBSEP 2] o amd[1 “\034” 2], o usando un bucle for, como de costumbre.

awk 'BEGIN {
  amd[1,2]="valor12"
  print "using 1, 2:", amd[1, 2]
  print "using SUBSEP:", amd[1 SUBSEP 2]
  print "using \\034:", amd[1 "\034" 2]
  for (k in amd) print "k=<"k">", "valor="amd[k]
}'
## using 1, 2: valor12
## using SUBSEP: valor12
## using \034: valor12
## k=<12> valor=valor12

Veamos ahora cómo leer una matriz en un arreglo bidimensional y cómo tra[n]sponerla

{
     # true only in the first line, when max_nf = 0; 
     # don't need to repeat on each row|record, to speed up
     if (max_nf < NF)
          max_nf = NF
     max_nr = NR
     
     # read the original matrix into a 1-dimensional vector
     # indexed by NF and NR
     for (x = 1; x <= NF; x++)
     # read each col-value, row by row, into 
       # the vector array, indexed by row/record
           vector[x, NR] = $x
}

END {
        # for each original col-value (x), print them out
        # using row indices (y) as the matrix row-dimension, 
        # to transpose the matrix
      for (x = 1; x <= max_nf; x++) {
          for (y = 1; y <= max_nr; y++)
                printf("%s ", vector[x, y])
          printf("\n")
      }
}
dada la siguiente matriz, con índices (i,j), donde i==fila y j==columna
11 12 13 14
21 22 23 24
31 32 33 34
el código mostrado arriba imprime su tra[n]spuesta como:
11 21 31 
12 22 32 
13 23 33 
14 24 34
  • Retos de programación
    • ¿Qué modificarías del código para que imprima la tabla original?
    • ¿y cómo para que rote la tabla original 90° en sentido horario?

12.7.2 Arreglos de arreglos

gawk implementa verdaderos arreglos de arreglos. Es decir, un arreglo puede contener como valores a otros arreglos. Los elementos de un sub-arreglo contenido en el arreglo principal son accesados con sus propios índices, encerrados igualmente en corchetes.

awk 'BEGIN {
    # asignamos valores a un arreglo de arreglos
    a["gen1"]["codones"]="ATG CCG TGT TAA"
    a["gen1"]["prot"]=" M   P   C   *"
    a["gen2"]["codones"]="GTG TCG TGT TGA"
    a["gen2"]["prot"]=" V   S   C   *"

    # imprimimos los valores
    for (g in a) {
       print a[g]["codones"]
       print a[g]["prot"]
    }
}'
## ATG CCG TGT TAA
##  M   P   C   *
## GTG TCG TGT TGA
##  V   S   C   *

Veremos el uso de hashes de hashes para guardar valores extraídos de la anotación de CDSs a partir de archivos GenBank en el script extract_CDSs_from_GenBank.awk, como muestra el siguiente fragmento de código:

     # All the CDS's-relevant information parsed from the annotations 
     #   will be stored in the CDSs_AoA (Array of Arrays)
     CDSs_AoA[geneID]["l"] = locus_tag
     CDSs_AoA[geneID]["LOCUS"] = locus_id
     CDSs_AoA[geneID]["s"] = start
     CDSs_AoA[geneID]["e"] = end
     CDSs_AoA[geneID]["c"] = complflag
     CDSs_AoA[geneID]["pseudo"] = pseudoflag

Para recorrer un arreglo de arreglos, podemos usar la sintaxis ya conocida de for (i in array) { code }, como se muestra segidamente para un arreglo bidimensional:

for (i in array)
    for (j in array[i])
        print array[i][j]

Cuando vamos a atravesar un arreglo multidimensional podemos hace uso de la función isarray() para verificar si un elemento de un arreglo es un arreglo.

for (i in array) {
    if (isarray(array[i])) {
        for (j in array[i]) {
            print array[i][j]
        }
    }
    else
        print array[i]
}

Los arreglos de arreglos de gawk pueden tener una profundidad arbitraria y cada sub-arreglo y el arreglo principal pueden ser de longitud diferente. Es más, los elementos de un arreglo y/o sub-arreglo no tienen que ser todos del mismo tipo.

En resumen, el arreglo principal y cualquiera de sus sub-arreglos pueden o no tener una estructura rectangular y contener elementos de diferentes tipos.

Los arreglos de arreglos de gawk son por tanto estructuras de datos muy versátiles y útiles, como demostraremos en el script extract_CDSs_from_GenBank.awk al final de la sección de awk.

12.8 \(awk\) scripts - aspectos básicos

Cuando el programa que escribimos es un poco largo y/o de interés general, es decir, que se vaya a usar regularmente, conviene guardarlo en un archivo. En este bloque les presentaré un primer \(script\) que integra múltiples aspectos presentados anteriormente y varios elementos nuevos del lenguaje. Pero más importante aún, el \(script\) count_genome_features_for_taxon.awk es un primer ejemplo de los detalles que como programadores debemos cuidar para que el código sea robusto y amigable con el usuario, para facilitarle su uso.

Para que un programa sea útil, debe resolver eficientemente una clase o tipo de problemas o acciones que se realicen rutinariamente. En secciones anteriores hemos mostrado múltiples ejemplos de uso de \(awk\) para parsear la tabla assembly_summary.txt.gz. Dado que esta tabla provee información clave sobre los ensambles disponibles en la división RefSeq de GenBank, podría ser útil tener un \(script\) que nos provea de estadísticas de resumen (conteos) para campos específicos de la misma y para un taxon de nuestro interés particular.

En la sección sobre uso del operador ++ para contar ocurrencias de un patrón vimos dos ejemplos muy parecidos, uno para contar el número de ensambles liberados por año para un taxon y otro para contar el número de genomas para un taxon por nivel del ensamble. No sería una buena práctica guardarlos como \(scripts\) individuales. Más bien debemos pensar en cómo escribir el código para que un solo script puedan calcular estas estadísticas de resumen sobre cualquier campo relevante de la tabla. El \(script\) lo llamaremos count_genome_features_for_taxon.awk y haremos su uso flexible y práctico al pasarle dos argumentos desde la línea de comandos: <tax=TAXON> <f=numero_de_columna>.

El \(script\) count_genome_features_for_taxon.awk implementa una gama de elementos sintácticos, como funciones integradas y variables reservadas, la mayoría de las cuales hemos descrito en secciones previas. Las siguientes subsecciones explican brevemente los que no hemos visto en detalle aún, como estructuras de control.

12.8.1 Paso de variables, argumentos y archivos a \(scripts\) de \(awk\) y uso de los arreglos \(ARGC\) y \(ARGV\)

\(awk\) define automáticamente las variables ARGC y ARGV para proveer al programa información sobre los argumentos pasados al mismo.

  • Los argumentos pasados al programa desde la línea de comandos después del ‘bloque de código’ se guardan en \(ARGV\)
  • Al contrario que la mayoría de los arreglos de \(awk\), ARGV se indexa desde 0 .. ARGC - 1, como se muestra en el siguiente ejemplo:
awk 'BEGIN { print "# ARGV contiene:"; for (i=0; i < ARGC; i++) print ARGV[i]; print "\n# ARGC == lenght(ARGV) ==", length(ARGV), "\nARGC =", ARGC}' archivo1 archivo2
## # ARGV contiene:
## awk
## archivo1
## archivo2
## 
## # ARGC == lenght(ARGV) == 3 
## ARGC = 3
  • Comentarios
    • \(ARGV[0]\) contiene a \(awk\) y \(ARGC\) es igual al número de argumentos recibidos + 1 o \(lenght(ARGV)\), es decir, al número de elementos en \(ARGV\)
    • Noten que el programa en sí no es guardado en ARGV

12.8.1.1 Paso de variables, argumentos y archivos a \(scripts\) de \(awk\)

Veamos el siguiente ejemplo muy similar al anterior, pero pasándole al \(script\) variables y argumentos, además de los dos archivos.

  • Noten que:
    • no se guardan en ARGV/ARGC las opciones y argumentos que se le asignan al programa antes de definir el bloque de código, como en awk –posix -F”\t” -v var=valor ‘código’ var2=valor2 archivo1 archivo2
    • las variables u opciones asignadas después del bloque de código no están disponibles en un bloque BEGIN{} ya que éste se ejecuta antes de leer argumentos
awk -v var1=123 -v var2=$RANDOM 'BEGIN { print "# ARGV contiene:"; for (i=0; i < ARGC; i++) print ARGV[i]; print "\n# ARGC == lenght(ARGV) ==", length(ARGV), "\nARGC =", ARGC; print "["param1"]", "["param2"]", "["var1"]", "["var2"]"}' param1=VALOR1 param2=valor2 archivo1 archivo2
## # ARGV contiene:
## awk
## param1=VALOR1
## param2=valor2
## archivo1
## archivo2
## 
## # ARGC == lenght(ARGV) == 5 
## ARGC = 5
## [] [] [123] [23150]
  • Comentarios y preguntas
    • Noten que corremos todo el código en un bloque \(BEGIN\) ya que no existen los archivos archivo1 archivo2
    • Noten que podemos pasar variables con \(-v\ nombreVar=valorVar\) antes del ‘bloque de código’
    • podemos pasar argumentos al \(script\) después del ‘bloque de código’, con la sintaxis \(arg1=valorArg1\)
    • noten que podemos pasarle variables del \(Shell\) a \(awk\) con la sintaxis \(varAWK=\$varShell\)
    • ¿porqué imprime dos [] []?
12.8.1.1.1 El \(script\) print_vars_and_params.awk y su lladada con awk -f script

Como el \(script\) anterior empieza a hacerse un poco largo, podemos escribirlo en un archivo que llamaremos print_vars_and_params.awk, lo que nos permite comentarlo y escribir las sentencias en líneas individuales para una mejor legibilidad.

BEGIN { 
    print "# ARGV contiene:"
    for (i=0; i < ARGC; i++) print ARGV[i]
    print "\n# ARGC == lenght(ARGV) =>", length(ARGV), "\nARGC =", ARGC
    print "["param1"]", "["param2"]", "["var1"]", "["var2"]"
}
  • Comentarios y preguntas
    • noten que el \(script\) sólo contiene el código del bloque BEGIN{}
    • para una lectura más fácil, usamos algo de indentación y separamos las sentencias en renglones individuales
    • noten que al hacerlo, ya no es necesario usar ‘;’ al final de cada sentencia; éstas van separadas por saltos de línea


  • ejemplos de llamada al \(script\) print_vars_and_params.awk
    • ejemplo 1
awk -v var1=$RANDOM -f ./print_vars_and_params.awk param1=valParam1 file1
## # ARGV contiene los siguientes argumentos posicionales:
## awk
## param1=valParam1
## file1
## 
## # ARGC == lenght(ARGV) => 3 
## ARGC = 3
## [] [] [20746] []
  • Comentario
    • noten el uso de awk -f ./print_vars_and_params.awk para llamar al archivo que contiene el código a ejecutar por el intérprete de comandos \(awk\)



awk -v var1=123 -v var2=$RANDOM -f ./print_vars_and_params.awk param1=valParam1 file1 file2 param2=valParam2 file3
## # ARGV contiene los siguientes argumentos posicionales:
## awk
## param1=valParam1
## file1
## file2
## param2=valParam2
## file3
## 
## # ARGC == lenght(ARGV) => 6 
## ARGC = 6
## [] [] [123] [29431]
  • Comentario
    • en este caso el param2 afectaría sólo al procesamiento del archivo file3, ya que se procesa después de file2

12.8.1.2 Otro ejemplo de paso de variables, argumentos y archivos: el \(script\) showargs.awk

Veamos el contenido del \(script\) showargs.awk que consta de 3 secciones:

  1. bloque BEGIN{}
  2. programa de procesamiento del archivo
  3. bloque END{}
cat showargs.awk
## # showargs.awk v0.3
## # bloque BEGIN{} de inicializacion
## BEGIN {
##    RS=">"
##    FS="\n"
##    printf "BEGIN block: A=%d, B=%s, NF=%s, NR=%s, FNR=%s\n", A, B, NF, NR, FNR
##    for (i=0; i < ARGC; i++)
##       printf "\tARGV[%d] = %s\n", i, ARGV[i] 
##    print "End of BEGIN block\n--------------------------------\n"
## }
## 
## # filtro
## NR > 1 && $1 == B
## 
## # bloque END{} de procesamiento final
## END { printf "END block1: A=%d, B=%s, NF=%s, NR=%s, FNR=%s\n", A, B, NF, NR, FNR }
## END { print  "\nEND block2:"; for (i=1; i<=NF; i++) print $i }
  • llamada a showargs.awk con dos variables (A y B) pasadas como opciones y argumento, respectivamente, además de un archivo FASTA pasado también como argumento
    • veamos el archivo mini_fasta.fst
cat mini_fasta.fst
## >especie12
## ATACCGACCATTAC
## TTAGGAACCCAGGC
## >especie2
## CCAGTAGTCGAGGC
## AGCAGCTTCCATAT
## >especie3
## CCAGGGCCCATATT
## ATACCGACCATTAC
## >especie4
## GCATAATCCACCAT
## GCACGAATGCAGAC
## >especie5
## GGGGTACCCATTTA
## CCCCCCCTTTTTAT
  • corramos el script
awk -v A=1 -f ./showargs.awk B=especie2 mini_fasta.fst
## BEGIN block: A=1, B=, NF=0, NR=0, FNR=0
##  ARGV[0] = awk
##  ARGV[1] = B=especie2
##  ARGV[2] = mini_fasta.fst
## End of BEGIN block
## --------------------------------
## 
## especie2
## CCAGTAGTCGAGGC
## AGCAGCTTCCATAT
## 
## END block1: A=1, B=especie2, NF=4, NR=6, FNR=6
## 
## END block2:
## especie5
## GGGGTACCCATTTA
## CCCCCCCTTTTTAT
  • Preguntas
    • ¿porqué A=1 pero B= en el bloque BEGIN{}?
    • ¿porqué valen 0 NF, NR y FNR en el bloque BEGIN{}?
    • ¿porqué se imprime la secuencia de especie2?
    • ¿porqué B=especie2 en bloque END{}?
    • ¿porqué NF == 4 en el bloque END{}?
    • ¿porqué NR == 6 en el bloque END{}?
    • ¿porqué FNR == 6 en el bloque END{}?
  • llamada a showargs.awk con dos variables (A y B) pasadas como opciones y argumentos
    • noten que la variable B la pasamos dos veces como argumentos
awk -v A=1 -f ./showargs.awk B=especie2 mini_fasta.fst B=especie1 mini_fasta2.fst  
## BEGIN block: A=1, B=, NF=0, NR=0, FNR=0
##  ARGV[0] = awk
##  ARGV[1] = B=especie2
##  ARGV[2] = mini_fasta.fst
##  ARGV[3] = B=especie1
##  ARGV[4] = mini_fasta2.fst
## End of BEGIN block
## --------------------------------
## 
## especie2
## CCAGTAGTCGAGGC
## AGCAGCTTCCATAT
## 
## especie1
## ataccaggaca
## ctcaggcacct
## 
## END block1: A=1, B=especie1, NF=4, NR=12, FNR=6
## 
## END block2:
## especie1
## ataccaggaca
## ctcaggcacct
  • Preguntas:
    • ¿porqué se imprimen ahora dos secuencias, en el orden especie2, especie1?
    • ¿de qué archivos salen especie1 y especie2?
    • ¿porqué FNR == 6 pero NR=12 en el bloque END{}?
    • ¿porqué B=especie1 en el bloque END{} #1?

12.8.1.3 Condicionales: if; if-else if; if-else if-else

Los condicionales son estructuras de control fundamentales para controlar el flujo de un programa en función de si son ciertas o falsas determinadas condiciones. Sólo si una condición es cierta, se ejecuta la acción subsiguiente, según la siguiente sintaxis:

  • if(condición){acción}

Se pueden evaluar varias condiciones secuencialmente con la siguiente sintaxis:

  • if(condición){acción}else if(condición){acción}else if(condición){acción} …

De no cumplirse ninguna de ellas, podemos indicar una acción a realizarse en dicho caso con la siguiente sintaxis:

  • if(condición){acción}else if(condición){acción}else if(condición){acción}else{acción}

Veamos un ejemplo sencillo:

awk 'BEGIN { n = 3; arg=ARGV[1]; gsub(/n=/, "", arg);  if (n == arg) { print n, "==", arg } else { print n, "!=", arg }} ' n=2
awk 'BEGIN { n = 3; arg=ARGV[1]; gsub(/n=/, "", arg);  if (n == arg) { print n, "==", arg } else { print n, "!=", arg }} ' n=3
## 3 != 2
## 3 == 3
  • preguntas:
    • ¿qué hace la expresión gsub(/n=/, ““, arg);?

12.8.2 El \(script\) count_genome_features_for_taxon.awk

El \(script\) count_genome_features_for_taxon.awk tiene por finalidad mostrarles un ejemplo de código un poco más complejo y extenso con el fin de:

  1. integrar y revisar los diferentes aspectos sintácticos del lenguaje discutidos hasta ahora
  2. mostrar cómo llamar un \(script\) y pasarle argumentos desde la línea de comandos usando la siguiente sintaxis:
  • awk -f script_name arg1=x arg2=y

Veamos el \(script\) completo y estudiemos sus secciones:

cat ./count_genome_features_for_taxon.awk
## # AUTHOR: Pablo Vinuesa, CCG-UNAM; https://www.ccg.unam.mx/~vinuesa/; twitter: @pvinmex
## # source: https://github.com/vinuesa/intro2linux
## # VERSION:0.1_2020-11-1
## # AIM: get summary statistics (counts) of the features found in user-provided field and taxon
## #      in NCBI's assembly_summary.txt.gz genome assembly table
## # ---------------------- #
## # >>> Initialization <<< #
## # ---------------------- #
## BEGIN { 
##     # ARGC counts the arguments; ARGV[0] is awk
##     if(ARGC < 3)   # needs two positional arguments 
##        Usage_Exit()
##    
##     # we need to set FS"\t" for propper parsing of assembly_summary.txt.gz, 
##     # which is a tsv file; set also OFS=FS
##     FS=OFS="\t"
##         
##     # Check that user provides correct arguments
##     #   use sub() to substitute the 'tax=' and 'f=' prefixes for ""
##     #   to retain only the option values passed on the command line   
##     tax = ARGV[1]
##     if( sub(/tax=/, "", tax) ) {       
##         sub(/tax=/, "", tax)
##     } 
##     else {
##        print "ERROR: you need to provide tax=TAXON as the first argument to the script!"
##        print "       ==> you provided", tax
##        print ""
##        Usage_Exit()   
##     }
##     
##     idx = ARGV[2]
##     if ( sub(/f=/, "", idx) ) {
##          sub(/f=/, "", idx)
##     }
##     else {
##        print "ERROR: you need to provide f=<table_field_number> as the second argument to the script!"
##        print "       ==> you provided", idx
##        print ""
##        Usage_Exit()   
##     }
##     
##     # check that meaningful column indexes are passed to the script
##     if ( idx !~ /^(2|5|11|12|13|14|15)$/ ) { Usage_Exit() }
##         
##     # sort array numeric output values in ascending order
##     PROCINFO["sorted_in"] = "@val_num_asc"
## }
## 
## # ------------ #
## # >>> MAIN <<< #
## # ------------ #
## # 1. fill the hash features with the field names, indexed by consecutive integers/positions
## #      using the split() function applied to record # 2 (the column header) 
## #      to add the header field names to consecutive numeric indices such as:
## #      features[1] = "assembly_accession"; features[2] = "bioproject"; ...
## NR==2 { split( $0, features, "\t", seps ) } 
## 
## # 2. filter taxon fields ($8) that match /tax/ 
## $8 ~ tax { 
##     # globally susbstitute months and days by nothing; 
##     # i.e: change 2019/10/04 for 2019
##     gsub( /\/.*$/, "" )
## 
##     # we use a hash named count_f, using the feature index ($idx) as the hash's key,
##     # to hold counts of the associated values as we encounter them in each new record
##     count_f[$idx]++
## }
## 
## # 3. END{} block. print the hash values in count_f indexed by f
## END {
##        #for (i in features)  { print i, features[i] }
##        if ( idx ~ /^(2|5|11|12|13|14|15)$/ ) {
##            print "taxon", features[idx], "n_genomes"
##            for ( f in count_f ) 
##               if ( f > 0 || f != "" ) 
##           print tax, f, count_f[f]
##        }          
## }
## 
## # --------------------------- #
## # >>> Function definition <<< #
## # --------------------------- #
## function Usage_Exit() {
##       # the function is called if the proper arguments are not passed to the script
##       print "# USAGE: zcat assembly_summary.txt.gz | awk -f count_genome_features_for_taxon.awk tax=Pseudomonas f=12"
##       print "#    note1: need to pass tax='TAXON' and f=<'field number'[2|5|11-15]>"
##       print "#           as positional arguments at the end of the awk call, as shown" 
##       print "#    note2: if using the gzip-compressed source file, you need to pipe it into awk with zcat,"
##       print "#           as shown above" 
##       print "# AIM: print a table with the the number of features found in assembly_summary.txt.gz for a taxon" 
##       print "#      both provided as arguments to the program in the format tax=TAXON f=feature_field_number" 
##       exit;
## }

Ahora exploremos diferentes llamadas al \(script\). Debes revisar la salida de cada una y cotejarla con el código, asegurándote que entiendes porqué se genera cada una.

  • llamada al \(script\) count_genome_features_for_taxon.awk con < 2 argumentos imprime una ayuda
zcat assembly_summary.txt.gz | awk -f ./count_genome_features_for_taxon.awk
## # USAGE: zcat assembly_summary.txt.gz | awk -f count_genome_features_for_taxon.awk tax=Pseudomonas f=12
## #    note1: need to pass tax='TAXON' and f=<'field number'[2|5|11-15]>
## #           as positional arguments at the end of the awk call, as shown
## #    note2: if using the gzip-compressed source file, you need to pipe it into awk with zcat,
## #           as shown above
## # AIM: print a table with the the number of features found in assembly_summary.txt.gz for a taxon
## #      both provided as arguments to the program in the format tax=TAXON f=feature_field_number
  • el \(script\) sólo se ejecuta si le pasamos los índices (números de columnas) adecuados de la tabla assembly_summary.txt.gz
zcat assembly_summary.txt.gz | awk -f ./count_genome_features_for_taxon.awk tax=Pseudomonas f=1
## # USAGE: zcat assembly_summary.txt.gz | awk -f count_genome_features_for_taxon.awk tax=Pseudomonas f=12
## #    note1: need to pass tax='TAXON' and f=<'field number'[2|5|11-15]>
## #           as positional arguments at the end of the awk call, as shown
## #    note2: if using the gzip-compressed source file, you need to pipe it into awk with zcat,
## #           as shown above
## # AIM: print a table with the the number of features found in assembly_summary.txt.gz for a taxon
## #      both provided as arguments to the program in the format tax=TAXON f=feature_field_number
  • el \(script\) sólo revisa que el usuario use la sintaxis requerida para pasar argumentos desde la línea de comandos
zcat assembly_summary.txt.gz | awk -f ./count_genome_features_for_taxon.awk Pseudomonas 5
## ERROR: you need to provide tax=TAXON as the first argument to the script!
##        ==> you provided  Pseudomonas
## 
## # USAGE: zcat assembly_summary.txt.gz | awk -f count_genome_features_for_taxon.awk tax=Pseudomonas f=12
## #    note1: need to pass tax='TAXON' and f=<'field number'[2|5|11-15]>
## #           as positional arguments at the end of the awk call, as shown
## #    note2: if using the gzip-compressed source file, you need to pipe it into awk with zcat,
## #           as shown above
## # AIM: print a table with the the number of features found in assembly_summary.txt.gz for a taxon
## #      both provided as arguments to the program in the format tax=TAXON f=feature_field_number
  • el campo #5 imprime conteos de refseq_category
zcat assembly_summary.txt.gz | awk -f ./count_genome_features_for_taxon.awk tax=Pseudomonas f=5
## taxon    refseq_category n_genomes
## Pseudomonas  reference genome    4
## Pseudomonas  representative genome   49
## Pseudomonas  na  8906
  • el campo #12 imprime conteos de assembly_level
zcat assembly_summary.txt.gz | awk -f ./count_genome_features_for_taxon.awk tax=Pseudomonas f=12
## taxon    assembly_level  n_genomes
## Pseudomonas  Chromosome  120
## Pseudomonas  Complete Genome 502
## Pseudomonas  Scaffold    2494
## Pseudomonas  Contig  5830
  • el campo #15 imprime conteos de seq_rel_date
zcat assembly_summary.txt.gz | awk -f ./count_genome_features_for_taxon.awk tax=Pseudomonas f=15
## taxon    seq_rel_date    n_genomes
## Pseudomonas  2003    1
## Pseudomonas  2005    3
## Pseudomonas  2007    4
## Pseudomonas  2008    4
## Pseudomonas  2006    6
## Pseudomonas  2009    6
## Pseudomonas  2010    15
## Pseudomonas  2011    31
## Pseudomonas  2012    94
## Pseudomonas  2013    274
## Pseudomonas  2014    612
## Pseudomonas  2017    861
## Pseudomonas  2019    873
## Pseudomonas  2016    1111
## Pseudomonas  2015    1218
## Pseudomonas  2018    3833

12.8.3 Shebang line #!/usr/bin/awk y scripts autocontenidos: el \(script\) filter_fasta_sequences.awk

Si colocas la sentencia #!/usr/bin/awk al inicio del archivo (conocida como línea shebang) que contiene tu programa de \(awk\) y lo haces ejecutable con \(chmod\ 755\ script.awk\), podrás ejecutarlo como un programa o \(script\) autocontenido.

La línea shebang indica al \(shell\) con qué intérprete de comandos ha de interpretar los comandos contenidos en el archivo.

Si el \(script\) autocontenido y ejecutable está en uno de los directorios incluidos en $PATH, entonces puedes ejecutarlo simplemente tecleando su nombre, como cualquier otro comando del sistema, desde cualquier directorio del mismo.

Si el \(script\) no está en un directorio del $PATH, entonces debes ejecutarlo con la siguiente sintaxis:

  • si estás en el directorio que contiene al \(script\) ./script.awk opc1=VALOR1 opc2=VALOR2 archivo1 archivo2

  • si estás en un directorio diferente al que contiene al \(script\), debes indicar su localización, por ruta absoluta o relativa /path/al/script.awk opc1=VALOR1 opc2=VALOR2 archivo1 archivo2

Aprenderemos más adelante cómo añadir directorios a \(PATH\).

Exploremos ahora un \(script\) muy sencillo pero sin duda útil para filtrar secuencias de un archivo multifasta, pasándole una cadena de caracteres que el \(script\) usará para filtrar el archivo, imprimiendo sólo aquellos registros que coinciden con la cadena de filtrado.

Veamos el \(script\) filter_fasta_sequences.awk, disponible en el repositorio GitHub - intro2linux.

  • sus permisos
[ -s filter_fasta_sequences.awk ] && ls -l filter_fasta_sequences.awk
## -rwxr-xr-x 1 vinuesa vinuesa 1326 dic  2  2020 filter_fasta_sequences.awk
  • el código
[ -s filter_fasta_sequences.awk ] && cat filter_fasta_sequences.awk
## #!/usr/bin/awk -f
## 
## # AUTHOR: Pablo Vinuesa, @pvinmex, https://www.ccg.unam.mx/~vinuesa/
## # source: https://github.com/vinuesa/intro2linux
## # Usage:  filter_fasta_sequences.awk  <filtering_string>  <multifasta_file>
## #   Read a string from STDIN to filter fasta_file, 
## #   to print out only the subset of the fasta_file 
## #   matching the filtering string
## # NOTE: this is a demo script to teach basic awk programming
## 
## BEGIN {
##          progname = "filter_fasta_sequences.awk"
##          version  = 0.3  # dec 02, 2020
##          
##          if  (ARGC < 2) Usage_Exit(progname, version)
## 
##          # save te filtering string in variable s
##          s = ARGV [1]
##          
##          # delete this first argument with delete.
##   # This is to avoid that in the main block below, 
##   # the command interpreter treats it as a file 
##          delete ARGV [1];
##          
##   RS=">"
## }
## 
## # MAIN; if line matches filtering string, print record ;)
## $0 ~ s { print ">"$0 }
## 
## # function definition
## function  Usage_Exit(prog, vers) {
##  
##    print "# USAGE FOR", prog, "v"vers > "/dev/stderr"
##    print  prog, "<filtering_string>  <multifasta_file>" > "/dev/stderr"
##    print "#   Pass a string as first argument to filter the FASTA_file," > "/dev/stderr"
##    print "#   provided as second argument, printing only records matching the string" > "/dev/stderr"
##    exit;
## }
  • Llamada al \(script\) sin argumentos imprime su ayuda
# como el directorio donde vive el script no está en el path, a pesar de ser ejecutable, 
#  hay que llamarlo indicando el path absoluto o relativo al mismo
./filter_fasta_sequences.awk
## # USAGE FOR filter_fasta_sequences.awk v0.3
## filter_fasta_sequences.awk <filtering_string>  <multifasta_file>
## #   Pass a string as first argument to filter the FASTA_file,
## #   provided as second argument, printing only records matching the string
  • Llamada al \(script\) con los argumentos posicionales
# como el directorio donde vive el script no está en el path, a pesar de ser ejecutable, 
#  hay que llamarlo indicando el path absoluto o relativo al mismo
# Le pasamos el nombre de la cepa BES-1 que está en el archivo recA_Bradyrhizobium_vinuesa.fna
./filter_fasta_sequences.awk BES-1 recA_Bradyrhizobium_vinuesa.fna
## >AY591548.1 Bradyrhizobium canariense bv. genistearum strain BES-1 recombination protein A (recA) gene, partial cds
## ATGAAGCTCGGCAAGAACGACCGCTCGATGGACGTCGAGGCGGTCTCCTCGGGCTCGCTCGGGCTCGACA
## TCGCGCTCGGGATCGGCGGCCTGCCGAAGGGGCGCGTCGTGGAGATCTACGGGCCGGAATCCTCGGGCAA
## GACCACGCTGGCGCTGCACACGGTGGCGGAAGGGCAGAAGAAGGGCGGCATCTGCGCCTTCATCGACGCC
## GAACACGCGCTCGACCCGGTCTATGCGCGCAAGCTGGGCGTCAATATCGACGAACTCCTGATTTCCCAGC
## CGGACACCGGCGAGCAGGCGCTGGAAATCTGCGACACGCTGGTGCGCTCCGGCGCGGTCGACGTGCTGGT
## GATCGATTCGGTCGCGGCCCTGGTGCCGAAGGCCGAGCTCGAGGGCGAAATGGGCGATGCGCTGCCGGGT
## CTGCAGGCGCGTCTGATGAGCCAGGCGCTGCGCAAGCTGACGGCGTCCATCAACAAGTCCAACACCATGG
## TGATCTTCTTCAACCAGATCC
  • pregunta
    • ¿porqué podemos parle el argumento BES-1 (como cadena de filtrado) sin usar una sintaxis como cadena=BES-1 como vimos en el script count_genome_features_for_taxon.awk?
  • Filtra las secuencias sin clasificar (genosp)
# como el directorio donde vive el script no está en el path, a pesar de ser ejecutable, 
#  hay que llamarlo indicando el path absoluto o relativo al mismo
# Le pasamos el nombre de la cepa BES-1 que está en el archivo recA_Bradyrhizobium_vinuesa.fna
# NOTA: filtro la salida con grep '^>' para ver sólo las cabeceras FASTA y evitar que sea demasiado copiosa
#       quítale el grep final, si quieres ver el archivo con sus secuencias
./filter_fasta_sequences.awk genosp recA_Bradyrhizobium_vinuesa.fna | grep '^>'
## >AY653750.1 Bradyrhizobium genosp. beta strain BC-MK1 recombination protein A (recA) gene, partial cds
## >AY591567.1 Bradyrhizobium genosp. alpha strain CIAT3101 recombination protein A (recA) gene, partial cds
## >AY591554.1 Bradyrhizobium genosp. beta strain BC-MK6 recombination protein A (recA) gene, partial cds
## >AY591551.1 Bradyrhizobium genosp. beta strain BRE-1 recombination protein A (recA) gene, partial cds
## >AY591543.1 Bradyrhizobium genosp. beta strain BC-P6 recombination protein A (recA) gene, partial cds
## >AY591540.1 Bradyrhizobium genosp. alpha bv. genistearum strain BC-C1 recombination protein A (recA) gene, partial cds
  • uso de filter_fasta_sequences.awk dentro de una tubería de comandos
# el script se puede llamar también desde dentro de una tubería, es decir,
# pasándole el STDOUT de un comando previo entubado con | al STDIN del script
# NOTA: filtro la salida con grep '^>' para ver sólo las cabeceras FASTA y evitar que sea demasiado copiosa
#       quítale el grep final, si quieres ver el archivo con sus secuencias
cat recA_Bradyrhizobium_vinuesa.fna | ./filter_fasta_sequences.awk MAM  | grep '^>'
## >AY653749.1 Bradyrhizobium canariense strain BC-MAM12 recombination protein A (recA) gene, partial cds
## >AY653748.1 Bradyrhizobium canariense strain BC-MAM11 recombination protein A (recA) gene, partial cds
## >AY653747.1 Bradyrhizobium canariense strain BC-MAM9 recombination protein A (recA) gene, partial cds
## >AY653746.1 Bradyrhizobium canariense strain BC-MAM8 recombination protein A (recA) gene, partial cds
## >AY653745.1 Bradyrhizobium canariense strain BC-MAM6 recombination protein A (recA) gene, partial cds
## >AY653744.1 Bradyrhizobium canariense strain BC-MAM2 recombination protein A (recA) gene, partial cds
## >AY591547.1 Bradyrhizobium canariense bv. genistearum strain BC-MAM5 recombination protein A (recA) gene, partial cds
## >AY591546.1 Bradyrhizobium canariense bv. genistearum strain BC-MAM1 recombination protein A (recA) gene, partial cds

12.8.4 Filtra un archivo multifasta usando un \(hash\) y una lista de etiquetas con get_sequences_from_list.awk

Este problema es una variante del anterior. En este caso queremos filtrar el multifasta usando un archivo que contiene la lista de etiquetas de nuestro interés.

Para ello vamos a usar el script get_sequences_from_list.awk, que recibe dos argumentos:

get_sequences_from_list.awk

El archivo de etiquetas debe tener el siguiente formato:
>etiqueta1
>etiqueta2
> ...

Veamos las etiquetas (archivo seq.list) que queremos extraer del archivo mini_fasta.fst

cat seq.list
## >especie5
## >especie3
## >especie4

Estudiemos ahora el \(script\) get_sequences_from_list.awk

cat get_sequences_from_list.awk
## # model FASTA
## BEGIN{ RS=">"; FS="\n" }
## 
## # process label_list
## NR > 1 && NR == FNR { labels_h[$1]=1 }
## 
## # loop over hash and filter fasta records
## NR > FNR { for (l in labels_h) 
##                if ($1 ~ l) printf ">%s", $0 
## }

Corramos el \(script\) get_sequences_from_list.awk, pasándole los dos nombres de archivo como argumentos, primero la lista, luego el FASTA a filtrar

awk -f get_sequences_from_list.awk seq.list mini_fasta.fst
## >especie3
## CCAGGGCCCATATT
## ATACCGACCATTAC
## >especie4
## GCATAATCCACCAT
## GCACGAATGCAGAC
## >especie5
## GGGGTACCCATTTA
## CCCCCCCTTTTTAT

Noten que ¡el orden de las secuencias en la salida del \(script\) get_sequences_from_list.awk no coincide con el de la lista! Ello se debe a que, como indicamos en la sección de introducción de \(arreglos\), los \(hashes\) o \(arreglos\ asociativos\) no guardan los índices o llaves en un orden particular.

12.8.4.1 Ordenar las secuencias de un archivo en formato FASTAB según las entradas de una lista en un bucle \(while\)

Si necesitan ordenar las secuencias siguiendo un orden particular, indicado en una lista, una opción es filtrar un archivo FASTAB con un bucle \(while\) de \(Bash\) como se muestra a continuación. Veremos la sintaxis de bucles while de \(Bash\) en una sección posterior.

while read seq; do grep "$seq" mini_fasta.fastab; done < seq.list
## >especie5    GGGGTACCCATTTACCCCCCCTTTTTAT
## >especie3    CCAGGGCCCATATTATACCGACCATTAC
## >especie4    GCATAATCCACCATGCACGAATGCAGAC

Ordenar las etiquetas acorde a una lista puede ser útil por ejemplo si queremos recuperar las secuencias de un clado filogenético en particular, para el cual tenemos la lista de sus miembros.

12.8.5 El script translate_dna.awk: integración de condicionales, bucles y funciones

Les presento ahora el script translate_dna.awk como ejercicio integrativo.

El programa recibe una cadena secuencia de nucleótidos y la traduce a aminoácidos usando el código genético universal. La conversión entre tripletes (codones) y aminoácidos se hace usando un \(hash\) que guarda las parejas triplete-aminoácido, inicializado en un bloque \(BEGIN{}\).

En el bloque principal el código revisa que la cadena de DNA pasada al \(script\) no esté vacía, contenga sólo caracteres válidos mediante la regexp ‘/[agctAGCT]+/’ y sea divisible por 3, imprimiendo mensajes de error adecuados si no pasa los tests.

Una vez validada la secuencia, es procesada mediante la función \(substr()\), dentro de un bucle do-while, recortando 3 nucleótidos en cada iteración. Este triplete de codones se convierte a mayúsculas, se imprime y se usa como llave del \(hash\) c para obtener el aminoácido correspondiente, el cual se imprime en una segunda línea, debajo del triplete correspondiente.

El código está ampliamente documentado para que puedan entender cada paso. Veámoslo:

cat translate_dna.awk
## #!/usr/bin/awk -f
## 
## # AUTHOR: Pablo Vinuesa, @pvinmex, https://www.ccg.unam.mx/~vinuesa/
## # source: https://github.com/vinuesa/intro2linux
## # translate_dna.awk VERSION:0.1
## # AIM: translates a valid DNA string into proteins 
## #      using the universal genetic code
## # NOTE: this is a demo script to teach basic awk programming
## 
## BEGIN {
##     
##     progname = "translate_dna.awk"
##     version  = 0.1  # nov 04, 2020
##     
##     if ( ARGC < 2 )
##       Usage_Exit(progname, version)
##     
##     # initialize a hash named "c" holding the codon-aminoacid pairs 
##     #   based on the universal genetic code
##     c["ATA"]="I"; c["ATC"]="I"; c["ATT"]="I"; c["ATG"]="M";
##     c["ACA"]="T"; c["ACC"]="T"; c["ACG"]="T"; c["ACT"]="T";
##     c["AAC"]="N"; c["AAT"]="N"; c["AAA"]="K"; c["AAG"]="K";
##     c["AGC"]="S"; c["AGT"]="S"; c["AGA"]="R"; c["AGG"]="R";
##     c["CTA"]="L"; c["CTC"]="L"; c["CTG"]="L"; c["CTT"]="L";
##     c["CCA"]="P"; c["CCC"]="P"; c["CCG"]="P"; c["CCT"]="P";
##     c["CAC"]="H"; c["CAT"]="H"; c["CAA"]="Q"; c["CAG"]="Q";
##     c["CGA"]="R"; c["CGC"]="R"; c["CGG"]="R"; c["CGT"]="R";
##     c["GTA"]="V"; c["GTC"]="V"; c["GTG"]="V"; c["GTT"]="V";
##     c["GCA"]="A"; c["GCC"]="A"; c["GCG"]="A"; c["GCT"]="A";
##     c["GAC"]="D"; c["GAT"]="D"; c["GAA"]="E"; c["GAG"]="E";
##     c["GGA"]="G"; c["GGC"]="G"; c["GGG"]="G"; c["GGT"]="G";
##     c["TCA"]="S"; c["TCC"]="S"; c["TCG"]="S"; c["TCT"]="S";
##     c["TTC"]="F"; c["TTT"]="F"; c["TTA"]="L"; c["TTG"]="L";
##     c["TAC"]="Y"; c["TAT"]="Y"; c["TAA"]="*"; c["TAG"]="*";
##     c["TGC"]="C"; c["TGT"]="C"; c["TGA"]="*"; c["TGG"]="W";
##     
##     unknown = "X   "  
## 
##  }
## # -------------------- # 
## # >>> MAIN PROGRAM <<< #  
## # -------------------- # 
## # Initialize variables: 
## #  do-while loop control variable i (nt counter) 
## #   and p, which will hold the translation product
## {i=1; p=""; triplet_counter=0}
## 
## {
##   # Here we run a do-while loop; the do loop is a variation of the while looping statement. 
##   #  The do loop executes the body once and then repeats the body as long as the condition is true
##   # We use the do-while loop, to get a first triplet string saved in s; 
##   #  then the while loop keeps going until substr() got the last triplet, resulting in an empty s="".
##   do {
##          # First check that the script got some input
##          #   if not, exit with an error message
##          if(length($0) == 0) {
##              print "ERROR: need a DNA sequence string to translate (valid DNA sequence, divisible by 3) "
##              exit 1
##          
##          # Check that the DNA sequence string is divisible by 3 using the modulo operator
##          #   if not, exit with an error message
##          } else if(length($1)%3) { 
##              print "ERROR: input DNA sequence not divisible by 3 ..."
##              exit 2
##          }
## 
##          # use substr() to split the input sequence ($1) into triplets saved in s         
##          s=substr($1, i, 3)
##          
##          # keep track of processed triplets
##          triplet_counter++
##          
##          # check that the input corresponds to valid nucleotides
##          if ( s ~ /[^acgtACGT]+/ ) { 
##              print "ERROR: input triplet", triplet_counter, "=", s, 
##                       "contains a non-valid nucleotide symbol ..."
##              exit 3
##          }
## 
##          # make sure that input nt symbols are uppercase to match the hash keys
##          s=toupper(s)
##          
##          # print the nucleotide sequence triplet, 
##          #   followed by a space for easier visualization
##          printf ("%s ", s)
##          
##          # use the codon hash c as lookup table to translate the s triplet
##          #   appending c[s] to the growing peptide p
##          { 
##              # break out of loop if we get no more triplets 
##              #   out of the input DNA string with substr()
##              if (c[s]=="") { 
##                 break
##              }
##              else if (s in c == 0) { 
##                 # if the triplet is not contained in c, append "X   " to p
##                 p=p unknown
##              } else { 
##                 # append aminoacid c[s] to growing peptide
##                 p=p c[s]"   "
##              }
##          }
##          i=i+3 # increment the counter of processed dna nucleotides by 3 
##     }
##   # run while loop until substring cannot retrieve any more triplets
##   while (s!="")
## }
## 
## # this printf block prints the protein string
## { printf("\n %s\n", p) }
## 
## 
## # function definition
## function  Usage_Exit (prog, vers) # (prog, vers)
##   {   
##      print "USAGE:", prog, "v"vers
##      print "   echo atggggtgttgtgggttgAAAGTGcccgggaaattaataCAG | ./translate_dna.awk -" > "/dev/stderr"
##      print "   or: ./translate_dna.awk dna_string.txt" > "/dev/stderr"
##      exit 1
##   }
  • Corramos el script sin pasarle ningún argumento para que imprima la ayuda
vinuesa@alisio:~/cursos/intro2linux$ ./translate_dna.awk 
USAGE: translate_dna.awk v0.1
   echo atggggtgttgtgggttgAAAGTGcccgggaaattaataCAG | ./translate_dna.awk -
   or: ./translate_dna.awk dna_string.txt
  • Corramos el \(script\) pasándole la secuencia mediante \(echo\) con un pipe |
    • Noten el uso de un ‘-’ como argumento. Esto es necesario ya que en el bloque BEGIN{} se requiere que se le pase un argumento al \(script\): if ( ARGC < 2 ) Usage_Exit(progname, version)
    • También podemos usar el archivo especial \(/dev/stdin\), o indicar un archivo vacío \(""\)
    • Con cualquiera de estas opciones le indicamos al intérprete de comandos \(awk\) que use \(STDIN\) en vez de un archivo para la entrada de datos, satisfaciendo la condición mostrada arriba
echo atggggtgttgtgggttgAAAGTGcccgggaaattaataCAG | ./translate_dna.awk -
## ATG GGG TGT TGT GGG TTG AAA GTG CCC GGG AAA TTA ATA CAG  
##  M   G   C   C   G   L   K   V   P   G   K   L   I   Q
  • Corramos el \(script\) igual, pero añadiéndole un nucleótido extra
echo atggggtgttgtgggttgAAAGTGcccgggaaattaataCAGt | ./translate_dna.awk ""
ERROR: input DNA sequence not divisible by 3 ...
  • Corramos el \(script\) igual, pero con un residuo que no es nucleótido
echo atggggtgttgtgggttgAAAGTGcccgggaaattaataCAx | ./translate_dna.awk /dev/stdin
ATG GGG TGT TGT GGG TTG AAA GTG CCC GGG AAA TTA ATA ERROR: input triplet 14 = CAx contains a non-valid nucleotide symbol ...

Como ven, el \(script\) fue programado “defensivamente”, es decir, previendo posibles errores que le impiden funcionar correctamente, imprimiendo mensajes de error informativos para que el usuario entienda cuál es el problema antes de terminar su ejecución. A ésto se le llama “morir grácilmente

En la siguiente sección veremos el script fasta_toolkit.awk que implementa una versión que trabaja directamente sobre archivos multifasta.

12.9 Modularización de código mediante funciones definidas por el usuario: function my_function_name(){ … }

En la sección anterior vimos varios ejemplos de scripts relativamente cortos y sencillos. Cuando los programas se hacen más largos y complejos, es fundamental escribir funciones definidas por el usuario para que el código sea más fácil de leer y mantener, contribuyendo a la modularidad y re-uso de código entre programas y dentro del mismo.

  • Sintaxis para definición de una función (my_function_name) function my_function_name([param1, param2, local_var1, local_var2 …]) { body-of-function }
  • Sintaxis de llamada de una función definida por el usuario (my_function_name) my_function_name([param1, param2])
  1. Para nombrar la función se siguen las reglas de nombramiento de variables: inician con [_[a-zA-Z]] y no deben contener espacios. No debe haber espacio ente el nombre de la función y los paréntesis que le siguen.
  2. La lista de parámetros se separa por comas. Estos parámetros son globales.
  3. Para definir variables locales a una función, se añaden a la lista de parámetros en la definición de la función. Estas variables quedan automáticamente inicializadas como cadena vacía o 0 (cero), según el contexto en el que son llamadas en la función. Internamente, awk distingue estas variables locales de los parámetros ya que en base al número de parámetros pasados a la función en su llamada. Para facilitar la identificación de variables locales de los parámetros que recibe una función, es útil la convención de separar a estas últimas de los parámetros globales con múltiples espacios en blanco, como se muestra en la sintaxis mostrada arriba.
  4. Durante la ejecución del cuerpo de una función, los argumentos y variables locales enmascaran a variables con el mismo nombre usadas en otras partes del programa. Estas variables enmascaradas no están disponibles en el cuerpo de la función. El resto de las variables (no localizadas en otra función) definidas en un programa están disponibles dentro del cuerpo de cualquier función, pudiendo ser modificadas desde el mismo.
  5. Los argumentos y variables locales de una función sólo existen mientras se ejecuta el cuerpo de la misma. Una vez finalizada su ejecución, se tiene nuevamente acceso a las variables enmascaradas por la función que terminó su ejecución.
  6. Se puede usar la sentencia return para que una función regrese un valor escalar determinado.
  7. Se pueden llamar funciones desde dentro de funciones, incluso a la misma (llamadas recursivas).
  8. Las funciones se pueden definir en cualquier punto del script, sea cerca de la cabecera o al final del mismo.

12.9.1 Ejemplos de definición y llamadas de funciones definidas por el usuario

  • La función delarray() elimina todos los elementos de un arreglo. Si bien en versiones recientes de gawk se puede usar delete arr para vaciar todo un arreglo, la función delarray() es más portable y nos evita tener que escribir el bucle cada vez que queremos vaciar un arreglo.
# a es el arreglo que se le pasa como parámetro
# i es una variable que se define como local de la función, 
#   para evitar interferencia con otra variable del mismo nombre
function delarray(a,      i)
{
   for (i in a)
      delete a[i]
}

a[1] = 1
a[5] = 2

delarray(a)
  • read_fasta() es un ejemplo de una función que no recibe parámetros y sólo define variables locales
function read_fasta(      i, h, s)
{ 
   # fill hash seqs with the DNA/CDS sequences passed to script as input file
   s=""
   h=$1
   for (i=2; i<=NF; i++) s=s$i
   seqs[h]=s
}

Como vimos anteriormente, en awk es posible definir arreglos de arreglos, es decir, arreglos multidimensionales. Dicho de otra manera, el valor asignado a una llave o índice de un arreglo puede ser otro arreglo.

La función walk_array() recorre la estructura y la imprime.

function walk_array(arr, name,      i)
{
    for (i in arr) {
        if (isarray(arr[i]))
            walk_array(arr[i], (name "[" i "]"))
        else
            printf("%s[%s] = %s\n", name, i, arr[i])
    }
}

BEGIN {
    a[1] = 1
    a[2][1] = 21
    a[2][2] = 22
    a[3] = 3
    a[4][1][1] = 411
    a[4][2] = 42

    walk_array(a, "a")
}

Asumiendo que hemos escrito el bloque mostrado arriba en el script walk_array.awk, si lo llamamos obtendremos el siguiente resultado:

gawk -f walk_array.awk

-| a[1] = 1
-| a[2][1] = 21
-| a[2][2] = 22
-| a[3] = 3
-| a[4][1][1] = 411
-| a[4][2] = 42

12.10 \(awk\) scripts - ejemplos avanzados

En esta sección les presentaré dos scripts más largos y complejos, que servirán para:

  1. repasar todos los elementos sintácticos vistos anteriormente e introducir algunos nuevos
  2. mostrar cómo se estructura y modulariza código de AWK y cómo se usan estructuras de datos complejas (arreglos de arreglos) junto con estructuras de control, regexps y banderas para parsear archivos de estructura más compleja, como son los de formato GenBank.

Para aprender a programar, no basta con conocer los elementos sintácticos y algunos idiomas de los lenguajes. La manera más eficiente de aprender es estudiando código de programas significativos, bien escritos, de cierta complejidad, modificándolos e incluso extendiendo su funcionalidad. Espero que fasta_toolkit.awk y extract_CDSs_from_GenBank.awk te sean de utilidad en este sentido, además de servirte como herramientas para el trabajo rutinario en bioinformática.

12.10.1 fasta_toolkit.awk, un script multifuncional para mainpular archivo FASTA que hace uso de getopt()

Les presento el script fasta_toolkit.awk. Es más largo que los anteriores, pero aún relativamente sencillo. Integra múltiples funciones para manipular archivos multi-FASTA de DNA. Este script les será de utilidad en su trabajo rutinario con secuencias (lo usaremos por ejemplo en las prácticas de BLAST de la LCG y en secciones posteriores de este tutorial).

En particular, el script tiene por objetivo enseñarles cómo modularizar el código mediante el uso de funciones definidas por el usuario

El script fasta_toolkit.awk implementa funciones para realizar las siguientes operaciones sobre archivos multi-FASTA de DNA:

  1. filtrar o seleccionar secuencias de un archivo multifasta usando una cadena de caracteres
  2. imprimir el reverso complementario de las secuencias en el archivo
  3. extraer un segmento de secuencia, dando las coordenadas de inicio y fin
  4. traducir CDSs a proteínas, usando el código genético universal
  5. imprimir una tabla con estadísticas básicas de las secuencias

Cada una de estas funcionalidades se define mediante un “runmode” numérico que llamaremos con la opción -R [1-5].

El script fasta_toolkit.awk implementa además la función getopt() escrita por Arnold Robin, el actual mantenedor de gawk, la cual es una implementación en AWK de la librería de C getopt. Ello permite manejar con mayor flexibilidad y conveniencia opciones y argumentos pasados a scripts de AWK desde la línea de comandos.

La implementación en awk de getopt se llama getopt.awk y se distribuye con la libraría de funciones de gawk, por lo que debes poderla localizar fácilmente en tu sistema con locate getopt.awk.

Puedes leer sobre los detalles de implementación de getopt.awk en The GNU Awk User’s Guide - GetOpt function y estudiar múltiples ejemplos que hacen uso de la misma en The GNU Awk User’s Guide - Sample programs, por lo que no los repetiré aquí.

  • Estudiemos el script
    • Desplegamos el menú de ayuda del script invocándolo sin argumentos: ./fasta_toolkit.awk
OPTIONS for fasta_toolkit.awk v0.2
 # Required options
    -R <int> [runmode]
         1 [filter sequences matching -m string]
         2 [reverse complement DNA sequence]
         3 [extract sequence by -s start -e end coordinates]
         4 [translate CDSs]
         5 [print basic sequence stats]

 # Runmode-specific options
     -m <string> [match string] for -R 1
     -s <int> [start coord] for -R 3
     -e <int> [end coord] for -R 3

 # Other options
     -d [FLAG; sets DEBUG=1 to print extra debugging info]

 # Usage examples:
    ./fasta_toolkit.awk -R 1 -m match_string input.fasta
    ./fasta_toolkit.awk -R 2 input.fasta
    ./fasta_toolkit.awk -R 3 -s 2 -e 5 input.fasta
    ./fasta_toolkit.awk -R 4 input.fasta
    ./fasta_toolkit.awk -R 5 input.fasta
    cat input.fasta | ./fasta_toolkit.awk -R 5

 # Notes:
    1. Pass only single FASTA files to fasta_toolkit.awk
    2. prints results to STDOUT

Veamos ahora las secciones del script fasta_toolkit.awk

12.10.1.1 Cabecera de fasta_toolkit.awk

Simplemente describe la funcionalidad y opciones del script

#!/usr/bin/awk -f

# fasta_toolkit.awk VERSION:0.1 released Dec 21, 2020
# AUTHOR: Pablo Vinuesa, @pvinmex, https://www.ccg.unam.mx/~vinuesa/
# Source: https://github.com/vinuesa/intro2linux
# AIM: munge fasta file containing one or more CDSs using one of 5 runmodes:
#       -R 1 [filter sequences matching -m string]
#       -R 2 [reverse complement DNA sequence] 
#       -R 3 [extract sequence by -s start -e end coordinates]
#       -R 4 [translate CDSs, using universal genetic code]
#       -R 5 [print basic sequence stats]
# USAGE: call the script without arguments to print the help menu
# NOTES:  
#   1. uses Arnold Robbin's getopt() function from the gawk distro to deal with options and arguments
#   2. can read FASTA files from file or STDIN
#   3. pass only single FASTA files to fasta_toolkit.awk
#   4. prints results to STDOUT

12.10.1.2 Funciones de fasta_toolkit.awk

Las funciones pueden ser definidas en cualquier parte del programa. En este caso las he puesto hacia la cabecera del script, en el orden asignado a los “runmodes” definidos con la opción -R . La función getopts() la he puesto junto con la de print_help() hacia el final del bloque de funciones para tenerlas más cerca del bloque BEGIN{}, donde se procesan las opciones.

#---------------------------------------------------------------------------------------------------------#
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> FUNCTION DEFINITIONS <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< #
#---------------------------------------------------------------------------------------------------------#
function read_fasta(      i, h, s)
{ 
   # fill hash seqs with the DNA/CDS sequences
   s=""
   h=$1
   for (i=2; i<=NF; i++) s=s$i
   seqs[h]=s
}
#---------------------------------------------------------------------------------------------------------
function rev_compl(header, dna_seq,        i, k, x) # reverse complement
{  # receives two arguments: the fasta header and the CDS sequence to be translated
    k=x=""
    
    dna_seq = toupper(dna_seq) # to match the keys of compl_nucl
    for( i = length(dna_seq); i !=0; i-- ) { 
         k = substr(dna_seq, i, 1)
         x = x compl_nucl[k]
    }
    printf ">%s\n%s\n", header, x
}
#---------------------------------------------------------------------------------------------------------
function extract_sequence_by_coordinates(header, dna_seq, start, end) {
       if(start == 1) print ">"header"\n"substr(dna_seq,start,end)
       if(start > 1)  print ">"header"\n"substr(dna_seq,start,end-start+1)
}
#---------------------------------------------------------------------------------------------------------
function translate_dna(header, dna_seq,      s, i, p)
{  # receives two arguments: the fasta header and the CDS sequence to be translated
   
   # Initialize variables: 
   #  do-while loop control variable i (nt counter) 
   #   and p, which will hold the translation product
   {i=1; p=""; triplet_counter=0}

   # Here we run a do-while loop; the do loop is a variation of the while looping statement. 
   #  The do loop always executes the body once and then repeats the body as long as the condition is true
   # We use the do-while loop, to get a first triplet string saved in s; 
   #  then the while loop keeps going until substr() got the last triplet, resulting in an empty s="".
   do {
      # First check that the script got some input
      #   if not, exit with an error message
      if(length(dna_seq) == 0) {
          print "ERROR: need a DNA sequence string to translate (valid DNA sequence, divisible by 3) "
          exit 1
       
      # Check that the DNA sequence string is divisible by 3 using the modulo operator
      #   if not, exit with an error message
      } else if(length(dna_seq)%3) { 
          printf "# WARNING: input DNA sequence for %s not divisible by 3. Will skip it!\n", header
            break
      }

      # use substr() to split the input sequence (dna_seq) into triplets saved in s     
      s=substr(dna_seq, i, 3)
       
      # keep track of processed triplets (codons)
      triplet_counter++
       
      # check that the input corresponds to valid nucleotides
      if ( s ~ /[^acgtACGT]+/ ) { 
          print "ERROR: input triplet", triplet_counter, "=", s, 
            "contains a non-valid nucleotide symbol ..."
          exit 3
      }

      # make sure that input nt symbols are uppercase to match the hash keys
      s=toupper(s)
        
      # use the codon hash c as lookup table to translate the s triplet
      #   appending codons[s] to the growing peptide p
      { 
          # break out of loop if we get no more triplets 
          #   out of the input DNA string with substr()
          if (codons[s]=="") { 
         break
          }
          else if (s in codons == 0) { 
         # if the triplet is not contained in c, append "X" to p
         p=p unknown
          } else { 
         # append aminoacid codons[s] to growing peptide
         p = p codons[s]
         }
     }
     i=i+3 # increment the counter of processed dna nucleotides by 3 
    }
    # run while loop until substring cannot retrieve any more triplets
    while (s!="")
    prots[header]=p
}
#---------------------------------------------------------------------------------------------------------
function print_sequence_stats(header, dna_seq,         i,l) 
{ # receives two arguments: the fasta header and the CDS sequence to be translated
   
   sumA=0;sumT=0;sumC=0;sumG=0;sumN=0;seq=""
   l=length(dna_seq)
   
   for (i=1;i<=l;i++) {
             if (substr(dna_seq,i,1)=="T") sumT+=1
        else if (substr(dna_seq,i,1)=="A") sumA+=1
        else if (substr(dna_seq,i,1)=="G") sumG+=1
        else if (substr(dna_seq,i,1)=="C") sumC+=1
        else if (substr(dna_seq,i,1)=="N") sumN+=1
   }
   # print stats
   printf "%s\t%d\t%d\t%d\t%d\t%d\t%d\t%3.2f\n", header, sumA, sumC, sumG, sumT, sumN, l, (sumC+sumG)/l*100
}
#---------------------------------------------------------------------------------------------------------

# available in /usr/share/awk/
# getopt.awk --- Do C library getopt(3) function in awk
#
# Arnold Robbins, arnold@skeeve.com, Public Domain
#
# Initial version: March, 1991
# Revised: May, 1993

# External variables:
#    Optind -- index in ARGV of first nonoption argument
#    Optarg -- string value of argument to current option
#    Opterr -- if nonzero, print our own diagnostic
#    Optopt -- current option letter

# Returns:
#    -1     at end of options
#    "?"    for unrecognized option
#    <c>    a character representing the current option

# Private Data:
#    _opti  -- index in multiflag option, e.g., -abc
function getopt(argc, argv, options,    thisopt, i)
{
    if (length(options) == 0)    # no options given
        return -1

    if (argv[Optind] == "--") {  # all done
        Optind++
        _opti = 0
        return -1
    } else if (argv[Optind] !~ /^-[^:[:space:]]/) {
        _opti = 0
        return -1
    }
    if (_opti == 0)
        _opti = 2
    thisopt = substr(argv[Optind], _opti, 1)
    Optopt = thisopt
    i = index(options, thisopt)
    if (i == 0) {
        if (Opterr)
            printf("%c -- invalid option\n", thisopt) > "/dev/stderr"
        if (_opti >= length(argv[Optind])) {
            Optind++
            _opti = 0
        } else
            _opti++
        return "?"
    }
    if (substr(options, i + 1, 1) == ":") {
        # get option argument
        if (length(substr(argv[Optind], _opti + 1)) > 0)
            Optarg = substr(argv[Optind], _opti + 1)
        else
            Optarg = argv[++Optind]
        _opti = 0
    } else
        Optarg = ""
    if (_opti == 0 || _opti >= length(argv[Optind])) {
        Optind++
        _opti = 0
    } else
        _opti++
    return thisopt
}
#---------------------------------------------------------------------------------------------------------

function print_help(prog, vers) # (prog, vers)
{   
   print "OPTIONS for " prog " v"vers > "/dev/stderr" 
   print " # Required options" > "/dev/stderr" 
   print "     -R <int> [runmode]" > "/dev/stderr" 
   print "          1 [filter sequences matching -m string]" > "/dev/stderr" 
   print "          2 [reverse complement DNA sequence]" > "/dev/stderr" 
   print "          3 [extract sequence by -s start -e end coordinates]" > "/dev/stderr" 
   print "          4 [translate CDSs]" > "/dev/stderr" 
   print "          5 [print basic sequence stats]" > "/dev/stderr" 

   print "\n # Runmode-speicic options" > "/dev/stderr" 
   print "     -m <string> [match string] for -R 1" > "/dev/stderr" 
   print "     -s <int> [start coord] for -R 3" > "/dev/stderr" 
   print "     -e <int> [end coord] for -R 3" > "/dev/stderr" 

   print "\n # Other options" > "/dev/stderr" 
   print "     -d [FLAG; sets DEBUG=1 to print extra debugging info]" > "/dev/stderr" 
  
   print "\nUSAGE EXAMPLES:"
   print "./" prog, "-R 1 -m match_string input.fasta" > "/dev/stderr" 
   print "./" prog, "-R 2 input.fasta" > "/dev/stderr" 
   print "./" prog, "-R 3 -s 2 -e 5 input.fasta" > "/dev/stderr" 
   print "./" prog, "-R 4 input.fasta" > "/dev/stderr" 
   print "./" prog, "-R 5 input.fasta" > "/dev/stderr" 
   print "cat input.fasta | ./"prog " -R 5" > "/dev/stderr" 

   print "\n # Notes:" > "/dev/stderr" 
   print "    1. Pass only single FASTA files to " prog > "/dev/stderr" 
   print "    2. prints results to STDOUT" > "/dev/stderr"

   exit 1
}
#---------------------------------------------------------------------------------------------------------

12.10.1.3 Bloque de inicialización BEGIN{} de fasta_toolkit.awk

Este es un bloque BEGIN{} bastante más largo que otros mostrados en ejemplos anteriores.

  1. Una primera sección define PROCINFO[“sorted_in”] = “@ind_num_asc para que los fastas almacenados en hashes salgan ordenados por su índice numérico
  2. luego llama a la función getopt dentro de un bucle while(), pasándole ARGC, ARGV y las opciones como cadena “dm:e:R:s:”, en la que las opciones que requieren argumentos son seguidas por un ‘:’, como ‘-e:’. while ((c = getopt(ARGC, ARGV, “dm:e:R:s:”)) != -1) { … }.
  3. Una segunda sección define a las variables RS, FS y OFS para modelar archivos FASTA, como hemos visto anteriormente
  4. La siguiente sección revisa que las opciones y sus valores sean los adecuados para cada runmode u operación a hacer con el FASTA de entrada
  5. Además se definen los hashes compl_nucl y codons sólo si van a ser usados por el runmode correspondiene (-R 2; -R 4) y se imprime la cabecera de la tabla de estadísticas para -R 5
BEGIN {
    # Initializations
    Debug  = 0
    Opterr = 1    # default is to diagnose
    Optind = 1    # skip ARGV[0]
    
    progname = "fasta_toolkit.awk"
    version  = 0.4  # v0.4 dec 25, 2020. added PROCINFO["sorted_in"] = "@ind_num_asc" to print sorted results
                    # v0.3 dec 23, 2020. Prints warning and does not exit, if dna_seq not divisible by 3
                    # v0.2 dec 22, 2020, improved layout; fixed typos
                    # v0.1 dec 21, 2020, first commit

    # print the FASTA sequences stored in hashes in ascending order by locus_tag number 
    PROCINFO["sorted_in"] = "@ind_num_asc"
    
    # check that the script receives input either from file or pipe
    if ( ARGC < 2 ) print_help(progname, version)

    while ((c = getopt(ARGC, ARGV, "dm:e:R:s:")) != -1) {
        if (c == "R") {
               runmode = Optarg
         } else if (c == "m") {
               string = Optarg
         } else if (c == "s") {
               start = Optarg
         } else if (c == "e") {
               end = Optarg
         } else if (c == "d") {
               Debug = 1
         } else {
            print "ERROR: option -" Optopt, "is not defined"
                    print_help(progname, version)
         }    
    }

    #Debug=1 --> check ARGS
    if(Debug) # activate with -d option
    {
        print "ARGC="ARGC
        print "ARGV[ARGC-1]="ARGV[ARGC-1]

        for(i=1; i<=ARGC; i++)
        {
              print "ARGV["i"]=" ARGV[i]
        }
    }
    
    # clear out options
    for (i = 1; i < Optind; i++)
       ARGV[i] = ""
  #----------------------------------------------------
    
    # Model FASTA file
    RS=">"
    FS=OFS="\n"

  #----------------------------------------------------
   
    if (runmode == 1 && length(string) == 0) {
        print "ERROR: -R 1 requires -m match_string to filter the input FASTA" > "/dev/stderr"
        print_help(progname, version)
    }   

    if (runmode == 3 && length(start) == 0) {
        print "ERROR: -R 3 requires -s <int> to provide the START coordinate to extract the sequence string from the input FASTA" > "/dev/stderr"
        print_help(progname, version)
    }   
    
    if (runmode == 3 && length(end) == 0) {
        print "ERROR: -R 3 requires -e <int> to provide the END coordinate to extract the sequence string from the input FASTA" > "/dev/stderr"
        print_help(progname, version)
    }   
 
    if (runmode == 2) {
        # complement sequences
        compl_nucl["T"]="A"
        compl_nucl["A"]="T"
        compl_nucl["C"]="G"
        compl_nucl["G"]="C"
        compl_nucl["N"]="N"
    }

    if(runmode == 4) {
        # initialize a hash named "codons" holding the codon-aminoacid pairs, 
        #   based on the universal genetic code
        codons["ATA"]="I"; codons["ATC"]="I"; codons["ATT"]="I"; codons["ATG"]="M";
        codons["ACA"]="T"; codons["ACC"]="T"; codons["ACG"]="T"; codons["ACT"]="T";
        codons["AAC"]="N"; codons["AAT"]="N"; codons["AAA"]="K"; codons["AAG"]="K";
        codons["AGC"]="S"; codons["AGT"]="S"; codons["AGA"]="R"; codons["AGG"]="R";
        codons["CTA"]="L"; codons["CTC"]="L"; codons["CTG"]="L"; codons["CTT"]="L";
        codons["CCA"]="P"; codons["CCC"]="P"; codons["CCG"]="P"; codons["CCT"]="P";
        codons["CAC"]="H"; codons["CAT"]="H"; codons["CAA"]="Q"; codons["CAG"]="Q";
        codons["CGA"]="R"; codons["CGC"]="R"; codons["CGG"]="R"; codons["CGT"]="R";
        codons["GTA"]="V"; codons["GTC"]="V"; codons["GTG"]="V"; codons["GTT"]="V";
        codons["GCA"]="A"; codons["GCC"]="A"; codons["GCG"]="A"; codons["GCT"]="A";
        codons["GAC"]="D"; codons["GAT"]="D"; codons["GAA"]="E"; codons["GAG"]="E";
        codons["GGA"]="G"; codons["GGC"]="G"; codons["GGG"]="G"; codons["GGT"]="G";
        codons["TCA"]="S"; codons["TCC"]="S"; codons["TCG"]="S"; codons["TCT"]="S";
        codons["TTC"]="F"; codons["TTT"]="F"; codons["TTA"]="L"; codons["TTG"]="L";
        codons["TAC"]="Y"; codons["TAT"]="Y"; codons["TAA"]="*"; codons["TAG"]="*";
        codons["TGC"]="C"; codons["TGT"]="C"; codons["TGA"]="*"; codons["TGG"]="W";
    }

    if (runmode == 5) {
         # print table header for sequence stats
         print "seq_name\tA\tC\tG\tT\tN\tlength\tGC%"
    }
}

12.10.1.4 Bloque de patrones/acciones de fasta_toolkit.awk

El bloque de código que hace las llamadas a las funciones es relativamente corto, haciendo muy fácil y clara su lectura.

1.Como hemos visto en múltiples ejemplos anteriores de scripts que procesan archivos FASTA modelando su estructura con BEGIN{RS=“>”; FS=OFS=“”}, iniciamos el bloque de código principal con el patrón NR > 1 para eliminar el primer registro que está vacío. 2. Sigue un condicional que asegura que el usuario ha pasado al script un “runmode” definido en el mismo (-R [1-5]). De ser así lee el FASTA mediante la función read_fasta(), capturando los registros en el hash global seqs. 3. Sigue un bloque END{} que llama a las funciones según el “runmode” que se le ha pasado al script (-R [1-5]), siempre y cuando las secuencias no sean ridículamente cortas.

# -------------------- # 
# >>> MAIN PROGRAM <<< #  
# -------------------- # 

NR > 1 { 
   # read the CDS (DNA) sequence into the global seqs hash

   if (runmode > 5) {
      printf "ERROR: runmomde %d is not defined\n", runmode
      print_help(progname, version)
   }
   else {
      read_fasta()
   }
}

END { 
    for (h in seqs) { 
          if( length(seqs[h] >= 2) && runmode == 1 ) { if(h ~ string) print ">"h, seqs[h] }
          if( length(seqs[h] >= 2) && runmode == 2 ) { rev_compl(h, seqs[h]) }
        if( length(seqs[h] >= 3) && runmode == 3 ) { extract_sequence_by_coordinates(h, seqs[h], start, end) }
        if( length(seqs[h] >= 3) && runmode == 4 ) {
            # 1. translate the CDS
              translate_dna(h, seqs[h]) 
           
              # 2. print tranlsated fasta
            for (h in prots) {
                   # make sure we print only non-redundant sequences
                   hcount[h]++
                   if ($1 != "" && hcount[h] == 1) printf ">%s\n%s\n", h, prots[h] 
             }    
      }
        if( length(seqs[h] >= 3) && runmode == 5 ) { print_sequence_stats(h, seqs[h]) }
   }
}

12.10.1.5 Ejemplos de uso de fasta_toolkit.awk

Usaremos para los ejemplos el archivo mini_CDS.fna

cat mini_CDS.fna
## >mini_CDS1
## atggggtgttgtgggttgAAAGTGcccgggaaattaataCAGCCG
## >mini_CDS2
## GTGcggtgttgtgggttgATGGTGcccgggaaattaaaaTTGTAA
  1. Filtra secuencias usando una cadena
./fasta_toolkit.awk -R 1 -m CDS2 mini_CDS.fna
## >mini_CDS2
## GTGcggtgttgtgggttgATGGTGcccgggaaattaaaaTTGTAA
  1. Calcula las secuencias reversas complementarias
./fasta_toolkit.awk -R 2 mini_CDS.fna
## >mini_CDS1
## CGGCTGTATTAATTTCCCGGGCACTTTCAACCCACAACACCCCAT
## >mini_CDS2
## TTACAATTTTAATTTCCCGGGCACCATCAACCCACAACACCGCAC
  1. Extrae las secuencias de la coordnada 4 a 12
./fasta_toolkit.awk -R 3 -s 4 -e 12 mini_CDS.fna
## >mini_CDS1
## gggtgttgt
## >mini_CDS2
## cggtgttgt
  1. Traduce las secuencias de CDSs a proteínas usando el código genético universal
./fasta_toolkit.awk -R 4 mini_CDS.fna
## >mini_CDS1
## MGCCGLKVPGKLIQP
## >mini_CDS2
## VRCCGLMVPGKLKL*
  1. Imprime unas estadísticas descriptivas de las secuencias
./fasta_toolkit.awk -R 5 mini_CDS.fna
## seq_name A   C   G   T   N   length  GC%
## mini_CDS1    4   3   4   1   0   45  15.56
## mini_CDS2    3   0   6   6   0   45  13.33
  1. Imprime las estadísticas descriptivas de las secuencias, pasadas por STDIN al script
cat mini_CDS.fna | ./fasta_toolkit.awk -R 5 
## seq_name A   C   G   T   N   length  GC%
## mini_CDS1    4   3   4   1   0   45  15.56
## mini_CDS2    3   0   6   6   0   45  13.33

12.10.2 extract_CDSs_from_GenBank.awk, un script hace uso avanzado de hashes de hashes (AoA), regexps, banderas, estructuras de control y funciones para el parseo y transformación de archivos GenBank

El script lee un archivo GenBank genera varios archivos a partir de él:

  • una tabla con las coordenadas y cadena de cada CDS, indicando además su LOCUS, locus_tag, producto y si es un pseudogen
  • un archivo multi-FASTA con todos los CDSs
  • un archivo multi-FASTA con todos los productos de traducción de CDSs que no corresponden a pseudogenes
  • un archivo FASTA con las cadenas completas de DNA de cada registro de un archivo GenBank

Revisemos el script extract_CDSs_from_GenBank.awk con cierto detalle, ya que tiene varios aspectos novedosos que no hemos visto anteriormente.

  1. hace uso frecuente de expresiones regulares más complejas, incluyendo paréntesis para captura de ciertos campos/patrones de la anotación, la cual se almacena en una estructura de datos compleja, un hash de hashes (o arreglo de arreglos AoA)
  2. el script escribe directamente a disco los cuatro archivos que produce. Antes de hacerlo, checa que no exista un versión previa de los mismos, borrándolos si existen antes de volverlos a generar. Para ello hace una llamada al shell mediante un pipe, pasándole un comando
  3. hace uso extensivo de banderas, estructuras de control y funciones, incluyendo funciones anidadas, para el parseo y transformación de archivos GenBank

12.10.2.1 Cabecera de extract_CDSs_from_GenBank.awk

Como de costumbre, da una sinopsis de uso del script

#!/usr/bin/awk -f

#---------------------------------------------------------------------------------------------------------
#: PROGRAM: extract_CDSs_from_GenBank.awk; first commmit Dec 23, 2020
#: AUTHOR: Pablo Vinuesa, @pvinmex, https://www.ccg.unam.mx/~vinuesa/
#: SOURCE: https://github.com/vinuesa/intro2linux
#: USAGE: extract_CDSs_from_GenBank.awk genbank_file.gbk   
#: AIMS: 
#:   1. extracts the CDSs from a single GenBank file and writes them to genbank_basename_CDSs.fna
#:   2. translates the CDSs and writes them to file genbank_basename_proteome.faa
#:   3. writes the GenBank file in tabular format as genbank_basename.tsv
#:   4. extracts the complete DNA string(s) from the input GenBank saving them to genbank_basename.fsa
#: NOTES: 
#:   1. the program currently does not deal with CDSs containing introns.
#:       CDSs containing the join statement: complement(join(4497616..4498557,4498557..4498814)), are skipped
#:       Use only for bacterial|mitochondrial|plastid genomes
#=========================================================================================================

12.10.2.2 Bloque BEGIN{} de inicialización

BEGIN {   
    # Initializations
    DEBUG = 0  # set to 1 if debugging messages should be activated (prints to "/dev/stderr")
    
    # print the FASTA sequences stored in hashes in ascending order by locus_tag number 
    PROCINFO["sorted_in"] = "@ind_num_asc"
    
    progname="extract_CDSs_from_GenBank.awk"
    VERSION=0.5 # v0.5 Dec 28, 2020; slightly more streamlined (awkish) code, by using more awk defaults (avoid $0 ~ /regexp/, etc)
                #    prints LOCUS_length to *tsv file using a nested AoA as the 1ary dna AoA key: dna[CDSs_AoA[k]["LOCUS"]]["len"]
                # v0.3 Dec 25, 2020; correctly appends the [pseudogene] label to the end of the product line
                # v0.2 Dec 24, 2020; now captures full product name, even when split over two lines
                # v0.1 Dec 23, 2020; first commit

    # check user input: needs the GenBank file to process
    #        as single argument on the command line
    if(ARGC < 2) 
        print_help(progname, VERSION)    
    
    # capture the gbk basename for naming output files written by this script
    input_gbk = ARGV[1]
    basename = input_gbk
    
    gsub(/\..*$/, "", basename)   
    gbk_tsv = basename".tsv"
    gbk_faa = basename"_proteome.faa"
    gbk_fsa = basename".fsa"
    gbk_CDSs = basename"_CDSs.fna"
    
    # hash used by rev_compl()
    compl_nucl["T"]="A"
    compl_nucl["A"]="T"
    compl_nucl["C"]="G"
    compl_nucl["G"]="C"
    compl_nucl["N"]="N"

    # initialize the "codons" hash holding the codon-aminoacid pairs, 
    #   based on the universal genetic code, to translate CDSs with translate_dna()
    codons["ATA"]="I"; codons["ATC"]="I"; codons["ATT"]="I"; codons["ATG"]="M";
    codons["ACA"]="T"; codons["ACC"]="T"; codons["ACG"]="T"; codons["ACT"]="T";
    codons["AAC"]="N"; codons["AAT"]="N"; codons["AAA"]="K"; codons["AAG"]="K";
    codons["AGC"]="S"; codons["AGT"]="S"; codons["AGA"]="R"; codons["AGG"]="R";
    codons["CTA"]="L"; codons["CTC"]="L"; codons["CTG"]="L"; codons["CTT"]="L";
    codons["CCA"]="P"; codons["CCC"]="P"; codons["CCG"]="P"; codons["CCT"]="P";
    codons["CAC"]="H"; codons["CAT"]="H"; codons["CAA"]="Q"; codons["CAG"]="Q";
    codons["CGA"]="R"; codons["CGC"]="R"; codons["CGG"]="R"; codons["CGT"]="R";
    codons["GTA"]="V"; codons["GTC"]="V"; codons["GTG"]="V"; codons["GTT"]="V";
    codons["GCA"]="A"; codons["GCC"]="A"; codons["GCG"]="A"; codons["GCT"]="A";
    codons["GAC"]="D"; codons["GAT"]="D"; codons["GAA"]="E"; codons["GAG"]="E";
    codons["GGA"]="G"; codons["GGC"]="G"; codons["GGG"]="G"; codons["GGT"]="G";
    codons["TCA"]="S"; codons["TCC"]="S"; codons["TCG"]="S"; codons["TCT"]="S";
    codons["TTC"]="F"; codons["TTT"]="F"; codons["TTA"]="L"; codons["TTG"]="L";
    codons["TAC"]="Y"; codons["TAT"]="Y"; codons["TAA"]="*"; codons["TAG"]="*";
    codons["TGC"]="C"; codons["TGT"]="C"; codons["TGA"]="*"; codons["TGG"]="W";
    
    unknown = "X"
}
#-------------------------------- END OF BEGIN BLOCK ----------------------------------#

El elemento que destacaremos del bloque BEGIN{} es: el código para obtener el nombre de base del archivo GenBank con

  • gsub(/..*$/, ““, basename)

y cómo generar a partir de él los nombres de los tres archivos que se van a escribir a disco usando simples concatenanciones

  • gbk_tsv = basename”.tsv”

Los hashes compl_nucl y codons ya los vimos en fasta_toolkit.awk

12.10.2.3 El bloque de patrón-acción

Este bloque es relativamente largo y está dominado por el uso de expresiones regulares combinado con condicionales y el uso de flags o banderas para hacer el parseo de la información relevante sobre coordenadas, cadena y producto de los CDSs anotados un GenBank.

Noten que toda la información extraída del GenBank se almacena en las estructuras de datos complejas CDSs_AoA y dna. Ambos son un arreglo de arreglos (o si lo prefieren, hash de hashes) en los que se va guardando la información relevante extraída de los registros de un archivo GenBank a medida que lo va leyendo. El arreglo principal CDSs_AoA está indexado con un índice numérico guardado en geneID, el cual contiene como valor a 7 sub-arreglos o hashes, que guardan los elementos de anotación relevantes como locus_tag, locus_id … El campo CDSs_AoA[geneID][“p”] = product captura incluso texto que a veces se encuentra distribuido en dos líneas, como se muestra más abajo. El AoA dna, guarda la secuencia de DNA extraída del final de cada registro en dna[locus_id][“seq”] = seq, y su longitud en dna[locus_id][“len”] = length(seq)

     # All the CDS's-relevant information parsed from the annotations 
     #   will be stored in the CDSs_AoA (Array of Arrays)
     CDSs_AoA[geneID]["l"] = locus_tag
     CDSs_AoA[geneID]["LOCUS"] = locus_id
     CDSs_AoA[geneID]["s"] = start
     CDSs_AoA[geneID]["e"] = end
     CDSs_AoA[geneID]["c"] = complflag
     CDSs_AoA[geneID]["pseudo"] = pseudoflag

El uso de diversas banderas es crítico para indicarle al script cuándo debe hacer o no, determinadas operaciones en función del tipo de línea que está leyendo, o si ha alcanzado en final de un registro.

# The following block will parse the relevant annotation features of CDSs,
#   saving them in the the CDSs_AoA (Array of Arrays)

# Capture the locus ID on the first line to add to the FASTA header
# Then skip all lines, from the first record (line) to the source FEATURE,
# Note the use of an expr-REGEXP range: expr, /regexp/
FNR == 1 || $0 ~ /^LOCUS/ { 
   locus_id = $2 
   
   printf(">>> Processing LOCUS %s of %s ...\n", locus_id, FILENAME) > "/dev/stderr"
}

# skip lines until the source attribute of the FEATURES block is reached
$0 ~ /^LOCUS/, /^\s{4,}source\s{4,}/ { next }

{
  # Exclude pseudogenes, indicated by truncated gene coordinates with [<>], 
  #  or by the join statement: complement(join(4497616..4498557,4498557..4498814))
  if (/^\s{4,}CDS\s{4,}[[:digit:]]+/ && flag == 0 && !/[<>,]/ && !/complem/ && !/join/)
  {
     flag  = 1 
    
     # note the pattern capturing parentheses used in this regexp, 
     #   which capture the coords 123..789 into the c_arr array
     match($0, /^\s{4,}CDS\s{4,}([[:digit:]]+)\.\.([[:digit:]]+)/, c_arr)
     start = c_arr[1]
     end   = c_arr[2]

     complflag = 0
  }
  else if (/^\s{4,}CDS\s{4,}complement/ && ! flag && ! /[<>,]/  && !/join/)
  {
     flag  = 1
     coord = $0     
     
     # for the sake of showing different string-manipulation functions
     #   in this case we combine sub(), gsub() and split()
     #   to capture the CDS's coords
     sub(/^\s{4,}CDS\s{4,}complement\(/, "", coord)
     gsub(/[\)]/, "", coord)

     split(coord, c_arr, /\.\./)
     start = c_arr[1]
     end   = c_arr[2]
     
     complflag = 1
  }
  
  if( flag && /^\s+\/locus_tag=/ )
  {
     match($0, /^\s{8,}\/locus_tag="([[:alnum:]_]+)"/, lt_arr)
     locus_tag = lt_arr[1]
  } 

  # mark pseudogenes
  if( flag && /^\s{8,}\/pseudo/ ) pseudoflag = 1

  if(flag && /^\s{8,}\/product=/)
  {
     line_no = FNR
     product = $0
     sub(/^\s{8,}\/product=/, "", product)
     gsub(/["]/, "", product)
     
     # we use geneID to generate a numeric index for the hash, so that it prints out in order!
     geneID++
     
     # All the CDS's-relevant information parsed from the annotations 
     #   will be stored in the CDSs_AoA (Array of Arrays)
     CDSs_AoA[geneID]["l"] = locus_tag
     CDSs_AoA[geneID]["LOCUS"] = locus_id
     CDSs_AoA[geneID]["s"] = start
     CDSs_AoA[geneID]["e"] = end
     CDSs_AoA[geneID]["c"] = complflag
     CDSs_AoA[geneID]["pseudo"] = pseudoflag

     flag=complflag=0
  }
  
  # if the product description is split in two lines, 
  #     capture it and append it to the first line already saved in product
  if(FNR == line_no+1)
  {
     # this regexp is critical to ensure the caputre of
     #   all possible product names, like ANT(3'')-Ia, that must end with "
     # Set DEBUG = 1 at the beginning of the BEGIN{} block
     #   to print all the second lines for producut names to "/dev/stderr"
     #if($0 ~/^\s+[[:alnum:] ]+\"$/)
     if($0 ~/^\s+[a-zA-Z0-9\(\)\'\-,;: ]+"$/) # <== matches patterns like 'gene, ANT(3'')-Ia'
     {
        prod_2cnd_line = $0
        gsub(/^\s+/, "", prod_2cnd_line)
        gsub(/["]/, "", prod_2cnd_line)
        product = product " " prod_2cnd_line
        
        if(DEBUG)
           print "FNR=" FNR, "line_no="line_no, "locus_tag="locus_tag, 
                    "prod_2cnd_line="prod_2cnd_line > "/dev/stderr"

        if (pseudoflag) product = product " [pseudogene]"

        CDSs_AoA[geneID]["p"] = product
        line_no=pseudoflag=0
        product=locus_tag=""
     }
     else
     {
        if (pseudoflag) product = product " [pseudogene]"

        CDSs_AoA[geneID]["p"] = product
        line_no=pseudoflag=0
        product=locus_tag=""
     }
  }
  
  # After the ORIGING mark, starts the dna string
  if (flag == 0 && !/^ORIGIN/)  
      next 
   
  # Remove spaces and digits preceding sequence strings
  #  remove also the single spaces between 10 bp sequence strings
  if (/^ORIGIN/) { flag = 1; seq = ""; next }
  
  if(flag && $0 ~ /^\s+[[:digit:]]+\s+[AGCTNagctn\s]+/)
  {   
     sub(/^\s+[[:digit:]]+\s+/, "", $0)
     gsub(/\/\//, "", $0)
     gsub(/[[:space:]]/, "", $0)

     seq = seq $0
  }

  # fill the dna hash, once we have reached the end of record mark '//'
  if(flag && $0 ~ /^\/\/$/)
  {
     # save the genbank's DNA string and lenght in the dna array of arrays, 
     # indexed by [locus_id]["seq"] and [locus_id]["len"], respectively
     seq = toupper(seq)
     dna[locus_id]["seq"] = seq
     dna[locus_id]["len"] = length(seq)
 
     flag = 0
     locus_id=""
  }
}
# ------------------------------- END OF PATTERN-ACTION BLOCK -----------------------------------#

Veamos la cabecera de un archivo GenBank correspondiente a un plásmido pIncC de Salmonella Typhimurium, para que puedan entender lo que hace el código


LOCUS       NZ_CP012682           161461 bp    DNA     circular CON 28-FEB-2017
DEFINITION  Salmonella enterica subsp. enterica serovar Typhimurium strain
            33676 plasmid p33676_IncA/C, complete sequence.
ACCESSION   NZ_CP012682
VERSION     NZ_CP012682.1
DBLINK      BioProject: PRJNA224116
            BioSample: SAMN03795193
            Assembly: GCF_001293505.1
KEYWORDS    RefSeq.
SOURCE      Salmonella enterica subsp. enterica serovar Typhimurium (Salmonella
            typhimurium)
  ORGANISM  Salmonella enterica subsp. enterica serovar Typhimurium
            Bacteria; Proteobacteria; Gammaproteobacteria; Enterobacterales;
            Enterobacteriaceae; Salmonella.
REFERENCE   1  (bases 1 to 161461)
  AUTHORS   Vinuesa,P., Silva,C. and Calva,E.
  TITLE     Complete genome sequencing of a multidrug-resistant and
            human-invasive Salmonella Typhimurium strain of the emerging
            sequence type 213 harboring a blaCMY-2-carrying IncF plasmid
  JOURNAL   Unpublished
REFERENCE   2  (bases 1 to 161461)
  AUTHORS   Vinuesa,P., Silva,C. and Calva,E.
  TITLE     Direct Submission
  JOURNAL   Submitted (17-SEP-2015) Departamento de Microbiologia Molecular,
            Instituto de Biotecnologia, UNAM, Av. Universidad 3000, Cuernavaca,
            Morelos 62210, Mexico
COMMENT     REFSEQ INFORMATION: The reference sequence was derived from
            CP012682.
            Source DNA and bacteria are available from:  Claudia Silva
            (csilvamex1@yahoo.com).
            Annotation was added by the NCBI Prokaryotic Genome Annotation
            Pipeline (released 2013). Information about the Pipeline can be
            found here: https://www.ncbi.nlm.nih.gov/genome/annotation_prok/
            
            ##Genome-Assembly-Data-START##
            Assembly Method       :: HGAP2 v. 2.3.0.139497
            Assembly Name         :: Senter_33676_1.0
            Long Assembly Name    :: Salmonella enterica subsp. enterica
                                     serovar Typhimurium 33676 1.0
            Genome Coverage       :: 119x
            Sequencing Technology :: PacBio RS
            ##Genome-Assembly-Data-END##
            
            ##Genome-Annotation-Data-START##
            Annotation Provider               :: NCBI
            Annotation Date                   :: 02/28/2017 06:20:01
            Annotation Pipeline               :: NCBI Prokaryotic Genome
                                                 Annotation Pipeline
            Annotation Method                 :: Best-placed reference protein
                                                 set; GeneMarkS+
            Annotation Software revision      :: 4.1
            Features Annotated                :: Gene; CDS; rRNA; tRNA; ncRNA;
                                                 repeat_region
            Genes (total)                     :: 5,263
            CDS (total)                       :: 5,143
            Genes (coding)                    :: 4,886
            CDS (coding)                      :: 4,886
            Genes (RNA)                       :: 120
            rRNAs                             :: 8, 7, 7 (5S, 16S, 23S)
            complete rRNAs                    :: 8, 7, 7 (5S, 16S, 23S)
            tRNAs                             :: 83
            ncRNAs                            :: 15
            Pseudo Genes (total)              :: 257
            Pseudo Genes (ambiguous residues) :: 0 of 257
            Pseudo Genes (frameshifted)       :: 159 of 257
            Pseudo Genes (incomplete)         :: 82 of 257
            Pseudo Genes (internal stop)      :: 62 of 257
            Pseudo Genes (multiple problems)  :: 40 of 257
            CRISPR Arrays                     :: 2
            ##Genome-Annotation-Data-END##
            COMPLETENESS: full length.
FEATURES             Location/Qualifiers
     source          1..161461
                     /organism="Salmonella enterica subsp. enterica serovar
                     Typhimurium"
                     /mol_type="genomic DNA"
                     /strain="33676"
                     /serovar="Typhimurium"
                     /isolation_source="blood-culture"
                     /host="Homo sapiens; female"
                     /sub_species="enterica"
                     /db_xref="taxon:90371"
                     /plasmid="p33676_IncA/C"
                     /country="Mexico: Mexico City"
                     /lat_lon="19.43 N 99.13 W"
                     /collection_date="2011"
                     /collected_by="Juan J. Calva"
                     /genotype="ST213"
     gene            295..861
                     /locus_tag="SE15cs_RS23200"
                     /old_locus_tag="SE15cs_04746"
     CDS             295..861
                     /locus_tag="SE15cs_RS23200"
                     /old_locus_tag="SE15cs_04746"
                     /inference="COORDINATES: similar to AA
                     sequence:RefSeq:WP_000375812.1"
                     /note="Derived by automated computational analysis using
                     gene prediction method: Protein Homology."
                     /codon_start=1
                     /transl_table=11
                     /product="hypothetical protein"
                     /protein_id="WP_000375812.1"
                     /translation="MDRNWEGIEVSLPTERWLDLLDPKLEEERSEITEDLLEAEGREF
                     VAEVRSKLDHALAVLAVEAQQEADMYWNAHKSAREEASEDEQGRVGTRVRILGVSLVA
                     EWYRNRFVEQVPGQKKRVLSTHIKKGRGHAYSMSHFKKEPVWAQELIQQVETRYAVLR
                     QRATALAKIRRALNEYERQLNKTHSDEV"
     gene            866..1153
                     /locus_tag="SE15cs_RS23205"
                     /old_locus_tag="SE15cs_04747"
     CDS             866..1153
                     /locus_tag="SE15cs_RS23205"
                     /old_locus_tag="SE15cs_04747"
                     /inference="COORDINATES: similar to AA
                     sequence:RefSeq:WP_000127322.1"
                     /note="Derived by automated computational analysis using
                     gene prediction method: Protein Homology."
                     /codon_start=1
                     /transl_table=11
                     /product="hypothetical protein"
                     /protein_id="WP_000127321.1"
                     /translation="MTASVAATELAKLGKCEAMIKKVASHPRPALSKRPQSPQGTDST
                     LRGEFAHFRYEAAALRFMSGTAGAKRRIYQLVFSATVAAGALTMLAAWTTS"
... TRUNCATED

El primer bloque de código se enfoca en la primera línea del GenBank para extraer el LOCUS ID, el cual se usará como cabecera del archivo FASTA que contendrá la cadena de DNA completa del genoma.

# Capture the locus ID on the first line to add to the FASTA header
# Then skip all lines, from the first record (line) to the source FEATURE,
# Note the use of an expr-REGEXP range: expr, /regexp/
FNR == 1 || /^LOCUS/ { 
   locus_id = $2 
   
   printf(">>> Processing LOCUS %s of %s ...\n", locus_id, FILENAME) > "/dev/stderr"
}

Sigue otra sentencia de patrón-acción, que le indica al script saltarse todas las líneas, desde la primera de un registro (que inica con LOCUS) hasta la que contenga el patrón /^source/ (ver ejemplo de GenBank arriba)

# skip lines until the source attribute of the FEATURES block is reached
/^LOCUS/, /^\s{4,}source\s{4,}/ { next }

Es decir, se especifica un rango de líneas a ignorar, las cuales se salta mediante el uso de next.

Las siguientes líneas se encargan de extraer las coordenadas y otra información de cada CDS. Vean el uso de regexps, condicionales y banderas para localizar los campos de atributos (FEATURES) de interés. Cuando por primera vez encuentra el campo ” CDS 295..861”, activa la bandera “flag”: flag = 1 y se prepara para capturar las coordenadas de inicio y fin del CDS. Cuando el CDS viene en la cadena complementaria, el GenBank lo indica, como en:

      CDS             complement(2886..3071)
                      /locus_tag="SE15cs_RS26325"
...

en cuyo caso el bloque de código también activa la bandera complflag = 1, como pueden ver abajo

{
  # Exclude pseudogenes, indicated by truncated gene coordinates with [<>], 
  #  or by the join statement: complement(join(4497616..4498557,4498557..4498814))
  if (/^\s{4,}CDS\s{4,}[[:digit:]]+/ && flag == 0 && !/[<>,]/ && !/complem/ && !/join/)
  {
     flag  = 1 
    
     # note the pattern capturing parentheses used in this regexp, 
     #   which capture the coords 123..789 into the c_arr array
     match($0, /^\s{4,}CDS\s{4,}([[:digit:]]+)\.\.([[:digit:]]+)/, c_arr)
     start = c_arr[1]
     end   = c_arr[2]

     complflag = 0
  }
  else if (/^\s{4,}CDS\s{4,}complement/ && ! flag && ! /[<>,]/  && !/join/)
  {
     flag  = 1
     coord = $0     
     
     # for the sake of showing different string-manipulation functions
     #   in this case we combine sub(), gsub() and split()
     #   to capture the CDS's coords
     sub(/^\s{4,}CDS\s{4,}complement\(/, "", coord)
     gsub(/[\)]/, "", coord)

     split(coord, c_arr, /\.\./)
     start = c_arr[1]
     end   = c_arr[2]
     
     complflag = 1
  }
  
  if( flag && /^\s+\/locus_tag=/ )
  {
     match($0, /^\s{8,}\/locus_tag="([[:alnum:]_]+)"/, lt_arr)
     locus_tag = lt_arr[1]
  } 

  # mark pseudogenes
  if( flag && /^\s{8,}\/pseudo/ ) pseudoflag = 1

  if(flag && /^\s{8,}\/product=/)
  {
     line_no = FNR
     product = $0
     sub(/^\s{8,}\/product=/, "", product)
     gsub(/["]/, "", product)
     
     # we use geneID to generate a numeric index for the hash, so that it prints out in order!
     geneID++
     
     # All the CDS's-relevant information parsed from the annotations 
     #   will be stored in the CDSs_AoA (Array of Arrays)
     CDSs_AoA[geneID]["l"] = locus_tag
     CDSs_AoA[geneID]["LOCUS"] = locus_id
     CDSs_AoA[geneID]["s"] = start
     CDSs_AoA[geneID]["e"] = end
     CDSs_AoA[geneID]["c"] = complflag
     CDSs_AoA[geneID]["pseudo"] = pseudoflag

     flag=complflag=0
  }
  
  # if the product description is split in two lines, 
  #     capture it and append it to the first line already saved in product
  if(FNR == line_no+1)
  {
     # this regexp is critical to ensure the caputre of
     #   all possible product names, like ANT(3'')-Ia, that must end with "
     # Set DEBUG = 1 at the beginning of the BEGIN{} block
     #   to print all the second lines for producut names to "/dev/stderr"
     #if($0 ~/^\s+[[:alnum:] ]+\"$/)
     if(/^\s+[a-zA-Z0-9\(\)\'\-,;: ]+"$/) # <== matches patterns like 'gene, ANT(3'')-Ia'
     {
        prod_2cnd_line = $0
        gsub(/^\s+/, "", prod_2cnd_line)
        gsub(/["]/, "", prod_2cnd_line)
        product = product " " prod_2cnd_line
        
        if(DEBUG)
           print "FNR=" FNR, "line_no="line_no, "locus_tag="locus_tag, "prod_2cnd_line="prod_2cnd_line > "/dev/stderr"

        if (pseudoflag) product = product " [pseudogene]"

        CDSs_AoA[geneID]["p"] = product
        line_no=pseudoflag=0
        product=locus_tag=""
     }
     else
     {
        if (pseudoflag) product = product " [pseudogene]"

        CDSs_AoA[geneID]["p"] = product
        line_no=pseudoflag=0
        product=locus_tag=""
     }
  }

... TRUNCATED
  • Noten arriba el uso de la función match()´ y regexps con paréntesis para capturar ciertas partes de un patrón, en este caso las coordenadas de incio y fin de un CDS, las cuales se almacenan en el arreglo c_arr
     match($0, /^\s{4,}CDS\s{4,}([[:digit:]]+)\.\.([[:digit:]]+)/, c_arr)
     start = c_arr[1]
     end   = c_arr[2]
  • Con el fin de mostrar más posibilidades de parseo, se presenta también el uso de las funcionbes sub(), gsub() y split() para extraer las coordenadas en múltiples pasos: primero se captura la línea que contiene las coordenadas, y luego se extrae la de inicio y la de fin del CDS.
  else if (/^\s{4,}CDS\s{4,}complement/ && ! flag && ! /[<>,]/  && !/join/)
  {
     flag  = 1
     coord = $0     
     
     # for the sake of showing different string-manipulation functions
     #   in this case we combine sub(), gsub() and split()
     #   to capture the CDS's coords
     sub(/^\s{4,}CDS\s{4,}complement\(/, "", coord)
     gsub(/[\)]/, "", coord)

     split(coord, c_arr, /\.\./)
     start = c_arr[1]
     end   = c_arr[2]
     
     complflag = 1
  }
  • Siguen líneas que capturan el locus_tag, la descripción del producto y si el gen es un pseudogen, todos estos son atributos del mismo CDS. Noten que si el CDS es un pseudogen, se agrega la cadena ’ [pseudogen]’ al producto.

  • Finalmente la información capturada en las líneas anteriores se guarda en CDSs_AoA, que es un hash de hashes, cuya llave principal es el contador geneID, variable que es auto-incrementada cada vez que se encuentra un CDS. Así, para guardar por ejemplo el atributo locus_tag de un CDS particular, se usa el siguiente código CDSs_AoA[geneID][“l”] = locus_tag, donde el locus_tag se identifica con la llave primaria geneID, específica del CDS en cuestión, y la llave secundaria “l”, en la siguiente estructura: CDSs_AoA->geneID->l = locus_tag.

  if( flag && /^\s+\/locus_tag=/ )
  {
     match($0, /^\s{8,}\/locus_tag="([[:alnum:]_]+)"/, lt_arr)
     locus_tag = lt_arr[1]
  } 

  # mark pseudogenes
  if( flag && /^\s{8,}\/pseudo/ ) pseudoflag = 1

  if(flag && /^\s{8,}\/product=/)
  {
     line_no = FNR
     product = $0
     sub(/^\s{8,}\/product=/, "", product)
     gsub(/["]/, "", product)
     
     # we use geneID to generate a numeric index for the hash, so that it prints out in order!
     geneID++
     
     # All the CDS's-relevant information parsed from the annotations 
     #   will be stored in the CDSs_AoA (Array of Arrays)
     CDSs_AoA[geneID]["l"] = locus_tag
     CDSs_AoA[geneID]["LOCUS"] = locus_id
     CDSs_AoA[geneID]["s"] = start
     CDSs_AoA[geneID]["e"] = end
     CDSs_AoA[geneID]["c"] = complflag
     CDSs_AoA[geneID]["pseudo"] = pseudoflag

     flag=complflag=0
  }

El siguiente bloque que inicia con if(FNR == line_no+1), está diseñado para capturar la descripción completa del producto génico cuando ésta es muy larga y se encuentra en dos líneas, como en el ejemplo mostrado abajo

        /product="multidrug efflux RND transporter periplasmic
         adaptor subunit OqxA"

El algoritmo del bloque en cuestión, mostrado abajo, es el siguiente:

  1. if(FNR == line_no+1) comprueba si estamos en la línea que sigue a un atributo ‘/product=““’
  2. Si es así, usa una regexp para comprobar la línea contiene un nombre de producto y no otro atributo (que tendrían caracteres como =./). Noten que es una regexp compleja, para capturar patrones complejos, como nombres de genes tipo ‘gene, ANT(3’‘)-Ia’
  3. Si este segundo condicional es cierto, se captura y procesa la línea, agregándola al final del contenido de la primera línea, guardada en la variable product product = product ” ” prod_2cnd_line, y la agregamos al hash CDSs_AoA[geneID][“p”] = product. Si el CDS corresponde a un pseudogene, le agregamos también al producto la etiqueta ’ [pseudogene]’
  4. reinicializamos line_no=pseudoflag=0; product=locus_tag=““
  # if the product description is split in two lines, 
  #     capture it and append it to the first line already saved in product
  if(FNR == line_no+1)
  {
     # this regexp is critical to ensure the caputre of
     #   all possible product names, like ANT(3'')-Ia, that must end with "
     # Set DEBUG = 1 at the beginning of the BEGIN{} block
     #   to print all the second lines for producut names to "/dev/stderr"
     #if($0 ~/^\s+[[:alnum:] ]+\"$/)
     if(/^\s+[a-zA-Z0-9\(\)\'\-,;: ]+"$/) # <== matches patterns like 'gene, ANT(3'')-Ia'
     {
        prod_2cnd_line = $0
        gsub(/^\s+/, "", prod_2cnd_line)
        gsub(/["]/, "", prod_2cnd_line)
        product = product " " prod_2cnd_line
        
        if(DEBUG)
           print "FNR=" FNR, "line_no="line_no, "locus_tag="locus_tag, "prod_2cnd_line="prod_2cnd_line > "/dev/stderr"

        if (pseudoflag) product = product " [pseudogene]"

        CDSs_AoA[geneID]["p"] = product
        line_no=pseudoflag=0
        product=locus_tag=""
     }
     else
     {
        if (pseudoflag) product = product " [pseudogene]"

        CDSs_AoA[geneID]["p"] = product
        line_no=pseudoflag=0
        product=locus_tag=""
     }
  }

Una vez capturada la información del CDS, se vuelven a inicializar a 0 todas las variables numéricas, incluidas las banderas. A medida que awk va leyendo las líneas del archivo, repite las operaciones arriba descritas, hasta que llega al final del bloque de “FEATURES” e inicia el bloque “ORIGIN” del GenBank, como se muestra abajo:


... TRUNCATED
     gene            complement(160301..161284)
                     /locus_tag="SE15cs_RS24170"
                     /old_locus_tag="SE15cs_04947"
     CDS             complement(160301..161284)
                     /locus_tag="SE15cs_RS24170"
                     /old_locus_tag="SE15cs_04947"
                     /inference="COORDINATES: similar to AA
                     sequence:RefSeq:WP_000077456.1"
                     /note="Derived by automated computational analysis using
                     gene prediction method: Protein Homology."
                     /codon_start=1
                     /transl_table=11
                     /product="plasmid stability protein StbA"
                     /protein_id="WP_000077458.1"
                     /translation="MSQFVLGLDIGYSNLKMAMGYKGEEARTVVMPVGAGPLELMPQQ
                     LTGGAGTCIQVVIDGEKWVAGVEPDRLQGWERELHGDYPSTNPYKALFYAALLMSEQK
                     EIDVLVTGLPVSQYMDVERREALKSRLEGEHQITPKRSVAVKSVVVVPQPAGAYMDVV
                     SSTKDEDLLEIIQGGKTVVIDPGFFSVDWVALEEGEVRYHSSGTSLKAMSVLLQETDR
                     LIQEDHGGAPGIEKIEKAIRAGKAEIFLYGEKVSIKDYFKKASTKVAQNALIPMRKSM
                     REDGMDADVVLLAGGGAEAYQDAAKELFPKSRIVLPNESVASNARGFWFCG"
ORIGIN      
        1 ataggctcag ataaacagac cttaccctcg catcgagaac cgcttgccct ccagcatcga
       61 gagacggtgg taaagaggca tttggaatct ttgatgccat atccaatata tctggaatct
      121 ttaaatatag attcatatgt aaagaggctg tgaaagaata agagcatcaa gattccagat
      181 agatagaggg aaatttgaca aattccaaag atgggttagc ctagtgacag aactagattc
      241 cagtattgga ataatcagct ttaaattcca gatagatagt tatgtggata ggaattggat
      301 aggaattggg agggtattga ggtgagtcta ccaacagagc gatggctaga tctgctagat
      361 ccgaagctcg aagaagaacg atccgagata acagaggatc tgctagaggc agagggacga
      421 gagtttgttg cagaggtacg gagcaaactg gatcacgcct tagcggttct tgctgtcgag
      481 gcgcagcagg aagcggacat gtactggaac gcgcacaaat cagcgcgtga agaagcgtca
      541 gaggacgaac aagggcgtgt cggtacacgg gttcgcattc taggcgtatc actcgttgca
      
... TRUNCATED

Las últimas líneas del bloque patrón-acción de extract_CDSs_from_GenBank.awk se encargan de extraer la secuencia de DNA, guardándola en la variable seq, haciendo uso una vez más de una combinación de condicionales con regexps y banderas

  • Este bloque se activa sólo cuando el script llega a la línea que inicia con ORIGIN, inicializando las variables flag y seq, pasando a la siguiente línea, la primera con secuencia, usando next if (/^ORIGIN/) { flag = 1; seq = ““; next }

  • Una vez que llega a la línea de las secuencias, comprueba que es el caso con if(flag && $0 ~ /^+[[:digit:]]++[AGCTNagctn]+/) y la procesa, eliminando los números del inicio de línea y los espacios entre bloques de 10 nucleótidos.

  # After the ORIGING mark, starts the dna string
  if (flag == 0 && !/^ORIGIN/)  
      next 
   
  # Remove spaces and digits preceding sequence strings
  #  remove also the single spaces between 10 bp sequence strings
  if (/^ORIGIN/) { flag = 1; seq = ""; next }
  
  if(flag && $0 ~ /^\s+[[:digit:]]+\s+[AGCTNagctn\s]+/)
  {   
     sub(/^\s+[[:digit:]]+\s+/, "", $0)
     gsub(/\/\//, "", $0)
     gsub(/[[:space:]]/, "", $0)

     seq = seq $0
  }

  # fill the dna AoA, once we have reached the end of record mark '//'
  if(flag && $0 ~ /^\/\/$/)
  {
     # save the genbank's DNA string and lenght in the dna array of arrays, 
     # indexed by [locus_id]["seq"] and [locus_id]["len"], respectively
     seq = toupper(seq)
     dna[locus_id]["seq"] = seq
     dna[locus_id]["len"] = length(seq)

     flag = 0
     locus_id=""
  }
}
# ------------------------------- END OF PATTERN-ACTION BLOCK -----------------------------------#
  • Cuando llega al final del registro, que viene marcado con ‘//’, la secuencia seq se convierte a mayúsculas seq = toupper(seq) y se guarda en el AoA dna, indexado con el locus_id dna[locus_id][“seq”] = seq, y su longitud se guarda en dna[locus_id][“len”] = length(seq). Finalmente es crítico reinicializar las variables flag y locus_id

12.10.2.4 Bloque de procesamiento de datos capturados en el arreglo dna y el hash de hashes CDSs_AoA mediante llamada a funciones

Una vez terminado el parseo de datos, al terminar de leer el último registro del archivo GenBank, se comienza a procesar los datos almacenados en las estructuras de datos dna y CDSs_AoA. Esta última se procesa mediante múltiples funciones anidadas. Noten también cómo se indexa el AoA \(dna\) con el AoA [CDSs_AoA[k][“LOCUS”] para imprimir en la tabla la longitud de cada locus, usando el siguient código: dna[CDSs_AoA[k][“LOCUS”]][“len”]. Este ejemplo ilustra muy bien la versatilidad y belleza de las estructuras de datos complejas en gawk.]’ - eliminar ‘genosp.’ - sustituir espacios por guiones bajos

Nota: hagan uso de expresiones regulares como ’.*’ y ‘[[:space:]]’

  1. Cuando estén satisfechos con el resultado, guarden la salida del comando en un archivo llamado recA_Bradyrhizobium_vinuesa.fnaed

12.11 Solución a la práctica y un ejercicio adicional

Este ejercicio está basado en un capítulo que escribí para el manual de Sistemática Molecular y Bioinformática. Guía práctica

  1. ¿Cuántas secuencias hay en el archivo recA_Bradyrhizobium_vinuesa.fna?
 grep '^>' recA_Bradyrhizobium_vinuesa.fna | head -5
## >EU574327.1 Bradyrhizobium liaoningense strain ViHaR5 recombination protein A (recA) gene, partial cds
## >EU574326.1 Bradyrhizobium liaoningense strain ViHaR4 recombination protein A (recA) gene, partial cds
## >EU574325.1 Bradyrhizobium liaoningense strain ViHaR3 recombination protein A (recA) gene, partial cds
## >EU574324.1 Bradyrhizobium liaoningense strain ViHaR2 recombination protein A (recA) gene, partial cds
## >EU574323.1 Bradyrhizobium liaoningense strain ViHaR1 recombination protein A (recA) gene, partial cds
  1. Explora la cabecera y cola del archivo con head y tail
 grep '^>' recA_Bradyrhizobium_vinuesa.fna | head -5
## >EU574327.1 Bradyrhizobium liaoningense strain ViHaR5 recombination protein A (recA) gene, partial cds
## >EU574326.1 Bradyrhizobium liaoningense strain ViHaR4 recombination protein A (recA) gene, partial cds
## >EU574325.1 Bradyrhizobium liaoningense strain ViHaR3 recombination protein A (recA) gene, partial cds
## >EU574324.1 Bradyrhizobium liaoningense strain ViHaR2 recombination protein A (recA) gene, partial cds
## >EU574323.1 Bradyrhizobium liaoningense strain ViHaR1 recombination protein A (recA) gene, partial cds
  1. Cuenta el numero de generos y especies que contiene el archivo FASTA
grep '^>' recA_Bradyrhizobium_vinuesa.fna | cut -d' ' -f2,3 | sort | uniq -c
##      18 Bradyrhizobium canariense
##      18 Bradyrhizobium elkanii
##       6 Bradyrhizobium genosp.
##      28 Bradyrhizobium japonicum
##      15 Bradyrhizobium liaoningense
##       8 Bradyrhizobium sp.
##      32 Bradyrhizobium yuanmingense
  1. Imprime una lista ordenada de mayor a menor, del numero de especies que contiene el archivo FASTA
grep '^>' recA_Bradyrhizobium_vinuesa.fna | cut -d' ' -f2,3 | sort | uniq -c | sort -nrk1
##      32 Bradyrhizobium yuanmingense
##      28 Bradyrhizobium japonicum
##      18 Bradyrhizobium elkanii
##      18 Bradyrhizobium canariense
##      15 Bradyrhizobium liaoningense
##       8 Bradyrhizobium sp.
##       6 Bradyrhizobium genosp.

12.11.1 Edición de las cabeceras FASTA mediante herramientas de filtrado de UNIX

  1. Exploremos todas las cabeceras FASTA del archivo recA_Bradyrhizobium_vinuesa.fna usando grep
# grep '^>' recA_Bradyrhizobium_vinuesa.fna | less # para verlas por página 
grep '^>' recA_Bradyrhizobium_vinuesa.fna  | head # para no hacer muy extensa la salida
## >EU574327.1 Bradyrhizobium liaoningense strain ViHaR5 recombination protein A (recA) gene, partial cds
## >EU574326.1 Bradyrhizobium liaoningense strain ViHaR4 recombination protein A (recA) gene, partial cds
## >EU574325.1 Bradyrhizobium liaoningense strain ViHaR3 recombination protein A (recA) gene, partial cds
## >EU574324.1 Bradyrhizobium liaoningense strain ViHaR2 recombination protein A (recA) gene, partial cds
## >EU574323.1 Bradyrhizobium liaoningense strain ViHaR1 recombination protein A (recA) gene, partial cds
## >EU574322.1 Bradyrhizobium liaoningense strain ViHaG8 recombination protein A (recA) gene, partial cds
## >EU574321.1 Bradyrhizobium liaoningense strain ViHaG7 recombination protein A (recA) gene, partial cds
## >EU574320.1 Bradyrhizobium liaoningense strain ViHaG6 recombination protein A (recA) gene, partial cds
## >EU574319.1 Bradyrhizobium yuanmingense strain ViHaG5 recombination protein A (recA) gene, partial cds
## >EU574318.1 Bradyrhizobium yuanmingense strain ViHaG4 recombination protein A (recA) gene, partial cds
  1. simplifiquemos las cabeceras FASTA usando el comando sed (stream editor)

El objetivo es eliminar redundancia y los campos gb|no.de.acceso, así como todos los caracteres ‘( , ; : )’ que impedirían el despliegue de un árbol filogenético, al tratarse de caracteres reservados del formato NEWICK. Dejar solo el numero de accesión, así como el género, especie y cepa indicados entre corchetes.

Es decir vamos a: - reducir Bradyrhizobium a ‘B.’ - eliminar ’ recombination …’ y reemplazarlo por ‘]’ - eliminar ‘genosp.’ - sustituir espacios por guiones bajos

Noten el uso de expresiones regulares como ’.*’ y ‘[[:space:]]’

sed 's/ Bra/ [Bra/; s/|gb.*| /|/; s/Bradyrhizobium /B_/; s/genosp\. //; s/ recomb.*/]/; s/[[:space:]]/_/g; s/_/ /' recA_Bradyrhizobium_vinuesa.fna | grep '>' | head -5
sed 's/ Bra/ [Bra/; s/|gb.*| /|/; s/Bradyrhizobium /B_/; s/genosp\. //; s/ recomb.*/]/; s/[[:space:]]/_/g; s/_/ /' recA_Bradyrhizobium_vinuesa.fna | grep '>' | tail -5
## >EU574327.1 [B_liaoningense_strain_ViHaR5]
## >EU574326.1 [B_liaoningense_strain_ViHaR4]
## >EU574325.1 [B_liaoningense_strain_ViHaR3]
## >EU574324.1 [B_liaoningense_strain_ViHaR2]
## >EU574323.1 [B_liaoningense_strain_ViHaR1]
## >AY591544.1 [B_japonicum_bv._genistearum_strain_BC-P14]
## >AY591543.1 [B_beta_strain_BC-P6]
## >AY591542.1 [B_canariense_bv._genistearum_strain_BC-P5]
## >AY591541.1 [B_canariense_bv._genistearum_strain_BC-C2]
## >AY591540.1 [B_alpha_bv._genistearum_strain_BC-C1]
  1. Cuando estamos satisfechos con el resultado, guardamos la salida del comando en un archivo usando ‘>’ para redirigir el flujo de STDOUT a un archivo de texto
sed 's/ Bra/ [Bra/; s/|gb.*| /|/; s/Bradyrhizobium /B_/; s/genosp\. //; s/ recomb.*/]/; s/[[:space:]]/_/g; s/_/ /' recA_Bradyrhizobium_vinuesa.fna > recA_Bradyrhizobium_vinuesa.fnaed
  • revisemos la salida
grep '^>' recA_Bradyrhizobium_vinuesa.fnaed | head -5 && grep '^>' recA_Bradyrhizobium_vinuesa.fnaed | tail -5
## >EU574327.1 [B_liaoningense_strain_ViHaR5]
## >EU574326.1 [B_liaoningense_strain_ViHaR4]
## >EU574325.1 [B_liaoningense_strain_ViHaR3]
## >EU574324.1 [B_liaoningense_strain_ViHaR2]
## >EU574323.1 [B_liaoningense_strain_ViHaR1]
## >AY591544.1 [B_japonicum_bv._genistearum_strain_BC-P14]
## >AY591543.1 [B_beta_strain_BC-P6]
## >AY591542.1 [B_canariense_bv._genistearum_strain_BC-P5]
## >AY591541.1 [B_canariense_bv._genistearum_strain_BC-C2]
## >AY591540.1 [B_alpha_bv._genistearum_strain_BC-C1]

Excelente, ésto ha funcionado perfectamente. Ya pueden decir que dominan las herramientas de filtrado básicas y su combinación en pipelines - ¡¡¡enhorabuena!!!

Para reforzar lo aprendido les dejo el siguiente ejercicio:

12.12 Reto de programación - ejercicio de parseo de archivos FASTA

Como ejercicio, para repasar lo que hemos aprendido en esta sesión les propongo repetir el ejercicio de parseo de archivos FASTA pero con secuencias del gen rpoB de Bradyrhizobium

  1. Descargar las secuencias de NCBI usando el portal ENTREZ nucleotides con: ‘Bradyrhizobium[orgn] AND vinuesa[auth] AND rpoB[gene]’
  2. Renombra el archivo descargado a rpoB_Bradyrhizobium_vinuesa.fna
  3. ¿Cuántas secuencias hay en el archivo rpoB_Bradyrhizobium_vinuesa.fna?
  4. Explora la cabecera y cola del archivo con \(head\) y \(tail\)
  5. Despliega las 5 primeras lineas de cabeceras fasta usando \(grep\) y \(head\) para explorar su estructura en detalle
  6. Calcula el número de géneros que contiene el archivo FASTA
  7. Calcula el número de especies que contiene el archivo FASTA
  8. Imprime una lista ordenada de mayor a menor, del numero de especies que contiene el archivo FASTA

Pasemos al siguiente nivel.


13 Fundamentos de programación en \(Bash\)

Revisaremos en esta sección algunos de los elementos sintácticos básicos para poder programar en \(Bash\), como son la asignación y uso de variables, control de flujo de un programa mediante condicionales y bucles. Estos son sólo algunos de los elementos esenciales del lenguaje \(Bash\), el más poderoso y usado para la programación Shell.

Revisa la manual GNU de referencia de Bash para cualquier aspecto adicional que necesites consultar.

13.1 Tipos, asignación y uso de variables

El primer paso es la asignación de variables. Todos los lenguajes hacen uso de ellas. Se usan para guardar valores sencillos (variables escalares) numéricos o de cadenas de caracteres, o listas y estructuras de datos más complejas, como \(arreglos\) indexados por índices posicionales, o \(hashes\), que son arreglos indexados por llaves especificas.

Independientemente del tipo de variable, los nombres que les damos deben inciar con un caracter alfabético o guión bajo, y el resto de caracteres deben ser alfanuméricos, es decir de la clase
[a-zA-Z0-9_]

y no contener espacios.

Las variables de \(Shell\) (\(Bash\) inclusive) son globales y no necesitan ser declaradas (salvo \(hashes\)).

Según su uso, las variables temporales que se usan como aliases temporales por ejemplo en bucles, suelen ser nombradas con una sola letra, por conveniencia y por ser obvio lo que contienen

for f in *.fna; do echo $f; done

Para otras variables que se usan en diferentes puntos de un programa, conviene hacer uso de nombres cortos pero informativos, como

formato_de_entrada=''
formato_de_salida=''
runmode=''

Es una buena práctica mantener un estilo consistente para los nombres de variables, evitando usar variables en puras mayúsculas, para minimizar el riesgo de interferencia con variables de ambiente, como USER, HOME, SHELL, PATH, que por convención se nombran en mayúsculas.

13.1.1 Variables escalares

La sintaxis básica de asignación de un valor simple a una variable escalar y uso de comillas

varName=VALUE

Noten que no puede haber un espacio entre el nombre de la variable, el ‘=’ y el valor.

  • para recuperar el valor de una varialbe, le añadimos el prefijo $. Para imprimir el valor asignado a la variable, usamos echo $varName
archivo_de_comandos_linux=linux_commands.tab
echo "$archivo_de_comandos_linux"

# asignación de cadena de texto, con espacios y otros símbolos, entre comillas sencillas
var2='Cadena con espacios, entre comillas sencillas'
echo "var2: $var2"

# asignación de cadena de texto, con espacios y otros símbolos, incluyendo variables (en este caso de ambiente) entre comillas dobles, para interpolación de variables
saludo_inicial="Bienvenido a $HOSTNAME, $USER! Te recuerdo que hoy es $(date)"
echo "$saludo_inicial"

# Llamado a variable entre comillas sencillas NO INTERPOLA!!!
echo ' >>> Ojo, VARIABLE ENTRE COMILLAS SENCILLAS NO INTERPOLA: $saludo_inicial; se imprime literalmente'

echo
## linux_commands.tab
## var2: Cadena con espacios, entre comillas sencillas
## Bienvenido a alisio, vinuesa! Te recuerdo que hoy es lun 26 sep 2022 22:11:58 CDT
##  >>> Ojo, VARIABLE ENTRE COMILLAS SENCILLAS NO INTERPOLA: $saludo_inicial; se imprime literalmente

No es necesario hacer llamados a las variables entre comillas dobles, pero es una buena práctica, como se muestra en los ejemplos del bloque anterior.

  • para concatenar los valores de dos variables, intercalando otros caracteres, debemos proteger el nombre de la variable entre llave, usando la sintaxis ${var}
nombre="Juan"
apellido="Aguirre"
n_a="${nombre}_${apellido}"
echo "Nombe y Apellido: $n_a"
## Nombe y Apellido: Juan_Aguirre
  • Recuerda: los nombres de las variables no pueden empezar con un dígito, ni contener caracteres más que los alfanuméricos estándar del idioma inglés: [a-zA-Z0-9_]
# Nombres válidos de variables
user_name=$USER
user1="María López"  " # noten que los valores a asignar que contengan espacios deben escaoarse con comillas

# Nombres INválidos de variables
ubicación=CDMX
1_argumento=1

13.2 Captura en una varialbe de la salida de un comando con var=$(comando)

wkdir=$(pwd)
dat=$(date | awk '{print $2"-"$3"-"$4}')
h=$(hostname)
echo ">>> working in <$wkdir> at <$h> on <$dat>"
## >>> working in </home/vinuesa/cursos/intro2linux> at <alisio> on <26-sep-2022>

13.3 Modificación de variables y operaciones con ellas

wkdir=$(pwd)
echo "wkdir: $wkdir"

# 1. cortemos caracteres por la izquierda (todos los caracteres por la izquierda, hasta llegar a último /)
basedir=${wkdir##*/}
echo "basedir: $basedir  # \${wkdir##*/}"

# 2. cortemos caracteres por la derecha (cualqier caracter hasta llegar a /)
echo "path to basedir: ${wkdir%/*}  # \${wkdir%/*}"

# 3. contar el número de caracteres (longitud) de la variable
echo "basedir has ${#basedir} characters  # \${#basedir}"
## wkdir: /home/vinuesa/cursos/intro2linux
## basedir: intro2linux  # ${wkdir##*/}
## path to basedir: /home/vinuesa/cursos  # ${wkdir%/*}
## basedir has 11 characters  # ${#basedir}

13.4 Condicionales

  • La sintaxis básica de un condicional simple en formato de una línea es así

if [ condición ]; then orden1; orden2 …; fi

  • también hay una versión más corta para test siples

[ condición ] && setecia1 && sentencia2

  • En un script, lo escibimos generalmente como un bloque indentado, para mejor legibilidad
if [ condición ]; then 
    orden1
    orden2 
fi

13.4.1 Comparación de íntegros en condicionales tipo \(test\) ([ ]) estándar.

i=5
j=3

if [ "$i" -lt "$j" ]; then
   echo "$i < $j"
elif [ "$i" -gt "$j" ]; then
   echo "$i > $j"
else
    echo "$i == $j"
fi
## 5 > 3

13.4.2 Comparación de íntegros en condicionales tipo ((( ))) exclusivos para íntegros.

i=3
j=3

if (( "$i" < "$j" )); then
   echo "$i < $j"
elif (("$i" > "$j" )); then
   echo "$i > $j"
else
    echo "$i == $j"
fi
## 3 == 3

13.4.3 Comparación de cadenas de caracteres en condicionales

c=carla
j=juan

if [ "$c" == "$j" ]; then
   echo "$c = $j"
elif [ "$c" != "$j" ]; then
   echo "c:$c != j:$j "
fi
## c:carla != j:juan

13.4.4 Comprobación de la existencia de un archivo de tamaño > 0 bytes

touch empty_file
ls -l empty_file
ls -l *gz
f=$(ls *gz)

if [ -e empty_file  ]; then
   echo "empty_file file exists"
fi

if [ ! -s empty_file  ]; then
   echo "empty_file file exists but is empty"
fi

if [ -s "$f" ]; then
   size=$(du -h assembly_summary.txt.gz | cut -f1)
   # o tambien 
   # size=$(ls -hs assembly_summary.txt.gz | cut -d' ' -f1)
   echo "$f exists and has size: $size"
fi
## -rw-rw-r-- 1 vinuesa vinuesa 0 sep 26 22:11 empty_file
## -rw-r--r-- 1 vinuesa vinuesa 6780296 oct 20  2020 assembly_summary.txt.gz
## empty_file file exists
## empty_file file exists but is empty
## assembly_summary.txt.gz exists and has size: 6.5M

13.4.5 La versión corta de test [ condición ] && comando1 && comando2

# también podemos usar la versión corta del test:
f=$(ls *.txt.gz)
[ -s "$f" ] && echo "$f exists and is non-empty"
## assembly_summary.txt.gz exists and is non-empty

13.4.6 Versión moderna de \(test\) para evaluar expresiones [[ $(expresión) ]] o expresiones regulares [[ $var =~ regexp ]]

Esta versión del \(test\), si es exitosa evalúa a 0, como muestra el siguiente código

  • evaluación de [[ $(expresión) ]]
[[ $(ls *txt.gz | grep '^assembly') ]] && echo -n "\$?=$?" && echo "; encontré un archivo con terminación txt.gz y que incia con 'assembly'"
## $?=0; encontré un archivo con terminación txt.gz y que incia con 'assembly'
  • ejemplo de evaluación de expresiones regulares [[ $var =~ regexp ]]
[[ "$OSTYPE" =~ ^linux.* ]] && echo "Excelente, la máquina '$HOSTNAME' corre $OSTYPE! ;)"
## Excelente, la máquina 'alisio' corre linux-gnu! ;)

13.4.7 if; elif; else con la versión moderna de test para evaluar expresiones regulares [[ “$var” =~ regexp ]]

  if [[ "$OSTYPE" =~ ^linux.* ]]
  then
    OS='linux'
    no_cores=$(awk '/^processor/{n+=1}END{print n}' /proc/cpuinfo)
    host=$(hostname)
    echo "running on $host under $OS with $no_cores cores :)"
  elif [[ "$OSTYPE" == "darwin"* ]]
  then
    OS='darwin'
    no_cores=$(sysctl -n hw.ncpu)
    host=$(hostname)
    echo "running on $host under $OS with $no_cores cores :)"
  else
       OS='windows'
       echo "oh no! another windows box :( ... you should better change to GNU/Linux :) "
  fi
## running on alisio under linux with 12 cores :)

13.5 Operaciones aritméticas con el shell - sólo con íntegros

El \(Shell\) implementa operadores aritméticos estándar: + - / **/

echo "3+4 = $((3+4))"
echo "3-4 = $((3-4))"
echo "12/4 = $((12/4))"
echo "3*2 = $((3*2))"
echo "3**2 = $((3**2))"
## 3+4 = 7
## 3-4 = -1
## 12/4 = 3
## 3*2 = 6
## 3**2 = 9
  • Es importante notar aquí que el \(Shell\) no puede realizar operaciones con números flotantes
echo $((3+4.1))
bash: 3+4.1: syntax error: invalid arithmetic operator (error token is ".1")

13.6 el comando bc para aritmética con números flotantes de precisión arbitraria en \(Bash\)

\(bc\) es un lenguaje para el cálculo con precisión arbitraria muy poderoso y con muchas opciones. Sólo les voy a mostrar las más básicas. Les recomiendo vean el manual del comando:

  • man bc

13.6.1 Opciones básicas de bc: scale= y bc -l

  • -l activa la librería de matemáticas. Usa \(bc\ -l\) siempre que trabajes con divisiones o funciones
  • con \(scale=<int>\) indicamos la precisión
echo -n "3.45 + 4.12 = "; echo '3.45+4.12' | bc
echo -n '13.7 / 7.9 sin usar opción -l da solo el dividendo = '; echo '13.7 / 7.9'  | bc
echo -n '13.7 / 7.9 con opción -l da por defecto 20 decimales = '; echo '13.7 / 7.9'  | bc -l
echo -n '13.7 / 7.9 con tres decimales = '; echo 'scale = 3; 13.7 / 7.9'  | bc -l
echo -n '2^3 = '; echo '2^3'  | bc 
echo -n 'raíz cuadrada de 12 con 2 decimales = '; echo 'scale = 2; sqrt(12)'  | bc -l
echo -n 'logaritmo natural de 10 = '; echo 'l(10)'  | bc -l
echo -n 'e(ln(10)) = '; echo 'e(l(10))'  | bc -l # noten el error de redondeo
pi=$(echo "scale=50; 4*a(1)" | bc -l); echo "pi con 50 decimales es = $pi"
## 3.45 + 4.12 = 7.57
## 13.7 / 7.9 sin usar opción -l da solo el dividendo = 1
## 13.7 / 7.9 con opción -l da por defecto 20 decimales = 1.73417721518987341772
## 13.7 / 7.9 con tres decimales = 1.734
## 2^3 = 8
## raíz cuadrada de 12 con 2 decimales = 3.46
## logaritmo natural de 10 = 2.30258509299404568401
## e(ln(10)) = 9.99999999999999999992
## pi con 50 decimales es = 3.14159265358979323846264338327950288419716939937508

13.7 Operaciones aritméticas y trigonométricas con \(awk\)

Es muy fácil y conveniente usar \(awk\) para todo tipo de operaciones. Puedes guardar el resultado de las operaciones en variables, con la sintaxis que muestro abajo, imprimiendo desde un bloque \(BEGIN{}\) el resultado de la operación.

div=$(awk 'BEGIN{print 13.7 / 7.9}') && echo "13.7 / 7.9 = $div"
pi=$(awk 'BEGIN{ print atan2(1,1)*4}') && echo "pi = $pi"
sr=$(awk 'BEGIN{print sqrt(24)}') && echo "raíz cuadrada de 24 = $sr"
## 13.7 / 7.9 = 1.73418
## pi = 3.14159
## raíz cuadrada de 24 = 4.89898

13.8 Bucles for

la sintaxis general de un bucle for en \(Bash\) es:

for ALIAS in LIST; do CMD1; CMD2; done

donde el usuario tiene que cambiar los términos en mayúsculas por opciones concretas. ALIAS es el nombre de una variable temporal a la que se asigna secuencialmente cada valor de LIST.

Así por ejemplo, si tuviéramos en un directorio muchos archivos en formato FASTA con secuencias homólogas y extensión *.faa, podríamos alinearlas secuencialmente con \(clustalo\), un excelente alineador, con un comando como el siguiente:

13.8.1 Sintaxis y ejemplo de bucle for en una línea

# sintaxis de bucle for en una línea, combinado con if [[ "$var" =~ $regexp ]]
regexp='[a-zA-Z_]+\.html$'
for file in *; do if [[ "$file" =~ $regexp ]]; then echo "found html file <$file>"; fi;  done
## found html file <working_with_linux_commands.html>

13.8.2 Sintaxis y ejemplo de bucle for en múltiples líneas con indentación

# sintaxis de bucle for en múltiples líneas, como se escribiría normalmente en un script
for file in *.faa
do 
    # llamada estándar a clustalo: -i <infile> -o <outfile>
    clustalo -i $file -o ${file%.*}_cluoAln.faa
done

Nota 1: Genera un directorio temporal fuera del directorio de la distribución \(intro2linux\) y pon en él ligas simbólicas a los archivos .faa que hay en el directorio data/faa_files de la distribución. En dicho directorio corre el comando indicado arriba.

Nota 2: si quieres aprender más sobre alineamientos múltiples y alineadores, puedes consultar el tutorial sobre alineamientos múltiples que preparé para los Talleres Internacionales de Bioinformática - 2019 (TIB19).

13.8.3 Generación de archivos FASTA especie-específicos combinando bucle \(for\) con herramientas de filtrado y \(awk\) script

En este ejercicio continuaremos procesando el archivo recA_Bradyrhizobium_vinuesa.fnaed que generamos en la sección de filtrado de cabeceras de archivos FASTA.

El reto que les planteo es escribir archivos FASTA especie-específicos en base a recA_Bradyrhizobium_vinuesa.fnaed.

Es decir, necesitamos escribir tantos archivos FASTA como especies diferentes tenemos en el archivo fuente. Para ello nos va a ser muy útil el \(script\) filter_fasta_sequences.awk que escribimos al final de la sección de \(gawk\).

Vamos por partes: - recordemos la estructura del archivo fuente

grep '^>' recA_Bradyrhizobium_vinuesa.fnaed | head -5
grep '^>' recA_Bradyrhizobium_vinuesa.fnaed | tail -5
## >EU574327.1 [B_liaoningense_strain_ViHaR5]
## >EU574326.1 [B_liaoningense_strain_ViHaR4]
## >EU574325.1 [B_liaoningense_strain_ViHaR3]
## >EU574324.1 [B_liaoningense_strain_ViHaR2]
## >EU574323.1 [B_liaoningense_strain_ViHaR1]
## >AY591544.1 [B_japonicum_bv._genistearum_strain_BC-P14]
## >AY591543.1 [B_beta_strain_BC-P6]
## >AY591542.1 [B_canariense_bv._genistearum_strain_BC-P5]
## >AY591541.1 [B_canariense_bv._genistearum_strain_BC-C2]
## >AY591540.1 [B_alpha_bv._genistearum_strain_BC-C1]
  • tenemos que generar la lista no redundante de especies en base al patrón de las cabeceras FASTA que desplegamos arriba
for sp in $(grep '^>' recA_Bradyrhizobium_vinuesa.fnaed | cut -d_ -f2 | sort -u | sed 's/\[//')
do 
   echo "processing species $sp ..."
done
## processing species alpha ...
## processing species beta ...
## processing species canariense ...
## processing species elkanii ...
## processing species japonicum ...
## processing species liaoningense ...
## processing species sp. ...
## processing species yuanmingense ...
  • ahora es trivial insertar nuestra herramienta de \(gawk\), diseñada justo para filtrar secuencias usando nombres de especies
for sp in $(grep '^>' recA_Bradyrhizobium_vinuesa.fnaed | cut -d_ -f2 | sort -u | sed 's/\[//')
do 
   echo "processing species $sp ..."
   ./filter_fasta_sequences.awk "$sp" recA_Bradyrhizobium_vinuesa.fnaed > "recA_B_${sp}.fasta"
done
## processing species alpha ...
## processing species beta ...
## processing species canariense ...
## processing species elkanii ...
## processing species japonicum ...
## processing species liaoningense ...
## processing species sp. ...
## processing species yuanmingense ...
  • como siempre, revisemos la salida, asegurándonos primero que los archivos existen y no están vacíos
ls -ltr *fasta
## -rw-rw-r-- 1 vinuesa vinuesa 10279 sep 26 22:11 recA_B_canariense.fasta
## -rw-rw-r-- 1 vinuesa vinuesa  2222 sep 26 22:11 recA_B_beta.fasta
## -rw-rw-r-- 1 vinuesa vinuesa  1131 sep 26 22:11 recA_B_alpha.fasta
## -rw-rw-r-- 1 vinuesa vinuesa  8465 sep 26 22:11 recA_B_liaoningense.fasta
## -rw-rw-r-- 1 vinuesa vinuesa 15830 sep 26 22:11 recA_B_japonicum.fasta
## -rw-rw-r-- 1 vinuesa vinuesa 10047 sep 26 22:11 recA_B_elkanii.fasta
## -rw-rw-r-- 1 vinuesa vinuesa 18024 sep 26 22:11 recA_B_yuanmingense.fasta
## -rw-rw-r-- 1 vinuesa vinuesa  4382 sep 26 22:11 recA_B_sp..fasta
  • ahora nos aseguramos que son especie-específicos
for sp in $(grep '^>' recA_Bradyrhizobium_vinuesa.fnaed | cut -d_ -f2 | sort -u | sed 's/\[//')
do 
  echo "processing species $sp ..."
  grep "$sp" "recA_B_${sp}.fasta" | head -2
  echo '-------------------------------------'
  grep "$sp" "recA_B_${sp}.fasta" | tail -2
  echo '.....................................'
done
## processing species alpha ...
## >AY591567.1 [B_alpha_strain_CIAT3101]
## >AY591540.1 [B_alpha_bv._genistearum_strain_BC-C1]
## -------------------------------------
## >AY591567.1 [B_alpha_strain_CIAT3101]
## >AY591540.1 [B_alpha_bv._genistearum_strain_BC-C1]
## .....................................
## processing species beta ...
## >AY653750.1 [B_beta_strain_BC-MK1]
## >AY591554.1 [B_beta_strain_BC-MK6]
## -------------------------------------
## >AY591551.1 [B_beta_strain_BRE-1]
## >AY591543.1 [B_beta_strain_BC-P6]
## .....................................
## processing species canariense ...
## >AY653749.1 [B_canariense_strain_BC-MAM12]
## >AY653748.1 [B_canariense_strain_BC-MAM11]
## -------------------------------------
## >AY591542.1 [B_canariense_bv._genistearum_strain_BC-P5]
## >AY591541.1 [B_canariense_bv._genistearum_strain_BC-C2]
## .....................................
## processing species elkanii ...
## >EU574276.1 [B_elkanii_strain_BuNoR4]
## >EU574275.1 [B_elkanii_strain_BuNoR3]
## -------------------------------------
## >AY591569.1 [B_elkanii_strain_USDA94]
## >AY591568.1 [B_elkanii_strain_USDA76]
## .....................................
## processing species japonicum ...
## >EU574316.1 [B_japonicum_strain_NeRa16]
## >EU574315.1 [B_japonicum_strain_NeRa15]
## -------------------------------------
## >AY591555.1 [B_japonicum_bv._glycinearum_strain_DSMZ30131]
## >AY591544.1 [B_japonicum_bv._genistearum_strain_BC-P14]
## .....................................
## processing species liaoningense ...
## >EU574327.1 [B_liaoningense_strain_ViHaR5]
## >EU574326.1 [B_liaoningense_strain_ViHaR4]
## -------------------------------------
## >AY591574.1 [B_liaoningense_strain_Spr3-7]
## >AY591564.1 [B_liaoningense_bv._glycinearum_strain_LMG18230]
## .....................................
## processing species sp. ...
## >EU574272.1 [B_sp._BuNoG5]
## >EU574262.1 [B_sp._BuMiT10]
## -------------------------------------
## >AY591570.1 [B_sp._BTAi1]
## >AY591561.1 [B_sp._CICS70]
## .....................................
## processing species yuanmingense ...
## >EU574319.1 [B_yuanmingense_strain_ViHaG5]
## >EU574318.1 [B_yuanmingense_strain_ViHaG4]
## -------------------------------------
## >AY591566.1 [B_yuanmingense_strain_CCBAU_10071]
## >AY591565.1 [B_yuanmingense_strain_TAL760]
## .....................................

13.9 Bucles \(while\)

la sintaxis es simple:

  • while test-commands; do consequent-commands; done

Mientras la condición evalúe a cierta, se ejecuta el código en el bloque entre \(do\) y \(done\), como muestra este sencillo ejemplo:

Veamos dos ejemplos:

13.9.1 Contador en bucle \(while\) con condicional

contador=1

while [ $contador -le 5 ]
do
    echo $contador
    contador=$((contador + 1))
done
## 1
## 2
## 3
## 4
## 5

13.9.2 Bucle \(while\) leyendo de un archivo, doble condicional para evaluar dos expresiones regulares y builtin let para incrementar contador

Un bucle \(while\) puede tomar su entrada desde . Ello permite leer líneas de archivos o provenientes de la salida de un pipeline, como muestra el siguiente ejemplo, que también incluye:

  • un doble condicional para evaluar dos regexes: ‘[[ “$line” =~ “$patron” ]] && [[ ! “$line” =~ “$patron2” ]]’, donde debe cumplir el primer test [[ =~ ]] pero no el segundo [[ ! =~ ]]
  • el builtin \(let\), para hacer más fácil incrementar un contador.
contador=0
patron='Steno'
patron2='UNAM$'

while read line
do
    # noten el uso de [[ texto =~ regexp ]]
    if [[ "$line" =~ $patron ]] && [[ ! "$line" =~ $patron2 ]]
    then 
        echo "$line"
        let contador++
    fi
done < mini_tabla.tsv

echo "$contador líneas contienen el patrón $patron"
## GCF_000072485.1  Stenotrophomonas maltophilia K279a  2008/06/10  ASM7248v1   Wellcome Trust Sanger Institute
## GCF_000284595.1  Stenotrophomonas maltophilia D457   2012/04/11  ASM28459v1  University of Valencia
## 2 líneas contienen el patrón Steno

Es importante notar que al usar \(echo\) para imprimir las líneas se pierden los tabuladores que separaban los campos de la tabla.

13.9.3 Correr iteraciones de bucles \(while\) o \(until\), hasta que se cumpla una condición, saliendo del mismo con \(break\)

La siguiente expresión correría el bucle \(while\) indefinidamente, imprimiendo íntegros al azar que el sistema alberga en $RANDOM

while true; do i=$(echo $RANDOM); done

Veamos ahora el uso de este bucle \(while\) para generación de íntegros pares al azar. Noten el uso del operador \(\%\) y de la función \(break\) para salir del bucle, una vez satisfecha la condición.

while true; do i=$(echo $RANDOM); if ! (( $i % 2 )); then echo $i && break; fi; done;
while true; do i=$(echo $RANDOM); if ! (( $i % 2 )); then echo $i && break; fi; done;
while true; do i=$(echo $RANDOM); if ! (( $i % 2 )); then echo $i && break; fi; done;
## 20140
## 29904
## 12080

Los bucles \(until\) son útiles para ejecutar un bucle hasta que se cumpla o no una determinada condición.

Su sintaxis es idéntica al del bucle \(while\).

until TEST-COMMAND; do CONSEQUENT-COMMANDS; done

El siguiente ejemplo corre el bucle \(until\) hasta que la variable $RANDOM, que almacena íntegros al generados al azar, sea impar, saliendo del bucle con un \(break\).

until ! true; do i=$(echo $RANDOM); if (( $i % 2 )); then echo $i && break; fi; done;
until ! true; do i=$(echo $RANDOM); if (( $i % 2 )); then echo $i && break; fi; done;
until ! true; do i=$(echo $RANDOM); if (( $i % 2 )); then echo $i && break; fi; done;
## 30829
## 32301
## 30551

13.10 Estructuras de datos en \(Bash\) - arreglos y hashes (en construcción)

TODO

# finalmente borremos los nuevos archivos generados en los ejercicios anteriores 
#   para mantener limpio nuestro directorio de trabajo ;)
rm *.cmds
rm *fnaed *.fnaedtab *.fas empty_file Pseudomonas_species* *.fasta

14 \(Bash\) scripting - siguientes pasos (en construcción …)

En esta sección veremos ejemplos muy sencillos de Shell scripts escritos en \(Bash\) que integran varios aspectos de la sintaxis básica del lenguaje descritas en la sección anterior, así como extensiones adicionales como son argumentos posicionales y funciones.

14.1 La “shebang line” (#!/usr/bin/env bash) y permisos de ejecución - el script ls_dir

Ya discutimos en la sección de \(AWK\ scripts\) lo que es la línea shebang. Para \(bash\), se puede usar la ruta a \(bash\) que obtenemos con \(which\) para escribir la \(shebang\).

which bash
## /usr/bin/bash

que acorde a la salida mostrada arriba sería: #!/usr/bin/bash

Pero en diferentes sistemas operativos el binario puede estar localizado en otros directorios. De ahí que es mejor usar la siguiente \(shebang\), que garantiza encontrar automáticamente al intérprete de comandos \(bash\) en cualquier sistema:

  • \(shebang\) portable: #!/usr/bin/env bash

Como ya vimos en la sección de \(AWK\), se conocen como scripts a archivos de texto plano (codificación ASCII) que contienen los comandos a ser ejecutados secuencialmente por un intérprete de comandos particular, como \(awk\), \(bash\), \(perl\), \(R\)

Les muestro abajo el código del script ls_dir, que pueden descargar desde el repositorio githut de intro2linux como un primer ejemplo de un \(Bash\ script\) autocontenido, con la \(shebang\) portable.

#!/usr/bin/env bash

#  the 1st line in a script is the so-called shebang line
#  which indicates the system which command interpreter 
#  should read and execute the following code, bash in this case
#  The shebang line shown above, is a portable version for bash scripts

# AUTHOR: Pablo Vinuesa
# AIM: learning basic BASH-programming constructs for intro2linux
# https://github.com/vinuesa/intro2linux

for file in $(ls)
do 
    if [ -d "$file" ]
    then 
        echo "$file"
    fi
done

Puedes copiar el código a un archivo que vas a nombrar como ls_dir. Usa para ello el comando \(cat\), de la manera abajo indicada:

cat > ls_dir
PEGA AQUÍ EL CÓDIGO ARRIBA MOSTRADO
CTRL-D

Noten que la primera línea inicia con lo que se conoce como una shebang line:

#!/usr/bin/env bash

Les recuerdo que esta línea, en la cabecera del archivo (¡sin dejar espacios a la izquierda o arriba de la línea!), le indica al sistema operativo qué intérprete de comandos usar para la ejecución del código que sigue (\(bash\) en este caso).

Noten también que cualquier línea que inicie con # después de la shebang line, representa un comentario, es decir, que el texto que sigue al gato no es interpretado por \(bash\)

¿Qué hace el \(script\)?

ls_dir busca los directorios presentes en el directorio actual usando un bucle \(for\) para analizar cada archivo encontrado por \(ls\), evaluando seguidamente si el archivo en turno es un directorio con un condicional:

  • if [ -d “$file” ]

Para ejecutar el \(script\) como si fuera un comando cualquiera de \(Linux\), le damos permisos de ejecución:

chmod 755 ls_dir

Comprueba los permisos:

$ ls -l ls_dir

-rwxr-xr-x 1 vinuesa vinuesa 493 ago 11 18:37 ls_dir

Como puedes ver el usuario, el grupo al que pertenece y todos los demás pueden ejecutarlo (\(x\))

Finalmente copia o mueve el \(script\) a un directorio que esté en el \(PATH\), típicamente a \(\$HOME/bin\)

mv ls_dir ~/bin

y ya puedes usarlo:

# ls_dir si está en un directorio del PATH
./ls_dir
## bin
## data
## docs
## intro2linux
## participantes_Taller_CG_FC
## pics
## src
## tmp
## tutorials
## working_with_linux_commands_files

Nota: el script ls_dir funciona perfectamente, pero no es la manera más eficiente de buscar directorios. La función \(find\) de \(Linux\) es mucho más versátil y eficiente para ello.

14.2 ¿Dónde debo guardar mis \(scripts\) para que sean visibles desde cualquier directorio del sistema?

Como explicábamos en la tutoral de introducción, el \(Shell\) busca comandos en los directorios especificados en la variable de ambiente PATH. Los directorios van separados por \(:\) y se leen en ese orden.

echo "PATH = $PATH"
## PATH = /home/vinuesa/.local/bin:/home/vinuesa/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/vinuesa/edirect:/home/vinuesa/soft_download/get_homologues-x86_64-20190805:/home/vinuesa/soft_download/sratoolkit.2.10.5-ubuntu64/bin:/home/vinuesa/soft_download/Fiji.app:/home/vinuesa/soft_download/circos/current/bin:/usr/local/genome/bin:/home/vinuesa/soft_download/cgview_comparison_tool/scripts:/usr/lib/rstudio/bin/quarto/bin:/usr/lib/rstudio/bin/postback

Recuerda, es fácile generar una lista de estos directorios más legible con un comando como ésta

echo "$PATH" | sed 's/:/\n/g'
## /home/vinuesa/.local/bin
## /home/vinuesa/bin
## /usr/local/sbin
## /usr/local/bin
## /usr/sbin
## /usr/bin
## /sbin
## /bin
## /usr/games
## /usr/local/games
## /snap/bin
## /home/vinuesa/edirect
## /home/vinuesa/soft_download/get_homologues-x86_64-20190805
## /home/vinuesa/soft_download/sratoolkit.2.10.5-ubuntu64/bin
## /home/vinuesa/soft_download/Fiji.app
## /home/vinuesa/soft_download/circos/current/bin
## /usr/local/genome/bin
## /home/vinuesa/soft_download/cgview_comparison_tool/scripts
## /usr/lib/rstudio/bin/quarto/bin
## /usr/lib/rstudio/bin/postback

14.2.1 Agregando directorios a la variable de ambiente PATH

A veces necesitamos agregar un nuevo directorio a la variable de ambiente PATH

  • Añade el directorio $HOME/bin al final de la lista de directorios de PATH
PATH=$PATH:$HOME/bin

Nota: en versiones recientes de \(Ubuntu\) y otras distribuciones de Linux, el directorio \(HOME/bin* se exporta automáticamente. Revisa tu archivo *\)HOME/.bashrc. Ya que estamos con archivos de configuración, profundicemos un poco más en este tópico.

14.3 Variables de ambiente - \(printenv\)

Al iniciar una sesión local o remota, el \(SHELL\) carga en memoria una serie de \(VARIABLES\_DE\_AMBIENTE\) que son parte de la configuración del sistema.

Ya hemos visto algunas antes, pero aquí les muestro algunas muy importantes:

echo "HOME = $HOME"
echo "USER = $USER"
echo "SHELL = $SHELL"
## HOME = /home/vinuesa
## USER = vinuesa
## SHELL = /bin/bash

Pueden ver todas las variables de su ambiente con el comando \(printenv\)

printenv | sort | less

14.3.1 Archivos de configuración .bash_profile y .bashrc

Si quieres agregar un directorio permanentemente a \(PATH\), puedes añadirlo al archivo de configuración \(.bash\_profile\) de tu \(HOME\) en un servidor remoto o al \(.bashrc\) de tu \(HOME\) en una máquina local y exportarlo.

PATH=$PATH:$HOME/bin

export PATH

Cualquier \(script\) o programa que pongas ahí con permisos de ejecución (chmod 755 script) será ejecutable desde cualquier directorio del sistema.

14.3.2 ¿Cuál es el binario o script que estoy usando? - which

A veces tenemos un \(script\) o un binario (por ejemplo \(blastn\)) instalado en varios directorios. Esto puede ser intencional o inadvertidamente. El comando \(which\) nos indica cual es el \(PATH\) del comando que ve el \(Shell\), es decir, el primero en \(PATH\)

which blastn
which perl
which trunc_seq.pl
which transpose_pangenome_matrix.sh
## /usr/local/bin/blastn
## /usr/bin/perl
## /home/vinuesa/bin/trunc_seq.pl
## /home/vinuesa/bin/transpose_pangenome_matrix.sh

14.4 Argumentos posicionales en \(Bash\) - el script find_dir

Los \(scripts\) pueden recibir opciones dadas por los usuarios para comportarse acorde a ellas. Esto les da flexibilidad ya que su comportamiento puede modificarse acorde a las opciones y/o argumentos que se le pasen.

La manera más sencilla de pasarle opciones al \(script\) es mediante argumentos posicionales, como los usados por find_dir, que pueden descargar desde el repositorio githut de intro2linux.

Este \(script\) llama al binario \(find\) del sistema para buscar directorios bajo el actual. Requiere al menos un argumento, como podrás deducir del código que se muestra seguidamente y se explica después:

#!/usr/bin/env bash

#  The 1st line in a script is the so-called shebang line
#  which indicates the system which command interpreter 
#  should read and execute the following code, bash in this case
#  The shebang line shown above, is a portable version for bash scripts

# AUTHOR: Pablo Vinuesa
# AIM: learning basic BASH-programming constructs for intro2linux
# https://github.com/vinuesa/intro2linux

# global variables
progname=${0##*/}      # find_dir
VERSION='0.1_29Jul19' 

# Function definition
function print_help()
{
   # this is a so-called HERE-DOC, to easily print out formatted text
   cat << HELP

    $progname v$VERSION usage synopsis:

    $progname <int [maximal search depth for (sub)directories, e.g.:1>

    AIM: find directories below the current one with the desired max_depth 

    USAGE EXAMPLES:
          $progname 1
          $progname 2

HELP

exit 1
}

# capture user-provided arguments in variables $1, $2 ... $9
# and save them to named variables, for better readability
max_depth=$1
type=${2:-d}  # if not provided, the second argument is set to 'd' by default.

# check arguments
[ -z $max_depth ] && print_help

# execute find command with the provided arguments
find . -maxdepth $max_depth -type $type

Guarda el código mostrado arriba en un archivo llamado find_dir, dale permisos de ejecución, y cópialo a \(HOME/bin\), como se mostró en el ejemplo anterior.

Como puedes ver en el código, find_dir hace la siguiente llamada a \(find\)

find . -maxdepth $max_depth -type $type

para encontrar directorios y subdirectorios por debajo del actual, al nivel de profundidad indicado por el usuario, quien pasa un íntegro al \(script\) como único argumento. Este argumento se guardará en la variable \(max_depth\), para indicarle al comando \(find\) la profundidad máxima para buscar subdirectorios por debajo del actual:

find -maxdepth $max_depth …

find_dir introduce la función print_help(),

function print_help(){
  YOUR CODE GOES INHERE ...
}

que simplemente imprime un mensaje de ayuda si es llamado sin argumentos

[ -z $max_depth ] && print_help

Probemos el \(script\):

  • primero invocándolo sin argumentos:

find_dir

    find_dir v0.1_29Jul19 usage synopsis:

    find_dir <int [maximal search depth for (sub)directories, e.g.:1>

    AIM: find directories below the current one at the desired max_depth 

    USAGE EXAMPLES
          find_dir 1
          find_dir 2
  • ahora llámalo pasándole un argumento:
  ./find_dir 1
## .
## ./tutorials
## ./.git
## ./data
## ./pics
## ./participantes_Taller_CG_FC
## ./intro2linux
## ./bin
## ./tmp
## ./working_with_linux_commands_files
## ./docs
## ./src
  • Los argumentos posicionales pasados a un \(Shell\ script\) quedan guardados en las variables reservadas $1 … $9
  • Los argumentos posicionales pasados a una \(función\) también quedan guardados en las variables reservadas $1 … $9

14.5 \(Scripts\) que reciben argumentos|parámetros posicionales con interfaz de usario simple

En el repositorio githut de intro2linux encontrarán los scripts align_seqs_with_clustal_or_muscle.sh y convert_alnFormats_using_clustalw.sh para alineamiento múltiple de secuencias e interconversión de formatos. Son ejemplos de \(scripts\) que reciben parámetros posicionales y que despliegan un mensaje de ayuda, indicando los argumentos posicionales requeridos.

./align_seqs_with_clustal_or_muscle.sh

que imprime lo siguiente a STDOUT

# ./align_seqs_with_clustal_or_muscle.sh vers.1.2 needs two arguments: 
#   1) the input fasta file extension_name <[fas|fasta|fna|faa]>
#   2) the alignment program to use <[muscle|clustalw]>
# usage example: ./align_seqs_with_clustal_or_muscle.sh fna muscle

# NOTE: the script assumes that clustalw and muscle are in found in $PATH
# will check now for the presence of both binaries in $PATH
# looks ok, found clustalw here: /usr/bin/clustalw
# looks ok, found muscle here: /usr/bin/muscle  
  • Ejercicio:
    1. Explora también el código del \(script\) convert_alnFormats_using_clustalw.sh, que permite hacer interconversión de formatos con \(clustalw\).
    2. Explora también el código del \(script\) align_seqs_with_clustal_or_muscle.sh, que permite hacer alineamientos múltiples con \(clustalw\) o con \(muscle\), dos programas muy populares para este fin.

Los alineamientos múltiples y las llamadas a \(clustalw\) se explican en sesión4_alineamientos.

Con lo aprendido hasta ahora y los comentarios que documentan los \(scripts\), deberías entender lo que hacen.

Vuelve a explorar ambos \(scripts\) después de haber revisado la sesión4_alineamientos.

14.6 \(Scripts\) con interfaz de usuario, paso de opciones y su parseo con getopts

Para \(scripts\) más complejos, que pueden recibir muchas opciones, es mejor escribir una interfaz de usuario que permita el parseo de opciones pasadas al \(script\) desde la línea de comandos y describir adecuadamente estas opciones en un menú de ayuda que se despliega en pantalla.

Les presento seguidamente secciones del archivo de templado o machote para escribir \(scripts\) con parseo de opciones llamado bash_script_template_with_getopts.sh disponible en el repositorio githut de intro2linux

function print_help()
{
   cat <<EOF
   $progname v.$VERSION usage:
   
   REQUIRED:
    -a <string> alignment algorithm [clustalo|muscle; default:$alignment_algorithm]
    -i <string> input fasta file name
    -h <FLAG> print this help
    -v <FLAG> print version
    -R <integer> RUNMODE
          1     standard msa
            2     profile-profile alingnment
            3     sequence to profile alignment
    
   OPTIONAL:
    
  
   NOTE1: XXX
   
   TODO: 

EOF

   check_dependencies
   
   exit 2  
}
#----------------------------------------------------------------------------------------- 


#------------------------------------#
#----------- GET OPTIONS ------------#
#------------------------------------#

input_fasta=
runmode=

alignment_algorithm=clustalo
DEBUG=0

# See bash cookbook 13.1 and 13.2
while getopts ':i:d:R:hD?:' OPTIONS
do
   case $OPTIONS in

   a)   alignment_algorithm=$OPTARG
        ;;
   i)   input_fasta=$OPTARG
        ;;
   h)   print_help
        ;;
   v)   echo "$progname v.$VERSION"
        ;;
   R)   runmode=$OPTARG
        ;;
   D)   DEBUG=1
        ;;
   \:)   printf "argument missing from -%s option\n" $OPTARG
     print_help
         exit 2 
         ;;
   \?)   echo "need the following args: "
     print_help
         exit 3
     ;;
    *)   echo "An  unexpected parsing error occurred"
         echo
         print_help
     exit 4
     ;;  
   esac >&2   # print the ERROR MESSAGES to STDERR
done

shift $((OPTIND - 1))

if [ -z "$input_fasta_extension" ]
then
       echo "# ERROR: no input fasta file extension defined!"
       print_help
       exit 1    
fi

if [ -z "$runmode" ]
then
       echo "# ERROR: no runmode defined!"
       print_help
       exit 1    
fi

if [ -z "$DEBUG" ]
then
     DEBUG=0 
fi

Ahora puedo llamar a este \(script\) (templado) de la siguiente manera

bash bash_script_template_with_getopts.sh

que imprime lo siguiente a STDOUT

# ERROR: no input fasta file extension defined!
   bash_script_template_with_getopts.sh v.0.1 usage:
   
   REQUIRED:
    -a  alignment algorithm [clustalo|muscle; default:clustalo]
    -i  input fasta file name
    -h  print this help
    -v  print version
    -R  RUNMODE
         1     standard msa
           2     profile-profile alingnment
           3     sequence to profile alignment
    
   OPTIONAL:
    
  
   NOTE1: XXX
   
   TODO: 


# Run check_dependencies() ... looks good: all required binaries and perl scripts are in place.

Haz una copia del esqueleto que provee bash_script_template_with_getopts.sh para modificarlo y adaptarlo a lo que necesite tu \(script\). El machote te facilita el arranque.

  • Ejercicio
  1. usa el archivo de machote bash_script_template_with_getopts.sh para modificar el script align_seqs_with_clustal_or_muscle.sh, añadiéndole una interfaz de usuario.

14.7 Funciones y modularización de código

A medida que los programas se hacen más grandes y complejos, se vuelven más difíciles de diseñar, escribir y mantener.

Escribir funciones permite reducir la extensión, redundancia y complejidad de un programa grande, facilitando su mantenimiento. Los programas extensos típicamente tienen que ejecutar múltiples veces una misma acción, como por ejemplo verificar que se ha escrito a disco un archivo con resultados generados por el programa. Escribir una función que verifique la existencia de un archivo, pasado a la misma como argumento, permite reducir redundancia y extensión del código, ya que en el lugar apropiado del script principal, se llama a la función, en vez de repetir el cógido encapsulado en la misma. Si hubiera un error en la función, sólo hay que corregirlo en un bloque de código correspondiente, y no en múltiples secciones del script principal.

Para evitar el copiar y pegar funciones frecuentemente usadas entre scripts, conviene generar librerías de funciones relacionadas. Estas son simplemente archivos que contienen a diversas funciones. Las funciones de una o más librerías se hacen accesibles a un script mediante el comando \(source\), como se muestra en el siguiente bloque y en el machote de script mostrado más abajo.

# import functions defined in the aux_fun_lib file
source $HOME/bin/lib/aux_fun_lib

De nuevo, tener a las funciones reunidas en una o más librerías facilita su uso y mantenimiento.

Por tanto, una práctica fundamental en programación, es la modularización del cógido en funciones y librerías de funciones relacionadas.

La sintaxis básica de una función es la siguiente:

function fun_name1 {
    local posvar1=$1
    local posvar2=$2
    ...
    command1
    command2
    ...
    return
}

La llamada a la función, desde el script principal, se hace llamándola como si fuera un comando más, pudiendo pasarle argumentos posicionales

fun_name1 arg1 arg2

Una buena función debe estar especializada en realizar una sola tarea, eso sí, bajo diferentes condiciones u opciones, que se le pasan como argumentos posicionales.

La llamada a librería de funciones y la definición de funciones dentro del script princial típicamente deben ir al inicio del script, antes de que sean llamadas por el mismo, como se muestra en el “machote” de script que se presenta más adelante.

14.7.1 Llamada a la función \(print\_numbered\_table\_header\_fields\) con uno o dos argumentos posicionales

Si usas con frecuencia algunas funciones, puedes ponerlas también el archivo de inicialización \(\$HOME/.bashrc\) para sesiones locales en tu máquina, o en \(\$HOME/.bash\_profile\), en una máquina remota. De esta manera, cada vez que inicias una sesión local o remota, las funciones serán exportadas al ambiente y las podrás usar como si fuera cualquier otro comando de Linux.

Como ejemplo, veamos la función \(print\_numbered\_table\_header\_fields\), que tengo en mi \(\$HOME/.bashrc\).

  • Los argumentos posicionales $1, $2 … son capturados en la función como variables localizadas o locales usando local. Localizar una variable a una función quiere decir que sólo es visible dentro de ésta. Esto es importante para evitar que interfieran con otras variables definidas en el script principal

  • El número total de argumentos recibidos queda guardado en la variable $#

  • Examina la función ¿Qué crees que hace?

function print_numbered_table_header_fields()
{
   #: AUTHOR: Pablo Vinuesa, @pvinmex, CCG-UNAM
   # provide table name to parse (tsv format expected; can skip a certain number of comment lines)
   local table=$1
   local skip_lines=$2
   
   [ $# -lt 1 ] && echo "$FUNCNAME usage: <table_name> [<number_of_top_comment_lines_to_skip>]"
   
   if [ $# -eq 1 ]; then
       head -1 "$table" | sed 's/\t/\n/g' | nl
   elif [ $# -eq 2 ]; then
       tail -n +"$((skip_lines+1))" "$table" | sed 's/\t/\n/g' | nl
   fi
}
  • Copia la función al archivo \(\$HOME/.bash\_profile\) en buluc y ejecuta source $HOME/.bash_profile para releer el archivo y que se exporte la función al ambiente.

  • llamado a la función sin argumentos, imprime la ayuda

print_numbered_table_header_fields
print_numbered_table_header_fields usage: <table_name> [<number_of_top_comment_lines_to_skip>]
  • llamado a la función con el nombre de la tabla a procesar como único argumento
print_numbered_table_header_fields linux_basic_commands.tab 
     1  IEEE Std 1003.1-2008 utilities Name 
     2  Category 
     3  Description 
     4  First appeared

14.8 Machote de un \(Bash\) script que llama a una librería de funciones y define otras en la cabecera del script principal

El siguiente bloque muestra un machote para un \(script\) básico, que llama una librería de funciones y define otras en la cabecera del script principal, y que recibe argumentos posicionales desde la línea de comandos. Puedes usarlo para facilitarte la escritura de tus propios scripts.

#!/usr/bin/env bash

#: PROGRAM: 
#: AUTHOR:  
#
#: PROJECT START: 
#
#: AIM: 

progname=${0##*/}     # nombre_del_programa
vers='0.1' 

# GLOBALS
#DATEFORMAT_SHORT="%d%b%y"
#TIMESTAMP_SHORT=$(date +${DATEFORMAT_SHORT})

#date_F=$(date +%F |sed 's/-/_/g')-
#date_T=$(date +%T |sed 's/:/./g')
#start_time="$date_F$date_T"
#current_year=$(date +%Y)

#----------------------------------------------------------------------------#
#>>>>>>>>>>>>>>>>>>>>>>>>>>>> FUNCTION DEFINITIONS <<<<<<<<<<<<<<<<<<<<<<<<<<#
#----------------------------------------------------------------------------#

# import functions defined in the aux_fun_lib file
source $HOME/bin/lib/aux_fun_lib

function check_dependencies()
{
    for programname in prog1 prog2
    do
       bin=$(type -P $programname)
       if [ -z "$bin" ]; then
          echo
          echo "# ERROR: $programname not in place!"
          echo "# ... you will need to install \"$programname\" first or include it in \$PATH"
          echo "# ... exiting"
          exit 1
       fi
    done

    echo
    echo '# Run check_dependencies() ... looks good: all required binaries and perl scripts are in place.'
    echo
}
#----------------------------------------------------------------------------------------- 

function print_help()
{
  cat << HELP

    $progname v$vers usage synopsis:
    
    $progname   

    AIM: 
     
    OUTPUT: 

HELP
    check_dependencies
    
    exit 0
}
#----------------------------------------------------------------------------------------- 

function check_output()
{
    #>>> set color in bash 
    #  SEE: echo http://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux
    #  And very detailed: http://misc.flogisoft.com/bash/tip_colors_and_formatting
    # ANSI escape codes
    # Black        0;30     Dark Gray     1;30
    # Red          0;31     Light Red     1;31
    # Green        0;32     Light Green   1;32
    # Brown/Orange 0;33     Yellow        1;33
    # Blue         0;34     Light Blue    1;34
    # Purple       0;35     Light Purple  1;35
    # Cyan         0;36     Light Cyan    1;36
    # Light Gray   0;37     White         1;37
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    #YELLOW='\033[1;33m'
    #BLUE='\033[0;34m'
    #CYAN='\033[0;36m'
    NC='\033[0m' # No Color => end color
    #printf "I ${RED}love${NC} ${GREEN}Stack Overflow${NC}\n"

    outfile=$1
    
    if [ -s "$outfile" ]
    then
         echo -e "${GREEN} >>> wrote file $outfile ...${NC}"
    else
        echo
    echo -e "${RED} >>> ERROR! The expected output file $outfile was not produced, will exit now!${NC}"
        echo
    exit 1
    fi
}
#----------------------------------------------------------------------------------------- 

#
# >>>> MAIN CODE <<<<
#

var1=$1
var2=${2:-10}


[ -z "$var1" ] && print_help

cat << PARAMS
   
    $progname $var1 $var2 
    
PARAMS


prog1 $var1 $var2


15 Programando un pipeline en \(Bash\)

Para finalizar este tutorial, revisaremos el \(script\) run_phylip.sh. Es un poco más largo y complejo que los anteriores, e integra muchos de los aspectos mostrados en secciones anteriores.

Cuando estamos aprendiendo a programar, es muy importante leer mucho código bien escrito. No se aprende a programar bien sólo conociendo la sintaxis y generalidades, sino viendo cómo se pueden escribir programas completos de manera limpia, fácil de leer, mantener y modificar. Estos atributos, junto con el buen juicio y principios de programación, incluyendo la modularización en funciones, documentación adecuada de cada componente, incluyendo la interfaz de usuario, son los que debemos desarrollar. El estudio detallado e imitación de buenos programas es lo que más rápidamente nos lleva a escribir mejores programas.

El \(script\) run_phylip.sh, además de útil para hacer análisis filogenéticos basados en matrices de distancias, te presenta código que implementa buenas prácticas de programación \(Bash\).

15.1 Construcción de filogenias NJ|UPGMA con bootstrapping y modelos de DNA|PROT definidos por el usuario, usando programas del paquete PHYLIP

El \(script\) run_phylip.sh toma alineamientos múltiples de DNA o proteína con al menos 4 secuencias distintas para construir filogenias UPGMA o NJ, con o sin bootstrapping, llamando a diferentes programas del famoso paquete de inferencia filogenética PHYLIP desarrollado por el Dr. Joseph Felsenstein.

El paquete PHYLIP y su código fuente fue el primero en ser liberado al dominio público para hacer inferencia filogenética. Consta de unos 40 programas, los cuales han de ser llamados secuencialmente (como en un pipeline de utilerías de GNU/Linux) con los parámetros y opciones adecuados para hacer un análisis, como muestra el siguiente esquema genérico:

Nuestro objetivo es construir una filogenia de Neighbor-joining (NJ) con 100 pseudoréplicas de bootstrap para el archivo GDP_12_prokEuc.phy. Usaremos para ello la matriz empírica de sustitución de Jones, Taylor y Thornton (JTT) y corrección gamma de la heterogeneidad de tasas de sustitución entre sitions (JTT+G), asumiendo un valor de \(alpha=0.6\). El archivo GDP_12_prokEuc.phy ya está alineado y en formato phylip.

Para ello deberemos de llamar a los programas del paquete PHYLIP mostrados en la siguiente figura

  1. \(seqboot\) realiza el muestreo al azar con reemplazo (bootstrapping) de las columnas del alineamiento tantas veces como se le indique, escribiendo en su outfile ese número de alineamientos re-muestreados
  2. \(protdist\) calcula las matrices de distancias de cada alineamiento (pseudoréplica) bajo el modelo de sustitución indicado
  3. \(neighbor\) reconstruye los árboles tipo neighbor-joining de cada una de las matrices de distancia
  4. \(consense\) construye un árbol de consenso de mayoría

La llamada a cada binario se acompaña del archivo de entrada requerido, que los programas de PHYLIP esperan que se llamen infile o intree, escribiendo sus resultados en archivos outfile y/o outtree, según el programa.

Además, debemos pasarle a cada programa un archivo con los parámetros correspondientes, que controlan, según el caso, el número de réplicas de bootstrap a realizar, qué modelo de sustitución usar, qué tipo de árbol de distancia inferir (NJ|UPGMA), etc. Estos parámetros de le pasan a cada programa con la siguiente sintaxis:

  • seqboot < seqboot.params &> /dev/null
  • protdist < protdist.params &> /dev/null
  • neighbor < neighbor.params &> /dev/null
  • consense < consense.params &> /dev/null

Estos archivos de parámetros tienen una estructura muy sencilla, con un parámetro por línea, como muestra la siguiente imagen:

15.1.1 Cómo puedo saber cuáles son los parámetros que recibe un programa de phylip?

Simplemente llama al binario correspondiente con el archivo de entrada requerido (\(infile\)) y desplegará un menú textual como el que se muestra abajo para \(seqboot\).

Teclea la letra correspondiente del menú para hacer las midificaciones que quieras a los valores por defecto. Al final deberás confirmar con “Y”. Algunos programas todavía pedirán un número impar para echar a andar.

cp primates.phy infile

seqboot

Bootstrapping algorithm, version 3.69

Settings for this run:
  D      Sequence, Morph, Rest., Gene Freqs?  Molecular sequences
  J  Bootstrap, Jackknife, Permute, Rewrite?  Bootstrap
  %    Regular or altered sampling fraction?  regular
  B      Block size for block-bootstrapping?  1 (regular bootstrap)
  R                     How many replicates?  100
  W              Read weights of characters?  No
  C                Read categories of sites?  No
  S     Write out data sets or just weights?  Data sets
  I             Input sequences interleaved?  Yes
  0      Terminal type (IBM PC, ANSI, none)?  ANSI
  1       Print out the data at start of run  No
  2     Print indications of progress of run  Yes

  Y to accept these or type the letter for one to change
y

Random number seed (must be odd)?
99

completed replicate number   10
completed replicate number   20
completed replicate number   30
completed replicate number   40
completed replicate number   50
completed replicate number   60
completed replicate number   70
completed replicate number   80
completed replicate number   90
completed replicate number  100

Output written to file "outfile"

Done.
  • Veamos ahora la entrada a \(dnadist\) para calcular una distancia F84+G con alpha=0.3 (CV=1.8257), aplicado a los 100 sets de alineamientos remuestreados mediante bootstrap.
mv outfile infile

dnadist

Nucleic acid sequence Distance Matrix program, version 3.697

Settings for this run:
  D  Distance (F84, Kimura, Jukes-Cantor, LogDet)?  F84
  G          Gamma distributed rates across sites?  No
  T                 Transition/transversion ratio?  2.0
  C            One category of substitution rates?  Yes
  W                         Use weights for sites?  No
  F                Use empirical base frequencies?  Yes
  L                       Form of distance matrix?  Square
  M                    Analyze multiple data sets?  No
  I                   Input sequences interleaved?  Yes
  0            Terminal type (IBM PC, ANSI, none)?  ANSI
  1             Print out the data at start of run  No
  2           Print indications of progress of run  Yes

  Y to accept these or type the letter for one to change
g

Nucleic acid sequence Distance Matrix program, version 3.697

Settings for this run:
  D  Distance (F84, Kimura, Jukes-Cantor, LogDet)?  F84
  G          Gamma distributed rates across sites?  Yes
  T                 Transition/transversion ratio?  2.0
  W                         Use weights for sites?  No
  F                Use empirical base frequencies?  Yes
  L                       Form of distance matrix?  Square
  M                    Analyze multiple data sets?  No
  I                   Input sequences interleaved?  Yes
  0            Terminal type (IBM PC, ANSI, none)?  ANSI
  1             Print out the data at start of run  No
  2           Print indications of progress of run  Yes

  Y to accept these or type the letter for one to change
t
Transition/transversion ratio?
4.5


Nucleic acid sequence Distance Matrix program, version 3.697

Settings for this run:
  D  Distance (F84, Kimura, Jukes-Cantor, LogDet)?  F84
  G          Gamma distributed rates across sites?  Yes
  T                 Transition/transversion ratio?  4.5000
  W                         Use weights for sites?  No
  F                Use empirical base frequencies?  Yes
  L                       Form of distance matrix?  Square
  M                    Analyze multiple data sets?  No
  I                   Input sequences interleaved?  Yes
  0            Terminal type (IBM PC, ANSI, none)?  ANSI
  1             Print out the data at start of run  No
  2           Print indications of progress of run  Yes

  Y to accept these or type the letter for one to change
m
Multiple data sets or multiple weights? (type D or W)
d
How many data sets?
100


Nucleic acid sequence Distance Matrix program, version 3.697

Settings for this run:
  D  Distance (F84, Kimura, Jukes-Cantor, LogDet)?  F84
  G          Gamma distributed rates across sites?  Yes
  T                 Transition/transversion ratio?  4.5000
  W                         Use weights for sites?  No
  F                Use empirical base frequencies?  Yes
  L                       Form of distance matrix?  Square
  M                    Analyze multiple data sets?  Yes, 100 data sets
  I                   Input sequences interleaved?  Yes
  0            Terminal type (IBM PC, ANSI, none)?  ANSI
  1             Print out the data at start of run  No
  2           Print indications of progress of run  Yes

  Y to accept these or type the letter for one to change
y

Coefficient of variation of substitution rate among sites (must be positive)
 In gamma distribution parameters, this is 1/(square root of alpha)
1.8257
Data set # 1:

Distances calculated for species
    Tarsius_sy   ...........
    Lemur_catt   ..........
    Homo_sapie   .........
    Pan          ........
    Gorilla      .......
    Pongo        ......
    Hylobates    .....
    Macaca_fus   ....
    M_mulatta    ...
    M_fascicul   ..
    M_sylvanus   .
    Saimiri_sc

Distances written to file "outfile"

Data set # 2:

Distances calculated for species
    Tarsius_sy   ...........
    Lemur_catt   ..........
    Homo_sapie   .........
    Pan          ........
    Gorilla      .......
    Pongo        ......
    Hylobates    .....
    Macaca_fus   ....
    M_mulatta    ...
    M_fascicul   ..
    M_sylvanus   .
    Saimiri_sc

...

De este último ejemplo vemos que las opciones que usamos fueron las siguientes, es este orden:

g
t
4.5
m
d
100
y
1.8257

Si guardamos estos valores en un archivo que podemos llamar por ejemplo dnadist.params, podríamos repetir la corrida con el siguiente código, asumiendo que tenemos un infile adecuado:

dnadist < dnadist.params.

Te dejo como ejercicio que explores las opciones que controlan a los programas \(protdist\), \(neighbor\) y \(consense\). Compara sus parámetros con los mostrados para los archivos de configuración de la figura anterior.

Con estos ejemplos debería ser suficiente para que entiendas bien cómo funcionan los programas del paquete PHYLIP. Esto es esencial para que puedas entender lo que hace el \(script\) run_phylip.sh que veremos en la siguiente sección, que nos permite automatizar todo el proceso, incluyendo el despliegue del árbol.

No es éste el espacio para describir con mayor detalle el funcionamiento de PHYLIP ni de los métodos filogenéticos, pero te pueden servir los siguientes tutoriales que he preparado al respecto para otros cursos:

15.1.2 Tutorial de uso de PHYLIP desde la línea de comandos de Linux

15.1.3 Tutoriales sobre filogenética de Pablo Vinuesa

Aquí encontrarás mucho material: teoría y práctica. Todo libremente disponible en GitHub: - Taller 3 - Análisis comparativo de genomas microbianos: Pangenómica y filoinformática

15.2 El \(script\) run_phylip.sh

Estimar una filogenia con PHYLIP “a mano”, siguiendo los pasos arriba mostrados, es sin duda tedioso y tardado, con muchas operaciones repetitivas en las que es fácil equivocarse.

El \(script\) run_phylip.sh se encarga de automatizar \(pipelines\) similares a los mostrados en las figuras anteriores.

  • Para ello implementa funciones que, entre otras, hacen lo siguiente:
    • verifican que la versión de \(bash\) que corre en la computadora sea adecuada para el \(script\)
    • verifican detalles de configuración como la variable \(LC\_NUMERIC\) que controla cómo se manejan los números según la configuración local.
    • imprimen menús de ayuda
    • verifican que el archivo de entrada está en formato PHYLIP y contiene al menos 4 secuencias
    • verifican que los parámetros pasados por el usuario al \(script\) sean adecuados
    • escriben los archivos de parámetros en base a los argumentos que el usuario le pasa al \(script\).
    • hacen las llamadas a los programas de PHYLIP (seqboot, dnadist|protdist, consense, neighbor) en el orden correcto, pasándole los archivos de parámetros pertinentes
    • verifican que existen los archivos de entrada y salida (infile, outfile, intree, outtree), renombrándolos adecuadamente con nombres informativos
    • borra los archivos que ya no se necesitan

Al concluir los análisis, el \(script\) nos despliega la(s) filogenia(s) resultantes en la pantalla. Si están instalados correctamente los programas \(nw\_support\) y \(nw\_display\), son llamados para que calculen los valores de soporte de las biparticiones y las escriban sobre la filogenia original, la cual es desplegada con \(nw\_display\), mostrando los valores de soporte. En caso de no estar instalados, la función \(extract\_tree\_from\_outfile\) extrae las líneas que contienen los árboles NJ|UPGMA y consenso de mayoría de los archivos de salida correspondientes, desplegándolos igualmente en pantalla.

A modo de ejemplo, el siguiente bloque de código muestra la función write_dnadis_params encargadas de escribir el archivo de parámetros para correr el programa \(dnadist\) de PHYLIP. Compara el código de la función con las capturas del menú textual del programa desplegado arriba.


function write_dnadist_params
{
    # writes a parameter file to run dnadist, based on provided arguments
    local model=$1
    local boot=$2
    local TiTv=$3
    local sequential=$4
    local gamma=$5
    local CV=$6

    # Runmode 1 = dnadist 
    if [ "$model" = 'F84' ] || [ -z "$model" ]
    then 
        {
          [ "$model" = 'Kimura' ]       && echo "D"                 
          [ "$model" = 'Jukes-Cantor' ] && echo -ne "D\nD\n"        
          [ "$model" = 'LogDet' ]       && echo -ne "D\nD\nD\n"     
          [ "$TiTv" != "2" ]            && echo -ne "T\n$TiTv\n"    
          [ "$gamma" != "0" ]           && echo -ne "G\n"           
          [ "$boot" -gt 0 ]             && echo -ne "M\nD\n$boot\n" 
          [ "$sequential" -eq 1 ]       && echo -ne "I\n"           
                                           echo -ne "Y\n"             
          [ "$gamma" != "0" ]           && echo -ne "$CV\n"         
    
        } > dnadist.params
   fi
}

Veamos ahora el \(script\) run_phylip.sh completo, sección por sección, comentando algunos aspectos importantes de cada una.

Debes revisar con atención cada línea y consultar secciones previas de este tutorial o la guía de usuario de bash para aclarar dudas sobre aspectos sintácticos que te surjan. Trata además de entender la lógica de las secciones de código.

Lo ideal es que modifiques partes pequeñas del código para que entiendas cómo funciona. Copia las funciones, pégalas en la terminal y llámalas con diversos parámetros para que entiendas bien lo que hacen.

15.2.1 Cabecera de run_phylip.sh - settings globales e inicialización de variables

En esta primera sección se definen:

  1. la línea shebang
  2. settings globales
  • con el comando \(set\) para obligar al intérprete de comandos \(bash\) a ser muy estricto en la interpretación de las órdenes (ver comentarios)
  • LC_NUMERIC=en_US.UTF-8 y exportamos LC_NUMERIC para forzar que los decimales se lean como 1.34
  1. Declaración e inicialización de variables globales
#!/usr/bin/env bash

#-------------------------------------------------------------------------------------------------------
#: PROGRAM: run_phylip.sh
#: AUTHOR: Pablo Vinuesa, Center for Genomic Sciences, UNAM, Mexico
#:         https://www.ccg.unam.mx/~vinuesa/ twitter: @pvinmex
#
#: PROJECT START: October 16th, 2013
#    This program has been developed mainly for teaching purposes, 
#    with improvements/new features added as the script was used in 
#    diverse courses taught to undergrads at https://www.lcg.unam.mx
#    (LCG-UNAM) and the International Workshops on Bioinformatics (TIB)

#: AIM: run PHYLIP's distance methods [NJ|UPGMA] for DNA and proteins (dnadist|protdist) with optional bootstrapping
#       This script was written to teach intermediate Bash scripting to my students at the 
#       Bachelor's Program in Genome Sciences at the Center for Genome Sciences, UNAM, Mexico
#       https://www.lcg.unam.mx
#
#: INPUT: multiple sequence alignments (PROT|DNA) with at least 4 distinct sequences in phylip format
#
#: OUTPUT: [NJ|UPGMA] phylogenies and, if requested, bootstrap consensus trees

#: SOURCE: Freely available on GitHub @ https://github.com/vinuesa/intro2linux
#          Released under the GPLv3 License. 
#          http://www.gnu.org/copyleft/gpl.html

#### DEPENDENCIES 
# Assumes that the following binaries and scripts are all in $PATH, checked by check_dependencies()
#
#   1) Binaries from the PHYLIP package: 
#   seqboot dnadist protdist neighbor consense
#   NOTE: Linux-compatible PHYLIP binaries are supplied in the distro\'s bin/ directory
#           https://github.com/vinuesa/intro2linux/tree/master/bin  

#: TODO:
#    implement also parsimony and ML analyses and parallelize bootstrapping

#: KNOWN BUGS: None.
#    Please report any errors you may encounter through the GitHub issue pages
#-------------------------------------------------------------------------------------------------------

# make sure the user has at least bash version 4, since the script uses standard arrays (introduced in version 4),
#  but future development may require hashes, introduced in version 4
[ "${BASH_VERSION%%.*}" -lt 4 ] && echo "$HOSTNAME is running an ancient bash: ${BASH_VERSION}; ${0##*/} requires bash version 4 or higher" && exit 1

# 0. Define strict bash settings
set -e              # exit on non-zero exit status
set -u              # exit if unset variables are encountered
set -o pipefail     # exit after unsuccessful UNIX pipe command

# set and export LC_NUMERIC=en_US.UTF-8, to avoid problems with locales tha use 1,32
LC_NUMERIC=en_US.UTF-8
export LC_NUMERIC

args="$*"
progname=${0##*/} # run_phylip.sh
VERSION=2.0 

# GLOBALS
#DATEFORMAT_SHORT="%d%b%y" # 16Oct13
#TIMESTAMP_SHORT=$(date +${DATEFORMAT_SHORT})

date_F=$(date +%F |sed 's/-/_/g')-   # 2013_10_20
date_T=$(date +%T |sed 's/:/./g')    # 23.28.22 (hr.min.secs)
start_time="$date_F$date_T"
#current_year=$(date +%Y)

wkdir=$(pwd)

# initialize variables
def_DNA_model=F84
def_prot_model=JTT
input_phylip=
runmode=
boot=100
CV=1
model=
DEBUG=0
sequential=0
TiTv=2
upgma=0
outgroup=0
gamma="0.0"
outgroup=1

nw_utils_ok=

declare -a outfiles # to collect the output files written to disk

15.2.2 Funciones de run_phylip.sh - modularización del código

Las funciones en programas de \(Bash\) se definen siempre en la parte alta del \(script\), es decir tiene que estar definidas antes de que se llamen desde el bloque de código principal. A mí me gusta agruparlas por bloques de funciones relacionadas.

Es importante localizar las variables de las funciones con \(local\), para evitar que los valores de las variables modificados en las funciones interfieran con las globales.

Las funciones son fundamentales para modularizar el código y evitar llamadas repetidas al mismo bloque de código. Una función como \(check\_output\) se llama múltiples veces desde run_phylip.sh

Puede ser conveniente separar grupos de funciones relacionadas en archivos separados, que llamaremos librerías de funciones. Estas puden ser importadas al \(script\), con cualquiera de las dos llamadas equivalentes que se muestran seguidamente:

  • source /path/to/lib_name
  • . /path/to/lib_name
#---------------------------------------------------------------------------------#
#>>>>>>>>>>>>>>>>>>>>>>>>>>>> FUNCTION DEFINITIONS <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<#
#---------------------------------------------------------------------------------#
function print_dev_history()
{
    cat <<EOF_
    
    $progname $VERSION has been developed mainly for teaching purposes, 
      with improvements/new features added as the script was used in 
      diverse courses taught to undergrads at https://www.lcg.unam.mx
      and the International Workshops on Bioinformatics (TIB)
      
    TODO: * implement also parsimony and ML analyses
          * parallelize bootstrapping  
    
    # v2.0 2020-11-19; * fixed bug in write_protdist_params in the if [ $model == JTT ] || [ ...] conditional
               * fixed an error in the model name input checks, changing PMG for PMB
               * fixed grep in check_nw_utils_ok and made more robust conditional checking to call the function
               * use variable expansion only once in naming outfiles outtress, saving it to a variable
             to make the code more streamlined and possibly a tiny bit faster
               * removed useless [ -s outtre|outfile ] tests for the orignal tress|outfiles in the bootstrapping block, 
               as these are already checked in the first code block computing tress from the original alignment
    
    # v1.6 2020-11-19; * streamlined write_PHYLIP_param functions by grouping echo calls in { } > params; avoiding concatenation 
    #                  * improved description of write_PHYLIP_param functionality

    # v1.5 2020-11-15; * added check_phylip_ok to validate input phylip file with -ge 4 sequences and -ge 4 characters
                   * added remove_phylip_param_files
                   * added option -I to call print_install_notes
                   * added [ "${BASH_VERSION%%.*}" -lt 4 ] && die
                   * makes more extensive use of internal Bash variables and variable expansions, including \${var/pattern/string}

    # v1.4 2020-11-14; * added function check_nw_utils_ok, to check that nw_display and nw_support can be executed
                  as they may be in path but cannot find /lib64/libc.so.6: version GLIBC_2.14
                  when the binaries are compiled with dynamic linking to the libs
    
    # v1.3 2020-11-14; * improved layout of output messages; 
                   * improved regex in extract_tree_from_outfile (now also for NJ tree)
                   * if nw_support and nw_display are not available, 
                 prints both the NJ and boot trees to screen if bootstrap analysis was performed
    
    # v1.2 2020-11-11; set and export LC_NUMERIC=en_US.UTF-8, to avoid problems with locales that use 1,32 instead of 1.32
    
    # v1.1  2020-11-11; added function extract_tree_from_oufile and calls it on NJ|UPGMA bootRepl consensus trees
                          if nw_display is not available in PATH
    
    #v1.0  2020-11-09; This version has thorough error checking and reporting with improved code flow
         *  re-ingeneered the main code block. It now first runs the standard distance-matrix + clustering computations
            before doing bootstrapping. This helps in rapidly detecting problematic model parameters, as reflected in negative distances
     *  displays NJ|UPGMA tree without bootstrap values, if no bootstrapping is requested   
     *  added functions:
          - check_matrix to check if they contain negative values, issuing an error message
          - display_treeOI display trees with nw_display, only if they do not contain negative branch lengths, issuing an error message
          - print_dev_history to clean up the script\'s header section
     *  made 100% shellcheck-compliant; prints all error messages to >&2 (STDERR)
         *  changed/fixed defaults for CV=1 & gamma="0.0"; 
     *  use outfiles+=() to capture outfiles for -R 1 and -R 2 when boot -eq 0; now runs with -i -R1|2 as single opts
     *  Changed default boot=100
     *  now accepts TiTv=real like 6.7 and checks for real numbers passed to -t
     *  checks if outtree contains negative branches before attempting to display with nw_display & prints proper message

    # v0.5 2020-11-09; added functions:
         *  rdm_odd_int to compute random odd integers to pass to PHYLIP programs
         *  check_output (silently checks for expected output files, dying if not found)

    #v0.4 2020-11-08; released to the public domain @ https://github.com/vinuesa/intro2linux
           * added nw_support and nw_display calls to map and display bottstrap support values onto NJ or UPGMA trees
           * fixed several warnings issued by shellcheck
           * added strict bash interpreter calling with set -e; set -u; set -o pipefail
           * localized variables received in functions
           * explicitly set default_DNA_model=F84 and default_prot_model=JTT
           * added option -v to print version and exit
            
    #v0.3 September 15, 2017; improved checking of user input
    #v0.2 September 16, 2015; basic options and parameter checking for protdist phylogenies
    #v0.1 October 16, 2013; basic options and parameter checking for dnadist phylogenies
EOF_

exit 0
}
#---------------------------------------------------------------------------------#

function print_install_notes()
{
   cat <<INSTALL
   
    #1. PHYLIP PHYLogeny Inference Package by Joe Felensetein https://evolution.genetics.washington.edu/phylip.html
    
    The bin/ directory of the intro2linux distribution provides precompiled Linux-x86_64 binaries 
          for seqboot dnadist protdist neighbor consense. Copy them into your \$HOME/bin directory or any other in your PATH
    
    If you need executables for a different architecture, visit https://evolution.genetics.washington.edu/phylip/executables.html
    
    #2. Newick Utilities http://cegg.unige.ch/newick_utils
    The Newick Utilities have been described in an Open-Access paper (that is, available online for everyone):

    The Newick Utilities: High-throughput Phylogenetic tree Processing in the UNIX Shell
    Thomas Junier and Evgeny M. Zdobnov
    Bioinformatics 2010 26:1669-1670
    http://bioinformatics.oxfordjournals.org/content/26/13/1669.full
    doi:10.1093/bioinformatics/btq243
    
    The src/ dir contains the newick-utils-1.6-Linux-x86_64-disabled-extra.tar.gz source file
    
    Visit http://cegg.unige.ch/newick_utils if you need other pre-compiled versions
    
    Brief compilation notes
    
    1. Copy the newick-utils-1.6-Linux-x86_64-disabled-extra.tar.gz to a suitable directory in your \$HOME
    2. cd into the directory hoding the source *tar.gz file
    3. unpack and compile with the following commands 
          tar -xvzf newick-utils-1.6-Linux-x86_64-disabled-extra.tar.gz
      cd newick-utils-1.6
      ./configure --prefix=$HOME  # if you do NOT have administrator privileges
          #./configure                # if you have superuser privileges
      make
      make check
      make install                # if you do NOT have administrator privileges
      # sudo make install         # if you have superuser privileges

INSTALL

exit 0

}
#---------------------------------------------------------------------------------#

function check_output()
{
    local outfile=$1
    if [ ! -s "$outfile" ]
    then
        echo
    echo " >>> ERROR! The expected output file $outfile was not produced, will exit now!" >&2
        echo
    exit 2
    fi
}
#---------------------------------------------------------------------------------#

function check_phylip_ok()
{
    # implements a rudimentary format validation check
    local phyfile=$1
    local num_tax=
    local num_char=

    num_tax=$(awk 'NR == 1 {print $1}' "$phyfile")
    num_char=$(awk 'NR == 1 {print $2}' "$phyfile")
    
    if [[ ! "$num_tax" =~ ^([0-9]+)$ ]] ||  [[ ! "$num_char" =~ ^([0-9]+)$ ]]
    then
         echo "ERROR: $phyfile is not a phylip-formatted input file!"
     echo
     exit 1  
    elif [[ "$num_tax" =~ ^([0-9]+)$ ]] && [ "$num_tax" -lt 4 ]
    then
         echo "ERROR: $phyfile should contain at least 4 taxa"
     exit 1
    elif [[ "$num_char" =~ ^([0-9]+)$ ]] && [ "$num_char" -lt 5 ]
    then
         echo "ERROR: $phyfile should contain at least 5 aligned residues"
     exit 1
    else
        echo "# File validation OK: $phyfile seems to be a standard phylip file ..."
        echo '-------------------------------------------------------------------------------------------'
    fi
}
#---------------------------------------------------------------------------------#

function check_dependencies
{
    # check that the following PHYLIP binaries are in $PATH; die if not
    # optional: the Newick_utilities src is found in the src/ dir
    dependencies=(seqboot dnadist protdist neighbor consense) 
    for programname in "${dependencies[@]}"
    do
       local bin=
       bin=$(type -P "$programname")
       if [ -z "$bin" ]; then
          echo
          echo "# ERROR: $programname not in place!" >&2
          echo "# ... you will need to install \"$programname\" first or include it in \$PATH" >&2
          echo "# ... exiting" >&2
          exit 1
       fi
    done

    echo
    echo '# Run check_dependencies() ... looks good: all required external dependencies are in place!'
    echo '-------------------------------------------------------------------------------------------'
}
#---------------------------------------------------------------------------------#

function check_nw_utils_ok()
{
    # This function checks that nw_display and nw_support can be executed
    #  as they may be in path but cannot find /lib64/libc.so.6: version GLIBC_2.14
    #  when the binaries are compiled with dynamic linking to the libs
    
    # The function returns 0 when nw_support nw_display cannot be executed and 1 when they can
    # The function does not run and return anything if nw_support nw_display are not in PATH
    
    dependencies=(nw_support nw_display)
    
    local nw_utils_ok=
    local bin=
    
    for programname in "${dependencies[@]}"
    do
        bin=$(type -P "$programname")
        if [ -n "$bin" ]; then
       # prints something like nw_support: /lib64/libc.so.6: version `GLIBC_2.14' not found
       "$programname" 2>> "nw_utils_check.out.tmp" 
        fi
    done
    
    # check the contents of nw_utils_check.out.tmp and set proper flag
    if [ -s nw_utils_check.out.tmp ]
    then
           # set the nw_utils_ok flag to 0 if the lib is not found, to check later in the code when nw_support and nw_display are called
           grep -i error nw_utils_check.out.tmp &> /dev/null
       nw_utils_ok=$? 
       [ "$nw_utils_ok" -eq 0 ] && echo "# WARNING: ${dependencies[*]} are in PATH but cannot find /lib64/libc.so.6: version GLIBC_2.14" >&2 
           rm nw_utils_check.out.tmp 
           echo  "$nw_utils_ok"
    fi
}
#---------------------------------------------------------------------------------#

function rdm_odd_int()
{
  # generate a random odd integer for seqboot and neighbor
  while true
  do 
     i=$RANDOM
     if (( "$i" % 2 )) # true when odd
     then 
         echo "$i" 
     break
     fi
done

}
#---------------------------------------------------------------------------------#

function check_matrix()
{
    # check that the distance matrix does not contain negative values due to wrong model parameters
    local matrix=$1
    
    if grep -El ' \-[0-9\.]+' "$matrix"
    then
        echo "ERROR: computed negative distances!" >&2
         [ "$runmode" -eq 1 ] && echo "You may need to ajust the model and|or gamma value; try lowering TiTv if > 6" >&2
         [ "$runmode" -eq 2 ] && echo "You may need to ajust the matrix and|or gamma value" >&2
    exit 3
    fi
}
#---------------------------------------------------------------------------------#

function display_treeOK()
{
     # checks that tree does not contain branches with negative lengths, 
     #    as these cannot be displayed  with nw_dsiplay
     local tree=$1
     
     # check that there are no negative branch lengths in the nj_tree
     if ! grep -El '\-[0-9\.]+' "$tree"
     then 
         echo "# displaying $tree ..."
         echo
         nw_display -w 80 -Ir "$tree"
         echo
     else
         echo "ERROR: cannot display $nj_tree with nw_dsiplay, as it contains branches with negative lengths!" >&2
         [ "$runmode" -eq 1 ] && echo "You may need to adjust (lower?) TiTv=$TiTv" >&2
         [ "$runmode" -eq 2 ] && echo "You may need to use another matrix or adjunst gamma" >&2
         exit 4
     fi
}
#---------------------------------------------------------------------------------#

function extract_tree_from_outfile()
{
    # grep out lines containing tree characters | - and print to STDOUT
    local outfile=$1
   
    grep --color=never -E '[[:blank:]]+\||-' "$outfile" | grep -Ev 'Neighbor-|^---'
}

#------------------------------------- PHYLIP FUNCTIONS ---------------------------------------#
# >>> these are fucntions to write the command files to pass parameters to PHYLIP programs <<< # 

function remove_phylip_param_files()
{
   [ -s dnadist.params ] && rm dnadist.params
   [ -s protdist.params ] && rm protdist.params
   [ -s seqboot.params ] && rm seqboot.params
   [ -s neighbor.params ] && rm neighbor.params
   [ -s consense.params ] && rm consense.params
   return 0
}
#------------------------------------- PHYLIP FUNCTIONS ---------------------------------------#

function write_dnadist_params
{
    # writes a parameter file to run dnadist, based on provided arguments
    local model=$1
    local boot=$2
    local TiTv=$3
    local sequential=$4
    local gamma=$5
    local CV=$6

    # Runmode 1 = dnadist 
    if [ "$model" = 'F84' ] || [ -z "$model" ]
    then 
        {
          [ "$model" = 'Kimura' ]       && echo "D"                 
          [ "$model" = 'Jukes-Cantor' ] && echo -ne "D\nD\n"        
          [ "$model" = 'LogDet' ]       && echo -ne "D\nD\nD\n"     
          [ "$TiTv" != "2" ]            && echo -ne "T\n$TiTv\n"    
          [ "$gamma" != "0" ]           && echo -ne "G\n"           
          [ "$boot" -gt 0 ]             && echo -ne "M\nD\n$boot\n" 
          [ "$sequential" -eq 1 ]       && echo -ne "I\n"           
                                           echo -ne "Y\n"             
          [ "$gamma" != "0" ]           && echo -ne "$CV\n"         
    
        } > dnadist.params
   fi
}
#---------------------------------------------------------------------------------#

function write_protdist_params
{
    # write_protdist_params "$model" "$boot" "$sequential" "$gamma" "$CV"
    # writes a parameter file to run protdist, based on provided arguments
    # Models: JTT, PMB, PAM, Kimura
    local model=$1
    local boot=$2
    local sequential=$3
    local gamma=$4
    local CV=$5

    if [ "$model" = 'JTT' ] || [ -n "$model" ]
    then
       { # Runmode 2 = protdist 
         [ "$model" = 'PMB' ]     && echo "P"                 
         [ "$model" = 'PAM' ]     && echo -ne "P\nP\n"        
         [ "$model" = 'Kimura' ]  && echo -ne "P\nP\nP\n"     
         [ "$gamma" != "0" ]      && echo -ne "G\n"           
         [ "$boot" -gt 0 ]        && echo -ne "M\nD\n$boot\n" 
         [ "$sequential" -eq 1 ]  && echo -ne "I\n"    
                                     echo -ne "Y\n"       
         [ "$gamma" != "0" ]      && echo -ne "$CV\n"                  
       } > protdist.params
    fi  
}
#---------------------------------------------------------------------------------#

function write_seqboot_params
{
    # writes a parameter file to run seqboot, based on provided arguments
    local boot=$1
    local sequential=$2
    
    # Write Seqboot params
    {
      [ "$boot" -gt 0 ]       && echo -ne "R\n$boot\n" 
      [ "$sequential" -eq 1 ] && echo -ne "I\n"        
                                 echo -ne "Y\n"
                                 echo -ne "$ROI\n"
    } > seqboot.params                
}
#---------------------------------------------------------------------------------#

function write_neighbor_params
{
    # writes a parameter file to run neighbor, based on provided arguments
    local boot=$1
    local upgma=$2
    local outgroup=$3
    
    {
       # Write $ROI params
       [ "$upgma" -gt 0 ]    && echo -ne "N\n"                 
       [ "$outgroup" -gt 1 ] && echo -ne "O\n$outgroup\n"      
       [ "$boot" -gt 0 ]     && echo -ne "M\n$boot\n$ROI\n"    
                                echo -ne "Y\n"        
    } > neighbor.params
}
#---------------------------------------------------------------------------------#

function write_consense_params
{
    #writes a parameter file to run consense; very difficult ;)
    [ -s consense.params ] && rm consense.params
    {
      [ "$outgroup" -gt 1 ]  && echo -ne "O\n$outgroup\n"  
                                echo -ne "Y\n"           
    } > consense.params             
}
#---------------------------------------------------------------------------------#

# don't leave litter behind ... remove intermediate input/output files
function cleanup_dir 
{
   [ -s infile ]  && rm infile
   [ -s outfile ] && rm outfile
   [ -s intree ]  && rm intree
   [ -s outtree ] && rm outtree
   
   for file in *.params
   do
       [ -s "$file" ] && rm "$file"
   done    
}

#-------------------------------- END PHYLIP FUNCTIONS----------------------------#

function print_help
{
   #':b:c:m:I:o:R:t:hHIDsuv'
   cat<<EOF
   $progname v.$VERSION [OPTIONS]
   
   -h  prints this help message
   
 REQUIRED
   -i <input_phylip_file> (if sequential, use also flag -s)
   -R <integer>  RUNMODE
        1    run dnadist  (expects DNA  alignment in phylip format)
    2    run protdist (expects PROT alignment in phylip format)

 OPTIONAL
   -b <integer> No. of bootstrap pseudoreplicates [default: $boot]
   -g <real number> alpha for gamma distribution  [default: $gamma]
   -o <integer> outgroup sequence no.             [default: $outgroup]
   -s sequential format; -s (flag; no val)!       [default: interleaved]
   -t <digit> Transition/Transversion ratio       [default: $TiTv]
   -m <model name>  NOTE: TYPE AS SHOWN!
        DNA:  F84, Kimura, Jukes-Cantor, LogDet   [default: $def_DNA_model]
        PROT: JTT, PMB, PAM, Kimura               [default: $def_prot_model]
   -u <flag> use UPGMA for clustering             [default:  NJ]
   -v <flag> print program version and exit
   -D <flag> Activate debugging to keep cmd files [default: $DEBUG]
   -H <flag> print development history and exit   
   -I <flag> print installation notes and exit   
   
 AIM: run PHYLIP\'s distance methods [NJ|UPGMA] for DNA and proteins with bootstrapping
      This code was written to teach basic Bash scripting to my students at the 
      Bachelor\'s Program in Genome Sciences, Center for Genome Sciences, UNAM, Mexico
      https://www.lcg.unam.mx/      

 OUTPUT: [NJ|UPGMA] phylogenies and, if requested, bootstrap consensus trees 
         and  [NJ|UPGMA] phylogenies with bootstrap support values mappend on bipartitions
 
 EXTERNAL DEPENDENCIES: 
   * PHYLIP (https://evolution.genetics.washington.edu/phylip.html) programs:
      seqboot dnadist protdist neighbor consense 
   * Newick utilities programs (http://cegg.unige.ch/newick_utils) programs:
      optional: nw_support and nw_display; need to install separately
   - Notes: PHYLIP Linux 64bit binaries available in the bin/ dir
            Copy them to your \$HOME/bin directory or any other in your PATH
  
 LICENSING & SOURCE
       Author: Pablo Vinuesa | https://www.ccg.unam.mx/~vinuesa/ | twitter: @pvinmex
       Released under the GNU General Public License version 3 (GPLv3)
       http://www.gnu.org/copyleft/gpl.html
       source: https://github.com/vinuesa/intro2linux

EOF

exit 1   
}

15.2.3 El bloque de getopts - procesado y validación de opciones y argumentos pasados al \(script\) por el usuario

Este bloque contiene código clave para procesar y validar las opciones y argumentos que el usuario le pasa al \(script\). Es crítico que el programador verifique que el \(script\) recibe los parámetros requeridos y que cada opción y parámetro tengan valores razonables. Verás una gran cantidad de condicionales para verificar los datos recibidos por el \(script\).

Es muy deseable además, que el \(script\) imprima mensajes de error informativos para el usuario, con el fin de hacerle claro y sencillo el manejo del programa.

Noten la sentencia ROI=$(rdm_odd_int) que define a la variable global ROI, que almacena un íntegro impar aleatorio generado por la función rdm_odd_int. Este número impar es requerido por varios programas del paquete PHYLIP como \(seqboot\) y \(neighbor\).

En un bloque final, si se pasan los tests, se imprimen los valores de las opciones, para que quede clara la parametrización de la corrida del \(script\).

#-------------------------------------------------------------------------------------------------#
#---------------------------------------- GET OPTIONS --------------------------------------------#
#-------------------------------------------------------------------------------------------------#

# GETOPTS
while getopts ':b:g:i:m:R:o:t:hDHIsuv' OPTIONS
do
   case $OPTIONS in

   b)   boot=$OPTARG
        ;;
   g)   gamma=$OPTARG
    [ "$gamma" != 0 ] && CV=$(echo "1/sqrt($gamma)" | bc -l) && printf -v CV "%.4f\n" "$CV"
        ;;
   h)   print_help
        ;;
   i)   input_phylip=$OPTARG
        ;;
   s)   sequential=1
        ;;
   m)   model=$OPTARG
        ;;
   o)   outgroup=$OPTARG
        ;;
   R)   runmode=$OPTARG
        ;;
   t)   TiTv=$OPTARG
        ;;
   u)   upgma=1
        ;;
   v)   echo "$progname v$VERSION" && exit
        ;;
   D)   DEBUG=1
        ;;
   H)   print_dev_history
        ;;
   I)   print_install_notes
        ;;
   :)   printf "argument missing from -%s option\n" "$OPTARG"
    print_help
        exit 2 
        ;;
   \?)  echo "need the following args: "
    print_help
        exit 3
    ;;
   esac >&2   # print the ERROR MESSAGES to STDERR

done

shift $((OPTIND - 1))

#--------------------------#
# >>> Check User Input <<< #
#--------------------------#

if [ -z "$input_phylip" ]
then
       echo
       echo "# ERROR: no input phylip file defined!"
       print_help
       exit 1    
fi

if [ -z "$runmode" ]
then
       echo
       echo "# ERROR: no runmode defined!"
       print_help
       exit 1    
fi

# automatically set TiTv=0 when running with protein matrices or the LogDet DNA model
[ "$runmode" -eq 2 ] && TiTv=0
[ "$model" == LogDet ] && TiTv=0


# check that bootstrap value is an integer
re='^[0-9]+$'
if [[ ! "$boot" =~ $re ]]
then
   echo
   echo "# ERROR: boot:$boot is not a positive integer >= 0; provide a value between 100 and 1000" >&2 
   echo
   print_help
   echo
   exit 3
fi

# check that Ti/Tv is a real number, integer or decimal
re_TiTv='^[0-9.]+$'
if [[ ! "$TiTv" =~ $re_TiTv ]] && [ "$runmode" -eq 1 ]
then
   echo
   echo "# ERROR: TiTv:$TiTv is not an integer >= 0; provide a value between 0-10" >&2 
   echo
   print_help
   echo
   exit 3
elif [[ "$TiTv" =~ $re_TiTv ]] && [ "$runmode" -eq 1 ] && { [ "$model" == Jukes-Cantor ] || [ "$model" == LogDet ]; }
then
       echo
       echo "# ERROR: $model is only valid when analyzing DNA alignments under the F84 or K2P models" >&2
       echo
       print_help
       exit 1    
fi

if [ "$gamma" != 0 ] # note, need to treat as string, since gamma will be generalle a float like 0.5
then
    # this is to avoid having dots within filename (converts gamma==0.2 to gammaf=02)
    gammaf=${gamma/\./} #gammaf=${gamma%.*}${gamma##*.}
else
    gammaf="$gamma"
fi    

# check model vs. runmode compatibility and suitable bootstrap values are provided
#   using two alternative sintaxes for educational purposes
if [ "$runmode" -eq 1 ] && { [ "$model" = JTT ] || [ "$model" = PMB ] || [ "$model" = PAM ] || [ "$model" = "$def_prot_model" ]; }
then
       echo
       echo "# ERROR: $model is only valid when analyzing protein alignments under -R 2" >&2 
       echo
       print_help
       exit 1    
elif [ "$runmode" -eq 2 ] && [[ "$model" =~ ^(F84|Jukes-Cantor|LogDet|"$def_DNA_model")$ ]]
then
       echo
       echo "# ERROR: $model is only valid when analyzing DNA alignments under -R 1" >&2
       echo
       print_help
       exit 1    
elif [ "$boot" -lt 0 ]
then
       echo
       echo "# ERROR: bootstrap value=$boot is < 0 and not permitted" >&2
       echo
       print_help
       exit 1    
elif [ "$boot" -gt 1000 ]
then
       echo
       echo "# WARNING: bootstrap value=$boot is > 1000. This may take a long time to run."
       echo -n "# Are you sure you want to continue anyway? Type Y|N "
       read -r answer
       if [ "$answer" = N ] || [ "$answer" = n ] 
       then
           echo
       echo "# Ok, will exit then!"
       echo
       exit 2
       else
           echo
       echo "# Ok, will proceed running with $boot bootstrap pseudoreplicates ..."
       echo
       fi 
elif [ "$boot" -gt 0 ] && [ "$boot" -lt 100 ]
then
       echo
       echo "# WARNING: bootstrap value=$boot is a bit low. Use a value >= 100."
       echo -n "# Are you sure you want to continue anyway? Type Y|N "
       read -r answer
       if [ "$answer" = N ] || [ "$answer" = n ] 
       then
           echo
       echo "# Ok, will exit then!"
       echo
       exit 2
       else
           echo
       echo "# Ok, will proceed ..."
       echo
       fi 
fi

# set the default DNA or PROT models, if not defined by the user
if [ "$runmode" -eq 1 ] && [ -z "$model" ]
then
       model="$def_DNA_model"
       #echo "# DNA model set to default: $def_DNA_model..."       
fi

if [ "$runmode" -eq 2 ] && [ -z "$model" ]

then
       model="$def_prot_model"
       #echo "# Protein matrix set to default: $def_prot_model ..."       
fi

# check that the user provided a valid DNA substitution model
if [ "$runmode" -eq 1 ] && [[ ! "$model" =~ ^(F84|Kimura|Jukes-Cantor|LogDet|"$def_DNA_model")$ ]]
then
   echo
   echo "# ERROR: $model is not a recognized substitution model for DNA sequences used by PHYLIP" >&2 
   echo
   print_help
   echo
   exit 3
fi

# check that the user provided a valid name for empirical substitution matrix
#    note the much shorter and cleaner notation of this test using extended regexes, than the previous one
if [ "$runmode" -eq 2 ] && [[ ! "$model" =~ ^(JTT|PMB|PAM|Kimura)$ ]]
then
   echo
   echo "# ERROR: $model is not a recognized substitution matrix for protein sequences used by PHYLIP" >&2 
   echo
   print_help
   echo
   exit 3
fi

#>>> Set the script's run random odd number required by diverse PHYLIP programs <<<
ROI=$(rdm_odd_int)

# print the run settings
echo
echo "### $progname v.$VERSION run on $start_time with the following parameters:"
echo "# work_directory=$wkdir"
echo "# input_phylip=$input_phylip"
echo "# model=$model | gamma=$gamma | CV=$CV | gammaf=$gammaf | ti/tv ratio=$TiTv |
     outgroup=$outgroup | UPGMA=$upgma | bootstrap no.=$boot | ROI=$ROI"
echo "# runmode=$runmode"
echo "# DEBUG=$DEBUG" 
echo "# command: $progname ${args[*]}"
echo

run_phylip requiere necesariamente definir dos opciones:

  • -i el archivo de entrada con secuencias alineadas en formato phylip
  • -R definir un runmode:
    • -R 1 para secuencias de DNA, llamando a dnadist o
    • -R 2 para secuencias de proteína, llamando a protdist

Si no se pasan estos argumentos, el \(script\) muere grácilmente imprimiendo un mensaje de ayuda

  • run_phylip puede aceptar otras opciones que le permiten al usuario definir:
    • el modelo de sustitución (para DNA o Proteína)
    • el número de pseudoréplicas de bootstrap a correr (por defecto corre 100 réplicas)
    • el tipo de árbol a construir: NJ|UPGMA

Todas estas opciones vienen definidas y explicadas en el menú de ayuda. El bloque getopts recibe las opciones pasadas por el usuario y verifica que sean adecuadas.

15.2.4 El bloque principal de run_phylip.sh

El siguiente bloque les muestra el código principal, que hace las llamadas a las funciones mostradas en la sección anterior y controla el flujo del programa acorde a las opciones y parámetros pasado al \(script\) por el usuario, procesados y validados en la sección anterior.

El \(script\) run_phylip.sh consta de dos bloques principales. 1. El primero calcula la matriz de distancias (-R 1 = DNA; -R 2 = prot) y el árbol (NJ|UPGMA) correspondiente directamente a partir de los datos del alineamiento en formato PHYLIP que le pasa el usuario al \(script\) (-i input_phylip). 2. El segundo se corre cuando el usuario le pide al \(script\) hacer un análisis de bootstrap, el cual se realiza por defecto con 100 pseudoréplicas. Este bloque no se corre cuando el usuario le pasa \(-b\ 0\) el \(script\)

Este bloque además se encarga de verificar que se van produciendo los resultados esperados, imprimiendo algunos mensajes del progreso del programa.

Si se corren réplicas de bootstrap, los valores de consenso se remapean sobre la filogenia NJ o UPGMA inferida directamente de los datos, usando \(nw\_support\). Esta filogenia, con sus valores de bootstrap, se despliega en pantalla con \(nw\_display\).

Una vez concluido el trabajo, se limpia el directorio, borrando los archivos temporales como los de argumentos escritos para cada uno de los programas de PHYLIP.

Finalmente se imprime un resumen de los archivos escritos a disco, cuyos nombres se fueron almacenados en el arreglo \(outfiles\) a medida que se iban generando a lo largo del programa.

15.2.4.1 Sección 1 del cuerpo principal del \(script\), encargado de calcular una filogenia de distancias directamente del alineamiento (sin réplias de bootstrap)

En este bloque se calculan primero las matrices de distancia con la parametrización indicada por el usuario llamando a \(dnadist\) o a \(protdist\) desde funciones, según los datos sean de DNA o proteína.

La función \(check\_matrix\) verifica que no se hayan estimado distancias negativas.

Finalmente se llama a \(neighbor\) para que reconstruya el árbol (NJ|UPGMA).

Si el usuario le pasa \(-b\ 0\) el \(script\), éste termina desplegando el árbol de distancia en pantalla llamando a \(nw\_display\) e imprime un resumen de los archivos escritos a disco.


#-------------------------------------------------------------------------------------------------#
#------------------------------------------ MAIN CODE --------------------------------------------#
#-------------------------------------------------------------------------------------------------#

# 1. make sure the external dependencies are found in PATH
#     and that nw_display and nw_support find the required GLIBC_2.14 in /lib64/libc.so.6
check_dependencies
nw_utils_ok=$(check_nw_utils_ok) # nw_utils_ok -eq 1 when OK, -eq 0 when not

# 2. Start processing the input file
# make sure there are no old outfile or outtree files lying around from previous runs
[ -s infile ] && rm infile
[ -s outfile ] && rm outfile
[ -s outtree ] && rm outtree
# nor old params files
remove_phylip_param_files

# 2.1) make sure we have an input file or die
if [ -s "$input_phylip" ]
then
     # make basic format validation check
     check_phylip_ok "$input_phylip"
     cp "$input_phylip" infile
else
     echo
     echo "# FATAL ERROR: input phylip file $input_phylip does not exist or is empty" >&2 
     echo "# Make sure $input_phylip is in $wkdir. Exiting ..." >&2
     echo
     exit 1
fi

# ----------------------------------------------------------- #
# >>>>>>>>>>>>>>>> Compute distance matrices <<<<<<<<<<<<<<<< #
# ----------------------------------------------------------- #

# 3. In any case (with or without bootstrap) compute the NJ tree for the original phylip file
#     Run dnadist or protdist, as required   
echo
echo ">>> Computing distance matrix for $input_phylip ..."

if [ "$runmode" -eq 1 ]      
then
    echo "# running write_dnadist_params $model 0 $TiTv $sequential $gamma $CV"
    write_dnadist_params "$model" "0" "$TiTv" "$sequential" "$gamma" "$CV"
    echo "# running dnadist < dnadist.params"
    dnadist < dnadist.params &> /dev/null
    check_output outfile
    
    # https://fvue.nl/wiki/Bash:_Error_%60Unbound_variable%27_when_appending_to_empty_array
    # Set last item specifically
    # nstead of appending one element, set the last item specifically, without any "unbound variable" error
    # t[${#t[*]}]=foo
    dnadist_outfile="${input_phylip%.*}_${model}${gammaf}gamma_distMat.out"
    cp outfile "$dnadist_outfile" && \
      outfiles+=("$dnadist_outfile")
    
    # check that the distance matrix does not contain negative values due to wrong model parameters
    check_matrix "$dnadist_outfile"    
    
    mv outfile infile
elif [ "$runmode" -eq 2 ]
then
    echo "# running write_protdist_params $model 0 $sequential $gamma $CV"
    write_protdist_params "$model" "0" "$sequential" "$gamma" "$CV"
    echo "# running protdist < protdist.params"
    protdist < protdist.params &> /dev/null
    check_output outfile
    
    protdist_outfile="${input_phylip%.*}_${model}${gammaf}gamma_distMat.out"
    cp outfile "$protdist_outfile" && \
      outfiles+=("$protdist_outfile")
    
    # check that the distance matrix does not contain negative values due to wrong model parameters
    check_matrix "$protdist_outfile"
    
    mv outfile infile
fi


# ------------------------------------------------------------------------------------ #
# >>>>>>>>>>>>>>>> Computing NJ|UPGMA trees from original alignments <<<<<<<<<<<<<<<<< #
# ------------------------------------------------------------------------------------ #

echo
echo ">>> Computing distance tree for $input_phylip ..."  

# 4. now that we have the dist matrix, do the clustering with NJ or UPGMA
echo "# running write_neighbor_params 0 $upgma"
write_neighbor_params "0" "$upgma" "$outgroup"
echo "# running neighbor < neighbor.params"
neighbor < neighbor.params &> /dev/null
check_output outfile
check_output outtree

# 5.1 rename outtrees and tree outfiles; remap bootstrap values to bipartitions and display tree to screen
if [ "$upgma" -gt 0 ]
then
     # https://fvue.nl/wiki/Bash:_Error_%60Unbound_variable%27_when_appending_to_empty_array
     # Set last item specifically
     # instead of appending one element, set the last item specifically, without any "unbound variable" error
     # t[${#t[*]}]=foo
     upgma_tree=
     upgma_outfile=
     
     if [ -s outtree ] 
     then
          upgma_tree="${input_phylip%.*}_${model}${gammaf}gamma_UPGMA.ph"
          mv outtree "$upgma_tree"
      echo "# wrote tree $upgma_tree to disk" 
      outfiles[${#outfiles[*]}]="$upgma_tree"
     fi 
     
     if [ -s outfile ]
     then
         upgma_outfile="${input_phylip%.*}_${model}${gammaf}gamma_UPGMA.outfile"
         mv outfile "$upgma_outfile"
         outfiles[${#outfiles[*]}]="$upgma_outfile"
     fi 
       
     # check that there are no negative branch lengths in the nj_tree
     #  and display with nw_display, only if no bootstrapping is requested
     if [ "$boot" -eq 0 ] && [[ $(type -P nw_display) ]] && [[ "$nw_utils_ok" -eq 1 ]]
     then 
         display_treeOK "$upgma_tree"
     elif [ "$boot" -eq 0 ] && { [[ ! $(type -P nw_display) ]] || [[ "$nw_utils_ok" -ne 1 ]]; }
     then
          echo "# extract_tree_from_outfile $upgma_outfile"
      echo
      extract_tree_from_outfile "$upgma_outfile"
      echo
     fi  
else
     nj_tree=
     nj_outfile=

     if [ -s outtree ] 
     then
      nj_tree="${input_phylip%.*}_${model}${gammaf}gamma_NJ.ph"
          mv outtree "$nj_tree"
          echo "# wrote tree $nj_tree to disk"
          outfiles[${#outfiles[*]}]="$nj_tree"
     fi
     
     if [ -s outfile ] 
     then
          nj_outfile="${input_phylip%.*}_${model}${gammaf}gamma_NJ.outfile"
          mv outfile "$nj_outfile"
          outfiles[${#outfiles[*]}]="$nj_outfile"
     fi

     # check that there are no negative branch lengths in the nj_tree
     #  and display with nw_display, only if no bootstrapping is requested
     if [ "$boot" -eq 0 ] && [[ $(type -P nw_display) ]] && [[ "$nw_utils_ok" -eq 1 ]]
     then
         display_treeOK "$nj_tree"
     elif [ "$boot" -eq 0 ] && { [[ ! $(type -P nw_display) ]] || [[ "$nw_utils_ok" -ne 1 ]]; }
     then
         echo "# extract_tree_from_outfile $nj_outfile"
     echo
     extract_tree_from_outfile "$nj_outfile"
     echo
     fi  
fi

echo "# > finished computing distance matrix and tree for $input_phylip!"

if [ "$boot" -eq 0 ]
then
    # 6. Print final output summary message
    echo
    echo '===================== OUTPUT SUMMARY ====================='
    no_outfiles=${#outfiles[@]}
    echo "# $no_outfiles output files were generated:"
    printf "%s\n" "${outfiles[@]}"

    echo
    echo -n "# FINISHED run at: "; date 
    echo "   ==> Exiting now ..."
    echo
else
    echo "=================================================================="
    echo
fi

15.2.4.2 Sección 2 del código principal - análisis de bootstrap

Este bloque sólo se corre si [ “$boot” -gt 0 ]. Por defecto el \(script\) está programado para realizar 100 pseudoréplicas de bootstrap del alineamiento original, las cuales se ejecutan inmediatamente después de haber reconstruido las filogenias NJ|UPGMA a parir del alineamiento original en el bloque inicial.

Al igual que en el bloque anterior, el \(script\) se encarga de hacer copias de los archivos infile|outfile|outtree con nombres informativos, que contienen el modelo y número de réplicas de bootstrap usados, y los guarda en el arreglo outfiles. Además verifica que cada archivo de salida fue realmente escrito con la función check_output.

Una vez estimada la filogenia de consenso de mayoría a partir de las réplicas de bootstrap el \(script\) verifica si se encuentran en el PATH las aplicaciones externas nw_support y nw_display, llamándolas en su caso para desplegar el árbol original con los valores de soporte de bootstrap indicados sobre las biparticiones correspondientes. Si no se encuentran en el PATH, o no están correctamente instaladas (esto lo checa la función check_nw_utils_ok()), usa la función extract_tree_from_outfile() que, como indica su nombre, extrae las líneas que contienen el árbol que PHYLIP escribe en los archivos outfile correspondientes.

Finalmente el \(script\) hace un resumen de los archivos escritos a disco.

# ----------------------------------------------------------- #
# >>>>>>>>>>>>>>>>>>>> Bootstrap Analysis <<<<<<<<<<<<<<<<<<< #
# ----------------------------------------------------------- #

# Run seqboot if requested
# run seqboot if -b > 0

if [ "$boot" -gt 0 ] 
then
     # 1. restore the original infile for the bootstrapping and standard NJ/UPGMA analysis below
     cp "$input_phylip" infile
     check_output infile

     echo
     echo ">>> Bootstrap Analysis based on $boot pseudoreplicates for $input_phylip ..."
     echo "# running  write_seqboot_params $boot $sequential"
     write_seqboot_params "$boot" "$sequential"
     echo "# running seqboot < seqboot.params &> /dev/null"
     seqboot < seqboot.params &> /dev/null
     check_output outfile
     mv outfile infile

     echo "# > computing distance matrices on $boot bootstrapped alignments ..."
     # 2. if bootstrapping, then compute consensus tree
     # we need to run dnadist or protdist, depending on runmode
     if [ "$runmode" -eq 1 ]
     then
     echo "# running write_dnadist_params $model $boot $TiTv $sequential $gamma $CV"
         write_dnadist_params "$model" "$boot" "$TiTv" "$sequential" "$gamma" "$CV"
     echo "# running dnadist < dnadist.params &> /dev/null"
         dnadist < dnadist.params &> /dev/null
     check_output outfile
     
     # check that matrix does not contain negative values
     check_matrix outfile
     
     mv outfile infile
     elif [ "$runmode" -eq 2 ]
     then
     echo
     echo "# running write_protdist_params $model $boot $sequential $gamma $CV"
         write_protdist_params "$model" "$boot" "$sequential" "$gamma" "$CV"
     echo "# running protdist < protdist.params &> /dev/null"
         protdist < protdist.params &> /dev/null
     check_output outfile
     
     # check that matrix does not contain negative values
     check_matrix outfile

     mv outfile infile
     fi

     # 3. Now we have the distance matrices and we can proceed equaly for runmodes 1 and 2
     #    >>> Run neighbor
     echo "# > Computing distance trees from bootstrapped data ..."   
     echo "# running write_neighbor_params $boot $upgma $outgroup"
     write_neighbor_params "$boot" "$upgma" "$outgroup"
     echo "# running neighbor < neighbor.params &> /dev/null"
     neighbor < neighbor.params &> /dev/null
     check_output outtree
     
     boot_trees=
     if [ -s outtree ]
     then
            # this is the file holding the trees for the n-distance matrices for n-boot replicated alignments
            boot_trees="${input_phylip%.*}_${model}${gammaf}gamma_${boot}bootRepl_trees.nwk"
            cp outtree "$boot_trees"
     
            # append to array with +=, otherwise will complain as unset with set -u 
            # https://fvue.nl/wiki/Bash:_Error_%60Unbound_variable%27_when_appending_to_empty_array
            outfiles+=("$boot_trees")
     fi
     mv outtree intree
     rm outfile

     # 4. Compute consensus tree with consense
     echo "# > Computing MJR consensus tree from trees reconstructed from bootstrap pseudoreplicates ..."
     echo "# running write_consense_params"
     write_consense_params
     echo "# running consense < consense.params &> /dev/null"
     consense < consense.params &> /dev/null
     check_output outtree
     check_output outfile
     
     # variables holding consensus trees
     upgma_consensus_tree=
     upgma_consensus_outfile=
     nj_consensus_tree=
     nj_consensus_outfile=

     if [ "$upgma" -gt 0 ]
     then
           # https://fvue.nl/wiki/Bash:_Error_%60Unbound_variable%27_when_appending_to_empty_array
         # Set last item specifically
           # instead of appending one element, set the last item specifically, without any "unbound variable" error
           # t[${#t[*]}]=foo
           upgma_consensus_tree="${input_phylip%.*}_UPGMAconsensus_${model}${gammaf}gamma_${boot}bootRepl.ph"
           mv outtree "$upgma_consensus_tree"
           outfiles[${#outfiles[*]}]="$upgma_consensus_tree"
         
           upgma_consensus_outfile="${input_phylip%.*}_UPGMAconsensus_${model}${gammaf}gamma_${boot}bootRepl.outfile"
           mv outfile "$upgma_consensus_outfile"
           outfiles[${#outfiles[*]}]="$upgma_consensus_outfile"
     else
         nj_consensus_tree="${input_phylip%.*}_NJconsensus_${model}${gammaf}gamma_${boot}bootRepl.ph"
           mv outtree "$nj_consensus_tree"
           outfiles[${#outfiles[*]}]="$nj_consensus_tree"
         
           nj_consensus_outfile="${input_phylip%.*}_NJconsensus_${model}${gammaf}gamma_${boot}bootRepl.outfile"
           mv outfile "$nj_consensus_outfile"
           outfiles[${#outfiles[*]}]="$nj_consensus_outfile"
     fi 

     # 5. Rename outtrees and tree outfiles; remap bootstrap values to bipartitions and display tree on screen
     if [ "$upgma" -gt 0 ]
     then
          # if we requested bootstrapping, map bootstrap values onto UPGMA tree using
          #   nw_support upgma.ph bootRepl_tree.ph > UPGMA_with_boot_support.ph
          if [ -s "$upgma_tree" ] && [ -s "$boot_trees" ] && [[ $(type -P nw_support) ]] && [ "$nw_utils_ok" -eq 1 ]
          then
                upgma_tree_with_boot="${input_phylip%.*}_${model}${gammaf}gamma_UPGMA_with_${boot}boot_support.ph"
                echo "# mapping bootstrap values on UPGMA tree with nw_support ..."
                nw_support "$upgma_tree" "$boot_trees" > "$upgma_tree_with_boot"

                if [ -s "$upgma_tree_with_boot" ] && [[ $(type -P nw_display) ]] && [ "$nw_utils_ok" -eq 1 ]
                then
                    outfiles[${#outfiles[*]}]="$upgma_tree_with_boot"
              
                      # check that there are no negative branch lengths in the nj_tree 
                      #   before displaying with nw_display
                      display_treeOK "$upgma_tree_with_boot"
                fi
            elif [ -s "$upgma_outfile" ] && [ -s "$upgma_consensus_outfile" ] && { [[ ! $(type -P nw_support) ]] || [ "$nw_utils_ok" -ne 1 ]; }
            then
               echo "# extract_tree_from_outfile $upgma_outfile"
                 echo
                 extract_tree_from_outfile "$upgma_outfile"
                 echo
                 echo "# extract_tree_from_outfile $upgma_consensus_outfile"
                 echo
                 extract_tree_from_outfile "$upgma_consensus_outfile"
                 echo
            fi
     else
         # if we requested bootstrapping, map bootstrap values onto NJ tree using
         #   nw_support NJ.ph bootRepl_tree.ph > NJ_with_boot_support.ph
         if [ -s "$nj_tree" ] && [ -s "$boot_trees" ] && [[ $(type -P nw_support) ]] && [ "$nw_utils_ok" -eq 1 ]
         then
             nj_tree_with_boot="${input_phylip%.*}_${model}${gammaf}gamma_NJ_with_${boot}boot_support.ph"
               echo "# mapping bootstrap values on NJ tree with nw_support ..."
               nw_support "$nj_tree" "$boot_trees" > "$nj_tree_with_boot"
     
               check_output "$nj_tree_with_boot"
     
               if [ -s "$nj_tree_with_boot" ] && [[ $(type -P nw_display) ]] && [ "$nw_utils_ok" -eq 1 ]
               then
                   outfiles+=("$nj_tree_with_boot")
             
                     # check that there are no negative branch lengths in the nj_tree 
                     #   before displaying with nw_display
                     display_treeOK "$nj_tree_with_boot"
               fi
            elif [ -s "$nj_outfile" ] && [ -s "$nj_consensus_outfile" ] && { [[ ! $(type -P nw_support) ]] || [ "$nw_utils_ok" -ne 1 ]; }
            then
                echo "# extract_tree_from_outfile $nj_outfile"
                  echo
                  extract_tree_from_outfile "$nj_outfile"
                    echo

                    echo "# extract_tree_from_outfile $nj_consensus_outfile"
                    echo
                    extract_tree_from_outfile "$nj_consensus_outfile"
                    echo
         fi
    fi
fi

# 6. Tidy up: remove the *params files and other temporary files 
# that could interfere with future runs and litter the directory

[ "$DEBUG" -eq 0 ] && cleanup_dir

# 7. Print final output summary message
echo
echo '===================== OUTPUT SUMMARY ====================='

no_outfiles=${#outfiles[@]}
echo "# $no_outfiles output files were generated:"
printf "%s\n" "${outfiles[@]}"

echo
echo -n "# FINISHED run at: "; date 
echo "   ==> Exiting now ..."
echo

15.2.5 Llamada a run_phylip para construir filogenia NJ de proteínas con 100 réplicas de bootstrap y modelo JTT+G (alpha=0.6)

La siguiente línea muestra una llamada a run_phylip para hacer lo arriba indicado usando el archivo phylip GDP_12_prokEuc.phy, que con tiene 12 secuencias de gliceraldehido-fosfato deshidrogenasas de representantes de los dominios Eukarya y Bacteria

  • Veamos primero la cabecera del archivo GDP_12_prokEuc.phy
head -13 GDP_12_prokEuc.phy
##  12 234
## gpd1yeast    IAKVVAENQN VKYLPGITLP DNLVANPDLI DSVKDVDIIV FNIPHQFLPR
## gpdadrome    IAKIVGANEN VKYLKGHKLP PN-VAVPDLV EAAKNADILI FVVPHQFIPN
## gpdhuman     IAKIVGGNEN VKYLPGHKLP PNVVAVPDVV QAAEDADILI FVVPHQFIGK
## gpdamouse    IAKIVGSNEN VKYLPGHKLP PNVVAIPDVV QAATGADILV FVVPHQFIGK
## gpdarabit    IAKIVGGNEN VKYLPGHKLP PNVVAVPDVV KAAADADILI FVVPHQFIGK
## gpdacaeel    IARIVGSTEN IKYLPGKVLP NNVVAVTDLV ESCEGSNVLV FVVPHQFVKG
## gpdleish     LAMVLSKKEN VLFLKGVQLA SNITFTSDVE KAYNGAEIIL FVIPTQFLRG
## gpdtrybb     LACVLAKKEN VYFLPGAPLP ANLTFTADAE ECAKGAEIVL FVIPTQFLRG
## gpdaecoli    LAITLARNCN AAFLPDVPFP DTLHLESDLA TALAASRNIL VVVPSHVFGE
## gpdahaein    LAITFSRNQN YRFLPDVIFP EDLHLESNLA QAMEYSQDIL IVVPSHAFGE
## gpdabacsu    LALVLTDNEN KDYLPNVKLS TSIKGTTDMK EAVSDADVII VAVPTKAIRE
## gpdpseu      VAKTRWRNEN TAYLPGHPLP AALKATADFS LALDHVAQGD GLLIAATSVA

Un alineamiento en formato phylip debe contener una cabecera indicando las dimensiones de la matriz, en este caso ” 12 234”, es decir: 12 secuencias y 234 columnas o caracteres. Noten también que el formato phylip estándar acepta etiquetas con un máximo de 10 caracteres de longitud, por lo que los nombres de los taxa están truncados.

  • Veamos ahora la salida de la siguiente llamada a run_phylip.sh, usando el modelo por defecto para proteínas (JTT), pero asumiendo una heterogeneidad de tasas entre sitios modelada por una distribución gamma con un valor de alpha=0.6.
    • ./run_phylip.sh -i GDP_12_prokEuc.phy -R 2 -g 0.6
### run_phylip.sh v.2.0 run on 2020_11_21-11.09.04 with the following parameters:
# work_directory=/home/vinuesa/cursos/intro2linux
# input_phylip=GDP_12_prokEuc.phy
# model=JTT | gamma=0.6 | CV=1.2910
 | gammaf=06 | ti/tv ratio=0 |
     outgroup=1 | UPGMA=0 | bootstrap no.=100 | ROI=6475
# runmode=2
# DEBUG=0
# command: run_phylip.sh -i GDP_12_prokEuc.phy -R 2 -g 0.6


# Run check_dependencies() ... looks good: all required external dependencies are in place!
-------------------------------------------------------------------------------------------
# File validation OK: GDP_12_prokEuc.phy seems to be a standard phylip file ...
-------------------------------------------------------------------------------------------

>>> Computing distance matrix for GDP_12_prokEuc.phy ...
# running write_protdist_params JTT 0 0 0.6 1.2910

# running protdist < protdist.params

>>> Computing distance tree for GDP_12_prokEuc.phy ...
# running write_neighbor_params 0 0
# running neighbor < neighbor.params
# wrote tree GDP_12_prokEuc_JTT06gamma_NJ.ph to disk
# > finished computing distance matrix and tree for GDP_12_prokEuc.phy!
==================================================================


>>> Bootstrap Analysis based on 100 pseudoreplicates for GDP_12_prokEuc.phy ...
# running  write_seqboot_params 100 0
# running seqboot < seqboot.params &> /dev/null
# > computing distance matrices on 100 bootstrapped alignments ...

# running write_protdist_params JTT 100 0 0.6 1.2910

# running protdist < protdist.params &> /dev/null
# > Computing distance trees from bootstrapped data ...
# running write_neighbor_params 100 0 1
# running neighbor < neighbor.params &> /dev/null
# > Computing MJR consensus tree from trees reconstructed from bootstrap pseudoreplicates ...
# running write_consense_params
# running consense < consense.params &> /dev/null
# mapping bootstrap values on NJ tree with nw_support ...
# displaying GDP_12_prokEuc_JTT06gamma_NJ_with_100boot_support.ph ...

                                                          +---+ gpdleish        
                                        +-100-------------+                     
                                        |                 +--------+ gpdtrybb   
                                        |                                       
                                        |                      +----+ gpdaecoli 
 +-100----------------------------------+          +-94--------+                
 |                                      |   +-79---+           +-+ gpdahaein    
 |                                      |   |      |                            
 |                                      +-54+      +-----------------+ gpdpseu  
 |                                          |                                   
 |                                          +---------+ gpdabacsu               
 |                                                                              
 |        +---+ gpdadrome                                                       
=| 100 +-90                                                                     
 |     |  +------+ gpdacaeel                                                    
 +-87--+                                                                        
 |     | | gpdamouse                                                            
 |     | |                                                                      
 |     +-79 gpdhuman                                                            
 |       | 78                                                                   
 |       +-+ gpdarabit                                                          
 |                                                                              
 +--------------+ gpd1yeast                                                     
                                                                                
 |---------------|----------------|---------------|----------------|--          
 0               1                2               3                4            
 substitutions/site                                                             
                                                                                


===================== OUTPUT SUMMARY =====================
# 7 output files were generated:
GDP_12_prokEuc_JTT06gamma_distMat.out
GDP_12_prokEuc_JTT06gamma_NJ.ph
GDP_12_prokEuc_JTT06gamma_NJ.outfile
GDP_12_prokEuc_JTT06gamma_100bootRepl_trees.nwk
GDP_12_prokEuc_NJconsensus_JTT06gamma_100bootRepl.ph
GDP_12_prokEuc_NJconsensus_JTT06gamma_100bootRepl.outfile
GDP_12_prokEuc_JTT06gamma_NJ_with_100boot_support.ph

# FINISHED run at: sáb 21 nov 2020 11:09:25 CST
   ==> Exiting now ...

15.2.6 Llamada a run_phylip con un archivo de secuencias FASTA

El \(script\) revisa que el archivo de entrada tenga formato PHYLIP, comprobando además que tenga al menos 4 secuencias y 5 columnas de datos de secuencia (caracteres)

La función \(check\_phylip\_ok\) es la encargada de ello:

function check_phylip_ok()
{
    # implements a rudimentary format validation check
    local phyfile=$1
    local num_tax=
    local num_char=

    num_tax=$(awk 'NR == 1 {print $1}' "$phyfile")
    num_char=$(awk 'NR == 1 {print $2}' "$phyfile")
    
    if [[ ! "$num_tax" =~ ^([0-9]+)$ ]] ||  [[ ! "$num_char" =~ ^([0-9]+)$ ]]
    then
         echo "ERROR: $phyfile is not a phylip-formatted input file!"
           echo
           exit 1  
    elif [[ "$num_tax" =~ ^([0-9]+)$ ]] && [ "$num_tax" -lt 4 ]
    then
         echo "ERROR: $phyfile should contain at least 4 taxa"
           exit 1
    elif [[ "$num_char" =~ ^([0-9]+)$ ]] && [ "$num_char" -lt 5 ]
    then
         echo "ERROR: $phyfile should contain at least 5 aligned residues"
           exit 1
    else
        echo "# File validation OK: $phyfile seems to be a standard phylip file ..."
        echo '-------------------------------------------------------------------------------------------'
    fi
}

./run_phylip.sh -i recA_Byuanmingense_muscle.aln -R 1

### run_phylip.sh v.1.5 run on 2020_11_15-20.10.11 with the following parameters:
# work_directory=/home/vinuesa/cursos/intro2linux
# input_phylip=recA_Byuanmingense_muscle.aln
# model=F84 | gamma=0.0 | CV=1 | gammaf=00 | ti/tv ratio=2 |
     outgroup=1 | UPGMA=0 | bootstrap no.=100 | ROI=12349
# runmode=1
# DEBUG=0
# command: run_phylip.sh -i recA_Byuanmingense_muscle.aln -R 1


# Run check_dependencies() ... looks good: all required external dependencies are in place!
-------------------------------------------------------------------------------------------
ERROR: recA_Byuanmingense_muscle.aln is not a phylip-formatted input file!

15.2.7 Llamada a run_phylip para construir filogenia NJ de proteínas bajo modelo erróneo (de DNA)

El \(script\) revisa que el usuario use correctamente las opciones de modelos para DNA (-R 1) o proteína (-R 2), haciendo uso del código que se muestra seguidamente:

# check model vs. runmode compatibility and suitable bootstrap values are provided
#   using two alternative sintaxes for educational purposes
if [ "$runmode" -eq 1 ] && { [ "$model" = JTT ] || [ "$model" = PMB ] || [ "$model" = PAM ] || [ "$model" = "$def_prot_model" ]; }
then
       echo
       echo "# ERROR: $model is only valid when analyzing protein alignments under -R 2" >&2 
       echo
       print_help
       exit 1    
elif [ "$runmode" -eq 2 ] && [[ "$model" =~ ^(F84|Jukes-Cantor|LogDet|"$def_DNA_model")$ ]]
then
       echo
       echo "# ERROR: $model is only valid when analyzing DNA alignments under -R 1" >&2
       echo
       print_help
       exit 1
...
  • Veamos ahora la salida de la siguiente llamada a run_phylip.sh para correr con proteínas (-R 2), usando parámetros por defecto, salvo el modelo, que el usuario erróneamente quiere definir como F84. ./run_phylip.sh -i GDP_12_prokEuc.phy -R 2 -m F84
# ERROR: F84 is only valid when analyzing DNA alignments under -R 1

   run_phylip.sh v.2.0 [OPTIONS]
   ...
  • Otro ejemplo de invocación errónea: querer definir tasas de transiciones/transversiones para alineamiento de proteínas
    • ./run_phylip.sh -i GDP_12_prokEuc.phy -R 2 -t 10
### run_phylip.sh v.2.0 run on 2020_11_21-11.09.04 with the following parameters:
# work_directory=/home/vinuesa/cursos/intro2linux
# input_phylip=GDP_12_prokEuc.phy
# model=JTT | gamma=0.0 | CV=1 | gammaf=00 | ti/tv ratio=0 |
     outgroup=1 | UPGMA=0 | bootstrap no.=100 | rnd_no=31435
# runmode=2
# DEBUG=0
   ...

En este caso corre, pero vean que lo hace bajo ti/tv = 0, es decir, ignorando la opción inadecuada ya que ésta no se le pasa a \(protdist\) para calcular las distancias.

15.2.8 Llamada a run_phylip para construir filogenia NJ de ADN con valor incorrecto de Ti/Tv

Vimos en el caso anterior que el \(script\) simplemente ignora que se le pase \(-t\ valor\) cuando se va a correr con proteínas. Pero en el caso de que el usuario corra con secuencias de DNA, entonces sí se debe revisar que el usuario provea de un valor razonable a la opción \(-t\). También revisa que no se pase \(-t\) cuando los modelos especificados por el usuario son Jukes-Cantor o *LogDet.

Esto lo hace con el siguiente código:


# check that Ti/Tv is a real number, integer or decimal
re_TiTv='^[0-9.]+$'
if [[ ! "$TiTv" =~ $re_TiTv ]] && [ "$runmode" -eq 1 ]
then
   echo
   echo "# ERROR: TiTv:$TiTv is not an integer >= 0; provide a value between 0-10" >&2 
   echo
   print_help
   echo
   exit 3
elif [[ "$TiTv" =~ $re_TiTv ]] && [ "$runmode" -eq 1 ] && { [ "$model" == Jukes-Cantor ] || [ "$model" == LogDet ]; }
then
       echo
       echo "# ERROR: $model is only valid when analyzing DNA alignments under the F84 or K2P models" >&2
       echo
       print_help
       exit 1    
fi
  • ejemplo de llamada incorrecta al \(script\): en modo -R 1 se revisa que el argumento pasado a la opción \(-t\) sea correcto ./run_phylip.sh -i primates.phy -R 1 -g 0.3 -t ttt
# ERROR: TiTv:ttt is not an integer >= 0; provide a value between 0 and 10
...

15.2.9 Llamada a run_phylip para construir filogenia NJ de proteínas bajo con número de réplicas <0, 0 < repl < 100, o > 1000 o pasando números decimales o caracteres no numéricos.

El \(script\) hace uso del siguiente código para asegurarse que el usuario sólo le pasa íntegros positivos a la opción \(-b\)


# check that bootstrap value is an integer
re='^[0-9]+$'
if [[ ! "$boot" =~ $re ]]
then
   echo
   echo "# ERROR: boot:$boot is not a positive integer >= 0; provide a value between 100 and 1000" >&2 
   echo
   print_help
   echo
   exit 3
fi

Veamos algunos ejemplos

  • ./run_phylip.sh -i GDP_12_prokEuc.phy -R 2 -t 2.3 -b -1000
# ERROR: boot:-1000 is not a positive integer >= 0; provide a value between 100 and 1000
...
  • ./run_phylip.sh -i GDP_12_prokEuc.phy -R 2 -t 2.3 -b 10
# WARNING: bootstrap value=10 is a bit low. Use a value >= 100.
# Are you sure you want to continue anyway? Type Y|N n

# Ok, will exit then!
  • ./run_phylip.sh -i GDP_12_prokEuc.phy -R 2 -t 2.3 -b 10000
# WARNING: bootstrap value=10000 is > 1000. This may take a long time to run.
# Are you sure you want to continue anyway? Type Y|N N

# Ok, will exit then!

15.2.10 Verificación de que el modelo o matriz pasados por el usuario al \(script\) con \(-m\ modelo\) son los que puede usar PHYLIP

Los “dedazos” son comunes al teclear texto. El \(script\) revisa que los nombres de los modelos o matrices recibidos sean los correctos.

./run_phylip.sh -i GDP_12_prokEuc.phy -R 2 -m PBM # PBM en vez de PMB

# ERROR: PBM is not a recognized substitution matrix for protein sequences used by PHYLIP

./run_phylip.sh -i primates.phy -R 1 -g 0.2 -m K2P

# ERROR: K2P is not a recognized substitution model for DNA sequences used by PHYLIP

15.2.11 Comprobación de resultados

Dado que run_phylip.sh sólo hace análisis de matrices de distancias, no puede obtener estimas de los valores adecuados de los parámetros de los modelos de sustitución. Para ello hay que usar el criterio de optimización de máxima verosimilitud. Por tanto el usuario debe pasas estos valores al programa, en base a su experiencia o conocimiento previo. A veces ciertas combinaciones de valores de los parámetros, particularmente alpha y ti/tv pueden estar tan desviados de los que se ajustan a los datos, que puede hacer imposible la estima de las distancias. PHYLIP automáticamente da valores de -1.000 a distancias que no pudo estimar. Por ello, es importante revisar las salidas del programa:

  1. Comprobar que se hayan producido los archivos esperados después de la llamada de cada programa de PHYLIP
  2. Comprobar que la matriz de distancias no contenga valores negativos
  3. Comprobar que los árboles resultantes no tengan ramas negativas

./run_phylip.sh -i primates.phy -R 1 -g 0.2 -t 70.3

### run_phylip.sh v.2.0 run on 2020_11_21-11.16.04 with the following parameters:
# work_directory=/home/vinuesa/cursos/intro2linux
# input_phylip=primates.phy
# model=F84 | gamma=0.2 | CV=2.2361
 | gammaf=02 | ti/tv ratio=70.3 |
     outgroup=1 | UPGMA=0 | bootstrap no.=100 | ROI=21609
# runmode=1
# DEBUG=0
# command: run_phylip.sh -i primates.phy -R 1 -g 0.2 -t 70.3


# Run check_dependencies() ... looks good: all required external dependencies are in place!
-------------------------------------------------------------------------------------------
# File validation OK: primates.phy seems to be a standard phylip file ...
-------------------------------------------------------------------------------------------

>>> Computing distance matrix for primates.phy ...
# running write_dnadist_params F84 0 70.3 0 0.2 2.2361

# running dnadist < dnadist.params
primates_F8402gamma_distMat.out
ERROR: computed negative distances!
You may need to ajust the model and|or gamma value; try lowering TiTv if > 6

Estos son algunos ejemplos de cómo pueden programar una interfaz para que el programa, además de robusto, sea amigable y sencillo de usar para el usuario.

15.2.12 Llamada a run_phylip para construir filogenia NJ de CDSs (DNA) con valores por defecto

Veamos la salida de la siguiente llamada al \(script\) usando el archivo clásico de secuencias nucleotídicas (gen mitocondrial COI) de primates (primates.phy; usado por Hasayaka et al. (1988). Molecular phylogeny and evolution of primate mitochondrial DNA. Mol. Biol. Evol. 5:626-644) y opciones por defecto.

./run_phylip.sh -i primates.phy -R 1


### run_phylip.sh v.2.0 run on 2020_11_21-11.32.02 with the following parameters:
# work_directory=/home/vinuesa/cursos/intro2linux
# input_phylip=primates.phy
# model=F84 | gamma=0.0 | CV=1 | gammaf=00 | ti/tv ratio=2 |
     outgroup=1 | UPGMA=0 | bootstrap no.=100 | ROI=22589
# runmode=1
# DEBUG=0
# command: run_phylip.sh -i primates.phy -R 1


# Run check_dependencies() ... looks good: all required external dependencies are in place!
-------------------------------------------------------------------------------------------
# File validation OK: primates.phy seems to be a standard phylip file ...
-------------------------------------------------------------------------------------------

>>> Computing distance matrix for primates.phy ...
# running write_dnadist_params F84 0 2 0 0.0 1
# running dnadist < dnadist.params

>>> Computing distance tree for primates.phy ...
# running write_neighbor_params 0 0
# running neighbor < neighbor.params
# wrote tree primates_F8400gamma_NJ.ph to disk
# > finished computing distance matrix and tree for primates.phy!
==================================================================


>>> Bootstrap Analysis based on 100 pseudoreplicates for primates.phy ...
# running  write_seqboot_params 100 0
# running seqboot < seqboot.params &> /dev/null
# > computing distance matrices on 100 bootstrapped alignments ...
# running write_dnadist_params F84 100 2 0 0.0 1
# running dnadist < dnadist.params &> /dev/null
# > Computing distance trees from bootstrapped data ...
# running write_neighbor_params 100 0 1
# running neighbor < neighbor.params &> /dev/null
# > Computing MJR consensus tree from trees reconstructed from bootstrap pseudoreplicates ...
# running write_consense_params
# running consense < consense.params &> /dev/null
# mapping bootstrap values on NJ tree with nw_support ...
# displaying primates_F8400gamma_NJ_with_100boot_support.ph ...

 +--------------------------------+ Lemur catt                                  
 |                                                                              
 |                     +-----------------------------------------+ Saimiri sc   
 |                     |                                                        
 |                     |                                     +--+ Macaca fus    
 |                     |                                  +-99                  
 |                     |                             +-97-+  +--+ M mulatta     
 +-100-----------------+                             |    |                     
 |                     |        +-100----------------+    +---------+ M fascicul
 |                     |        |                    |                          
 |                     |        |                    +-----------+ M sylvanus   
=| 100                 +-96-----+                                               
 |                              |         +--------------------+ Hylobates      
 |                              |         |                                     
 |                              +-100-----+   +------------------+ Pongo        
 |                                        |   |                                 
 |                                        +-97+         +-------+ Homo sapie    
 |                                            |       +-87                      
 |                                            +-100---+ +---------+ Pan         
 |                                                    |                         
 |                                                    +---------+ Gorilla       
 |                                                                              
 +------------------------------------------+ Tarsius sy                        
                                                                                
 |----------------|-----------------|----------------|---------------           
 0              0.1               0.2              0.3                          
 substitutions/site                                                             
                                                                                


===================== OUTPUT SUMMARY =====================
# 7 output files were generated:
primates_F8400gamma_distMat.out
primates_F8400gamma_NJ.ph
primates_F8400gamma_NJ.outfile
primates_F8400gamma_100bootRepl_trees.nwk
primates_NJconsensus_F8400gamma_100bootRepl.ph
primates_NJconsensus_F8400gamma_100bootRepl.outfile
primates_F8400gamma_NJ_with_100boot_support.ph

# FINISHED run at: sáb 21 nov 2020 11:32:03 CST
   ==> Exiting now ...

15.2.13 Llamada a run_phylip para construir filogenia NJ de CDSs (DNA) con valores extremos de gamma y ti/tv

Veamos la salida de la siguiente llamada al \(script\), al que pasamos valores un tanto extremos de -g 0.2 y -t 10

./run_phylip.sh -i primates.phy -R 1 -t 10 -g 0.2 -m F84 -b 1000

### run_phylip.sh v.2.0 run on 2020_11_21-11.33.10 with the following parameters:
# work_directory=/home/vinuesa/cursos/intro2linux
# input_phylip=primates.phy
# model=F84 | gamma=0.2 | CV=2.2361
 | gammaf=02 | ti/tv ratio=10 |
     outgroup=1 | UPGMA=0 | bootstrap no.=1000 | ROI=6101
# runmode=1
# DEBUG=1
# command: run_phylip.sh -i primates.phy -R 1 -t 10 -g 0.2 -m F84 -b 1000 -D


# Run check_dependencies() ... looks good: all required external dependencies are in place!
-------------------------------------------------------------------------------------------
# File validation OK: primates.phy seems to be a standard phylip file ...
-------------------------------------------------------------------------------------------

>>> Computing distance matrix for primates.phy ...
# running write_dnadist_params F84 0 10 0 0.2 2.2361

# running dnadist < dnadist.params
primates_F8402gamma_distMat.out
ERROR: computed negative distances!
You may need to ajust the model and|or gamma value; try lowering TiTv if > 6

  • Error de ajuste de parámetros
    • la función check_matrix nos avisa que se calcularon distancias negativas! Nos indica que probemos bajar el valor de Ti/Tv

Probemos ahora con valores más razonables (menos extremos) de gamma y ti/tv

15.2.14 Llamada a run_phylip para construir filogenia NJ de CDSs (DNA) con 1000 réplicas de bootstrap y modelo F84+G (alpha=0.3) y tasa de ti/tv = 5.5

Veamos la salida de la siguiente llamada al \(script\), al que activamos la opción -D para que no borre los archivos de parámetros, los cuales desplegaremos también

./run_phylip.sh -i primates.phy -R 1 -t 5.5 -g 0.3 -m F84 -b 1000 -D

### run_phylip.sh v.2.0 run on 2020_11_21-11.37.25 with the following parameters:
# work_directory=/home/vinuesa/cursos/intro2linux
# input_phylip=primates.phy
# model=F84 | gamma=0.3 | CV=1.8257
 | gammaf=03 | ti/tv ratio=5.5 |
     outgroup=1 | UPGMA=0 | bootstrap no.=1000 | ROI=17167
# runmode=1
# DEBUG=1
# command: run_phylip.sh -i primates.phy -R 1 -t 5.5 -g 0.3 -m F84 -b 1000 -D


# Run check_dependencies() ... looks good: all required external dependencies are in place!
-------------------------------------------------------------------------------------------
# File validation OK: primates.phy seems to be a standard phylip file ...
-------------------------------------------------------------------------------------------

>>> Computing distance matrix for primates.phy ...
# running write_dnadist_params F84 0 5.5 0 0.3 1.8257

# running dnadist < dnadist.params

>>> Computing distance tree for primates.phy ...
# running write_neighbor_params 0 0
# running neighbor < neighbor.params
# wrote tree primates_F8403gamma_NJ.ph to disk
# > finished computing distance matrix and tree for primates.phy!
==================================================================


>>> Bootstrap Analysis based on 1000 pseudoreplicates for primates.phy ...
# running  write_seqboot_params 1000 0
# running seqboot < seqboot.params &> /dev/null
# > computing distance matrices on 1000 bootstrapped alignments ...
# running write_dnadist_params F84 1000 5.5 0 0.3 1.8257

# running dnadist < dnadist.params &> /dev/null
# > Computing distance trees from bootstrapped data ...
# running write_neighbor_params 1000 0 1
# running neighbor < neighbor.params &> /dev/null
# > Computing MJR consensus tree from trees reconstructed from bootstrap pseudoreplicates ...
# running write_consense_params
# running consense < consense.params &> /dev/null
# mapping bootstrap values on NJ tree with nw_support ...
# displaying primates_F8403gamma_NJ_with_1000boot_support.ph ...

 +---------------------------+ Lemur catt                                       
 |                                                                              
 |                                +---------------------------------+ Saimiri sc
 |                                |                                             
 |                                |                           ++ Macaca fus     
 |                                |                           | 709             
 |                                |                         +-822 M mulatta     
 +-1000---------------------------+                         | |                 
 |                                |        +-1000-----------+ +---+ M fascicul  
 |                                |        |                |                   
 |                                |        |                +---+ M sylvanus    
=| 1000                           +-886----+                                    
 |                                         |         +---------+ Hylobates      
 |                                         |         |                          
 |                                         +-995-----+  +---------+ Pongo       
 |                                                   |  |                       
 |                                                   +-923    +--+ Homo sapie   
 |                                                      |    ++855              
 |                                                      +-992++--+ Pan          
 |                                                           |                  
 |                                                           +--+ Gorilla       
 |                                                                              
 +-------------------------------------+ Tarsius sy                             
                                                                                
 |------------|-----------|------------|------------|------------|---           
 0         0.25         0.5         0.75            1         1.25              
 substitutions/site                                                             
                                                                                


===================== OUTPUT SUMMARY =====================
# 7 output files were generated:
primates_F8403gamma_distMat.out
primates_F8403gamma_NJ.ph
primates_F8403gamma_NJ.outfile
primates_F8403gamma_1000bootRepl_trees.nwk
primates_NJconsensus_F8403gamma_1000bootRepl.ph
primates_NJconsensus_F8403gamma_1000bootRepl.outfile
primates_F8403gamma_NJ_with_1000boot_support.ph

# FINISHED run at: sáb 21 nov 2020 11:37:39 CST
   ==> Exiting now ...
  • Comparen los árboles de ambas salidas. ¿Qué diferencias notan?
    • comparen las escalas
    • comparen las longitudes de rama en el clado de los homínidos

15.2.15 Despliegue de árboles cuando no están disponibles los binarios nw_support y nw_display del paquete Newick utilities

en el bloque final de cada sección, el \(script\) comprueba si están disponibles nw_support y nw_display, como se muestra abajo para el bloque de corrida NJ+Bootstrap

# if we requested bootstrapping, map bootstrap values onto NJ tree using
#   nw_support NJ.ph bootRepl_tree.ph > NJ_with_boot_support.ph
if [ -s "$nj_tree" ] && [ -s "$boot_trees" ] && [[ $(type -P nw_support) ]] && [ "$nw_utils_ok" -eq 1 ]
then
    nj_tree_with_boot="${input_phylip%.*}_${model}${gammaf}gamma_NJ_with_${boot}boot_support.ph"
      echo "# mapping bootstrap values on NJ tree with nw_support ..."
      nw_support "$nj_tree" "$boot_trees" > "$nj_tree_with_boot"
     
      check_output "$nj_tree_with_boot"
      if [ -s "$nj_tree_with_boot" ] && [[ $(type -P nw_display) ]] && [ "$nw_utils_ok" -eq 1 ]
      then
          outfiles+=("$nj_tree_with_boot")
             
            # check that there are no negative branch lengths in the nj_tree 
            #   before displaying with nw_display
            display_treeOK "$nj_tree_with_boot"
      fi
elif [ -s "$nj_outfile" ] && [ -s "$nj_consensus_outfile" ] && { [[ ! $(type -P nw_support) ]] || [ "$nw_utils_ok" -ne 1 ]; }
then
    echo "# extract_tree_from_outfile $nj_outfile"
      echo
      extract_tree_from_outfile "$nj_outfile"
        echo

        echo "# extract_tree_from_outfile $nj_consensus_outfile"
        echo
        extract_tree_from_outfile "$nj_consensus_outfile"
        echo
fi

Si falla [[ \((type -P nw_display) ]] && [ "\)nw_utils_ok” -eq 1 ], se pasa a probar:

elif [ -s “\(nj_outfile" ] && [ -s "\)nj_consensus_outfile” ] && { [[ ! \((type -P nw_support) ]] || [ "\)nw_utils_ok” -ne 1 ]; }

en cuyo caso se llama a la función \(extract\_tree\_from\_outfile\) para extraer los árboles que PHYLIP escribe a los archivos outfile generados por los comandos \(neighbor\) y \(consense\), los cuales son desplegados a pantalla, como se muesra abajo:

# running consense < consense.params &> /dev/null
# extract_tree_from_outfile primates_F84035gamma_NJ.outfile

  +------------------------Lemur_catt
  !                          +------------------------------Saimiri_sc
  !                          !                        +-2 
  1--------------------------5                      +-3 +-M_mulatta 
  !                          !      +---------------4 +----M_fascicul
  !                          !      !               +----M_sylvanus
  !                          +------6 
  !                                 !        +----------Hylobates 
  !                                 +--------7   +--------Pongo     
  !                                          +---8     +--Homo_sapie
  !                                              !   +-9 
  !                                              +--10 +---Pan       
  !                                                  +---Gorilla   
  +---------------------------------Tarsius_sy

# extract_tree_from_outfile primates_NJconsensus_F84035gamma_100bootRepl.outfile

                                                  +---------------M fascicul
                                          +--87.0-|
                                          |       |       +-------Macaca fus
                          +---------100.0-|       +--83.0-|
                          |               |               +-------M mulatta
                          |               |
                          |               +-----------------------M sylvanus
                          |
                  +--89.0-|                               +-------Homo sapie
                  |       |                       +--89.0-|
                  |       |               +-100.0-|       +-------Pan
                  |       |               |       |
                  |       |       +--93.0-|       +---------------Gorilla
          +-100.0-|       |       |       |
          |       |       +--99.0-|       +-----------------------Pongo
          |       |               |
  +-------|       |               +-------------------------------Hylobates
  |       |       |
  |       |       +-----------------------------------------------Saimiri sc
  |       |
  |       +-------------------------------------------------------Tarsius sy
  |
  +---------------------------------------------------------------Lemur catt

15.2.16 Los archivos de parámetros

Si quieren ver los parámetros pasados a los programas del paquete PHYLIP, deberán correr con el flag -D activado, como hicimos en la última llamada al \(script\).

Ello le indica que no borre los archivos de parámetros, los cuales podremos visualizar convenientemente con este simple bucle for.

for f in *params; do echo “#$f”; cat $f; echo ‘——–’; done

#consense.params
Y
--------
#dnadist.params
T
6
G
Y
1.8257
--------
#neighbor.params
Y
--------
#seqboot.params
R
1000
Y
123

16 Consideraciones finales y referencias recomendadas para continuar aprendiendo programación \(Shell\)

Llegamos al final de este tutorial, ¡¡¡enhorabuena!!! Has aprendido mucho, pero lo bueno es que todavía queda un largo camino por andar.

\(Bash\) sin duda es muy útil si vas a usarlo como lenguaje pegamento para conectar múltiples utilerías o programas como demuestra el \(script\) run_phylp.sh que presentamos en la sección anterior, pero no es el apropiado para escribir programas complejos que tienen que hacer procesamiento numérico, gráfico, estadístico de datos, interactuar con bases de datos, parsear archivos de estructura compleja, etc. Para ello deberás aprender otros lenguajes más poderosos, capaces de manejar estructuras de datos complejas (multidimensionales como hashes de hashes, arreglos de hashes, etc.) y que disponen de repositorios gigantescos de librerías o paquetes especializados de código para todo lo que puedas imaginar. Pero aprender primero \(Shell\) y \(AWK\) es sin duda lo mejor que puedes hacer para iniciarte en la programación en sistemas UNIX o GNU/Linux. Te facilitará el camino posterior y te permitirá dominar el sistema operativo, ya que el \(Shell\) es su interfaz programática.

Sabiendo \(Bash\) y \(AWK\) te será muy fácil entender \(Perl\), que con sus 42,000 paquetes y 197,000 módulos en el repositorio CPAN, sería una excelente elección como siguiente lenguaje a aprender. Sin duda Python y R hay que añadirlos también a la lista de lenguajes interpretados a aprender.

Recomiendo las siguientes guías para apoyarte en tu proceso de aprendizaje del \(Shell\). Disfruta el camino, saludos!