Esta página puede ser redistribuida libremente bajo los términos de la licencia GPL. Vease ( GPL texto original ) o si lo prefiere (Traducción española no oficial de la GPL) Al margen de las obligaciones legales que se derivan del uso de esta licencia rogamos sea respetada la referencia a su lugar de publicación original www.ciberdroide.com. y a su autor original Antonio Castro Snurmacher (Madrid 01/01/2000).

Ausencia de Garantía

Esta ausencia de garantía se hace extensa a cualquier tipo de uso de este material y muy especialmente a las prácticas, ejercicios, y de ejemplos que encuentre en estas páginas. Deberá trabajar siempre salvo indicación contraria con un SO Linux y con un usario distinto de 'root' sin privilegios especiales. Como directorio de trabajo se procurará usar el directorio '/tmp' o algún otro que no contenga información valiosa. Tampoco se considera buena idea practicar en una máquina que contenga información valiosa.

Todo esto son recomendaciones de prudencia. En cualquier caso si algo sale mal toda la responsabilidad será únicamente suya. En ningún caso podrá reclamar a nadie por daños y perjuicios derivados del uso de este material. Para más información lea el contenido de la licencia GPL y abstengase de hacer prácticas si no está dispuesto a asumir toda la responsabilidad.

PROGRAMACION SHELL-SCRIPT (Tercera parte)

Sentencia condicional 'if'
Ya hemos visto una forma de hacerlo usando los operadore '||' o '&&' de la shell, pero existen formas que pueden resultar más versatiles y más legibles.


if  _condición_ 
   then _lista_de_ordenes_ 
   [ elif _condición_
       then _lista_de_ordenes_ ] ... 
   [ else _condición_ ] 
fi

En lugar de _condición_ podríamos haber puesto _lista_de_ordenes_ pero queremos resaltar que el código de retorno va a ser evaluado.

Las partes entre corchetes son opcionales y si van seguidas de tres puntos '...' indica que puede presentarse varias veces. Todas las sentencias condicionales 'if' empiezan con la palabra reservada 'if' y terminan con la palabra reservada 'fi'.

Mejor vamos a ilustrar con algunos ejemplos. Deberá editarlos y ejecutarlos.

# Esto siempre mostrara '123'
if true
  then echo '123'
fi

Acabamos de utilizar una condición que siempre se cumplirá. Vamos a ver algo un poco más util.

# Si la variable CMPRT01 está definida y contiene el nombre de un fichero 
# con permiso de lectura se mandará a la impresora con 'lp'
if test -r  $CMPRT01
   then lp $CMPRT01
fi

También podemos programar una acción para cuando se cumpla una condición y otra para cuando no se cumpla.

# Si La variable 'modo' contiene el valor 'lp' imprimir el fichero $FICH
# en caso contrario sacarlo por pantalla.
if [ "$modo" = "lp" ]
   then lp  $FICH
   else cat $FICH
fi

El siguiente ejemplo editelo con el nombre 'tipofichero'. Nos servirá para ilustrar el uso de 'elif' y para repasar algunas de las opciones de 'test'.

# tipofichero
FILE=$1
if test -b  $FILE
   then echo $FILE 'Es un dispositivo de bloques'
   elif test -c $FILE
     then echo $FILE 'Es un dispositivo especial de caracteres'
   elif test -d $FILE
     then echo $FILE 'Es un directorio'
   elif test -f $FILE
     then echo $FILE 'Es un fichero regular (ordinario)'
   elif test -L $FILE
     then echo $FILE 'Es un Link simbólico'
   elif test -p $FILE
     then echo $FILE 'Es un pipe con nombre'
   elif test -S $FILE
     then echo $FILE 'Es un Socket (dispositivo de comunicaciones)'
   elif test -e $FILE
     then echo $FILE 'Existe pero no sabemos que tipo de fichero es'
   else echo $FILE 'No existe o no es accesible'
fi

Para usar este último ejemplo ejectute './tipofichero ..' o './tipofichero tipofichero'

Tenga en cuenta que si bien la 'bash' admite el uso de 'elif' quizas otras shell no lo admitan.

Setencia condicional 'case'
Ya hemos visto que con 'elif' podemos evaluar distintas condiciones programando una acción para cada caso.

Existe un caso especial. Imaginemos que deseamos evaluar una variable y en función de lo que contenga hacer una cosa u otra. Podríamos usar una sentencia condicional con abundantes 'elif' pero hay que tener en cuenta una cosa. Con 'elif' tenemos que ir evaluando a cada paso hasta que se cumpla una vez y entonces se ejecuta la acción y ya no se ejecuta ninguna otra porque habremos finalizado. En el caso anterior si hubieramos introducido el nombre de un pipe con nombre se habrían ejecutado varios test antes de determinar que era un pipe con nombre, y eso resulta lento.

Si lo que deseamos hacer está en función de un valor utilizaremos la sentencia condicional 'case' que es mucho más eficiente porque no requiere ejecutar varias ordenes o una orden varias veces como en el caso anterior.

La sintaxis para la sentencia condicional 'case' sería de la siguiente forma:


case valor in 
  [ _lista_de_patrones_ ) _lista_de_ordenes_ ;; ] ...
esac

Por valor queremos indicar el resultado de cualquier expansión producida por la shell. La _lista_de_patrones_ son uno o más patrones separados por el caracter '|' y cada patrón es del mismo tipo que los patrones utilizados para la expansión de nombres de ficheros. El orden es importante porque se van comprobando por orden y en el momento que uno de los patrones corresponda con el valor se ejecutará la _lista_de_ordenes_ que le corresponda y ya no se ejecutará nada más. Es frecuente utilizar en último lugar un patrón '*)' para que en caso de no encontrar ninguna correspondencia con los patrones anteriores se ejecute siempre una acción por defecto. Edite el siguiente ejemplo y salvelo con nombre 'prueba_case'.

# prueba_case
case $1 in
1) echo Uno ;;
2) echo Dos ;;
[3-7]) echo "Entre tres y siete ambos incluidos" ;;
8|9|0) echo Ocho; echo Nueve; echo Cero ;;
[a-zA-Z]) echo Una Letra ;;
start|begin) echo Comienzo ;;
stop|end) echo Fin ;;
*) echo 'Fallo'
esac

Ahora le damos permiso de ejecución y probamos como funciona.

$ ./prueba_case 1

Uno
$ ./prueba_case 2

Dos
$ ./prueba_case 7

Entre tres y siete ambos incluidos
$ ./prueba_case h

Una Letra
$ ./prueba_case start

Comienzo
$ ./prueba_case begin

Comienzo
$ ./prueba_case aa

Fallo
$ ./prueba_case 12

Fallo
$ ./prueba_case 9

Ocho
Nueve
Cero
$ ./prueba_case stop

Fin

Recuerde que la ejecución de una orden externa en un shell-srcipt es una operación costosa. Hacer un script ineficiente puede no tener ninguna importancia dependiendo del uso que le demos y de lo sobrada de recursos que esté la máquina, pero en otros caso si será más importante así que haciendo las cosas lo mejor posible estaremos preparados en un futuro para trabajos más exigentes.

Entrada de datos con read

La instrucción 'read' permite capturar datos desde la entrada estandar. Para ello queda a la espera de un fin de linea. Edite el fichero siguiente y llameló 'pru_read'.

echo -e "Introduzca su nombre : \c"
read NOMBRE
banner Hola $NOMBRE

Ejecute './pru_read'. Observe que los datos introducidos son guardados en la variable NOMBRE. Si pulsa directamente la variable tomará valor "".

Cuando se usa la instrucción 'read' sin una variable el contenido se guardará en la variable REPLY pero si lo que desea es guardar el contenido queda más claro guardarlo en una variable concreta. Más adelante veremos ejemplos que usan esta instrucción.

Bucles 'while' y 'until'


while _condición_
do 
  _lista_de_ordenes_
done

En lugar de _condición_ podríamos haber puesto _lista_de_ordenes_ pero queremos resaltar que el código de retorno va a ser evaluado.

Mientras se cumpla la condición se ejecutará _lista_de_ordenes_. Resulta evidente que si dentro de la _lista_de_ordenes_ no hay nada capaz de alterar la condición, resultará un blucle que se ejecutará de forma ininterrumpida. (Bucle infinito). Si esto ocurre habrá que matar el proceso enviando alguna señal.

Existe un bucle muy similar y que solo se diferencia en que no para nunca hasta que se cumpla la condición. Es decir justo al revés que antes.


until _condición_ 
do
  _lista_de_ordenes_
done

Edite como 'pru_while1' el siguiente ejemplo.

# pru_while1
# Hacer un bucle de captura por consola (terminara cuando se pulse
# solo <INTRO> ) y mostrar el resultado de cada captura entre parentesis.
# Inicializamos primero la variable LINEA con un valor cualquiera
# pero distinto de ""
LINEA="x" 
while test $LINEA
do
   read LINEA
   echo '('$LINEA')'
done

Cuando pulsemos <INTRO> directamente sin nada más LINEA valdrá "" y 'test $LINEA' devolverá FALSE y el bucle finalizará.

Vamos a ejecutar el ejemplo para comprobar su funcionamiento.

$ ./pru_while1
aaaaa

(aaaaa)

bbbbb

(bbbbb)


()

Vemos que en la última ejecución LINEA valía "".

Bucle 'for'
Se proporciona una lista de elementos y se ejecuta una lista de órdenes haciendo que una variable vaya tomando el valor de cada uno de estos elementos. Entre estas ordenes se puede utilizar un nuevo bucle 'for'.

El siguiente ejemplo funciona pero es una mala idea ya que supone Varias ejecuciones de un comando cuando se podía haber hecho con uno solo. Habría sido mejor hacer 'chmod +x *'.

for i in $*
do 
   chmod +x  $i
done

El siguiente ejemplo no funcionará si no prepara un poco una serie de cosas que se necesitan. Concretamente deberá de existir un fichero 'lista' con varios elementos. Varios ficheros con nombre que empiecen por SCAN. Varios ficheros '*.doc' de los cuales algunos deberan contener la palabra 'CODIGO'. Estudie el contenido del ejemplo que sigue. Editelo y ejecutelo pasando varios argumentos. Su objetivo es hacerlo funcionar y entender como funciona.

# Para ver distintos modos de manejar listas vamos a generar todos los 
# nombres posibles formados por combinacion de las partes siguientes: 
# Como parte 1 los nombres de ficheros de directorio actual que
#    empiezan por 'SCAN'. 
# Como parte 2 los nombres contenidos en el fichero 'lista'.
# Como parte 3 los identificadores 'cxx0 cxx2 bxx5' 
# Como parte 4 la lista de parametros $1 $2 $3 .. etc, 
# Como parte 5 los ficheros '*.doc' que contienen la palabra 'CODIGO'.
###
for i in `ls SCAN*`
do
  for j in `cat lista`
  do
    for k in cxx0 cxx2 bxx5
    do
      for l in $*
      do
        for m in `grep -l "CODIGO" *.doc`
        do
          echo $i.$j.$k.$l.$m
	done
      done
    done
  done
done

Cuando tengamos bucles unos dentro de otros decimos que son bucles anidados. El nivel de anidamiento de un bucle es el número de bucles que hay unos dentro de otros.

'break' y 'continue'
Existe una forma de controlar un blucle desde el interior del mismo. Para eso podemos usar 'break', o 'continue'. Se pueden usar en cualquiera de los bucles que acabamos de ver (while, until, y for).

La palabra reservada 'break' provoca la salida de un bucle por el final. Si viene seguido de un numero 'n' saldrá de 'n' niveles. No poner número equivale a poner el número 1.

La palabra reservada 'continue' provoca un salto al comienzo del bucle para continuar con la siguiente iteración. Si viene seguida de un numero 'n' saldrá de 'n' niveles.

# pru_break_continue
# Definir la variable j como una variable de tipo entero e
# inicializarla a cero. Luego la incrementamos a cada iteración
# del bucle y si j es menor que diez mostramos el doble de su valor. 
# y en caso contrario salimos del bucle
typeset -i j=0
while true
do
     let j=j+1
     if [ $j -et 3 ]
    	then continue
     fi
     if [ $j -et 4 ]
    	then continue
     fi
     if [ $j -lt 10 ]
       then expr $j \* 2
       else break
     fi
done

Probamos ahora a ejecutarlo y obtendremos

$ ./pru_break_continue

2
4
10
12
14
16
18

Ahora edite y pruebe el siguiente ejemplo que llamaremos 'pru_break_continue2'

# ./pru_break_continue2
for i in uno dos tres 
  do
  for j in a b c 
    do
    for k in 1 2 3 4 5 
      do
        echo $i $j $k
        if [ "$j" = "b" ]
	   then break
	fi
        if [ "$k" = "2" ]
	   then continue 2
	fi
      done
    done
 done

El resultado de la ejecución sería como sigue.

$ ./pru_break_continue2

uno a 1
uno a 2
uno b 1
uno c 1
uno c 2
dos a 1
dos a 2
dos b 1
dos c 1
dos c 2
tres a 1
tres a 2
tres b 1
tres c 1
tres c 2

Arreglos
Vamos a estudiar ahora un tipo de variables que se caracterizan porque permiten guardar valores asociados a una posición. Los llamamos 'arreglos' (en ingles array) también se pueden llamar tablas. En el siguiente ejemplo usaremos una varirable 'tab[]' que se comporta como si estuviera formada por varias variables. tab[1], tab[2], tab[3], etc... Vamos a ilustrarlo con un ejemplo que tiene un poco de todo. Se trata de un programa que debe estudiar detenidamente.

# Desarrollar una función que admita un parametro de entrada. Si el
# parametro de entrada contiene una cadena que ya esta almacenada en
# la tabla 'tabnom' retornar sin mas, pero si no esta añadir esa
# palabra a la tabla.
GuardaNombre(){
  # Si numero de parametros distindo de 1 salir con error.
  if [ $# -ne 1 ]
     then
        echo "Numero de parametros invalido en GuardaNombre()"
        return 2
  fi
  typeset -i j=1
  for var in ${tab[*]}
  do
     if [ $var = $1 ]
        then
	   ind=$j
	   return 1
     fi
     let j=j+1
  done
  ind=$j
  tab[$ind]=$1
  return 0
}
########################## main ##################
while true
do
  echo -e "Introduzca algo o puse <INTRO> directamente para finalizar : \c"
  read DATO
  if [ ! "$DATO" ]
  	then break
  fi
  GuardaNombre $DATO
done
echo "Ahora visualizamos los datos introducidos"
for l in ${tab[*]}
do
    echo $l
done
echo 2 ${tab[2]}
echo 1 ${tab[1]}

El resultado de ejecutar esto introduciendo una serie de datos sería como sigue:

Introduzca algo o puse <INTRO> directamente para finalizar : hhhhh
Introduzca algo o puse <INTRO> directamente para finalizar : jjjj
Introduzca algo o puse <INTRO> directamente para finalizar : jjjj
Introduzca algo o puse <INTRO> directamente para finalizar : jjjj
Introduzca algo o puse <INTRO> directamente para finalizar : ooooooooo
Introduzca algo o puse <INTRO> directamente para finalizar : kk
Introduzca algo o puse <INTRO> directamente para finalizar : 
Ahora visualizamos los datos introducidos
hhhhh
jjjj
ooooooooo
kk
2 jjjj
1 hhhhh

Con esto comprobamos que podemos acceder no solo a la lista completa de los datos introducidos sino a uno solo de ellos proporcionando un número con la posición donde el dato ha sido almacenado.

También comprobamos que el valor 'jjjj' se ha indroducido varias veces pero nuestro programa solo lo guarda una vez gracias a que antes de guardar cada valor se comprueba si dicho valor ya fúe guardado antes.

Un ejempo sencillo: Construcción de un menu


#########################################################################
muestraopcionesmenuprin() {
	clear
	echo '1) Fecha y hora'
	echo '2) Calendario del més actual'
	echo '3) Calendario del año actual'
	echo '4) Calculadora de precisión arbitraria'
	echo '5) Lista de usuarios conectados' 
	echo '6) Memoria libre del sistema'
	echo '7) Carga del sistema'
	echo '8) Ocupacion de todo el sistema de ficheros'
	echo '0) Terminar'
	echo
	echo -e "Introduzca la opción deseada : \c"
}

#########################################################################
pausa () {
	echo
	echo -e "Pulse  para continuar"
	read 
}

#########################################################################
############################### MAIN  ###################################
#########################################################################
while true
do
	muestraopcionesmenuprin
	read OPT
	clear
	case $OPT in
	3|7) echo "Para salir deberá pulsar 'q'" ; pausa ;;
	4) echo "Para salir deberá introducir 'quit'" ; pausa ;;
	esac
	echo ; echo
	case $OPT in
	0) exit ;;
	1) date ; pausa ;;
	2) cal ; pausa ;;
	3) cal `date +%Y` | less ;;
	4) bc ;;
	5) who -iTH ; pausa ;;
	6) cat /proc/meminfo ; pausa ;; # Podría usarse el comando free
	7) top -s ;;
	8) df ; pausa ;;
	*) echo -e "Opcion erronea.\a" ; pausa ;;
	esac	
done
echo
echo 

Vamos a comentar algunos aspectos de este programa. Comienza con 'while true' (Antes de esto lo que hay es la declaración de un par de funciones). Un programa así se ejecutaría en un bucle sin fin pero existe una instrucción 'exit' en medio del programa para finalizar cuando se elija la opción adecuada.

Primero se llama a una función 'muestraopciones' que nos muestra las opciones disponibles y nos invita a introducir una de ellas que gracias a la instrucción read será almacenada en la variable OPT. El contenido de esta variable se usa en un par de sentencias 'case'. La primera solo considera si es alguna de las opciones '3,7, o 4' y en función de ello muestra un mensaje adicional. El segundo 'case' sirve para desencadenar la funcionalidad elegida. Existe un '*)' que sirve para advertir que la opción introducida no es correcta. Quizas ya ha notado que en ocasiones usamos la opción -e con echo. Esto se hace cuando usamos un caracter especial que debe ser interpretado como en el caso de '\a' (emite un pitido), o como '\c' (evita el salto de carro). El comando 'clear' se usa para limpiar la pantalla.

Recomendaciones finales
Acabamos de explicar un programa muy sencillo. Compliquelo un poco añadiendo una opción 9 que muestre un submenú que entre otras cosas contenga una opción que vuelva al menú principal. Use su imaginación y parctique para intentar mejorarlo.

En estos momentos ya dispone de toda la base de conocimientos que le permitirá personalizar su entorno de trabajo o construir sencillos scripts que le ayuden a realizar una gran variedad de tareas, o también puede investigar algunos de los programas del sistema escritos en shell-script. Si se conforma con lo estudiado y no intenta ponerlo en práctica pronto lo olvidará todo. Practique la programación en shell-script ya mismo antes de que olvide la tehoría. Empiece por las cosas que hemos explicado pero no se quede en eso. Intente llegar un poco más lejos.

En las lecciones que siguen continuaremos tratando temas que abrirán nuevas puertas a su conocimiento. En ningún caso podemos detenernos excesivamente en ninguna de ellas. A la programación en shell-script la hemos dedicado varias lecciones y anteriormente dedicamos algunos más a la shell como interprete de comandos, pero ha llegado el momento de decir que debe de poner mucho de su parte y que existe el 'man bash' para buscar las cosas que no pudimos explicar aquí.

No vamos a incluir ningún test ahora en lugar de ello y a riesgo de resultar pesados vamos a insistir. Practique ahora por su cuenta.

Si quiere hacernos llegar alguna duda, aclaración,
crítica, o contribución personal, utilice nuestro
formulario de contacto y nosotros le contestaremos
contacto