Shell Scripts

Oct 29, 2021

Overview

Teaching: 30 min
Exercises: 15 min
Preguntas

  • ¿Cómo puedo guardar y reutilizar comandos?

Objetivos

  • Escribir un script de shell que ejecute un comando o serie de comandos para un conjunto fijo de archivos.

  • Ejecutar un script de shell desde la línea de comandos.

  • Escribir un script de shell que opere sobre un conjunto de archivos definidos por el usuario en la línea de comandos.

  • Crear pipelines que incluyan scripts de shell que usted, y otros, hayan escrito.

Finalmente estamos listos para ver qué es lo que hace del shell un entorno de programación tan poderoso.Vamos a tomar los comandos que repetimos con frecuencia y a guardarlos en archivos para poder volver a ejecutar todas esas operaciones más adelante escribiendo un solo comando.Por razones históricas,un montón de comandos guardados en un archivo suele llamarse script de shell,pero no te equivoques:en realidad son pequeños programas.

Empecemos por volver a molecules/ y crear un nuevo archivo, middle.sh que se convertirá en nuestro shell script:

$ cd molecules$ nano middle.sh

El comando nano middle.sh abre el archivo middle.sh dentro del editor de texto ‘nano'(que se ejecuta dentro del shell).Si el archivo no existe, se creará.Podemos usar el editor de texto para editar directamente el archivo – simplemente insertaremos la siguiente línea:

head -n 15 octane.pdb | tail -n 5

Esta es una variación de la tubería que construimos antes:selecciona las líneas 11-15 del archivo octane.pdb.Recuerda que aún no lo estamos ejecutando como un comando:estamos poniendo los comandos en un archivo.

Luego guardamos el archivo (Ctrl-O en nano), y salimos del editor de texto (Ctrl-X en nano).Comprueba que el directorio molecules contiene ahora un archivo llamado middle.sh.

Una vez que hayamos guardado el archivo, podemos pedirle al shell que ejecute los comandos que contiene.Nuestro shell se llama bash, así que ejecutamos el siguiente comando:

$ bash middle.sh
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00

Ciertamente, la salida de nuestro script es exactamente la que obtendríamos si ejecutáramos ese pipeline directamente.

Texto frente a lo que sea

Normalmente llamamos «editores de texto» a programas como Microsoft Word o LibreOffice Writer, pero tenemos que ser un poco más cuidadosos cuando se trata de programar. Por defecto, Microsoft Word utiliza archivos .docx para almacenar no sólo el texto, sino también la información de formato sobre las fuentes, los encabezados, y pronto. Esta información adicional no se almacena como caracteres, y no significa nada para herramientas como head: esperan que los archivos de entrada no contengan nada más que las letras, los dígitos y la puntuación de un teclado de ordenador estándar. Por lo tanto, al editar programas, debe utilizar un editor de texto sin formato o tener cuidado de guardar los archivos como texto sin formato.

¿Qué pasa si queremos seleccionar líneas de un archivo arbitrario? Podríamos editar middle.sh cada vez para cambiar el nombre del archivo, pero eso probablemente tomaría más tiempo que escribir el comando de nuevo en el shell y ejecutarlo con un nuevo nombre de archivo.En lugar de eso, editemos middle.sh y hagámoslo más versátil:

$ nano middle.sh

Ahora, dentro de «nano», sustituye el texto octane.pdb por la variable especial llamada :

head -n 15 "" | tail -n 5

Dentro de un script de shell, significa ‘el primer nombre de archivo (u otro argumento) en la línea de comandos’.Ahora podemos ejecutar nuestro script así:

$ bash middle.sh octane.pdb
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00

o en un archivo diferente así:

$ bash middle.sh pentane.pdb
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00

Comillas dobles alrededor de los argumentos

Por la misma razón que ponemos la variable de bucle dentro de comillas dobles, en caso de que el nombre del archivo contenga algún espacio, rodeamos con comillas dobles.

Actualmente, tenemos que editar middle.sh cada vez que queremos ajustar el rango de líneas que se devuelve. Arreglemos esto configurando nuestro script para que utilice tres argumentos de línea de comandos. Después del primer argumento de línea de comandos (), cada argumento adicional que proporcionemos será accesible a través de las variables especiales , , , que se refieren al primer, segundo y tercer argumento de línea de comandos, respectivamente.

Sabiendo esto, podemos utilizar argumentos adicionales para definir el rango de líneas a pasar a head y tail respectivamente:

$ nano middle.sh
head -n "" "" | tail -n ""

Ya podemos ejecutar:

$ bash middle.sh pentane.pdb 15 5
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00

Cambiando los argumentos de nuestro comando podemos cambiar el comportamiento de nuestro script:

$ bash middle.sh pentane.pdb 20 5
ATOM 14 H 1 -1.259 1.420 0.112 1.00 0.00ATOM 15 H 1 -2.608 -0.407 1.130 1.00 0.00ATOM 16 H 1 -2.540 -1.303 -0.404 1.00 0.00ATOM 17 H 1 -3.393 0.254 -0.321 1.00 0.00TER 18 1

Esto funciona, pero puede que la próxima persona que lea middle.sh tarde un momento en averiguar lo que hace.Podemos mejorar nuestro script añadiendo algunos comentarios en la parte superior:

$ nano middle.sh
# Select lines from the middle of a file.# Usage: bash middle.sh filename end_line num_lineshead -n "" "" | tail -n ""

Un comentario comienza con un carácter # y llega hasta el final de la línea.El ordenador ignora los comentarios, pero son muy valiosos para ayudar a la gente (incluido tu futuro yo) a entender y utilizar los scripts.La única advertencia es que cada vez que modifiques el script, debes comprobar que el comentario sigue siendo correcto: una explicación que envía al lector en la dirección equivocada es peor que ninguna.

¿Qué pasa si queremos procesar muchos archivos en una sola tubería?Por ejemplo, si queremos ordenar nuestros archivos .pdb por su longitud, escribiríamos:

$ wc -l *.pdb | sort -n

porque wc -l enumera el número de líneas de los archivos (recuerde que wc significa «recuento de palabras», añadiendo la opción -l significa «recuento de líneas» en su lugar) y sort -n ordena las cosas numéricamente.Podríamos poner esto en un archivo, pero entonces sólo ordenaría una lista de .pdb archivos en el directorio actual.Si queremos obtener una lista ordenada de otros tipos de archivos, necesitamos una forma de introducir todos esos nombres en el script.No podemos usar , , etc. porque no sabemos cuántos archivos hay.En su lugar, usamos la variable especial $@, que significa,’Todos los argumentos de la línea de comandos del script del shell’.También debemos poner $@ dentro de comillas dobles para manejar el caso de los argumentos que contienen espacios ("$@" es una sintaxis especial y es equivalente a "" "" …).

He aquí un ejemplo:

$ nano sorted.sh
# Sort files by their length.# Usage: bash sorted.sh one_or_more_filenameswc -l "$@" | sort -n
$ bash sorted.sh *.pdb ../creatures/*.dat
9 methane.pdb12 ethane.pdb15 propane.pdb20 cubane.pdb21 pentane.pdb30 octane.pdb163 ../creatures/basilisk.dat163 ../creatures/minotaur.dat163 ../creatures/unicorn.dat596 total

Lista de Especies Únicas

Leah tiene varios cientos de archivos de datos, cada uno de los cuales tiene el siguiente formato:

2013-11-05,deer,52013-11-05,rabbit,222013-11-05,raccoon,72013-11-06,rabbit,192013-11-06,deer,22013-11-06,fox,12013-11-07,rabbit,182013-11-07,bear,1

Un ejemplo de este tipo de archivo se da en data-shell/data/animal-counts/animals.txt.

Podemos utilizar el comando cut -d , -f 2 animals.txt | sort | uniq para producir la especie única en animals.txt. Para evitar tener que escribir esta serie de comandos cada vez, un científico puede optar por escribir un script de shell en su lugar.

Escriba un script de shell llamado species.sh que tome cualquier número de nombres de archivos como argumentos de la línea de comandos, y utilice una variación del comando anterior para imprimir una lista de las especies únicas que aparecen en cada uno de esos archivos por separado.

Solución

# Script to find unique species in csv files where species is the second data field# This script accepts any number of file names as command line arguments# Loop over all filesfor file in $@doecho "Unique species in $file:"# Extract species namescut -d , -f 2 $file | sort | uniqdone

Supongamos que acabamos de ejecutar una serie de comandos que hicieron algo útil – por ejemplo, que crearon un gráfico que nos gustaría usar en un artículo.En lugar de escribirlos de nuevo (y potencialmente equivocarnos) podemos hacer lo siguiente:

$ history | tail -n 5 > redo-figure-3.sh

El archivo redo-figure-3.sh contiene ahora:

297 bash goostats NENE01729B.txt stats-NENE01729B.txt298 bash goodiff stats-NENE01729B.txt /data/validated/01729.txt > 01729-differences.txt299 cut -d ',' -f 2-3 01729-differences.txt > 01729-time-series.txt300 ygraph --format scatter --color bw --borders none 01729-time-series.txt figure-3.png301 history | tail -n 5 > redo-figure-3.sh

Tras un momento de trabajo en un editor para eliminar los números de serie de los comandos,y eliminar la línea final en la que llamamos al comando history, tenemos un registro completamente exacto de cómo hemos creado esa figura.

¿Por qué registrar los comandos en el historial antes de ejecutarlos?

Si ejecuta el comando:

$ history | tail -n 5 > recent.sh

el último comando del archivo es el propio comando history, es decir,el shell ha añadido history al registro de comandos antes de ejecutarlo. De hecho, el shell siempre añade comandos al registro antes de ejecutarlos. ¿Por qué cree que hace esto?

Solución

Si un comando hace que algo se bloquee o se cuelgue, podría ser útil saber cuál era ese comando, para investigar el problema.

En la práctica, la mayoría de la gente desarrolla scripts de shell ejecutando comandos en el prompt del shell unas cuantas veces para asegurarse de que están haciendo lo correcto, y luego guardándolos en un archivo para su reutilización.Este estilo de trabajo permite a la gente reciclar lo que descubren sobre sus datos y su flujo de trabajo con una llamada a historyy un poco de edición para limpiar la salida y guardarla como un script de shell.

Nelle’s Pipeline: Creación de un script

El supervisor de Nelle insistió en que todos sus análisis debían ser reproducibles. La forma más fácil de capturar todos los pasos es en un script.

Primero volvemos al directorio de datos de Nelle:

$ cd ../north-pacific-gyre/2012-07-03/

Ejecuta el editor y escribe lo siguiente:

# Calculate stats for data files.for datafile in "$@"do echo $datafile bash goostats $datafile stats-$datafiledone

Guarda esto en un archivo llamado do-stats.shpara que ahora pueda rehacer la primera etapa de su análisis escribiendo:

$ bash do-stats.sh NENE*.txt

También puede hacer esto:

$ bash do-stats.sh NENE*.txt | wc -l

para que la salida sea sólo el número de archivos procesados en lugar de los nombres de los archivos que se procesaron.

Una cosa a tener en cuenta sobre el script de Nelle es que permite a la persona que lo ejecuta decidir qué archivos procesar.Podría haberlo escrito como:

# Calculate stats for Site A and Site B data files.for datafile in NENE*.txtdo echo $datafile bash goostats $datafile stats-$datafiledone

La ventaja es que siempre selecciona los archivos correctos: no tiene que acordarse de excluir los archivos ‘Z’.La desventaja es que siempre selecciona sólo esos archivos – no puede ejecutarlo en todos los archivos (incluyendo los archivos ‘Z’), o en los archivos ‘G’ o ‘H’ que sus colegas en la Antártida están produciendo, sin editar el script.Si quisiera ser más aventurera, podría modificar su script para comprobar los argumentos de la línea de comandos y utilizar NENE*.txt si no se proporciona ninguno.

Variables en scripts de shell

En el directorio molecules, imagine que tiene un script de shell llamado script.sh que contiene los siguientes comandos:

head -n  tail -n  

Mientras está en el directorio molecules, escribe el siguiente comando:

bash script.sh '*.pdb' 1 1

¿Cuál de las siguientes salidas esperaría ver?

  1. Todas las líneas entre la primera y la última línea de cada archivo que termina en .pdben el directorio molecules
  2. La primera y la última línea de cada archivo que termina en .pdb en el molecules directorio
  3. La primera y la última línea de cada archivo en el directorio molecules
  4. Un error por las comillas que rodean a *.pdb

Solución

La respuesta correcta es la 2.

Las variables especiales $1, $2 y $3 representan los argumentos de la línea de comandos dados al script, de manera que los comandos ejecutados son:

$ head -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb$ tail -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb

El shell no expande '*.pdb' porque está encerrado entre comillas.Como tal, el primer argumento del script es '*.pdb' que se expande dentro del script por head y tail.

Encuentra el archivo más largo con una extensión dada

Escribe un script de shell llamado longest.sh que toma el nombre de un directorio y una extensión de nombre de archivo como sus argumentos, e imprime el nombre del archivo con más líneas en ese directorio con esa extensión. Por ejemplo:

$ bash longest.sh /tmp/data pdb

imprime el nombre del archivo .pdb de /tmp/data que tiene más líneas.

Solución

# Shell script which takes two arguments:# 1. a directory name# 2. a file extension# and prints the name of the file in that directory# with the most lines which matches the file extension.wc -l /*. | sort -n | tail -n 2 | head -n 1

La primera parte de la cadena, wc -l /*. | sort -n, cuenta las líneas de cada fichero y las ordena numéricamente (la mayor en último lugar). Cuando hay más de un archivo, wc también emite una línea de resumen final, dando el número total de líneas en todos los archivos. Usamos tail-n 2 | head -n 1 para tirar esta última línea.

Con wc -l /*. | sort -n | tail -n 1 veremos la línea de resumen final: podemos construir nuestra tubería en pedazos para estar seguros de entender la salida.

Comprensión de Lectura de Script

Para esta pregunta, considere el directorio data-shell/molecules una vez más.Este contiene un número de archivos .pdb además de cualquier otro archivo que pueda haber creado.Explique lo que cada uno de los siguientes tres scripts haría cuando se ejecuta comobash script1.sh *.pdb, bash script2.sh *.pdb y bash script3.sh *.pdb respectivamente.

# Script 1echo *.*
# Script 2for filename in   do cat $filenamedone
# Script 3echo [email protected]

Soluciones

En cada caso, el shell expande el comodín en *.pdb antes de pasar la lista resultante de nombres de archivos como argumentos al script.

El script 1 imprimiría una lista de todos los archivos que contienen un punto en su nombre.Los argumentos pasados al script no se utilizan realmente en ninguna parte del mismo.

El script 2 imprimiría el contenido de los 3 primeros archivos con una extensión de archivo .pdb., y se refieren al primer, segundo y tercer argumento respectivamente.

El script 3 imprimiría todos los argumentos del script (es decir, todos los archivos .pdb), seguidos de .pdb.$@ se refiere a todos los argumentos dados a un script de shell.

cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb.pdb

Secuencias de comandos de depuración

Suponga que ha guardado la siguiente secuencia de comandos en un archivo llamado do-errors.sh en el directorio north-pacific-gyre/2012-07-03 de Nelle:

# Calculate stats for data files.for datafile in "$@"do echo $datfile bash goostats $datafile stats-$datafiledone

Cuando lo ejecuta:

$ bash do-errors.sh NENE*.txt

la salida está en blanco.Para averiguar por qué, vuelva a ejecutar el script utilizando la opción -x:

bash -x do-errors.sh NENE*.txt

¿Qué le muestra la salida? Qué línea es la responsable del error?

Solución

La opción -x hace que bash se ejecute en modo de depuración.Esto imprime cada comando a medida que se ejecuta, lo que le ayudará a localizar errores.En este ejemplo, podemos ver que echo no está imprimiendo nada. Hemos cometido un error en el nombre de la variable del bucle, y la variable datfile no existe, por lo que devuelve una cadena vacía.

Puntos clave

  • Guarda los comandos en archivos (normalmente llamados shell scripts) para reutilizarlos.

  • bash ejecuta los comandos guardados en un archivo.

  • $@se refiere a todos los argumentos de la línea de comandos de un script de shell.

  • , , etc., se refiere al primer argumento de la línea de comandos, al segundo argumento de la línea de comandos, etc.

  • Coloque las variables entre comillas si los valores pueden tener espacios.

  • Dejar que los usuarios decidan qué archivos procesar es más flexible y más coherente con los comandos integrados de Unix.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.