105.2 Lección 2
Certificación: |
LPIC-1 |
---|---|
Versión: |
5.0 |
Tema: |
105 Shells y secuencias de comandos de Shell |
Objetivo: |
105.2 Personalizar o escribir scripts simples |
Lección: |
2 de 2 |
Introducción
Las secuencias de comandos del Shell están generalmente destinadas a automatizar las operaciones relacionadas con los archivos y directorios, las mismas operaciones que podrían realizarse manualmente en la línea de comandos. Sin embargo, el alcance de los scripts de shell no sólo se limita a los documentos de un usuario, ya que la configuración e interacción con muchos aspectos de un sistema operativo Linux también se realiza a través de archivos de script.
El shell Bash ofrece comandos útiles para escribir scripts de shell, pero todo el poder de estos scripts depende de la combinación de los comandos incorporados de Bash con las diferentes utilidades de línea de comandos disponibles en un sistema Linux.
Pruebas ampliadas
El Bash como lenguaje de scripts está mayormente orientado a trabajar con archivos, por lo que el comando incorporado test
tiene muchas opciones para evaluar las propiedades de los objetos del sistema de archivos (esencialmente archivos y directorios). Las pruebas que se centran en los archivos y directorios son útiles, por ejemplo, verificar si existen los archivos y directorios necesarios para realizar una determinada tarea y que los mismo se puedan leer. Luego, se asocia a una construcción condicional if y se ejecuta un conjunto de acciones si la prueba tiene éxito.
El comando test
puede evaluar expresiones usando dos sintaxis diferentes: las expresiones de prueba pueden darse como un argumento para el comando test
o pueden colocarse entre corchetes, donde el comando test
se da implícitamente. Así, la prueba para evaluar si /etc
es un directorio válido puede escribirse como test -d /etc
o como [ -d /etc]
:
$ test -d /etc $ echo $? 0 $ [ -d /etc ] $ echo $? 0
Como confirman los códigos de salida, en la variable $?
, un valor de 0 significa que la prueba fue exitosa, ambas formas evaluaron /etc
como un directorio válido. Asumiendo que la ruta de un archivo o directorio fue almacenada en la variable $VAR
, las siguientes expresiones pueden ser usadas como argumentos para test
o dentro de los corchetes:
-a "$VAR"
-
Evaluar si la ruta en
VAR
existe en el sistema de archivos y es un archivo. -b "$VAR"
-
Evaluar si la ruta en
VAR
es un archivo de bloque especial. -c "$VAR"
-
Evaluar si la ruta en
VAR
es un archivo de caracteres especiales. -d "$VAR"
-
Evaluar si la ruta en
VAR
es un directorio. -e "$VAR"
-
Evaluar si la ruta en
VAR
existe en el sistema de archivos. -f "$VAR"
-
Evaluar si la ruta en
VAR
existe y es un archivo regular. -g "$VAR"
-
Evaluar si la ruta en
VAR
tiene el permiso del SGID. -h "$VAR"
-
Evaluar si la ruta en
VAR
es un enlace simbólico. -L "$VAR"
-
Evaluar si la ruta en
VAR
es un enlace simbólico. (like-h
). -k "$VAR"
-
Evaluar si la ruta en
VAR
tiene el permiso de sticky. -p "$VAR"
-
Evaluar si la ruta en
VAR
es un archivo pipe. -r "$VAR"
-
Evaluar si la ruta en
VAR
es legible por el usuario actual. -s "$VAR"
-
Evaluar si la ruta en
VAR
existe y no está vacía. -S "$VAR"
-
Evaluar si la ruta en
VAR
es un archivo socket. -t "$VAR"
-
Evaluar si la ruta en
VAR
está abierto en una terminal. -u "$VAR"
-
Evaluar si la ruta en
VAR
tiene el permiso SUID. -w "$VAR"
-
Evaluar si la ruta en
VAR
es escribible por el usuario actual. -x "$VAR"
-
Evaluar si la ruta en
VAR
es ejecutable por el usuario actual. -O "$VAR"
-
Evaluar si la ruta en
VAR
es propiedad del usuario actual. -G "$VAR"
-
Evaluar si la ruta en
VAR
pertenece al grupo del usuario actual. -N "$VAR"
-
Evaluar si la ruta en
VAR
ha sido modificado desde la última vez que se accedió. "$VAR1" -nt "$VAR2"
-
Evaluar si la ruta en
VAR1
es más nuevo que la ruta en elVAR2
, según sus fechas de modificación. "$VAR1" -ot "$VAR2"
-
Evalúa si la ruta en el
VAR1
es más antiguo que elVAR2
. "$VAR1" -ef "$VAR2"
-
Esta expresión evalúa a "True" si la ruta en
VAR1
es un enlace duro (hardlink) conVAR2
.
Se recomienda usar las comillas dobles en una variable probada, porque si la variable resulta estar vacía, podría causar un error de sintaxis para el comando test
. Las opciones de prueba requieren un argumento de operando, y una variable vacía sin comillas causaría un error debido a la falta de un argumento requerido. También hay pruebas para variables de texto arbitrarias, que se describen a continuación:
-z "$TXT"
-
Evalúa si la variable
TXT
está vacía (tamaño cero). -n "$TXT"
otest "$TXT"
-
Evalúa si la variable
TXT
no está vacía. "$TXT1" = "$TXT2"
or"$TXT1" == "$TXT2"
-
Evalúa si la variable
TXT1
yTXT2
son iguales. "$TXT1" != "$TXT2"
-
Evalúa si la variable
TXT1
yTXT2
no son iguales.. "$TXT1" < "$TXT2"
-
Evalúa si
TXT1
esta antes queTXT2
, en orden alfabético. "$TXT1" > "$TXT2"
-
Evalúa si
TXT1
esta después queTXT2
, en orden alfabético.
Los distintos idiomas pueden tener reglas diferentes para el orden alfabético. Para obtener resultados consistentes, independientemente de la configuración de localización del sistema donde se ejecuta el script, se recomienda establecer la variable de entorno LANG
a C
, ejemplo LANG=C
, antes de realizar operaciones que impliquen un orden alfabético. Esta definición también mantendrá los mensajes del sistema en el idioma original, por lo que debe ser usada sólo dentro del ámbito del script.
Las comparaciones numéricas tienen sus propias opciones de prueba:
$NUM1 -lt $NUM2
-
Evalúa si
NUM1
es menor queNUM2
. $NUM1 -gt $NUM2
-
Evalúa si
NUM1
es mayor queNUM2
. $NUM1 -le $NUM2
-
Evalúa si
NUM1
es menor o igual queNUM2
. $NUM1 -ge $NUM2
-
Evalúa si
NUM1
es mayor o igual queNUM2
. $NUM1 -eq $NUM2
-
Evalúa si
NUM1
es igual aNUM2
. $NUM1 -ne $NUM2
-
Evalúa si
NUM1
no es igual aNUM2
.
Todas las pruebas pueden recibir los siguientes modificadores:
! EXPR
-
Evalúa si la expresión
EXPR
es falsa. EXPR1 -a EXPR2
-
Evalúa si tanto
EXPR1
comoEXPR2
son verdaderos. EXPR1 -o EXPR2
-
Evalúa si al menos una de las dos expresiones es verdadera.
Otra construcción condicional es case
, esta puede ser vista como una variación de if. La instrucción case
ejecutará una lista de comandos dados si un ítem especificado, — ejemplo, el contenido de una variable — puede ser encontrado en una lista de ítems separados por pipes (la barra vertical |
) y terminados por )
. En el siguiente ejemplo, el script muestra cómo la construcción case
puede ser usada para indicar el correspondiente formato de empaquetado para una distribución de Linux:
#!/bin/bash DISTRO=$1 echo -n "Distribution $DISTRO uses " case "$DISTRO" in debian | ubuntu | mint) echo -n "the DEB" ;; centos | fedora | opensuse ) echo -n "the RPM" ;; *) echo -n "an unknown" ;; esac echo " package format."
Cada lista de patrones y los comandos asociados deben terminar con ;;
, ;&
, o ;;&
. El último patrón, un asterisco, coincidirá si ningún otro patrón anterior correspondió de antemano. La instrucción esac
(case al revés) termina la construcción case
. Asumiendo que el script anterior se llamaba script.sh
y se ejecuta con opensuse
como primer argumento, se generará la siguiente salida:
$ ./script.sh opensuse Distribution opensuse uses the RPM package format.
Tip
|
Bash tiene una opción llamada |
El elemento buscado y los patrones se someten a la expansión de la tilde, la expansión de los parámetros, la sustitución de los comandos y la expansión aritmética. Si el elemento buscado se especifica con comillas, se eliminarán antes de que se intente la coincidencia.
Construcciones de bucle
Los scripts se utilizan a menudo como herramienta para automatizar tareas repetitivas, realizando el mismo conjunto de comandos hasta que se verifique un criterio. Bash tiene tres instrucciones de bucle — for
, until
y while
— diseñadas para construcciones de bucle ligeramente distintas.
La construcción for
camina a través de una lista dada de elementos — usualmente una lista de palabras o cualquier otro segmento de texto separado del espacio — ejecutando el mismo conjunto de comandos en cada uno de esos elementos. Antes de cada iteración, la instrucción for
asigna el elemento actual a una variable, que puede ser utilizada por los comandos incluidos. El proceso se repite hasta que no quedan más ítems. La sintaxis de la construcción for
es:
for VARNAME in LIST do COMMANDS done
VARNAME
es un nombre arbitrario de una variable de shell y LIST
es cualquier secuencia de términos separados. Los caracteres delimitadores válidos que dividen los elementos de la lista están definidos por la variable de entorno IFS
, que son los caracteres espacio, tabulación y nueva línea por defecto. La lista de comandos a ejecutar está delimitada por las instrucciones do
y done
, por lo que los comandos pueden ocupar tantas líneas como sean necesarias.
En el siguiente ejemplo, el comando for
tomará cada elemento de la lista proporcionada — una secuencia de números — y lo asignará a la variable NUM
, un elemento a la vez:
#!/bin/bash for NUM in 1 1 2 3 5 8 13 do echo -n "$NUM is " if [ $(( $NUM % 2 )) -ne 0 ] then echo "odd." else echo "even." fi done
En el ejemplo, un constructo anidado if
se utiliza junto con una expresión aritmética para evaluar si el número de la variable actual NUM
es par o impar. Asumiendo que el anterior script de muestra se llamaba script.sh
y está en el directorio actual, se generará la siguiente salida:
$ ./script.sh 1 is odd. 1 is odd. 2 is even. 3 is odd. 5 is odd. 8 is even. 13 is odd.
Bash también apoya un formato alternativo a las construcciones for
, con la notación de doble paréntesis. Esta notación se asemeja a la sintaxis de la instrucción for
del lenguaje de programación C y es particularmente útil para trabajar con arreglos:
#!/bin/bash SEQ=( 1 1 2 3 5 8 13 ) for (( IDX = 0; IDX < ${#SEQ[*]}; IDX++ )) do echo -n "${SEQ[$IDX]} is " if [ $(( ${SEQ[$IDX]} % 2 )) -ne 0 ] then echo "odd." else echo "even." fi done
Este script de muestra, generará exactamente la misma salida que el ejemplo anterior. Sin embargo, en lugar de usar la variable NUM
para almacenar un elemento a la vez, la variable IDX
se emplea para rastrear el índice de la matriz actual en orden ascendente, comenzando desde 0 y añadiéndole continuamente mientras está bajo el número de elementos de la matriz SEQ
. El ítem actual se recupera de su posición en la matriz con ${SEQ[$IDX]}
.
De la misma manera, la construcción until
ejecuta una secuencia de comandos hasta que un comando de prueba — como el propio comando test
— termina con el estado 0 (éxito). Por ejemplo, la misma estructura de bucle del ejemplo anterior puede implementarse con until
:
#!/bin/bash SEQ=( 1 1 2 3 5 8 13 ) IDX=0 until [ $IDX -eq ${#SEQ[*]} ] do echo -n "${SEQ[$IDX]} is " if [ $(( ${SEQ[$IDX]} % 2 )) -ne 0 ] then echo "odd." else echo "even." fi IDX=$(( $IDX + 1 )) done
Las construcciones until
pueden requerir más instrucciones que las de for
, pero puede ser más adecuado para los criterios (a la hora detener el bucle) no numéricos proporcionados por las expresiones de test
o cualquier otro comando. Es importante incluir acciones que aseguren un criterio de parada válido, como el incremento de una variable de contador, ya que de lo contrario el bucle puede ejecutarse indefinidamente.
La instrucción while
es similar a la instrucción until
, pero while
sigue repitiendo el conjunto de comandos si el comando de prueba termina con el estado 0 (éxito). Por lo tanto, la instrucción until [ $IDX -eq ${#SEQ[*]} ]` del ejemplo anterior es equivalente a while [ $IDX -lt ${#SEQ[*]} ]
, ya que el bucle debe repetirse mientras el índice de la matriz es menor que el total de los elementos de esta.
Un ejemplo más elaborado
Imagine que un usuario quiere sincronizar periódicamente una colección de sus archivos y directorios con otro dispositivo de almacenamiento montado en el sistema de archivos; dado que un sistema de respaldo con todas las funciones se considera una exageración. Dado que esta es una actividad que debe realizarse periódicamente, es una buena aplicación candidata para automatizar con un script de shell.
La tarea es sencilla: sincronizar cada archivo y directorio contenido en una lista, desde un directorio de origen informado como primer argumento en el script hasta un directorio de destino informado como segundo argumento de este. Para facilitar la adición o eliminación de elementos de la lista, se mantendrá en un archivo separado; un elemento por línea:
$ cat ~/.sync.list Documents To do Work Family Album .config .ssh .bash_profile .vimrc
El archivo contiene una mezcla de archivos y directorios, algunos con espacios en blanco en sus nombres. Este es un escenario adecuado para el comando incorporado de Bash mapfile
, que analizará cualquier contenido de texto y creará una variable de matriz a partir de este, colocando cada línea como un elemento de matriz individual. El archivo de script se llamará synnc.sh
, conteniendo el siguiente contenido:
#!/bin/bash set -ef # List of items to sync FILE=~/.sync.list # Origin directory FROM=$1 # Destination directory TO=$2 # Check if both directories are valid if [ ! -d "$FROM" -o ! -d "$TO" ] then echo Usage: echo "$0 <SOURCEDIR> <DESTDIR>" exit 1 fi # Create array from file mapfile -t LIST < $FILE # Sync items for (( IDX = 0; IDX < ${#LIST[*]}; IDX++ )) do echo -e "$FROM/${LIST[$IDX]} \u2192 $TO/${LIST[$IDX]}"; rsync -qa --delete "$FROM/${LIST[$IDX]}" "$TO"; done
La primera acción que hace el script es redefinir dos parámetros de shell con el comando set
: la opción -e
saldrá de la ejecución inmediatamente si un comando sale con un estado distinto de cero y la opción -f
deshabilitará el globbing de nombres de archivo. Ambas opciones se pueden acortar con -ef
. Este no es un paso obligatorio, pero ayuda a disminuir la probabilidad de un comportamiento inesperado.
Las instrucciones reales orientadas a la aplicación del archivo de script pueden dividirse en tres partes:
Recolectar y comprobar los parámetros del script
+
La variable FILE
es la ruta del archivo que contiene la lista de elementos a copiar: ~/.sync.list
. Las variables FROM
y TO
son el origen y el destino, respectivamente. Dado que estos dos últimos parámetros son proporcionados por el usuario, pasan por una simple prueba de validación realizada por la construcción if
: si alguno de los dos no es un directorio válido — evaluado por la prueba [ ! -d "$FROM" -o ! -d "$TO" ]` — el script mostrará un breve mensaje de ayuda y luego terminará con un estado de salida de 1.
-
Carga la lista de archivos y directorios
Después de definir todos los parámetros, se crea un arreglo que contiene la lista de elementos a copiar con el comando
mapfile -t LIST < $FILE
. La opción-t
demapfile
eliminará el carácter de la nueva línea (newline) de cada línea antes de incluirla en la variable del arregloLIST
. El contenido del archivo indicado por la variableFILE
—~/.sync.list
— se lee a través de la redirección de entrada. -
Realizar la copia e informar al usuario
Un bucle
for
usando notación de doble paréntesis atraviesa el conjunto de elementos, con la variableIDX
llevando la cuenta incremental del índice. El comandoecho
informará al usuario de cada elemento que se está copiando. El carácter unicode escape —\u2192
— para el carácter right arrow está presente en el mensaje de salida, por lo que la opción-e
del comandoecho
debe ser usada. El comandorsync
copiará selectivamente sólo las piezas del archivo modificado desde el origen, por lo que se recomienda su uso para tales tareas. Las opciones dersync
comoq
ya
, condensadas enqa
, inhibirán los mensajesrsync
y activarán el modoarchivo
, donde se conservan todas las propiedades del archivo. La opción--delete
hará quersync
elimine un elemento en el destino que ya no existe en el origen, por lo que debe ser usado con cuidado.
Asumiendo que todos los elementos de la lista existen en el directorio principal del usuario carol
, /home/carol
, y que el directorio de destino /media/carol/backup
apunta a un dispositivo de almacenamiento externo montado, el comando sync.sh /home/carol /media/carol/backup
generará la siguiente salida:
$ sync.sh /home/carol /media/carol/backup /home/carol/Documents → /media/carol/backup/Documents /home/carol/"To do" → /media/carol/backup/"To do" /home/carol/Work → /media/carol/backup/Work /home/carol/"Family Album" → /media/carol/backup/"Family Album" /home/carol/.config → /media/carol/backup/.config /home/carol/.ssh → /media/carol/backup/.ssh /home/carol/.bash_profile → /media/carol/backup/.bash_profile /home/carol/.vimrc → /media/carol/backup/.vimrc
El ejemplo también supone que el guión se ejecuta por root o por el usuario carol
, ya que la mayoría de los archivos serían ilegibles para otros usuarios. Si script.sh
no está dentro de un directorio listado en la variable de entorno PATH
, entonces debe ser especificado con su ruta completa.
Ejercicios guiados
-
¿Cómo podría usarse el comando
test
para verificar si la ruta del archivo almacenado en la variableFROM
es más reciente que el archivo cuya ruta está en la variableTO
? -
El siguiente guión debe imprimir una secuencia numérica del 0 al 9, pero en cambio imprime indefinidamente el 0. ¿Qué se debe hacer para obtener el resultado esperado?
#!/bin/bash COUNTER=0 while [ $COUNTER -lt 10 ] do echo $COUNTER done
-
Supongamos que un usuario escribió un script que requiere una lista ordenada de nombres de usuario. La lista ordenada resultante se presenta como la siguiente en su computadora:
carol Dave emma Frank Grace henry
Sin embargo, la misma lista está ordenada como la siguiente en la computadora de su colega:
Dave Frank Grace carol emma henry
¿Cómo podría explicar las diferencias entre las dos listas clasificadas?
Ejercicios de exploración
-
¿Cómo podrían usarse todos los argumentos en la línea de comandos del script para inicializar una matriz Bash?
¿Por qué, contrariamente a la intuición, el comando `test 1 > 2` se evalúa como verdadero?
-
¿Cómo podría un usuario cambiar temporalmente el separador de campos predeterminado por el carácter de nueva línea solamente, sin dejar de poder revertirlo a su contenido original?
Resumen
Esta lección profundiza en las pruebas disponibles para el comando test
y en otras construcciones condicionales y de bucle necesarias para escribir scripts de shell más elaborados. Se da un simple script de sincronización de archivos como ejemplo de una aplicación práctica de shell script. La lección abarcó los siguientes pasos:
-
Pruebas extendidas para las construcciones condicionales
if
ycase
. -
Construcciones de bucle en shell:
for
,until
ywhile
. -
Iterando a través de arreglos y parámetros.
Los comandos y procedimientos abordados fueron:
test
-
Realiza una comparación entre los artículos suministrados al comando.
if
-
Una construcción lógica utilizada en scripts para evaluar algo como verdadero o falso, luego bifurca la ejecución del comando en función de los resultados.
case
-
Evalúa varios valores frente a una sola variable. La ejecución del comando de secuencia de comandos se lleva a cabo dependiendo del resultado del comando
case
. for
-
Repite la ejecución de un comando según un criterio dado.
until
-
Repite la ejecución de un comando hasta que una expresión se evalúe como falsa.
while
-
Repite la ejecución de un comando mientras una expresión dada se evalúa como verdadera.
Respuesta a los ejercicios guiados
-
¿Cómo podría usarse el comando
test
para verificar si la ruta del archivo almacenado en la variableFROM
es más reciente que el archivo cuya ruta está en la variableTO
?El comando
test "$ FROM" -nt "$TO"
devolverá un código de estado en 0 si el archivo en la variableFROM
es más reciente que el archivo en la variableTO
. -
El siguiente guión debe imprimir una secuencia numérica del 0 al 9, pero en cambio imprime indefinidamente el 0. ¿Qué se debe hacer para obtener el resultado esperado?
#!/bin/bash COUNTER=0 while [ $COUNTER -lt 10 ] do echo $COUNTER done
La variable
COUNTER
debe ser incrementada, lo que podría hacerse con la expresión aritméticaCOUNTER=$$COUNTER + 1
, para eventualmente alcanzar el criterio de parada y terminar el bucle. -
Supongamos que un usuario escribió un script que requiere una lista ordenada de nombres de usuario. La lista ordenada resultante se presenta como la siguiente en su computadora:
carol Dave emma Frank Grace henry
Sin embargo, la misma lista está ordenada como la siguiente en la computadora de su colega:
Dave Frank Grace carol emma henry
¿Cómo podría explicar las diferencias entre las dos listas clasificadas?
La clasificación se basa en la ubicación del sistema actual. Para evitar las inconsistencias, las tareas de clasificación deben ser realizadas con la variable de entorno
LANG
puesta enC
.
Respuestas a los ejercicios de exploración
-
¿Cómo podrían usarse todos los argumentos en la línea de comandos del script para inicializar una matriz Bash?
Los comandos
PARAMS=( $* )
oPARAMS=( "$@" )
crearán una matriz llamadaPARAMS
con todos los argumentos. -
¿Por qué, contrariamente a la intuición, el comando
test 1 > 2
se evalúa como verdadero?El operador
>
está pensado para ser usado con cadenas de caracteres, no con pruebas numéricas. -
¿Cómo podría un usuario cambiar temporalmente el separador de campos predeterminado por el carácter de nueva línea solamente, sin dejar de poder revertirlo a su contenido original?
Una copia de la variable
IFS
puede ser almacenada en otra variable:OLDIFS=$IFS
. Entonces el nuevo separador de línea se define conIFS=$'\n'
y la variable IFS puede ser revertida conIFS=$OLDIFS
.