Scripts de shell
Última actualización: 2025-04-03 | Mejora esta página
Hoja de ruta
Preguntas
- ¿Cómo puedo guardar y reutilizar comandos?
Objetivos
- Escribe un script de shell que ejecute un comando o una serie de comandos para un conjunto fijo de archivos.
- Ejecuta un script de shell desde la línea de comandos.
- Escribe un script de shell que opera sobre un conjunto de ficheros definidos por el usuario en la línea de comandos.
- Crear pipelines que incluyan shell scripts que tú, y otros, hayáis escrito.
Por fin estamos listos para ver por qué el shell es un entorno de programación tan potente. Vamos a tomar los comandos que repetimos con frecuencia y guardarlos en archivos para que podamos volver a ejecutar todas esas operaciones más tarde escribiendo un solo comando. Por razones históricas, un montón de comandos guardados en un archivo se suele llamar script de shell, pero no te equivoques: en realidad son pequeños programas.
Escribir secuencias de comandos no sólo agilizará el trabajo, sino que también evitará tener que volver a escribir los mismos comandos una y otra vez. También lo hará más preciso (menos posibilidades de errores tipográficos) y más reproducible. Si vuelves a tu trabajo más tarde (o si alguien más encuentra tu trabajo y quiere basarse en él), podrás reproducir los mismos resultados simplemente ejecutando tu script, en lugar de tener que recordar o volver a escribir una larga lista de comandos.
Empecemos volviendo a alkanes/
y creando un nuevo
fichero, middle.sh
que se convertirá en nuestro shell
script:
El comando nano middle.sh
abre el archivo
middle.sh
dentro del editor de texto ‘nano’ (que se ejecuta
dentro del shell). Si el fichero no existe, se creará. Podemos utilizar
el editor de texto para editar directamente el archivo insertando la
siguiente línea:
head -n 15 octane.pdb | tail -n 5
Esta es una variación de la tubería que construimos antes, que
selecciona las líneas 11-15 del archivo octane.pdb
.
Recuerda, no estamos ejecutándolo como un comando todavía; sólo
estamos incorporando 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 alkanes
contiene ahora un fichero llamado
middle.sh
.
Una vez guardado el fichero, podemos pedir al intérprete de órdenes
que ejecute los comandos que contiene. Nuestro shell se llama
bash
, así que ejecutamos el siguiente comando:
SALIDA
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00
ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00
ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00
ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00
ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00
Efectivamente, la salida de nuestro script es exactamente la que obtendríamos si ejecutáramos ese pipeline directamente.
Texto vs. Lo que sea
Solemos llamar “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 texto, sino también
información de formato sobre fuentes, encabezados, etcétera. Esta
información adicional no se almacena como caracteres y no significa nada
para herramientas como head
, que espera que los archivos de
entrada no contengan más que las letras, dígitos y signos de 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.
¿Y 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 nos llevaría más tiempo que volver a
escribir el comando en el intérprete de comandos y ejecutarlo con un
nuevo nombre de archivo. En su lugar, vamos a editar
middle.sh
y hacerlo más versátil:
Ahora, dentro de “nano”, sustituye el texto octane.pdb
por la variable especial llamada $1
:
head -n 15 "$1" | tail -n 5
Dentro de un script de shell, $1
significa ‘el primer
nombre de archivo (u otro argumento) en la línea de comandos’. Ahora
podemos ejecutar nuestro script así
SALIDA
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00
ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00
ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00
ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00
ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00
o en un archivo diferente como este:
SALIDA
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00
ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00
ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00
ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00
ATOM 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 fichero contenga espacios,
rodeamos $1
con comillas dobles.
Actualmente, tenemos que editar middle.sh
cada vez que
queremos ajustar el rango de líneas que se devuelve. Vamos a
solucionarlo configurando nuestro script para que utilice tres
argumentos de línea de comandos. Después del primer argumento de línea
de órdenes ($1
), cada argumento adicional que
proporcionemos será accesible a través de las variables especiales
$1
, $2
, $3
, que se refieren al
primer, segundo y tercer argumento de línea de órdenes,
respectivamente.
Sabiendo esto, podemos usar argumentos adicionales para definir el
rango de líneas a pasar a head
y tail
respectivamente:
head -n "$2" "$1" | tail -n "$3"
Ahora podemos ejecutar:
SALIDA
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00
ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00
ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00
ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00
ATOM 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:
SALIDA
ATOM 14 H 1 -1.259 1.420 0.112 1.00 0.00
ATOM 15 H 1 -2.608 -0.407 1.130 1.00 0.00
ATOM 16 H 1 -2.540 -1.303 -0.404 1.00 0.00
ATOM 17 H 1 -3.393 0.254 -0.321 1.00 0.00
TER 18 1
Esto funciona, pero puede que la próxima persona que lea
middle.sh
tarde un momento en darse cuenta de lo que hace.
Podemos mejorar nuestro script añadiendo algunos
comentarios en la parte superior:
# Select lines from the middle of a file.
# Usage: bash middle.sh filename end_line num_lines
head -n "$2" "$1" | tail -n "$3"
Un comentario empieza con un carácter #
y llega hasta el
final de la línea. El ordenador ignora los comentarios, pero tienen un
valor incalculable 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 lleve al lector en la dirección equivocada
es peor que ninguna.
¿Qué pasa si queremos procesar muchos archivos en un solo proceso?
Por ejemplo, si queremos ordenar nuestros archivos .pdb
por
longitud, escribiríamos:
porque wc -l
lista el número de líneas en los ficheros
(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 fichero, pero entonces sólo ordenaría una lista de
ficheros .pdb
en el directorio actual. Si queremos obtener
una lista ordenada de otros tipos de ficheros, necesitamos una forma de
introducir todos esos nombres en el script. No podemos usar
$1
, $2
, etc. porque no sabemos cuántos
ficheros hay. En su lugar, usamos la variable especial $@
,
que significa, ‘Todos los argumentos de línea de comandos del script de
shell’. También debemos poner $@
entre comillas dobles para
manejar el caso de argumentos que contienen espacios ("$@"
es sintaxis especial y es equivalente a "$1"
"$2"
…).
He aquí un ejemplo:
# Sort files by their length.
# Usage: bash sorted.sh one_or_more_filenames
wc -l "$@" | sort -n
SALIDA
9 methane.pdb
12 ethane.pdb
15 propane.pdb
20 cubane.pdb
21 pentane.pdb
30 octane.pdb
163 ../creatures/basilisk.dat
163 ../creatures/minotaur.dat
163 ../creatures/unicorn.dat
596 total
Lista de especies únicas
Leah tiene varios cientos de ficheros de datos, cada uno de los cuales tiene el siguiente formato:
2013-11-05,deer,5
2013-11-05,rabbit,22
2013-11-05,raccoon,7
2013-11-06,rabbit,19
2013-11-06,deer,2
2013-11-06,fox,1
2013-11-07,rabbit,18
2013-11-07,bear,1
Un ejemplo de este tipo de fichero se da en
shell-lesson-data/exercise-data/animal-counts/animals.csv
.
Podemos utilizar el comando
cut -d , -f 2 animals.csv | sort | uniq
para producir las
especies únicas en animals.csv
. Para evitar tener que
teclear esta serie de comandos cada vez, un científico puede optar por
escribir un script de shell en su lugar.
Escribe un script de shell llamado species.sh
que tome
cualquier número de nombres de archivo como argumentos de 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.
Supongamos que acabamos de ejecutar una serie de comandos que han hecho algo útil, por ejemplo, crear un gráfico que nos gustaría utilizar en un artículo. Nos gustaría poder volver a crear el gráfico más tarde si lo necesitamos, así que queremos guardar los comandos en un archivo. En lugar de escribirlos de nuevo (y potencialmente equivocarnos) podemos hacer lo siguiente:
El fichero redo-figure-3.sh
contiene ahora:
297 bash goostats.sh NENE01729B.txt stats-NENE01729B.txt
298 bash goodiff.sh stats-NENE01729B.txt /data/validated/01729.txt > 01729-differences.txt
299 cut -d ',' -f 2-3 01729-differences.txt > 01729-time-series.txt
300 ygraph --format scatter --color bw --borders none 01729-time-series.txt figure-3.png
301 history | tail -n 5 > redo-figure-3.sh
Después de un momento de trabajo en un editor para eliminar los
números de serie en los comandos, y para eliminar la línea final donde
llamamos al comando history
, tenemos un registro
completamente exacto de cómo creamos esa figura.
¿Por qué registrar los comandos en el historial antes de ejecutarlos?
Si ejecutas el comando:
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 realmente. De hecho, el
shell siempre añade comandos al registro antes de ejecutarlos.
¿Por qué crees que lo hace?
Si un comando hace que algo se bloquee o se cuelgue, puede ser útil saber cuál era ese comando para investigar el problema. Si el comando sólo se registrara después de ejecutarlo, no tendríamos un registro del último comando ejecutado en caso de que se produjera un bloqueo.
En la práctica, la mayoría de la gente desarrolla secuencias de
comandos ejecutándolas unas cuantas veces para asegurarse de que están
haciendo lo correcto y guardándolas en un archivo para poder
reutilizarlas. Este estilo de trabajo permite a la gente reciclar lo que
descubren sobre sus datos y su flujo de trabajo con una llamada a
history
y 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 sencilla de capturar todos los pasos es en un script.
Primero volvemos al directorio del proyecto de Nelle:
Crea un fichero usando nano
…
…que contiene lo siguiente:
BASH
# Calculate stats for data files.
for datafile in "$@"
do
echo $datafile
bash goostats.sh $datafile stats-$datafile
done
Guarda esto en un archivo llamado do-stats.sh
de modo
que ahora puede volver a hacer la primera etapa de su análisis
escribiendo:
También puede hacer esto:
para que la salida sea sólo el número de archivos procesados en lugar de los nombres de los archivos que fueron procesados.
Una cosa a tener en cuenta sobre el script de Nelle es que deja que la persona que lo ejecuta decida qué archivos procesar. Podría haberlo escrito como:
BASH
# Calculate stats for Site A and Site B data files.
for datafile in NENE*A.txt NENE*B.txt
do
echo $datafile
bash goostats.sh $datafile stats-$datafile
done
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 atrevida, podría modificar el script para
comprobar si hay argumentos en la línea de comandos y utilizar
NENE*A.txt NENE*B.txt
si no se proporciona ninguno. Por
supuesto, esto introduce otro compromiso entre flexibilidad y
complejidad.
Variables en Shell Scripts
En el directorio alkanes
, imagine que tiene un script de
shell llamado script.sh
que contiene los siguientes
comandos:
Mientras estás en el directorio alkanes
, escribe el
siguiente comando:
¿Cuál de los siguientes resultados esperaría ver?
- Todas las líneas entre la primera y la última línea de cada fichero
que termina en
.pdb
en el directorioalkanes
- La primera y la última línea de cada fichero que termina en
.pdb
en el directorioalkanes
- La primera y la última línea de cada fichero en el directorio
alkanes
- Error debido a las comillas alrededor de
*.pdb
La respuesta correcta es 2.
Las variables especiales $1
, $2
y
$3
representan los argumentos de línea de comandos dados al
script, de tal forma que los comandos ejecutados son:
BASH
$ 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á entre comillas.
Como tal, el primer argumento del script es '*.pdb'
que se
expande dentro del script por head
y tail
.
Encuentra el fichero más largo con una extensión dada
Escribe un script de shell llamado longest.sh
que tome
como argumentos el nombre de un directorio y una extensión de nombre de
archivo, e imprima el nombre del archivo con más líneas en ese
directorio con esa extensión. Por ejemplo:
imprimiría el nombre del fichero .pdb
en
shell-lesson-data/exercise-data/alkanes
que tiene más
líneas.
Siéntete libre de probar tu script en otro directorio p.e.
BASH
# 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 $1/*.$2 | sort -n | tail -n 2 | head -n 1
La primera parte del proceso, wc -l $1/*.$2 | sort -n
,
cuenta las líneas de cada archivo y las ordena numéricamente (la más
grande en último lugar). Cuando hay más de un archivo, wc
también genera 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 desechar esta última línea.
Con wc -l $1/*.$2 | sort -n | tail -n 1
veremos la línea
de resumen final: podemos construir nuestro pipeline por partes para
estar seguros de que entendemos la salida.
Comprensión de lectura del script
Para esta pregunta, consideremos de nuevo el directorio
shell-lesson-data/exercise-data/alkanes
. Este contiene un
número de archivos .pdb
además de cualquier otro archivo
que pueda haber creado. Explique qué haría cada uno de los tres scripts
siguientes al ejecutarse como bash script1.sh *.pdb
,
bash script2.sh *.pdb
y bash script3.sh *.pdb
respectivamente.
En cada caso, el shell expande el comodín en *.pdb
antes
de pasar la lista resultante de nombres de archivo 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 script.
El script 2 imprimiría el contenido de los 3 primeros ficheros con
extensión .pdb
. los argumentos $1
,
$2
y $3
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.
SALIDA
cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb.pdb
Scripts de depuración
Suponga que ha guardado el siguiente script en un archivo llamado
do-errors.sh
en el directorio
north-pacific-gyre
de Nelle:
BASH
# Calculate stats for data files.
for datafile in "$@"
do
echo $datfile
bash goostats.sh $datafile stats-$datafile
done
Cuando lo ejecutas desde el directorio
north-pacific-gyre
:
la salida está en blanco. Para averiguar por qué, vuelva a ejecutar
el script utilizando la opción -x
:
¿Qué te muestra la salida? ¿Qué línea es la responsable del error?
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 imprime nada. Hemos cometido un error tipográfico
en el nombre de la variable de bucle, y la variable datfile
no existe, por lo que devuelve una cadena vacía.
Puntos Clave
- Guarda comandos en archivos (normalmente llamados shell scripts) para reutilizarlos.
-
bash [filename]
ejecuta los comandos guardados en un archivo. -
$@
se refiere a todos los argumentos de línea de comandos de un script de shell. -
$1
,$2
, etc., se refieren al primer argumento de la línea de comandos, al segundo argumento de la línea de comandos, etc. - Ponga las variables entre comillas si los valores pueden contener espacios.
- Dejar que los usuarios decidan qué archivos procesar es más flexible y más consistente con los comandos Unix incorporados.