105.2 Lección 1
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: |
1 de 2 |
Introducción
El entorno del shell de Linux permite el uso de archivos — llamados scripts — que contienen comandos de cualquier programa disponible en el sistema, combinado con comandos en shell para automatizar las tareas personalizadas de un usuario y/o un sistema. De hecho, muchas de las tareas de mantenimiento del sistema operativo son realizadas por scripts que consisten en secuencias de comandos, estructuras de decisión y bucles condicionales. Aunque la mayoría de las veces los scripts están destinados a tareas relacionadas con el propio sistema operativo, también son útiles para tareas orientadas al usuario, como el renombramiento masivo de archivos, la recopilación y el análisis sintáctico de datos o cualquier otra actividad repetitiva de la línea de comandos. Los scripts no son más que archivos de texto que se comportan como programas. En un programa — el intérprete — lee y ejecuta las instrucciones que aparecen en el script. El intérprete también puede iniciar una sesión interactiva donde los comandos — incluyendo los scripts — se leen y ejecutan a medida que se introducen, como es el caso de las sesiones de shell de Linux. Los archivos de script pueden agrupar esas instrucciones y comandos cuando se vuelven demasiado complejos para ser implementados como un alias o una función de shell personalizada. Además, los archivos de script pueden ser mantenidos como los programas convencionales y, al ser sólo archivos de texto, pueden ser creados y modificados con cualquier editor de texto simple.
Estructura y ejecución del script
Básicamente, un archivo de script es una secuencia ordenada de comandos que debe ser ejecutada por un intérprete de comandos correspondiente. La forma en que un intérprete lee un archivo de script varía y hay distintas maneras de hacerlo en una sesión de shell Bash, pero el intérprete por defecto de un archivo de script será el indicado en la primera línea del script, justo después de los caracteres #!
(conocido como shebang). En un script con instrucciones para el shell Bash, la primera línea debe ser #!/bin/bash
. Al indicar esta línea, el intérprete de todas las instrucciones del archivo será /bin/bash
. Excepto la primera línea, todas las demás líneas que empiezan con el carácter de hash #
serán ignoradas, así que pueden ser usadas para colocar recordatorios y comentarios. Las líneas en blanco también se ignoran. Por lo tanto, un archivo de script de shell muy conciso puede ser escrito de la siguiente manera:
#!/bin/bash # A very simple script echo "Cheers from the script file! Current time is: " date +%H:%M
Este script sólo tiene dos instrucciones para el intérprete de /bin/bash
: el comando incorporado echo
y el comando date
. La forma más básica de ejecutar un archivo de script es ejecutar el intérprete con la ruta del script como argumento. Así que, asumiendo que el ejemplo anterior fue guardado en un archivo de script llamado script.sh
en el directorio actual, será leído e interpretado por Bash con el siguiente comando:
$ bash script.sh Cheers from the script file! Current time is: 10:57
El comando echo
añadirá automáticamente una nueva línea después de mostrar el contenido, pero la opción -n
suprimirá este comportamiento. Por lo tanto, el uso de echo -n
en el script hará que la salida de ambos comandos aparezca en la misma línea:
$ bash script.sh Cheers from the script file! Current time is: 10:57
Aunque no es obligatorio, el sufijo .sh
ayuda a identificar los scripts de shell a la hora de listarlos o buscarlos.
Tip
|
Bash llamará a cualquier comando que se indique después de la |
Si el archivo de escritura está destinado a ser ejecutado por otros usuarios del sistema, es importante comprobar si se han establecido los permisos de lectura adecuados. El comando chmod o+r script.sh
dará permiso de lectura a todos los usuarios del sistema, permitiéndoles ejecutar script.sh
colocando la ruta del archivo como argumento del comando bash
. Alternativamente, el script puede tener el permiso de ejecución para que el archivo pueda ser ejecutado como un comando convencional. El bit de ejecución se activa en el archivo script con el comando chmod
:
$ chmod +x script.sh
Con el bit de ejecución activado, el archivo de script llamado script.sh
en el directorio actual puede ser ejecutado directamente con el comando ./script.sh
. Los scripts ubicados en un directorio listado en la variable de entorno PATH
también serán accesibles sin su ruta completa.
Warning
|
Un script que realiza acciones restringidas puede tener su permiso SUID activado, por lo que los usuarios normales también pueden ejecutar el script con privilegios de root. En este caso, es muy importante asegurarse de que ningún usuario que no sea root tenga el permiso para escribir en el archivo. De lo contrario, un usuario ordinario podría modificar el archivo para realizar operaciones arbitrarias y potencialmente dañinas. |
La colocación y la indentación de los comandos en los archivos de escritura no son demasiado rígidos. Cada línea de un script se ejecutará como un comando de shell ordinario. En la misma secuencia en que la línea aparece en el archivo de script, y las mismas reglas que se aplican al prompt, también se aplica a cada línea del script de manera individual. Es posible colocar dos o más comandos en la misma línea, separados por punto y coma:
echo "Cheers from the script file! Current time is:" ; date +%H:%M
Aunque este formato puede ser conveniente a veces, su uso es opcional, ya que los comandos secuenciales pueden colocarse un comando por línea y se ejecutarán tal como estaban separados por punto y coma. En otras palabras, el punto y coma puede ser reemplazado por un nuevo carácter de línea en los script Bash.
Cuando se ejecuta un script, los comandos contenidos en este no se ejecutan directamente en la sesión actual, sino que se ejecutan mediante un nuevo proceso Bash, llamado sub-shell. Este evita que el script sobrescriba las variables de entorno de la sesión actual y que deje modificaciones desatendidas en la sesión actual. Si el objetivo es ejecutar el contenido del script en la sesión de shell actual, entonces debe ser ejecutado con source script.sh
o . script.sh
(note que hay un espacio entre el punto y el nombre del script).
Como sucede con la ejecución de cualquier otro comando, el prompt del shell sólo estará disponible de nuevo cuando el script termine su ejecución y su código (estado) de salida estará disponible en la variable $?
. Para cambiar este comportamiento, de modo que el shell actual también termine cuando el script lo haga, el script — o cualquier otro comando — puede ser precedido por el comando exec
. Este comando también reemplazará el código de estado de salida de la sesión actual del shell por el suyo propio.
Variables
Las variables en los shell scripts se comportan de la misma manera que en las sesiones interactivas, dado que el intérprete es el mismo. Por ejemplo, el formato SOLUTION=42
(sin espacios alrededor del signo igual) asignará el valor 42
a la variable denominada SOLUTION
. Por convención, las letras mayúsculas se usan para los nombres de las variables, pero no es obligatorio. Los nombres de las variables no pueden, sin embargo, comenzar con caracteres no alfabéticos.
Además de las variables ordinarias creadas por el usuario; los scripts de Bash también tienen un conjunto de variables especiales llamadas parámetros. A diferencia de las variables ordinarias, los nombres de los parámetros comienzan con un carácter no alfabético que designa su función. Los argumentos que se pasan a un guión y otra información útil se almacenan en parámetros como $0
, $*
, $?
, etc., donde el carácter que sigue al signo del dólar indica la información que se debe buscar:
$*
-
Todos los argumentos pasaron al script.
$@
-
Todos los argumentos pasados al script. Si se usa con comillas dobles como en
"$@"
, todos los argumentos serán encerrados entre comillas dobles. $#
-
El número de argumentos
$0
-
El nombre del script.
$!
-
PID del último programa ejecutado
$$
-
PID del shell actual.
$?
-
El status (código) del último comando terminado. En los procesos POSIX, un valor numérico de
0
significa que el último comando se ejecutó con éxito, lo que también se aplica a los scripts de shell.
Un parámetro posicional es un parámetro denotado por uno o más dígitos, aparte del dígito 0
. Por ejemplo, la variable $1
corresponde al primer argumento dado al script (parámetro posicional uno), $2
corresponde al segundo argumento, y así sucesivamente. Si la posición de un parámetro es mayor que nueve, debe ser referenciada con llaves, como ${10}
, ${11}
, etc.
En cambio, las variables ordinarias están destinadas a almacenar valores insertados manualmente o la salida generada por otros comandos. Por ejemplo, el comando "read", puede ser usado dentro del script para pedirle al usuario que introduzca datos durante la ejecución del mismo:
echo "Do you want to continue (y/n)?" read ANSWER
El valor devuelto se almacenará en la variable ANSWER
. Si no se suministra el nombre de la variable, se usará por defecto el nombre de la variable REPLY
. También es posible usar el comando read
para leer más de una variable simultáneamente:
echo "Type your first name and last name:" read NAME SURNAME
En este caso, cada término separado del espacio será asignado a las variables NAME
y SURNAME
respectivamente. Si el número de términos dados es mayor que el número de variables, los términos excedentes se almacenarán en la última variable. Inclusive read
puede mostrar el mensaje al usuario con la opción -p
, haciendo que el comando echo
sea redundante en este caso:
read -p "Type your first name and last name:" NAME SURNAME
Los scripts que realizan tareas del sistema, a menudo requieren información proporcionada por otros programas. La notación backtick puede usarse para almacenar la salida de un comando en una variable:
$ OS=`uname -o`
En el ejemplo, la salida del comando "uname -o" se almacenará en la variable OS
. Un resultado idéntico se producirá con "$()`:
$ OS=$(uname -o)
La longitud de una variable, es decir, la cantidad de caracteres que contiene, se devuelve preparando un hash #
antes del nombre de la variable. Esta característica, requiere el uso de las llaves para indicar la variable:
$ OS=$(uname -o) $ echo $OS GNU/Linux $ echo ${#OS} 9
Bash también cuenta con variables de matriz unidimensional, por lo que un conjunto de elementos relacionados puede ser almacenado con un solo nombre de variable. Cada elemento de una matriz tiene un índice numérico, que debe utilizarse para escribir y leer valores en el elemento correspondiente. A diferencia de las variables ordinarias, las matrices deben ser declaradas con el comando incorporado en Bash declare
. Por ejemplo, para declarar una variable llamada SIZES
como una matriz:
$ declare -a SIZES
Las matrices también pueden declararse implícitamente partir de una lista predefinida de elementos, utilizando la notación de paréntesis:
$ SIZES=( 1048576 1073741824 )
En el ejemplo, los dos valores enteros se almacenaron en la matriz SIZES
. Se debe hacer referencia a los elementos de la matriz mediante llaves y corchetes. De lo contrario, Bash no cambiará ni mostrará el elemento correctamente. Como los índices de matriz comienzan en 0, el contenido del primer elemento está en ${SIZES[0]}
, el segundo elemento está en ${SIZES[1]}
, y así sucesivamente:
$ echo ${SIZES[0]} 1048576 $ echo ${SIZES[1]} 1073741824
A diferencia de la lectura, el cambio del contenido de un elemento de la matriz se realiza sin las llaves (Por ejemplo, SIZES[0]=1048576
). Al igual que con las variables ordinarias, la longitud de un elemento en una matriz se devuelve con el carácter númeral (por ejemplo, ${#SIZES[0]}
para la longitud del primer elemento en la matriz SIZES
). Se devuelve el número total de elementos en una matriz si se usa @
o *
como índice:
$ echo ${#SIZES[@]} 2 $ echo ${#SIZES[*]} 2
También se pueden declarar las matrices utilizando la salida de un comando como elementos iniciales mediante la sustitución de comandos. El siguiente ejemplo muestra cómo crear una matriz Bash cuyos elementos son los sistemas de archivos soportados por el sistema actual:
$ FS=( $(cut -f 2 < /proc/filesystems) )
El comando cut -f 2 < /proc/filesystems
mostrará todos los sistemas de archivos actualmente soportados por el kernel en ejecución (como se indica en la segunda columna del archivo /proc/filesystems
), por lo que el arreglo FS
ahora contiene un elemento para cada sistema de archivos soportado. Se puede utilizar cualquier contenido de texto para inicializar un arreglo ya que, por defecto, cualquier término delimitado por los caracteres espacio, tabulación o nueva línea (newline) se convertirá en un elemento del arreglo.
Tip
|
Bash trata cada carácter de una variable de entorno |
Expresiones aritméticas
Bash proporciona un método práctico para realizar operaciones aritméticas enteras con el comando incorporado expr
. Dos variables numéricas, $VAL1
y $VAL2
por ejemplo, se pueden sumar con el siguiente comando:
$ SUM=`expr $VAL1 + $VAL2`
El resultado del ejemplo estará disponible en la variable $SUM
. El comando expr
puede ser reemplazado por $(())
, así que el ejemplo anterior puede ser reescrito como SUM=$(($VAL1+$VAL2))
. Las expresiones de poder también están permitidas con el operador de doble asterisco, por lo que la declaración anterior del arreglo SIZES=(1048576 1073741824)
puede ser reescrita como SIZES=($((1024**2)) $((1024**3)) )
.
La sustitución de comandos también se puede utilizar en las expresiones aritméticas. Por ejemplo, el archivo /proc/meminfo
tiene información detallada sobre la memoria del sistema, incluyendo el número de bytes libres en la RAM:
$ FREE=$(( 1000 * `sed -nre '2s/[^[:digit:]]//gp' < /proc/meminfo` ))
El ejemplo muestra cómo el comando sed
puede ser usado para analizar el contenido de /proc/meminfo
dentro de la expresión aritmética. La segunda línea del archivo /proc/meminfo
contiene la cantidad de memoria libre en miles de bytes, así que la expresión aritmética la multiplica por 1000 para obtener el número de bytes libres en la RAM.
Ejecución condicional
Algunos scripts no suelen estar destinados a ejecutar todos los comandos de su contenido, sino sólo aquellos comandos que coinciden con un criterio predefinido. Por ejemplo, un script de mantenimiento puede enviar un mensaje de advertencia al correo electrónico del administrador sólo si la ejecución de un comando falla. Bash proporciona métodos específicos para evaluar el éxito de la ejecución del comando y estructuras condicionales generales, más similares a los que se encuentran en los lenguajes de programación más populares.
Al separar los comandos con &&
, el comando de la derecha se ejecutará sólo si el comando de la izquierda no encontró un error, es decir, si su estado de salida era igual a 0
:
COMMAND A && COMMAND B && COMMAND C
El comportamiento opuesto ocurre si los comandos se separan con ||
. En este caso, el siguiente comando se ejecutará sólo si el comando anterior encontró un error, es decir, si su código de estado retornó un valor difernte a 0.
Una de las características más importantes de todos los lenguajes de programación es la capacidad de ejecutar comandos dependiendo de condiciones previamente definidas. La forma más sencilla de ejecutar comandos condicionalmente es utilizar el comando incorporado en Bash if
, que ejecuta uno o más comandos sólo si el comando dado como argumento devuelve un código de estado "0" (éxito). Otro comando, test
puede ser usado para evaluar muchos criterios especiales, por lo que se usa mayormente en conjunto con if
. En el siguiente ejemplo, el mensaje Confirmed: /bin/bash is executable.
se mostrará si el archivo bin/bash
existe y es ejecutable:
if test -x /bin/bash ; then echo "Confirmed: /bin/bash is executable." fi
La opción -x
hace que el comando test
devuelva un código de estado "0" sólo si la ruta dada es un archivo ejecutable. El siguiente ejemplo muestra otra forma de conseguir exactamente el mismo resultado, ya que los corchetes pueden utilizarse como sustituto de`test`:
if [ -x /bin/bash ] ; then echo "Confirmed: /bin/bash is executable." fi
La instrucción else
es opcional a la estructura if
y puede, si está presente, definir un comando o secuencia de comandos a ejecutar si la expresión condicional no es verdadera:
if [ -x /bin/bash ] ; then echo "Confirmed: /bin/bash is executable." else echo "No, /bin/bash is not executable." fi
Las estructuras if
siempre deben terminar con fi
, así el intérprete de Bash sabe donde terminan los comandos condicionales.
Salidas de un Script
Incluso cuando la finalidad de una secuencia de comandos sólo implica operaciones orientadas a archivos, es importante mostrar mensajes relacionados con el progreso en la salida estándar, de modo que el usuario se mantenga informado de cualquier problema y pueda eventualmente utilizar esos mensajes para generar registros de operaciones.
El comando incorporado de Bash echo
se utiliza comúnmente para mostrar cadenas simples de texto, pero también proporciona algunas características extendidas. Con la opción -e
, el comando echo
es capaz de mostrar caracteres especiales usando secuencias de escape (una secuencia de barra invertida que designa un carácter especial). Por ejemplo:
#!/bin/bash # Obtiene el nombre genérico del sistema operativo OS=$(uname -o) # Obtiene la cantidad de memoria libre en bytes FREE=$(( 1000 * `sed -nre '2s/[^[:digit:]]//gp' < /proc/meminfo` )) echo -e "Operating system:\t$OS" echo -e "Unallocated RAM:\t$(( $FREE / 1024**2 )) MB"
Mientras que el uso de comillas es opcional cuando se utiliza echo
sin opciones, es necesario añadirlas cuando se utiliza la opción -e
, de lo contrario los caracteres especiales no se renderizarán correctamente. En el script anterior, ambos comandos echo
utilizan el carácter de tabulación \t
para alinear el texto, lo que resulta en la siguiente salida:
Operating system: GNU/Linux Unallocated RAM: 1491 MB
El carácter de nueva línea \n
puede ser usado para separar las líneas de salida, así que la misma salida se obtiene combinando los dos comandos echo
en uno solo:
echo -e "Operating system:\t$OS\nUnallocated RAM:\t$(( $FREE / 1024**2 )) MB"
Aunque es apto para mostrar la mayoría de los mensajes de texto, el comando echo
puede no ser adecuado para mostrar patrones de texto más específicos. El comando printf
incorporado en la barra de herramientas, brinda más control sobre cómo mostrar las variables. El comando printf
utiliza el primer argumento como formato de la salida, donde los marcadores de posición serán reemplazados por los siguientes argumentos en el orden en que aparecen en la línea de comandos. Por ejemplo, el mensaje del ejemplo anterior podría generarse con el siguiente comando printf
:
printf "Operating system:\t%s\nUnallocated RAM:\t%d MB\n" $OS $(( $FREE / 1024**2 ))
El marcador de posición %s
está destinado al contenido del texto (será reemplazado por la variable $OS
) y el marcador de posición %d
está destinado a los números enteros (será reemplazado por el número resultante de megabytes libres en la RAM). El comando`printf` no añade un carácter de nueva línea al final del texto, por lo que el carácter de nueva línea \n
debe ser colocado al final del patrón si es necesario. Todo el patrón debe ser interpretado como un único argumento, por lo que debe ser incluido entre comillas.
Tip
|
El formato de sustitución realizado por |
Con printf
, las variables se colocan fuera del patrón de texto, lo que permite almacenar el patrón de texto en una variable separada:
MSG='Operating system:\t%s\nUnallocated RAM:\t%d MB\n' printf "$MSG" $OS $(( $FREE / 1024**2 ))
Este método es particularmente útil para mostrar distintos formatos de salida según las necesidades del usuario. Por ejemplo, facilita la escritura de un script que utilice un patrón de texto distinto, como puede ser una lista CSV (Comma Separated Values) en lugar de un mensaje de salida predeterminado.
Ejercicios guiados
-
La opción
-s
del comandoread
es útil para introducir contraseñas, ya que no mostrará el contenido que se está escribiendo en la pantalla. ¿Cómo podría usarse este comando para almacenar la entrada del usuario en la variablePASSWORD
mientras se oculta el contenido escrito? -
El único propósito del comando
whoami
es mostrar el nombre de usuario que lo ha llamado, por lo que se utiliza principalmente dentro de los scripts para identificar al usuario que lo está ejecutando. Dentro de un script Bash, ¿cómo podría la salida del comandowhoami
ser almacenada en la variable llamadaWHO
? -
¿Qué operador de Bash debería estar entre los comandos
apt-get dist-upgrade
ysystemctl reboot
, si el usuario root quisiera ejecutar este último y solo si el comandoapt-get dist-upgrade
haya terminado con éxito?
Ejercicios de exploración
-
Después de intentar ejecutar un script Bash recién creado, el usuario recibe el siguiente mensaje de error:
bash: ./script.sh: Permission denied
Considerando que el archivo
./script.sh
fue creado por el mismo usuario, ¿cuál sería la causa probable de este error? -
Supongamos que un script llamado
do.sh
es ejecutable y el enlace simbólico llamadodo.sh
apunta a este. Dentro del script, ¿cómo podrías identificar si el nombre del archivo de llamada erado.sh
oundo.sh
? -
En un sistema con un servicio de correo electrónico correctamente configurado, el comando
mail -s "Error de mantenimiento" root <<<"Error de tarea programada"
envía el mensaje de correo electrónico de aviso al usuario root. Tal comando podría ser usado en tareas desatendidas, como cronjobs, para informar al administrador del sistema sobre un problema inesperado. Escriba una construcción if que ejecutará comando mencionadomail
si el estado de salida (sea cual sea) no tiene éxito.
Resumen
Esta lección cubre los conceptos básicos para comprender y escribir scripts de Bash shell. Los scripts de shell son una parte fundamental de cualquier distribución de Linux, ya que ofrecen una forma muy flexible de automatizar las tareas del usuario y del sistema que se realizan en el entorno de shell. En la lección se observaron los siguientes puntos:
-
Estructura y permisos correctos en la creción de scripts shell
-
Parámetros de script
-
Usando variables para leer la entrada del usuario y para almacenar la salida de los comandos
-
Arreglos en Bash
-
Pruebas simples y ejecución condicional
-
Formato de salida
Los comandos y procedimientos abordados fueron:
-
Notación incorporada de Bash para sustitución de comandos, expansión de matrices y expresiones aritméticas
-
Ejecución de comandos condicionales con los operadores
|| y `&&
-
echo
-
chmod
-
exec
-
read
-
declare
-
test
-
if
-
printf
Respuesta a los ejercicios guiados
-
La opción
-s
del comandoread
es útil para introducir contraseñas, ya que no mostrará el contenido que se está escribiendo en la pantalla. ¿Cómo podría usarse este comando para almacenar la entrada del usuario en la variablePASSWORD
mientras se oculta el contenido escrito?read -s PASSWORD
-
El único propósito del comando
whoami
es mostrar el nombre de usuario que lo ha llamado, por lo que se utiliza principalmente dentro de los scripts para identificar al usuario que lo está ejecutando. Dentro de un script Bash, ¿cómo podría la salida del comandowhoami
ser almacenada en la variable llamadaWHO
?WHO=`whoami`
orWHO=$(whoami)
-
¿Qué operador de Bash debería estar entre los comandos
apt-get dist-upgrade
ysystemctl reboot
, si el usuario root quisiera ejecutar este último y solo si el comandoapt-get dist-upgrade
haya terminado con éxito?El operador
&&
, como enapt-get dist-upgrade && systemctl reboot
.
Respuestas a los ejercicios de exploración
-
Después de intentar ejecutar un script Bash recién creado, el usuario recibe el siguiente mensaje de error:
bash: ./script.sh: Permission denied
Considerando que el archivo
./script.sh
fue creado por el mismo usuario, ¿cuál sería la causa probable de este error?El archivo
./script.sh
no tiene el permiso de ejecución habilitado. -
Supongamos que un script llamado
do.sh
es ejecutable y el enlace simbólico llamadodo.sh
apunta a este. Dentro del script, ¿cómo podrías identificar si el nombre del archivo de llamada erado.sh
oundo.sh
?La variable especial
$0
contiene el nombre de archivo usado para llamar al script. -
En un sistema con un servicio de correo electrónico correctamente configurado, el comando
mail -s "Error de mantenimiento" root <<<"Error de tarea programada"
envía el mensaje de correo electrónico de aviso al usuario root. Tal comando podría ser usado en tareas desatendidas, como cronjobs, para informar al administrador del sistema sobre un problema inesperado. Escriba una construcción if que ejecutará comando mencionadomail
si el estado de salida (sea cual sea) no tiene éxito.if [ "$?" -ne 0 ]; then mail -s "Maintenance Error" root <<<"Scheduled task error"; fi