Bucles
Última actualización: 2023-04-24 | Mejora esta página
Tiempo estimado: 15 minutos
Hoja de ruta
Preguntas
- ¿Cómo puedo realizar las mismas acciones en varios archivos diferentes?
Objetivos
- Escribir un bucle que aplique uno o más comandos por separado a cada archivo de un conjunto de archivos.
- Rastrear los valores tomados por una variable de bucle durante la ejecución del bucle.
- Explicar la diferencia entre el nombre de una variable y su valor.
- Explicar por qué los espacios y algunos caracteres de puntuación no deben usarse en nombres de archivo.
- Demostrar cómo ver qué comandos han sido ejecutados recientemente.
- Volver a ejecutar comandos ejecutados recientemente sin volver a escribirlos.
Los bucles (loops en inglés) son fundamentales para
mejorar la productividad a través de la automatización, ya que nos
permiten ejecutar comandos de forma repetitiva. Al igual que los
caracteres especiales y el autocompletado, el uso de bucles también
reduce la cantidad de tecleo (y los errores de escritura/dedo). Suponte
que tenemos varios cientos de archivos con datos genómicos denominados
basilisk.dat
,unicorn.dat
y así sucesivamente.
En este ejemplo, usaremos el directorio creatures
que sólo
tiene dos archivos de ejemplo, pero los principios se pueden aplicar a
muchos más archivos a la vez. Nos gustaría modificar estos archivos,
pero también guardar una versión de los archivos originales, nombrando
las copias original-basilisk.dat
yoriginal-unicorn.dat
. No podemos usar:
Porque se expandiría a:
Esto no respalda nuestros archivos, en su lugar obtenemos un error:
ERROR
cp: target `original-*.dat' is not a directory
Este problema surge cuando cp
recibe más de dos
argumentos de entrada. Cuando esto sucede, espera que la última entrada
sea un directorio donde pueda copiar todos los archivos que se le
pasaron. Dado que no existe ningún directorio denominado
original-*.fasta
en el directorio creatures
,
se genera un error.
En cambio, podemos usar un bucle para ejecutar una operación a la vez sobre cada cosa en una lista. Aquí un ejemplo sencillo que muestra las tres primeras líneas de cada archivo de una sola vez:
SALIDA
COMMON NAME: basilisk
CLASSIFICATION: basiliscus vulgaris
UPDATED: 1745-05-02
COMMON NAME: unicorn
CLASSIFICATION: equus monoceros
UPDATED: 1738-11-24
Cuando la terminal reconoce la palabra clave for
, sabe
que debe repetir un comando (o grupo de comandos) una vez para cada
elemento de una lista. Cada vez que el bucle se ejecuta (llamada
iteración), un elemento de la lista es asignado secuencialmente a una
variable, los comandos dentro del bucle se ejecutan
antes de pasar al siguiente elemento de la lista. Dentro del bucle,
pedimos el valor de la variable poniendo $
delante de ella.
El $
le dice al intérprete de la terminal que trate la
variable como un nombre de variable y substituya su
valor, en lugar de tratarlo como texto o como un comando externo.
En este ejemplo, la lista tiene dos nombres de archivo:
basilisk.dat
yunicorn.dat
. Cada vez que el
bucle se itera, asignará un nombre de archivo a la variable
filename
y ejecutará el comando head
. La
primera vez en el bucle, $filename
esbasilisk.dat
. El intérprete ejecuta el comando
head
en basilisk.dat
, e imprime las primeras
tres líneas de basilisk.dat
. Para la segunda iteración,
$filename
se convierte en unicorn.dat
. Esta
vez, la terminal ejecuta head
enunicorn.dat
e
imprime las tres primeras líneas de unicorn.dat
. Dado que
la lista era sólo de dos elementos, la terminal sale del bucle
for
.
Cuando se utilizan variables, también es posible poner sus nombres en
llaves para delimitar claramente el nombre de la variable:
$filename
es equivalente a${filename}
, pero es
diferente de ${file}name
. Puede ser que encuentres esta
notación en programas escritos por otras personas.
Siga las instrucciones
El prompt de la terminal cambia de $
a
>
y de nuevo a $
conforme escribimos
nuestro bucle. El segundo prompt, >
, es
diferente para recordarnos que todavía no hemos terminado de escribir un
comando completo. Un punto y coma, ;
, se puede utilizar
para separar dos comandos escritos en una sola línea.
Los mismos símbolos, diferentes significados
Aquí vemos >
siendo utilizado como un prompt de la
terminal, mientras que >
también se utiliza para
redirigir la salida de un comando. De forma similar, $
se
utiliza como prompt de la terminal, pero como vimos antes, también se
utiliza para pedir que la terminal obtenga el valor de una variable.
Si la terminal imprime >
o $
entonces espera que escribas algo, y el símbolo es un prompt.
Si tú escribes >
o $
, es una
instrucción de ti para la terminal indicándole redirigir la salida o
obtener el valor de una variable.
Hemos llamado a la variable en este bucle filename
con
el fin de hacer su propósito más claro para los lectores humanos. A la
terminal no le importa el nombre de la variable; si escribimos este
bucle como:
or:
Funcionaría exactamente de la misma manera. No lo hagas así.
Los programas sólo son útiles si la gente puede entenderlos, nombres
crípticos (como x
) o nombres engañosos
(comotemperature
) aumentan las probabilidades de que el
programa no haga lo que sus lectores piensan que hace.
He aquí un bucle un poco más complicado:
La terminal comienza expandiendo *.dat
para crear la
lista de archivos que procesará. Después de esto, el cuerpo del
bucle ejecuta dos comandos para cada uno de esos archivos. El
primero, echo
, simplemente imprime sus parámetros de línea
de comandos a la salida estándar. Por ejemplo:
regresa:
SALIDA
hello there
En este caso, ya que la terminal expande $filename
para
que sea el nombre de un archivo, echo $filename
sólo
imprime el nombre del archivo. Ten en cuenta que no podemos escribir
esto como:
porque entonces la primera vez a través del bucle, cuando
$filename
se expande a basilisk.dat
, la
terminal intentará ejecutar basilisk.dat
como un programa.
Finalmente, la combinación head
y tail
selecciona las líneas 81-100 de cualquier archivo que se esté procesando
(suponiendo que el archivo tiene al menos 100 líneas).
Espacios en los nombres
El espacio en blanco se utiliza para separar los elementos de la lista que vamos a usar en el bucle. Si en la lista tenemos elementos con espacios en blanco necesitamos entrecomillar esos elementos y nuestra variable al usarlo. Supongamos que nuestros archivos de datos se llaman:
red dragon.dat
purple unicorn.dat
Necesitamos usar
BASH
for filename in "red dragon.dat" "purple unicorn.dat"
do
head -n 100 "$filename" | tail -n 20
done
Es más sencillo simplemente evitar el uso de espacios en blanco (u otros caracteres especiales) en los nombres de archivo.
Los archivos mencionados no existen, si corremos el código del último
ejemplo, el comando head
será incapaz de encontrarlos, sin
embargo el mensaje de error regresará el nombre de los archivos que
espera:
SALIDA
head: cannot open ‘red dragon.dat' for reading: No such file or directory
head: cannot open ‘purple unicorn.dat' for reading: No such file or directory
Intenta remover las comillas en $filename
en el bucle
anterior para observar el efecto de las comillas en los espacios en
blanco:
SALIDA
head: cannot open ‘red' for reading: No such file or directory
head: cannot open ‘dragon.dat' for reading: No such file or directory
head: cannot open ‘purple' for reading: No such file or directory
head: cannot open ‘unicorn.dat' for reading: No such file or directory
Volviendo a nuestro problema original de copia de archivos, Podemos resolverlo usando este bucle:
Este bucle ejecuta el comando cp
una vez para cada
nombre de archivo. La primera vez, cuando $filename
se
convierte en basilisk.dat
, la terminal ejecuta:
La segunda vez, el comando es:
Como el comando cp
no produce una salida normalmente, es
difícil verificar que el bucle está funcionando de forma correcta. Al
antecederle echo
es posibe ver cada comando como
sería ejecutado. El siguiente diagrama muestra lo que sucede
cuando el código modificado es ejecutado, y demuestra cómo el uso de
echo
es una práctica útil.
Pipeline de Nelle: Procesando Archivos
Nelle ahora está lista para procesar sus archivos de datos. Dado que todavía está aprendiendo cómo utilizar la terminal, decide construir los comandos requeridos en etapas. Su primer paso es asegurarse de que puede seleccionar los archivos correctos (recuerda, aquellos cuyos nombres terminan en ‘A’ o ‘B’, en lugar de ‘Z’). Posicionada en su directorio home, Nelle teclea:
BASH
$ cd north-pacific-gyre/2012-07-03
$ for datafile in NENE*[AB].txt
> do
> echo $datafile
> done
SALIDA
NENE01729A.txt
NENE01729B.txt
NENE01736A.txt
...
NENE02043A.txt
NENE02043B.txt
Su siguiente paso es decidir cómo llamar a los archivos que creará el
programa de análisis goostats
. Prefijar el nombre de cada
archivo de entrada con “stats” parece simple, así que modifica su bucle
para hacer eso:
SALIDA
NENE01729A.txt stats-NENE01729A.txt
NENE01729B.txt stats-NENE01729B.txt
NENE01736A.txt stats-NENE01736A.txt
...
NENE02043A.txt stats-NENE02043A.txt
NENE02043B.txt stats-NENE02043B.txt
Ella todavía no ha ejecutado goostats
, pero ahora está
segura de que puede seleccionar los archivos correctos y generar los
nombres de archivo de salida correctos.
Escribir comandos una y otra vez es cada vez más tedioso, y a Nelle le preocupa cometer errores, así que en lugar de volver a crear su bucle, presiona la flecha hacia arriba. En respuesta, la terminal vuelve a mostrar el bucle completo en una línea (usando puntos y comas para separar las piezas):
Utilizando la tecla de flecha izquierda, Nelle realiza una copia de
seguridad y cambia el comando echo
a
bash goostats
:
Cuando presiona Enter, la terminal ejecuta el comando modificado. Sin
embargo, nada parece suceder porque no hay salida. Después de un
momento, Nelle se da cuenta de que, ya que su script no
imprime nada a la pantalla, no tiene ni idea de si está funcionando, y
mucho menos con qué rapidez. Mata el comando de ejecución escribiendo
Ctrl-C
, e utiliza la flecha hacia arriba para repetir el
comando, y lo edita para que se vea así:
BASH
$ for datafile in NENE*[AB].txt; do echo $datafile; bash goostats $datafile stats-$datafile; done
Principio y Fin
Podemos pasar al principio de una línea en la terminal escribiendo
Ctrl-A
y al final usando Ctrl-E
.
Ahora, cuando ejecuta su programa, produce una línea de salida cada cinco segundos aproximadamente:
SALIDA
NENE01729A.txt
NENE01729B.txt
NENE01736A.txt
...
1518 veces 5 segundos, dividido entre 60, le dice que su
script tomará alrededor de dos horas en terminar de
analizar todos los archivos. Como un chequeo final, abre otra ventana de
terminal, entra en north-pacific-gyre/2012-07-03
, e utiliza
cat stats-NENE01729B.txt
para examinar uno de los archivos
de salida. Se ve bien, así que decide tomarse un café y ponerse al día
con su lectura.
Aquellos que conocen la historia pueden elegir repetirla
Otra forma de repetir el trabajo anterior es usar el comando
history
para obtener una lista de los últimos cientos de
comandos que se han ejecutado, y usar !123
(donde” 123 “es
reemplazado por el número de comando) para repetir uno de esos comandos.
Por ejemplo, si Nelle escribe esto:
SALIDA
456 ls -l NENE0*.txt
457 rm stats-NENE01729B.txt.txt
458 bash goostats NENE01729B.txt stats-NENE01729B.txt
459 ls -l NENE0*.txt
460 history
Entonces puede volver a ejecutar goostats
enNENE01729B.txt
simplemente escribiendo
!458
.
Otros comandos del historial
Existen otros comandos de acceso directo para acceder al historial:
-
Ctrl-R
permite buscar en el historial en un modo denominado “búsqueda reversa” (“reverse-i-search” en inglés), que permite buscar el comando más reciente en tu historial que coincide con el texto que introduzcas a continuación. PresionarCtrl-R
una o más veces adicionales permite buscar coincidencias más antiguas. -
!!
recupera el comando anterior inmediato (puedes ser que esto te parezca más conveniente que usar la flecha hacia arriba). -
!$
recupera la última palabra del último comando. Esto es más útil de lo que pensarías: después debash goostats NENE01729B.txt stats-NENE01729B.txt
, puedes teclearless !$
para ver el archivoNENE01729B.txt
, que es más rápido que utilizar la flecha hacia arriba y editar el comando.
Variables en bucles
En el directorio data-shell/molecules
, ls
regresa la siguiente salida:
SALIDA
cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
¿Cuál es la salida del siguiente código?:
Ahora, ¿cuál es la salida de este código?:
¿Por qué estos dos bucles dan resultados diferentes?
El primer bloque de código regresa la misma salida en cada iteración
del bucle. La terminal expande el caracter especial *.pdb
en el cuerpo del bucle (y al principio del mismo) para coincidir con
todos los archivos que terminan en .pdb
, y los enumera
utilizando ls
. El bucel expandido se vería así:
BASH
for datafile in cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
do
ls cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
done
SALIDA
cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
El segundo bloque de código enumera a un archivo distinto en cada
iteración. El valor de la variable datafile
es evaluado
usando $datafile
y después enumerado usando
ls
.
SALIDA
cubane.pdb
ethane.pdb
methane.pdb
octane.pdb
pentane.pdb
propane.pdb
Guardar en un archivo dentro de un bucle - Primera parte
En el mismo directorio, ¿cuál es el efecto de este bucle?
- Imprime
cubane.pdb
,ethane.pdb
,methane.pdb
,octane.pdb
,pentane.pdb
ypropane.pdb
, y el texto depropane.pdb
se guarda en un archivo llamadoalkanes.pdb
. - Imprime
cubane.pdb
,ethane.pdb
,methane.pdb
, y concatena el texto de los tres archivos en un archivo llamadoalkanes.pdb
. - Imprime
cubane.pdb
,ethane.pdb
,methane.pdb
,octane.pdb
ypentane.pdb
, y guarda el texto depropane.pdb
en un archivo llamadoalkanes.pdb
. - Ninguna de las anteriores.
- El texto de cada archivo se escribe (uno a la vez) en
alkanes.pdb
. Sin embargo, el archivo se sobrescribe en cada iteración del bucle, por lo que el contenido final dealkanes.pdb
es sólo el texto proveniente depropane.pdb
.
Guardar en un archivo en un bucle - Segunda parte
En el mismo directorio, ¿cuál sería la salida del siguiente bucle?
- Todo el texto de
cubane.pdb
,ethane.pdb
,methane.pdb
,octane.pdb
ypentane.pdb
sería concatenado y guardado en un archivo llamadoall.pdb
. - El texto de
ethane.pdb
se guardará en un archivo llamadoall.pdb
. - Todo el texto de
cubane.pdb
,ethane.pdb
,methane.pdb
,octane.pdb
,pentane.pdb
ypropane.pdb
sería concatenado y guardado en un archivo llamadoall.pdb
. - Todo el texto de
cubane.pdb
,ethane.pdb
,methane.pdb
,octane.pdb
,pentane.pdb
ypropane.pdb
se imprimirá en pantalla y será salvado en un archivo llamadoall.pdb
.
La opción correcta es 3. >>
concatena en un
archivo, en lugar de sobrescribirlo con la salida del comando. Dado que
la salida del comando cat
ha sido redirigida, nada se
imprime en pantalla.
Limitación de conjuntos de archivos
La respuesta correcta es 4. *
coincide con cero o más
caracteres, así que cualquier nombre que comience con la letra c,
seguida de cero o más caracteres, coincidirá con el comando
Limitación de conjuntos de archivos (continued)
La respuesta correcta es 4. *
coincide con cero o más
caracteres, por lo que la expresión *c*
coincidirá con un
nombre de archivo con cero o más caracteres antes de la letra c, y cero
o más caracteres después de la letra c.
Haciendo una ejecución “en seco” (dry-run)
Un bucle es una forma de hacer muchas cosas a la vez, o de cometer
muchos errores a la vez si se diseña de forma incorrecta. Una manera de
comprobar lo que un bucle hará es usar el comando
echo
. En lugar de ejecutar los comandos, los mostrará en
pantalla.
Supongamos que queremos obtener una vista previa de los comandos que ejecutará el siguiente bucle, sin ejecutarlos realmente:
¿Cuál es la diferencia entre los dos bucles abajo, y cuál deberíamos usar?
La versión que querríamos utilizar es la 2. Esta versión imprime en
pantalla todo lo entrecomillado, expandiendo la variable del bucle
porque incluye el signo $
como prefijo.
La versión 1 redirige la salida del comando
echo analyze $file
a un archivo,
analyzed-$file
. Es decir, se genera una serie de archivos:
analyzed-cubane.pdb
, analyzed-ethane.pdb
,
etc.
Prueba ambas versiones en tu terminal para ver la salida. Asegúrate
de abrir los archivos analyzed-*.pdb
para ver su
contenido.
Bucles anidado
Suponte que queremos configurar una estructura de directorios para organizar algunos experimentos que miden constantes de velocidad de reacción que involucran distintos compuestos y distintas temperaturas. ¿Cuál sería el resultado del siguiente código?:
Tenemos un bucle anidado, es decir, un bucle dentro de otro bucle:
para cada specie
del bucle exterior, el bucle interior (el
bucle anidado) itera sobre la lista de temperaturas y crea un nuevo
directorio para cada combinación.
¡Intenta ejecutar el código para descubrir qué directorio se crean!
Puntos Clave
- Un bucle
for
repite comandos una vez para cada elemento de una lista. - Cada bucle
for
necesita una variable para referirse al elemento en el que está trabajando actualmente. - Uso de
$name
para expandir una variable (es decir, obtener su valor). También se puede usar${name}
. - No utilizar espacios, comillas o caracteres especiales como ‘*’ o ‘?’ en nombres de directorios, ya que complica la expansión de variables.
- Proporcionar a los archivos nombres coherentes que sean fáciles de combinar con los caracteres especiales para facilitar la selección de los bucles.
- Utilizar la tecla de flecha hacia arriba para desplazarse por los comandos anteriores para editarlos y repetirlos.
- Usar
Ctrl-R
para buscar a través de los comandos previamente introducidos. - Usar
history
para mostrar comandos recientes, y!number
para repetir un comando por número.