¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
  1. Libros
  2. Programación shell en Unix/Linux
  3. Aspectos avanzados de la programación shell
Extrait - Programación shell en Unix/Linux ksh, bash, estándar POSIX (con ejercicios corregidos) (5ª edición)
Extractos del libro
Programación shell en Unix/Linux ksh, bash, estándar POSIX (con ejercicios corregidos) (5ª edición) Volver a la página de compra del libro

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

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

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

TERM

15

Generado vía el comando kill. Usado para matar un proceso.

Morir

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...