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 alumn@s 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.
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.
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
This
work is licensed under a
Creative
Commons Attribution-NonCommercial 4.0
El código se distribuye bajo la licencia
GNU
General Public License v3
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
Tienes básicamente 2 opciones:
git
pull
, como se explica en el REAEME del
repositorioLa 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.
Bienvenid@s 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
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.
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”.
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:
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:
# cambia a tu home, y luego a intro2linux/sesion1
cd && cd intro2linux/sesion1
## /home/vinuesa/intro2linux/sesion1
cp /home/vinuesa/intro2linux/data/linux_basic_commands.tab . # <<< vean el punto, significa, dir actual
cp /home/vinuesa/intro2linux/data/linux_basic_commands.tab $HOME/intro2linux/sesion1/
cp /home/vinuesa/intro2linux/data/linux_basic_commands.tab ~/intro2linux/sesion1/
cp ~/tmp/working_with_linux_commands.html ~/cursos/docs/index.html
# Noten el punto '.' y cp -r (recursively), necesario para copiar directorios completos
cp -r /home/vinuesa/intro2linux/data/ .
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
Para llevar/copiar un archivo de tu máquina local al servidor, usamos el comando \(scp\)
Sintaxis: scp ARCHIVO_LOCAL USUARIO@ip.maquina:/home/USUARIO/ruta/al/dir/destino/
# 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/
scp
-r dir USUARIO@ip.maquina:/home/USUARIO/ruta/al/dir/destino/
scp -r dir destino
sólo si \(dir\) contiene pocos archivos
pequeños.Para traer/copiar un archivo del servidor al directorio actual en la máquina local, usamos el comando \(sftp\)
Sintaxis: sftp USUARIO@ip.maquina:/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
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 trabajoln -s /path/to/file nombre_nuevo
# que genera una liga
simbólica a /path/to/file, llamada nombre_nuevo, en el directorio de
trabajoln -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_fileshostn=$(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
mv comandos_de_linux.tab linux_commands.tab
ln -s nombre_largo_de_archivo.tgz arch_tmp.tgz
ln -s /export/data3/illumina_reads/run_stenos_2020-10-14/*fastq.gz .
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.
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
# 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
# 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 ;)
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
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.
Los comandos \(head\), \(tail\), \(cat\), \(less\) y \(more\) son algunos de los disponibles para visualizar el contenido de un archivo ASCII.
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
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 -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
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
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
El paginador \(less\) es el más poderoso (otra alternativa ess \(more\), pero extrañamente: \(less\) is more!)
less archivo
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
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 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
zless assembly_summary.txt.gz | head -5 | cut -f1-5
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 &
\(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
$ 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\)
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
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
head -1 linux_commands.tab | sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/'
## IEEE STD 1003.1-2008 UTILITIES NAME CATEGORY DESCRIPTION FIRST APPEARED
head -1 linux_commands.tab | sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/; s/ /_/g'
## IEEE_STD_1003.1-2008_UTILITIES_NAME_ CATEGORY_ DESCRIPTION_ FIRST_APPEARED
$ 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
$ sed '/^$/d' archivo
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
Esta fue una presentación minimalista de \(sed\), que no le hace justicia. Les recomiendo mucho el siguiente tutorial para profundizar:
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.
Para imprimir texto simple a la consola o archivo usa el comando \(echo\)
echo introduzca nombre de usuario:
## introduzca nombre de usuario:
echo vea como se ignoran múltiples espacios, colapsándolos a uno!
## vea como se ignoran múltiples espacios, colapsándolos a uno!
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
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
Siempre puedes consultar las opciones de echo con el comando \(man\ echo\). Pero te muestro algunas de las más comunes:
echo -e "texto\tseparado\tpor\ttabuladores"
## texto separado por tabuladores
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í!
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!
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
cat tmp1.txt
## L1
## L2\L3
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
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 ...
\(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
## ******************** *********************** ******** ******
echo -e "L1\nL2\L3" > tmp1.txt
cat tmp1.txt
## L1
## L2\L3
echo -e "L4\nL5\L6" > tmp1.txt
cat tmp1.txt
## L4
## L5\L6
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
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
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
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
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
# 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
# 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
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.
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\)
gzip
file
o también gzip *.gbk
gunzip
file.gz
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\).
tar -czf
tarro_de_archivos_genbank_comprimidos.tgz *.gbk
tar -cjvf
tarro_de_archivos_genbank_comprimidos.tar.bz2 *.gbk
tar -czvf tarro_de_directorio.tgz
mi_directorio
tar -xzvf
tarro_de_directorio.tgz
tar -tzvf tarro_de_directorio.tgz
Resumen de las opciones básicas de \(tar\):
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:
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
## ----------------------------
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
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
## ----------------------------
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
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
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\).
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\)).
La sintaxis es muy sencilla:
locate [OPTION]… PATTERN…
Donde las opciones que más uso son:
locate -c -r ‘.odp$’
1159
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.
>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\).
find ~/bin
-type d | wc -l
1316
find
~/bin -type f | wc -l
6300
Siguen unas tablas con algunos de los tests más importantes
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 |
Tipo de archivo | Descripción |
---|---|
b | ispositivos orientados a bloques |
c | dispositivos orientados a caracteres |
d | directorio |
f | archivo regular |
l | liga simbólica |
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
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
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}
find borrame/ -type f -name 'arch-A' | wc -l
## 100
find borrame/ -type f -name 'arch-A' -delete
find borrame/ -type f -name 'arch-A' | wc -l
## 0
-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
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
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
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*'
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.
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% /
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
\(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.
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 .
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?
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
...
time du -shc $HOME 2> /dev/null
408G /home/vinuesa
real 0m32.409s
user 0m1.840s
sys 0m8.524s
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
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:
ls /bin | wc -l
## 3424
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.
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.
grep -E
‘^XXX|YYY|zzz$’ FILE
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
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
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
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
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
cut -d’ ’
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
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.
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
\(wc\) es un comando muy sencillo
que cuenta líneas, palabras y caracteres de un archivo o del
El comando \(wcq\) suele ir al final de un pipeline para generar un resumen numérico.
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
SYNOPSIS uniq [OPTION]... [INPUT [OUTPUT]]
\(uniq\) filtra líneas adyacentes
iguales en INPUT (o
Por ello, para usar \(uniq\) efectivamente, necesitamos que \(sort\) ordene previamente las entradas.
\(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.
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
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
echo -e "Pseudo\nSteno\nEsch\nSteno\nKleb\nSalmo\nKleb\nKleb" | sort | uniq | nl
## 1 Esch
## 2 Kleb
## 3 Pseudo
## 4 Salmo
## 5 Steno
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
echo -e "Pseudo\nSteno\nEsch\nSteno\nKleb\nSalmo\nKleb\nKleb" | sort | uniq -D
## Kleb
## Kleb
## Kleb
## Steno
## Steno
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?
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?
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?
# 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
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!
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?
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
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
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
zgrep ‘\[Pseudomonas\]’ assembly_summary.txt.gz |
less -L
y tomar la decisión. Van dos soluciones de código para
cada caso:
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
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
# 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.
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.
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 (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).
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.
\(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.
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
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 ‘>’
Veamos un primer ejemplo de código AWK que ilustra lo arriba descrito en relación a las variables \(FILENAME\), \(RS\), \(FNR\) y \(NR\).
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
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.
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 $.
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:
{print NF,”[“$1”]“}
./patrón/ { acción }
, donde:
{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”]”}’\(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.
awk ‘CODIGO AWK’ ARCHIVO_A_PROCESAR
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
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.
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\).
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
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
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
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
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
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
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
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
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
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.
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.
La siguiente lista presenta algunos de los elementos y estructuras sintácticas fundamentales del lenguaje de programación \(gawk\)
if(condicion1){code1}else
if(condición2){code2}else{code3}
for (i in array){code}
;
for(initialization;condition;increment|decrement)
while(true){code}
+, -, *, /, %, =, ++,
–, +=, -=, …)
||,
&&
<, <=, == !=,
>=, >
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])
function
FUNNAME (arg1, arg1){code}
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.
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)
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:
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 cadenaawk ‘$1 ~ “/regex/” { print $2 }’ archivo
# imprime
campo dos si campo uno concuerda con regexawk ‘$1 !~ “/regex/” { print $2 }’ archivo
# imprime
campo dos si campo uno NO concuerda con regexawk ‘/cadena/ { print $0 }’ archivo
# imprime toda la
línea si concuerda con cadenaawk ‘/cadena1/ && !/cadena2/ { print $0 }’
archivo
# imprime toda la línea si concuerda con cadena1 y no con
cadena2Notació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 |
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 |
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 |
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
\(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(cadena, regexp [,
array])
/regexp/, “reemplazo”, [, diana]
/regexp/, “reemplazo”, [,
diana]
cadena, inicio, [,
longitud]
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)
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
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.
awk 'BEGIN { seq = "CCATCCATGCCGTGCTAA"; idx = index(seq, "ATG"); print idx}'
## 7
echo "CCATCCATGCCGTGCTAA" | awk '{ idx = index($0, "ATG"); gene = substr($0, idx); print idx, "\n"$0, "\n " gene }'
## 7
## CCATCCATGCCGTGCTAA
## ATGCCGTGCTAA
Notas:
index($0, /ATG/)
evalúa como error fatal.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]
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.
\(AWK\) implementa muchas más funciones para manipulación de cadenas de caracteres como las siguientes
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.
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\).
# 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
# Compara con la salida del bloque anterior
echo 'atattGAATTCTAGCACATACTAACGGACC' | wc -c
echo -n 'atattGAATTCTAGCACATACTAACGGACC' | wc -c
## 31
## 30
De este ejemplo debemos aprender que:
# 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
awk 'NF > 0' sequences.tmp | nl
## 1 >seq1
## 2 ATGCATGC
## 3 >seq2
## 4 TATTACCAG
## 5 >seq3
## 6 ATTATTGGC
awk 'NR <= 6' sequences.tmp | cat -n
## 1 >seq1
## 2 ATGCATGC
## 3
## 4 >seq2
## 5 TATTACCAG
## 6
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 &&).
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
# 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
# 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
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
# 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
# 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
# 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
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
# 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
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
# 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
# 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.
awk '/^>/ {s++} END{print FILENAME, "has", s, "sequences"}' recA_Bradyrhizobium_vinuesa.fna
## recA_Bradyrhizobium_vinuesa.fna has 125 sequences
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
Para este ejercicio necesitamos eliminar las líneas que contienen las cabeceras FASTA.
awk '! /(^>|^$)/' recA_Bradyrhizobium_vinuesa.fna | head -15
## ATGAAGCTCGGCAAGAACGACCGGTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTCGGGCTCGACA
## TCGCGCTCGGCATCGGCGGCCTGCCCAAGGGGCGTATCGTCGAGATCTACGGGCCGGAATCCTCGGGCAA
## GACCACGCTGGCGCTGCATACGGTGGCGGAAGCGCAGAAGAAGGGCGGCATCTGCGCCTTCATCGACGCC
## GAGCACGCGCTCGACCCGGTCTATGCCCGCAAGCTCGGCGTCAACATCGACGAGCTCCTGATCTCGCAGC
## CCGACACCGGCGAGCAGGCGCTGGAGATCTGCGACACGCTGGTGCGCTCGGGCGCTGTCGATGTGCTGGT
## GATCGACTCGGTTGCGGCGCTGGTGCCGAAGGCCGAGCTCGAAGGCGAGATGGGCGATGCGCTGCCAGGC
## TTGCAGGCCCGTCTGATGAGCCAGGCGCTGCGCAAGCTGACGGCCTCCATCAACAAGTCCAACACCATGG
## TGATCTTCATCAACCAGATC
## ATGAAGCTCGGCAAGAACGACCGGTCCATGGACATCGAGGCGGTGTCCTCCGGCTCGCTCGGGCTCGACA
## TCGCGCTCGGCATCGGCGGCCTGCCCAAGGGGCGTATCGTCGAGATCTACGGGCCGGAATCCTCGGGCAA
## GACCACGCTGGCGCTGCATACGGTGGCGGAAGCGCAGAAGAAGGGCGGCATCTGCGCCTTCATCGACGCC
## GAGCACGCGCTCGACCCGGTCTATGCCCGCAAGCTCGGCGTCAACATCGACGAGCTCCTGATCTCGCAGC
## CCGACACCGGCGAGCAGGCGCTGGAGATCTGCGACACGCTGGTGCGCTCGGGCGCTGTCGATGTGCTGGT
## GATCGATTCGGTTGCGGCGCTGGTGCCGAAGGCCGAGCTCGAAGGCGAGATGGGCGATGCGCTGCCGGGC
## TTGCAGGCCCGTCTGATGAGCCAGGCGCTGCGCAAGCTGACGGCCTCCATCAACAAGTCCAACACCATGG
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.
awk '! /(^>|^$)/' recA_Bradyrhizobium_vinuesa.fna | tr --delete '\n$' | wc -c
## 63795
awk '! /(^>|^$)/ {s=s$1} END{printf "%s", s}' recA_Bradyrhizobium_vinuesa.fna | wc -c
## 63795
awk '! /(^>|^$)/ {s=s$1} END{print s}' recA_Bradyrhizobium_vinuesa.fna | wc -c
## 63796
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
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.
# 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\)
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
# 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
# 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
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
# 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
# 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
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:
awk ‘programa’ var=VALOR
para que no interprete al
argumento como un nombre de archivoal[$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.
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:
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
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:
awk ‘programa’ tax=TAXON
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
$8 ~ tax {gsub(//.*$/, ““) … }
?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
En el código que se muestra abajo hacemos lo siguiente:
# 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
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.
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
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
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
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.
k[$1]++
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
k[$1]++
de todos los archivos recibidos como
argumentosseqs[h]=seqs[h]s
?if(k[h] == ARGC-1) { print “>”h,
seqs[h] }
? awk ‘cógigo’ mini_fasta*fst
,
lo cual es idealls 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.
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
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 34el código mostrado arriba imprime su tra[n]spuesta como:
11 21 31 12 22 32 13 23 33 14 24 34
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.
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.
\(awk\) define automáticamente las variables ARGC y ARGV para proveer al programa información sobre los argumentos pasados al mismo.
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
Veamos el siguiente ejemplo muy similar al anterior, pero pasándole al \(script\) variables y argumentos, además de los dos archivos.
awk –posix -F”\t” -v var=valor ‘código’ var2=valor2 archivo1
archivo2
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]
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"]"
}
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] []
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]
Veamos el contenido del \(script\) showargs.awk que consta de 3 secciones:
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 }
cat mini_fasta.fst
## >especie12
## ATACCGACCATTAC
## TTAGGAACCCAGGC
## >especie2
## CCAGTAGTCGAGGC
## AGCAGCTTCCATAT
## >especie3
## CCAGGGCCCATATT
## ATACCGACCATTAC
## >especie4
## GCATAATCCACCAT
## GCACGAATGCAGAC
## >especie5
## GGGGTACCCATTTA
## CCCCCCCTTTTTAT
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
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
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
gsub(/n=/, ““, arg);
?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:
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.
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
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
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
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
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
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
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.
[ -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
[ -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;
## }
# 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
# 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
cadena=BES-1
como vimos en el
script count_genome_features_for_taxon.awk?# 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
# 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
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
>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.
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.
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
## }
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
if ( ARGC < 2 )
Usage_Exit(progname, version)
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
echo atggggtgttgtgggttgAAAGTGcccgggaaattaataCAGt | ./translate_dna.awk ""
ERROR: input DNA sequence not divisible by 3 ...
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.
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.
function my_function_name([param1, param2, local_var1, local_var2 …]) {
body-of-function }
my_function_name([param1, param2])
return
para que una función
regrese un valor escalar determinado.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)
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
En esta sección les presentaré dos scripts más largos y complejos, que servirán para:
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.
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:
Cada una de estas funcionalidades se define mediante un
“runmode” numérico que llamaremos con la opción
-R
.
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í.
./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
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
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
#---------------------------------------------------------------------------------------------------------#
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 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
}
#---------------------------------------------------------------------------------------------------------
Este es un bloque BEGIN{} bastante más largo que otros mostrados en ejemplos anteriores.
PROCINFO[“sorted_in”] = “@ind_num_asc”
para que los fastas
almacenados en hashes salgan ordenados por su índice numéricowhile ((c = getopt(ARGC, ARGV,
“dm:e:R:s:”)) != -1) { … }
.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%"
}
}
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
# -------------------- #
# >>> 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]) }
}
}
Usaremos para los ejemplos el archivo mini_CDS.fna
cat mini_CDS.fna
## >mini_CDS1
## atggggtgttgtgggttgAAAGTGcccgggaaattaataCAGCCG
## >mini_CDS2
## GTGcggtgttgtgggttgATGGTGcccgggaaattaaaaTTGTAA
./fasta_toolkit.awk -R 1 -m CDS2 mini_CDS.fna
## >mini_CDS2
## GTGcggtgttgtgggttgATGGTGcccgggaaattaaaaTTGTAA
./fasta_toolkit.awk -R 2 mini_CDS.fna
## >mini_CDS1
## CGGCTGTATTAATTTCCCGGGCACTTTCAACCCACAACACCCCAT
## >mini_CDS2
## TTACAATTTTAATTTCCCGGGCACCATCAACCCACAACACCGCAC
./fasta_toolkit.awk -R 3 -s 4 -e 12 mini_CDS.fna
## >mini_CDS1
## gggtgttgt
## >mini_CDS2
## cggtgttgt
./fasta_toolkit.awk -R 4 mini_CDS.fna
## >mini_CDS1
## MGCCGLKVPGKLIQP
## >mini_CDS2
## VRCCGLMVPGKLKL*
./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
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
El script lee un archivo GenBank genera varios archivos a partir de él:
Revisemos el script extract_CDSs_from_GenBank.awk con cierto detalle, ya que tiene varios aspectos novedosos que no hemos visto anteriormente.
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
#=========================================================================================================
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
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
match($0, /^\s{4,}CDS\s{4,}([[:digit:]]+)\.\.([[:digit:]]+)/, c_arr)
start = c_arr[1]
end = c_arr[2]
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:
if(FNR == line_no+1)
comprueba si estamos en la línea
que sigue a un atributo ‘/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]’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 -----------------------------------#
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_idUna 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:]]’
Este ejercicio está basado en un capítulo que escribí para el manual de Sistemática Molecular y Bioinformática. Guía práctica
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
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
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
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.
# 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
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]
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
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:
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
Pasemos al siguiente nivel.
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.
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.
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.
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.
${var}
nombre="Juan"
apellido="Aguirre"
n_a="${nombre}_${apellido}"
echo "Nombe y Apellido: $n_a"
## Nombe y Apellido: Juan_Aguirre
# 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
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>
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}
if [ condición ]; then orden1; orden2 …; fi
[ condición ] && setecia1 &&
sentencia2
if [ condición ]; then
orden1
orden2
fi
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
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
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
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
# 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
Esta versión del \(test\), si es exitosa evalúa a 0, como muestra el siguiente código
[[ $(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'
[[ "$OSTYPE" =~ ^linux.* ]] && echo "Excelente, la máquina '$HOSTNAME' corre $OSTYPE! ;)"
## Excelente, la máquina 'alisio' corre linux-gnu! ;)
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 :)
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
echo $((3+4.1))
bash: 3+4.1: syntax error: invalid arithmetic operator (error token is ".1")
\(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
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
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
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:
# 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>
# 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).
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]
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 ...
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 ...
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
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]
## .....................................
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:
contador=1
while [ $contador -le 5 ]
do
echo $contador
contador=$((contador + 1))
done
## 1
## 2
## 3
## 4
## 5
Un bucle \(while\) puede tomar su
entrada desde
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.
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
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
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.
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:
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.
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
A veces necesitamos agregar un nuevo directorio a la variable de ambiente 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.
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
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.
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
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\):
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
./find_dir 1
## .
## ./tutorials
## ./.git
## ./data
## ./pics
## ./participantes_Taller_CG_FC
## ./intro2linux
## ./bin
## ./tmp
## ./working_with_linux_commands_files
## ./docs
## ./src
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
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.
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: -aalignment 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.
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.
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>]
print_numbered_table_header_fields linux_basic_commands.tab
1 IEEE Std 1003.1-2008 utilities Name
2 Category
3 Description
4 First appeared
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
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\).
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
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:
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.
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:
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
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.
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.
En esta primera sección se definen:
#!/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
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
}
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:
Si no se pasan estos argumentos, el \(script\) muere grácilmente imprimiendo un mensaje de ayuda
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.
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.
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
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
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
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.
./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 ...
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!
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
...
./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]
...
./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.
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
./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
...
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!
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
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:
./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.
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 ...
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
Probemos ahora con valores más razonables (menos extremos) de gamma y ti/tv
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 ...
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
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
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!