Aspectos avanzados de la programación shell
Presentación
Este capítulo presenta otras funcionalidades utilizadas en la programación shell que completan las abordadas en el capítulo Las bases de la programación shell.
Cuando los scripts de ejemplo no sean compatibles con el intérprete utilizado por el lector, le invitamos a recuperar los ejemplos del área de descarga, que se proporcionan para diferentes shells presentados en este libro.
Comparación de las variables $* y $@
1. Utilización de $* y de $@ sin comillas
Las variables $* y $@ contienen la lista de los argumentos de un script shell. Cuando no están entre comillas dobles, son equivalentes.
Ejemplo
El script test_var1.sh muestra el valor de cada argumento de la línea de comandos:
$ nl test_var1.sh
1 #! /usr/bin/ksh
2 # compatibilidad del script: posix, ksh, bash
3
4 contador=1 ;
5 # $* : todos los espacios se ven como separadores de palabras
6 for arg in $ # Equivalente a $@
7 do
8 echo "Argumento $contador : $arg"
9 contador=$(( contador + 1 ))
10 done
A continuación, un ejemplo de llamada al script:
$ test_var1.sh a b c "d e f" g
Primera etapa: el shell actual trata los caracteres de protección antes de ejecutar el script. A este nivel, los espacios internos en "d e f" se protegen y no son vistos como separadores de palabras, sino como caracteres cualesquiera.
Segunda etapa: el shell hijo interpreta el script sustituido...
Manipulación de variables
posix |
ksh |
bash |
La manipulación de variables ha sido tratada en el capítulo Las bases de la programación shell - Las variables de usuario. Esta sección presenta nuevas funcionalidades disponibles en los shells bash y ksh.
1. Longitud del valor contenido en una variable
Sintaxis
${#variable}
Ejemplo
$ var="mi cadena"
$ echo ${#var}
9
$
2. Eliminar el fragmento al inicio de la cadena
Sintaxis
${variable#patrón}
donde patrón es una cadena de caracteres que puede incluir los caracteres especiales *, ?, [], ?(expresión), +(expresión), *(expresión), @(expresión), !(expresión) (ver capítulo Mecanismos esenciales del shell - Sustitución de nombres de archivos).
El carácter # significa "Cadena lo más corta posible al inicio de la cadena".
Ejemplo
Mostrar la variable linea sin su primer campo:
$ linea=”campo1:campo2:campo3”
$ echo ${linea#*:}
campo2:campo3
La expresión "*:" significa: 0 a n caracteres seguidos del carácter ":".
3. Eliminar el fragmento más grande al inicio de la cadena
Sintaxis
${variable##patrón}
Los caracteres ## significan "Cadena lo más larga posible al inicio de la cadena".
Ejemplo
Mostrar el último campo de la variable...
Tablas con índices numéricos
ksh |
bash |
Los shells recientes permiten trabajar con tablas de una dimensión. Los elementos de una tabla se indexan a partir del número 0. El término "índice" es sinónimo de "clave numérica".
1. Definición e inicialización de una tabla
Los elementos de una tabla se pueden asignar de manera global o uno a uno.
bash |
Sintaxis
Definición de una tabla:
declare -a nombretabla
Inicialización global de una tabla:
nombretabla=( val1 val2 ... valn )
Definición e inicialización global de una tabla:
declare -a nombretabla=( val1 val2 ... valn )
Ejemplos
$ declare -a tab
$ tab=( 10 11 12 13 palabrat1 palabra2 )
o
$ declare -a tab=( 10 11 12 13 palabra1 palabra2 )
En bash, el comando typeset es un sinónimo de declare.
ksh 88 y 93 |
Sintaxis
Definición de una tabla:
set -A nombretabla
Definición e inicialización global de una tabla:
set -A nombretabla val1 val2 ... valn
Ejemplo
$ set -A tab 10 11 12 palabra1 palabra2
ksh93 |
Otra sintaxis ksh93
Definición e inicialización global de una tabla:
nombretabla=( val1 val2 ... valn )
2. Asignar un elemento de una tabla
Los elementos se pueden inicializar, modificar o añadir de la siguiente manera:
Sintaxis
nombretabla[índice]=valor
Ejemplo
$ tab[0]=10
$ tab[2]=12
Una casilla de la tabla no inicializada es vacío.
Este manera de proceder crea directamente la tabla si no se ha definido. La sintaxis de definición que hemos visto anteriormente (sección Definición e inicialización de una tabla) es más clara, pero no es obligatoria.
3. Valor de un elemento
Sintaxis
${nombretabla[índice]}
Ejemplo
Mostrar el elemento de índice 0:
$ echo ${tab[0]}
10
Mostrar el elemento de índice 2:
$ echo ${tab[2]}
12
Las llaves son obligatorias.
4. Referenciar todos los elementos...
Tablas asociativas
ksh93 |
bash4 |
Las tablas asociativas son tablas cuyas claves son cadenas de caracteres. Funcionan como las tablas con índice numérico, pero a diferencia de estas, es imposible incrementar las claves, ya que estas últimas no son numéricas.
1. Definir e inicializar una tabla asociativa
bash4 |
Encontramos el mismo comando que para tablas con índice numérico, pero con la opción -A.
Definición de una tabla:
$ declare -A tabAsoc # declare y typeset son sinónimos en bash
Inicialización de una tabla:
$ tabAsoc=([apellido]="Pérez López" [nombre]=Cristina)
Definición e inicialización de una tabla (declare o typeset) :
$ declare -A tabAsoc=([apellido]="Pérez López" [nombre]=Cristina)
ksh93 |
bash4 |
$ typeset -A tabAsoc
$ tabAsoc=([apellido]="Pérez López" [nombre]=Cristina)
o
$ typeset -A tabAssoc=([apellido]="Pérez López" [nombre]=Cristina)
2. Mostrar el valor asociado a una clave
$ echo ${tabAsoc[apellidos]}
Perez Lopez
3. Mostrar la lista de las claves
$ echo ${!tabAsoc[*]}
apellidos nombre
4. Mostrar la lista de valores
$ echo ${tabAsoc[*]}
Perez Lopez Cristina
5. Bucle sobre una tabla asociativa
$ for clave in ${!tabAsoc[*]}
> do
> echo "Clave : $clave...
Inicialización de parámetros posicionales con set
El comando set llamado sin ninguna opción pero seguido de argumentos asigna estos últimos a los parámetros posicionales ($1, $2, ..., $*, $@, $#). Esto permite manipular fácilmente el resultado de sustituciones diversas.
Ejemplo
Ejecución del comando date:
$ date
Vier Mar 18 11:23:50 CET 2022
El resultado del comando date se asigna a los parámetros posicionales:
$ set $(date)
$ echo $1
vier
$ echo $2
marzo
$ echo $4
11:23:50
$ echo $*
vier. marzo 18 11:23:50 CET 2022
$ echo $#
6
$
Funciones
Las funciones sirven para agrupar comandos que tienen que ejecutarse en varios sitios en el transcurso de la ejecución de un script.
1. Definición de una función
La definición de una función tiene que hacerse antes de su primera llamada.
Primera sintaxis
bourne |
posix |
ksh |
bash |
Los paréntesis indican al shell que mifuncion es una función.
Definición de la función:
mifuncion() {
comando1
comando2
...
}
Llamada a la función:
mifuncion
Segunda sintaxis
ksh |
bash |
La palabra clave function remplaza los paréntesis usados en la primera sintaxis.
Definición de la función:
function mifuncion {
comando1
comando2
...
}
Llamada a la función:
mifuncion
En un script que contenga funciones, los comandos situados fuera del cuerpo de las funciones se ejecutan secuencialmente.
Para que los comandos localizados en una función se ejecuten, hay que realizar una llamada a una función. Una función puede llamarse tanto desde del programa principal como desde otra función.
Ejemplos
Uso de la primera sintaxis:
$ nl func1.sh
1 f1() { # Definición de la función
2 echo "En f1"
3 }
4 echo "1º comando"
5 echo "2º comando"
6 f1 # Llamada a la función
7 echo "3º comando"
$ func1.sh
1º comando
2º comando
En f1
3º comando
$
El mismo script usando la segunda sintaxis:
$ nl func2.sh
1 function f1 { # Definición de la función
2 echo "En f1"
3 }
4 echo "1º comando"
5...
Comandos de salida
1. El comando print
ksh |
Este comando aporta funcionalidades que no existen con echo.
a. Uso simple
Ejemplo
$ print Error de impresión
Error de impresión
$
b. Supresión del salto de línea natural de print
Hay que usar la opción -n.
Ejemplo
$ print -n Error de impresión
Error de impresión$
c. Mostrar argumentos que comienzan por el carácter "-"
Ejemplo
En el ejemplo siguiente, la cadena de caracteres -i forma parte del mensaje. Por desgracia, print interpreta -i como una opción y no como un argumento:
$ print -i: Opción inválida
ksh: print: bad option(s)
$ print "-i: Opción inválida"
ksh: print: bad option(s)
Es inútil poner protecciones alrededor de los argumentos de print. En efecto, "-" no es un carácter especial de shell; por tanto, no sirve protegerlo. No se interpreta por el shell, sino por el comando print.
Con la opción - del comando print, los caracteres siguientes se interpretarán como argumentos, sea cual sea su valor.
Ejemplo
$ print - "-i: Opción inválida"
-i: Opción inválida
$
d. Escritura hacia un descriptor determinado
La opción -u permite enviar un mensaje hacia un descriptor determinado.
print -udesc mensaje
donde desc represente el descriptor de archivo.
Ejemplo
Enviar un mensaje hacia la salida...
Gestión de entradas/salidas de un script
1. Redirección de entradas/salidas estándar
El comando interno exec permite manipular los descriptores de archivo del shell en ejecución. Usado en el interior de un script, permite redirigir de manera global las entradas/salidas de este.
Redirigir la entrada estándar de un script
exec 0< archivo 1
Todos los comandos del script situados después de esta directiva y que leen de su entrada estándar extraerán sus datos desde archivo1. Por tanto, no habrá más interacción con el teclado.
Redirigir la salida estándar y la salida de error estándar de un script
exec 1> archivo1 2> archivo2
Todos los comandos del script situados después de esta directiva y que escriben en su salida estándar enviarán sus resultados a archivo1. Los que escriban en su salida de error estándar enviarán sus errores a archivo2.
Redirigir la salida estándar y la salida de error estándar de un script al mismo archivo
exec 1> archivo1 2>&1
Todos los comandos del script situados después de esta directiva enviarán sus resultados y sus errores a archivo1 (ver capítulo Mecanismos esenciales del shell - Redirecciones).
Primer ejemplo
El script batch1.sh envía su salida estándar a /tmp/resu y su salida de error estándar a /tmp/log:
$ nl batch1.sh
1 #! /bin/ksh
2 # compatibilidad del script: posix, ksh, bash
3 exec 1> /tmp/resu 2> /tmp/log
4 echo "Inicio del tratamiento: $(date)"
5 ls
6 cp *.c /tmp
7 rm *.c
8 sleep 2 # simulación de la duración
9 echo "Fin del tratamiento: $(date)"
$
No hay archivos que terminen por ".c" en el directorio actual:
$ ls
Shell resu.txt
Ejecución del script:
$ batch1.sh
Contenido del archivo /tmp/resu:
$ nl /tmp/resu
1 Inicio del tratamiento: Wed Mar 13 20:04:47 MET 2022
2...
El comando eval
Sintaxis
eval expr1 exp2 ... expn
El comando eval permite la realización de una doble evaluación en la línea de comandos. Recibe como argumento un conjunto de expresiones en el que efectúa las operaciones siguientes:
-
Primera etapa: los caracteres especiales contenidos en las expresiones se tratan. El resultado del tratamiento genera una o varias expresiones: eval otra_exp1 otra_exp2 ... otra_expn. La expresión otra_exp1 representará el comando Unix que se debe ejecutar en la segunda etapa.
-
Segunda etapa: eval va a ejecutar el comando otra_exp1 otra_exp2 ... otra_expn. Sin embargo, previamente, esta línea se va a someter a una nueva evaluación. Los caracteres especiales se tratan y después el comando se lanza.
Ejemplo
Definición de la variable nombre que contiene "cristina":
$ nombre=cristina
Definición de la variable var que contiene el nombre de la variable definida justo arriba:
$ var=nombre
¿Cómo imprimir por pantalla el valor "cristina" sirviéndose de la variable var? En el comando siguiente, el shell sustituye $$ por el PID del shell actual:
$ echo $$var
17689var
En el comando siguiente, el nombre de la variable está aislado. Este no podrá funcionar: el shell genera un error de sintaxis, ya que no puede tratar dos caracteres "$" simultáneamente:
$ echo ${$var}
ksh: ${$var}:...
Gestión de señales
El comportamiento del shell actual respecto a las señales puede modificarse utilizando el comando trap.
1. Señales principales
Nombre de la señal |
N° |
Significado |
Comportamiento por defecto de un proceso ante la recepción de la señal |
¿Disposición modificable? |
HUP |
1 |
Ruptura de una línea de terminal. Durante una desconexión, la señal se recibe por cualquier proceso ejecutado en segundo plano desde el shell en cuestión. |
Morir |
sí |
INT |
2 |
Generado desde el teclado (ver parámetro intr del coman-do stty -a). Usado para matar el proceso que corre en primer plano. |
Morir |
sí |
TERM |
15 |
Generado vía el comando kill. Usado para matar un proceso. |
Morir |
sí |
KILL |
9 |
Generado vía el comando kill. Usado para matar un proceso. |
Morir |
no |
En los comandos, las señales pueden ser expresadas en forma numérica o simbólica. Las señales HUP, INT, TERM y KILL poseen el mismo valor numérico en todas las plataformas Unix, particularidad que no cumplen todas las señales. Por tanto, se aconseja usar la forma simbólica.
2. Ignorar una señal
Sintaxis
trap '' sig1 sig2
Ejemplo
El shell actual tiene el PID 18033:
$ echo $$
18033
El usuario solicita al shell ignorar la posible recepción de las señales HUP y TERM:
$ trap '' HUP TERM
Envío de las señales HUP y TERM (todas son sintaxis shell):
$ kill -HUP 18033
$ kill -TERM 18033
Las señales se ignoran; por tanto, el proceso shell...
Gestión de menús con select
ksh |
bash |
Sintaxis
select var in item1 item2 ... itemn
do
comandos
done
El comando interno select es una estructura de control de tipo bucle que permite escribir de manera cíclica un menú. La lista de items, item1 item2 ... itemn, se mostrará por pantalla a cada iteración del bucle. Los ítems son indexados automáticamente. La variable var se inicializará con el ítem correspondiente a elección del usuario.
Este comando usa también dos variables reservadas:
-
La variable PS3 representa el prompt utilizado para que el usuario teclee su elección. Su valor por defecto es #?. Se puede modificar a gusto del programador.
-
La variable REPLY contiene el índice del ítem seleccionado.
La variable var contiene la etiqueta de la elección, y REPLY, el índice de esta.
Ejemplo
$ nl menuselect.sh
1 #! /bin/ksh/bash
2 # compatibilidad del script: ksh, bash
3 function guardar {
4 echo "Se ha escogido la copia de seguridad"
5 # Ejecución de la copia de seguridad
6 }
7 function restaurar {
8 echo...
Análisis de las opciones de un script con getopts
bourne |
posix |
ksh |
bash |
Sintaxis
getopts lista-opciones-esperadas opción
El comando interno getopts permite a un script analizar las opciones que le han sido pasadas como argumento. Cada llamada a getopts analiza la opción siguiente de la línea de comandos. Para verificar la validez de cada una de las opciones, hay que llamar a getopts desde un bucle.
Definición de una opción
Para getopts, una opción se compone de un carácter precedido por un signo "+" o "-".
Ejemplo
"-c" y "+c" son opciones, mientras que "cristina" es un argumento:
# gestusuario.sh -c cristina
# gestusuario.sh +c
Una opción puede funcionar sola o estar asociada a un argumento.
Ejemplo
A continuación se muestra el script gestusuario.sh, que permite archivar y restaurar cuentas de usuario. Las opciones -c y -x significan respectivamente "Crear un archivo" y "Extraer un archivo". Estas son opciones sin argumento. Las opciones -u y -g permiten especificar la lista de usuarios y la lista de grupos que se han de tratar. Estas tienen que estar seguidas de un argumento.
# gestusuario.sh -c -u cristina,roberto,olivia
# gestusuario.sh -x -g curso -u cristina,roberto
Para comprobar si las opciones y los argumentos pasados al script gestusuario.sh son los esperados, el programador escribirá:
getopts "cxu:g:" opcion
Explicación de los argumentos de getopts:
-
Primer argumento: las opciones se citan una tras otra. Una opción seguida de ":" significa que se trata de una opción con argumento.
-
Segundo argumento: opcion es una variable de usuario que será inicializada con la opción en curso del tratamiento.
Una llamada a getopts recupera la opción siguiente y devuelve verdadero mientras queden opciones para analizar. Cuando una opción tiene asociado un argumento, este se deposita en la variable reservada OPTARG.
La variable reservada OPTIND contiene el índice de la siguiente opción que se ha de tratar.
$ nl gestusuario1.sh
1 #! /bin/bash
2 # compatibilidad del script y: bourne, posix, ksh, bash
3 while getopts "cxu:g:" opcion
4 do
5 echo "getopts ha encontrado...
Gestión de un proceso en segundo plano
bourne |
posix |
ksh |
bash |
El comando wait permite al shell esperar la finalización de un proceso ejecutado en segundo plano.
Sintaxis
Esperar la finalización del proceso cuyo PID se pasa como argumento:
wait pid1
Esperar la finalización de todos los procesos ejecutados en segundo plano desde el shell actual:
wait
En ksh y bash, el proceso también se puede expresar por su número de tarea (consulte el capítulo Mecanismos esenciales del shell - Procesos en segundo plano - Control de tareas (trabajos)).
Ejemplo
El script esperaProc.sh ejecuta una copia de seguridad en segundo plano. Durante su ejecución, el shell realiza otras acciones. Después, espera el final de la copia antes de realizar su verificación:
$ nl esperaProc.sh
1 #! /bin/bash
2 # compatibilidad del script: bourne, posix, ksh, bash
3 # Ejecución de un comando de copia de seguridad en segundo plano
4 find / | cpio -ocvB > /dev/rmt/0 &
5 echo "El PID del proceso en segundo plano es: $!"
6 # Mientras que el comando de copia de seguridad se ejecuta,
7 # el script hace otras acciones
8...
Compatibilidad de un script entre bash y ksh
Esta sección trata de la manera de escribir un script para que sea compatible con los shells bash y ksh.
1. Recuperar el nombre del shell intérprete del script
Vamos a tener que verificar si el script está siendo interpretado por bash o ksh. Aquí están las instrucciones para hacer estas verificaciones:
$$ representa el PID del shell actual, tail permite recuperar la última línea del resultado y awk permite recuperar el último campo de la línea (consulte el capítulo El lenguaje de programación awk).
$ shell=$( ps -p $$ | tail -1 | awk '{ print $NF}' )
$ echo $shell
bash
$
A continuación, enumeramos dos incompatibilidades clásicas entre ksh y bash. Por último, explicaremos cómo escribir un script único gracias al nombre del shell, que acabamos de recuperar previamente.
2. Gestión de las secuencias de escape con echo
En el comportamiento predeterminado de bash, se requiere la opción -e para que se interpreten las secuencias de escape. En ksh, no se necesita ninguna opción.
Ejemplo en ksh
$ echo " a\nb"
a
b
$
Ejemplo en bash
$ echo -e " a\nb"
a
b
$
Para evitar tener que utilizar la opción...
Script de archivado incremental y transferencia SFTP automática
1. Objetivo
Se trata de escribir un script que guarde una copia de seguridad de forma incremental de un directorio de una máquina de producción. Los archivos de copia (archivos cpio comprimidos) se transferirán a un servidor de copias de seguridad (servidor venus), en un directorio cuyo nombre dependerá del mes y año de la copia.
Directorios de la máquina de producción:
-
/root/admin/backup: directorio de los scripts de copia de seguridad.
-
/home/document: directorio de los documentos que se han de guardar.
-
/home/lbackup: directorio local de archivos. Este directorio se limpiará todos los meses.
Directorios de la máquina de copias de seguridad:
-
/home/dbackup/2022/01: archivos del mes de enero de 2022.
-
/home/dbackup/2022/02: archivos del mes de febrero de 2022.
En el ejemplo que se presenta a continuación, estos directorios estarán creados previamente. No es el script de copia de seguridad el que los crea (pero sería fácilmente realizable).
La figura 2 representa el sistema de archivos de los dos servidores.
La copia de seguridad incremental usará tantos niveles de copia de seguridad como días tenga el mes. En principio, una copia de nivel 0 (copia de todos los archivos del directorio /home/document) se realiza el primer día de cada mes. Los días siguientes, solamente se archivarán los archivos modificados desde el día anterior.
Se utilizarán archivos indicadores de nivel (nivel0, nivel1...) para reflejar la fecha en la que las copias se llevan a cabo.
Ejemplo
El 01/01/2022: Copia de seguridad de nivel 0: creación del archivo de control "nivel0" y copia de seguridad de todos los archivos y directorios que estén en /home/document. A continuación, transferencia del archivo al servidor de copias.
El 02/01/2022: Copia de seguridad de nivel 1: creación del archivo de control "nivel1". A continuación, los archivos que sean más recientes que el archivo de control "nivel0" se copian. Transferencia del archivo al servidor de copia.
El 03/01/2022: Copia de seguridad de nivel 2: creación del archivo de control "nivel2". A continuación, los archivos que sean más recientes que el archivo de control "nivel1" se copian. Transferencia del archivo al servidor...
Ejercicios
Los archivos proporcionados para los ejercicios están disponibles en la carpeta dedicada al capítulo, en el directorio Ejercicios/archivos.
1. Funciones
a. Ejercicio 1: funciones simples
Comandos útiles: df, who.
Escriba un script audit.sh:
-
Escriba una función users_connect que mostrará la lista de los usuarios conectados actualmente.
-
Escriba una función disk_space que mostrará el espacio en disco disponible.
-
El programa principal mostrará el siguiente menú:
- 0 - Fin
- 1 - Mostrar la lista de usuarios conectados
- 2 - Mostrar el espacio en disco
Su opción:
-
Introducir la opción del usuario y llamar a la función adecuada.
b. Ejercicio 2: funciones simples, valor de retorno
Comandos filtro útiles: awk, tr -d (ver capítulo Los comandos filtro). Otros comandos útiles: df, find.
Escriba un script explore_sa.sh:
-
Programa principal:
-
El programa principal mostrará el menú siguiente:
0 - Fin
1 - Eliminar los archivos de tamaño 0 de mi directorio principal
2 - Controlar el espacio de disco del SA raíz
Su opción:
-
Introduzca la opción del usuario.
-
La opción 0 provocará la finalización del script.
-
La opción 1 llamará a la opción limpieza.
-
La opción 2 causará la llamada a la función sin espacio_d.
-
En función del valor retornado por la función, mostrar el mensaje adecuado.
-
Escriba la función limpieza: busque, a partir del directorio de inicio del usuario, todos los archivos que tengan tamaño 0 con objeto de eliminarlos (después de solicitar confirmación para cada archivo).
-
Escriba la función sin_espacio_d: esta función verifica la utilización del sistema de archivos raíz y retorna verdadero si la tasa es superior al 80% y falso en caso contrario.
Ejemplos de ejecución...