domingo, 1 de febrero de 2009

Programando en shell script

Ejecución de un script

Los scripts deben empezar por el número mágico #! seguido del programa a usar para interpretar el script:
  • #!/bin/bash script de bash
  • #!/bin/sh script de shell
  • #!/usr/bin/perl script de perl
Las forma usuales de ejecutar un script es:
  • darle permiso de ejecución al fichero y ejecutarlo como un comando:
    $ chmod +x helloworld
    ./helloworld
  • ejecutar una shell poniendo como argumento el nombre del script (sólo necesita permiso de lectura)
    $ bash helloworld

Ejecución con la shell actual

Los métodos anteriores arrancan una sub-shell que lee las ordenes del fichero, la ejecuta y después termina cediendo el control nuevamente a la shell original
Existe una forma de decirle a la shell actual que lea y ejecute una serie de ordenes por si misma sin arrancar una sub-shell:
$ . helloworld
o bien:
$ source helloworld
Ejemplo:
$ cat shellid.sh
#!/bin/bash
echo "Shell ejecutando el script, PID = $$"
$ echo "PID actual = $$"
PID actual = 6919
$ bash shellid.sh
Shell ejecutando el script, PID = 26824
$ . shellid.sh
Shell ejecutando el script, PID = 6919




Paso de parámetros

Es posible pasar parámetros a un scripts: los parámetros se recogen en las variables $1 a $9
Variable Uso
$0 el nombre del script
$1 a $9 parámetros del 1 al 9
${10}, ${11},... parámetros por encima del 10
$# número de parámetros
$ * , $@ todos los parámetros
Ejemplo:
$ cat parms1.sh
#!/bin/bash
VAL=$((${1:-0} + ${2:-0} + ${3:-0}))
echo $VAL
$ bash parms1.sh 2 3 5
10
$ bash parms1.sh 2 3
5

shift

Desplaza los parámetros hacia la izquierda el número de posiciones indicado:
$ cat parms2.sh
#!/bin/bash
echo $#
echo $ *
echo "$1 $2 $3 $4 $5 $6 $7 $8 $9 ${10} ${11}"
shift 9
echo $1 $2 $3
echo $#
echo $ *
$ bash parms2.sh a b c d e f g h i j k l
12
a b c d e f g h i j k l
a b c d e f g h i j k
j k l
3
j k l

Diferencia entre $ * y $@

Los parámetros $ * y $@ sólo se diferencian si van entrecomillados:
  • "$ * "
    se expande a una sola palabra, conteniendo todos los parámetros y con el valor de cada parámetro separado por el primer carácter de la variable especial IFS (por defecto, un espacio)
  • "$@"
    cada parámetro se expande a una palabra separada; los parámetros entrecomillados se consideran uno solo aunque lleven espacios
Ejemplo:
$ cat parms3.sh
#!/bin/bash
IFS=":"
for par in "$*"
do echo "Parámetro es: $par"
done
echo "==================================="
for par in "$@"
do echo "Parámetro es: $par"
done
$ bash parms3.sh hola "como estas hoy?" bien gracias
Parámetro es: hola:como estas hoy?:bien:gracias
===================================
Parámetro es: hola
Parámetro es: como estas hoy?
Parámetro es: bien
Parámetro es: gracias

set --

El comando set -- $var copia las palabras de la variable $var en los parámetros posicionales $1, $2, ...
$ cat setdata
hola
como estás?
$
$ cat set.sh
#!/bin/bash
file=$(cat $1) # Guarda en file el fichero pasado
set -- $file
echo $#
echo "$1 - $2 - $3"
$
$ bash set.sh setdata
3
hola - como - estás?



Entrada/salida

Es posible leer desde la entrada estándar o desde fichero usando read y redirecciones:
#!/bin/bash
echo -n "Introduce algo: "
read x
echo "Has escrito $x"
echo -n "Escribe 2 palabras: "
read x y
echo "Primera palabra $x; Segunda palabra $y"
Si queremos leer o escribir a un fichero utilizamos redirecciones:
echo $X > fichero
read X <>
Este último caso lee la primera línea de fichero y la guarda en la variable X
  • Si queremos leer un fichero línea a línea podemos usar while:
    #!/bin/bash
    # FILE: linelist
    # Usar: linelist filein fileout
    # Lee el fichero pasado en filein y
    # lo salva en fileout con las lineas numeradas
    count=0
    while read BUFFER
    do
    count=$((++count))
    echo "$count $BUFFER" » $2
    done < $1
    • el fichero de entrada se va leyendo línea a línea y almacenando en BUFFER
    • count cuenta las líneas que se van leyendo
  • El uso de lazos para leer ficheros es bastante ineficiente
    • deberían evitarse (por ejemplo, usar cat fichero)
Ejemplo de lectura de fichero
#!/bin/bash
# Usa $IFS para dividir la línea que se está leyendo
# por defecto, la separación es "espacio"
echo "Lista de todos los usuarios:"
OIFS=$IFS # Salva el valor de IFS
IFS=: # /etc/passwd usa ":" para separar los campos
cat /etc/passwd |
while read name passwd uid gid fullname ignore
do
echo "$name ($fullname)"
done
IFS=$OIFS # Recupera el $IFS original
  • El fichero /etc/passwd se lee línea a línea
    • para cada línea, sus campos se almacenan en las variables que siguen a read
    • la separación entre campos la determina la variable $IFS (por defecto, espacio en blanco)

Redirecciones

Las redirecciones y pipes pueden usarse en otras estructuras de control

Ejemplo: lee las 2 primeras líneas de un fichero

if true
then
read x
read y
fi <>
Ejemplo: lee líneas de teclado y guardalas en un fichero temporal convirtiendo minúsculas en mayúsculas
#/bin/bash
read buf
while [ "$buf" ]
do
echo $buf
read buf
done | tr ´a-z´ ´A-Z´ > tmp.$$
Ejemplo: usa exec para redireccionar la entrada estándar
# redirige la entrada estándar al descriptor 7
exec 7<&0 # redirige /etc/passwd a la entrada estándar exec 0< /etc/passwd # lee la primera línea de /etc/passwd read rootpasswd # lee de la entrada estándar echo -n "Escribe algo en el teclado " read entrada_de_teclado <&7 echo "Has escrito: $entrada_de_teclado" # eliminamos las redirecciones exec 0<&7 # cierra el descriptor de fichero 7 exec 7<&- # lee otra vez de la entrada estándar echo -n "Escribe otra cosa " read entrada_otra_vez echo "Ahora escribes: $entrada_otra_vez"


Tests

Los comandos que se ejecutan en un shell tienen un código de salida, que se almacena en la variable $?
  • si $? es 0 el comando terminó bien
  • si $? es > 0 el comando terminó mal
Ejemplo:
$ ls /bin/ls
/bin/ls
$ echo $?
0
$ ls /bin/ll
ls: /bin/ll: Non hai tal ficheiro ou directorio
$ echo $?
1
Podemos chequear la salida de dos comandos mediante los operadores && (AND) y || (OR)
  • estos operadores actúan en cortocircuito:
    comando1 && comando2
    comando2 sólo se ejecuta si comando1 acaba bien
    comando1 || comando2
    comando2 sólo se ejecuta si comando1 falla
  • comandos true y false: devuelven 0 y 1, respectivamente
Ejemplo con &&:
$ ls /bin/ls && ls /bin/ll
/bin/ls
ls: /bin/ll: Non hai tal ficheiro ou directorio
$ echo $?
1
$ ls /bin/ll && ls /bin/ls
ls: /bin/ll: Non hai tal ficheiro ou directorio
$ echo $?
1
Ejemplo con ||:
$ ls /bin/ls || ls /bin/ll
/bin/ls
$ echo $?
0
$ ls /bin/ll || ls /bin/ls
ls: /bin/ll: Non hai tal ficheiro ou directorio
/bin/ls
$ echo $?
0

Estructura if...then...else

Podemos usar el estado de salida de uno o varios comandos para tomar decisiones:
if comando1
then
ejecuta otros comandos
elif comando2
then
ejecuta otros comandos
else
ejecuta otros comandos
fi
  • debe respetarse la colocación de los then, else y fi
    • también puede escribirse if comando1 ; then
  • el elif y el else son opcionales, no así el fi
Ejemplo:
$ cat if.sh
#!/bin/bash
if (ls /bin/ls && ls /bin/ll) >/dev/null 2>&1
then
echo "Encontrados ls y ll"
else
echo "Falta uno de los ficheros"
fi
$ bash if.sh
Falta uno de los ficheros

Comando test

Notar que if sólo chequea el código de salida de un comando, no puede usarse para comparar valores: para eso se usa el comando test
El comando test permite:
  • chequear la longitud de un string
  • comparar dos strings o dos números
  • chequear el tipo de un fichero
  • chequear los permisos de un fichero
  • combinar condiciones juntas
test puede usarse de dos formas:
test expresión
o bien
[ expresión ]6
Si la expresión es correcta test devuelve un código de salida 0, si es falsa, devuelve 1:
  • este código puede usarse para tomar decisiones:

    if [ "$1" = "hola" ]
    then
    echo "Hola a ti también"
    else
    echo "No te digo hola"
    fi
    if [ $2 ]
    then
    echo "El segundo parámetro es $2"
    else
    echo "No hay segundo parámetro"
    fi
  • en el segundo if la expresión es correcta si $2 tiene algún valor; falsa si la variable no está definida o contiene null ("")

Expresiones

Existen expresiones para chequear strings, números o ficheros

Chequeo de strings

Expresión Verdadero sí
string el string es no nulo ("")
-z string la longitud del string es 0
-n string la longitud del string no es 0
string1 = string2 los strings son iguales
string1 != string2 los strings son distintos

Chequeo de enteros

Expresión Verdadero sí
int1 -eq int2 los enteros son iguales
int1 -ne int2 los enteros son distintos
int1 -gt int2 int1 mayor que int2
int1 -ge int2 int1 mayor o igual que int2
int1 -lt int2 int1 menor que int2
int1 -le int2 int1 menor o igual que int2

Chequeo de ficheros

Expresión Verdadero sí
-e file file existe
-r file file existe y es legible
-w file file existe y se puede escribir
-x file file existe y es ejecutable
-f file file existe y es de tipo regular
-d file file existe y es un directorio
-c file file existe y es un dispositivo de caracteres
-b file file existe y es un dispositivo de bloques
-p file file existe y es un pipe
-S file file existe y es un socket
-L file file existe y es un enlace simbólico
-u file file existe y es setuid
-g file file existe y es setgid
-k file file existe y tiene activo el sticky bit
-s file file existe y tiene tamaño mayor que 0

Operadores lógicos con test

Expresión Propósito
! invierte el resultado de una expresión
-a operador AND
-o operador OR
( expr ) agrupación de expresiones; los paréntesis tienen un significado especial para el shell, por lo que hay que escaparlos
Ejemplos:
$ test -f /bin/ls -a -f /bin/ll ; echo $?
1
$ test -c /dev/null ; echo $?
0
$ [ -s /dev/null ] ; echo $?
1
$ [ ! -w /etc/passwd ] && echo "No puedo escribir"
No puedo escribir
$ [ $$ -gt 0 -a \( $$ -lt 5000 -o -w file \) ]

Comando de test extendido

A partir de la versión 2.02 de Bash se introduce el extended test command: [[ expr ]]
  • permite realizar comparaciones de un modo similar al de lenguajes estándar:
    • permite usar los operadores && y || para unir expresiones
    • no necesita escapar los paréntesis
Ejemplos:
$ [[ -f /bin/ls && -f /bin/ll ]] ; echo $?
1
$ [[ $$ -gt 0 && ($$ -lt 5000 || -w file) ]]


Control de flujo

Además del if bash permite otras estructuras de control de flujo: case, for, while y until

Estructura case

Formato:
case valor in
patrón_1)
comandos si value = patrón_1
comandos si value = patrón_1 ;;
patrón_2)
comandos si value = patrón_2 ;;
* )
comandos por defecto ;;
esac
  • si valor no coincide con ningún patrón se ejecutan los comandos después del * )
    • esta entrada es opcional
  • patrón puede incluir comodines y usar el símbolo | como operador OR
Ejemplo:
#!/bin/bash
echo -n "Respuesta: "
read RESPUESTA
case $RESPUESTA in
S* | s*)
RESPUESTA="SI" ;;
N* | n*)
RESPUESTA="NO" ;;
* )
RESPUESTA="PUEDE" ;;
esac
echo $RESPUESTA

Lazos for

Formato:
for var in lista
do
comandos
done
  • var toma los valores de la lista
    • puede usarse globbing para recorrer los ficheros
Ejemplo: recorrer una lista
LISTA="10 9 8 7 6 5 4 3 2 1"
for var in $LISTA
do
echo $var
done
Ejemplo: recorrer los ficheros * .bak de un directorio
dir="/var/tmp"
for file in $dir/ * .bak
do
rm -f $file
done
Sintaxis alternativa, similar a la de C
LIMIT=10
for ((a=1, b=LIMIT; a <= LIMIT; a++, b--))
do
echo "$a-$b"
done

Bucle while

Formato:
while comando
do
comandos
done
  • se ejecuta mientras que el código de salida de comando sea cierto
Ejemplo:
while [ $1 ]
do
echo $1
shift
done

Bucle until

Formato:
until comando
do
comandos
done
  • se ejecuta hasta que el código de salida de comando sea hace cierto
Ejemplo:
until [ "$1" = "" ]
do
echo $1
shift
done

break y continue

Permiten salir de un lazo (break) o saltar a la siguiente iteración (continue)
  • break permite especificar el número de lazos de los que queremos salir (break n)
Ejemplo con break:
# Imprime el contenido de los ficheros hasta que
# encuentra una línea en blanco
for file in $*
do
while read buf
do
if [ -z "$buf" ]
then
break 2
fi
echo $buf
done < $file
done
Ejemplo con continue:
# Muestra un fichero pero no las líneas de más
# de 80 caracteres
while read buf
do
cuenta=`echo $buf | wc -c`
if [ $cuenta -gt 80 ]
then
continue
fi
echo $buf
done < $1



Funciones

Podemos definir funciones en un script de shell:
funcion() {
comandos
}
y para llamarla:
funcion p1 p2 p3
Siempre tenemos que definir la función antes de llamarla:
#!/bin/bash
# Definición de funciones
funcion1() {
comandos
}
funcion2() {
comandos
}
# Programa principal
funcion1 p1 p2 p3

Paso de parámetros

La función referencia los parámetros pasados por posición, es decir, $1, $2, ..., y $ * para la lista completa:
$ cat funcion1.sh
#!/bin/bash
funcion1()
{
echo "Parámetros pasados a la función: $ * "
echo "Parámetro 1: $1"
echo "Parámetro 2: $2"
}
# Programa principal
funcion1 "hola" "que tal estás" adios
$
$ bash funcion1.sh
Parámetros pasados a la función: hola que tal estás adios
Parámetro 1: hola
Parámetro 2: que tal estás

Variables locales

Es posible definir variables locales en las funciones:
$ cat locales.sh
#!/bin/bash
testvars() {
local localX="localX en función"
X="X en función"
echo "Dentro de la función: $localX, $X, $globalX"
}
# Programa principal
localX="localX en main"
X="X en main"
globalX="globalX en main"
echo "Dentro de main: $localX, $X, $globalX"
# Llama a la función
testvars
echo "Otra vez dentro de main: $localX, $X, $globalX"
$ bash locales.sh
Dentro de main: localX en main, X en main, globalX en main
Dentro de la función: localX en función, X en función, globalX en main
Otra vez dentro de main: localX en main, X en función, globalX en main

return

Después de llamar a una función, $? tiene el código se salida del último comando ejecutado:
  • podemos ponerlo de forma explícita usando return
#!/bin/bash
funcion2() {
if [ -f /bin/ls -a -f /bin/ln ]; then
return 0
else
return 1
fi
}
# Programa principal
if funcion2; then
echo "Los dos ficheros existen"
else
echo "Falta uno de los ficheros - adiós"
exit 1
fi
return permite devolver un entero entre 0 y 255
#!/bin/bash
# maximum.sh: Máximo de dos enteros
EQUAL=0
max() {
if [ "$1" -eq "$2" ]; then
return $EQUAL
elif [ "$1" -gt "$2" ]; then
return $1
else
return $2
fi }
max 36 34
salida=$?
if [ "$salida" -eq $EQUAL ]; then
echo "Los números son iguales"
else
echo "El mayor es $salida"
fi

Recursión

Bash permite recursión, no recomendable debido a su coste
Ejemplo: recorre recursivamente los directorios
#!/bin/bash
tree() {
local DIR=$1
local ACTUALDIR=$(pwd)
cd $DIR
local filelist=$(ls)
for file in $filelist; do
if [ -d $file ]; then
tree "$DIR/$file"
else
echo "Dir. $(basename $DIR)--> Fichero $file"
fi
done
cd $ACTUALDIR
}
tree $1


Arrays

Bash soporta arrays unidimensionales
  • No es necesario especificar su tamaño ni reservar memoria
  • Los índices empiezan a contar en cero
  • Los elementos de un array pueden ser de tipos diferentes
  • Formas de inicialización:
    • array[índice]=valor
    • array=(valor0 valor1 ...)
    • array=([índice]=valor [índice]=valor ...)
    • declare -a array
  • Para acceder a un elemento de un array tenemos que ponerlo entre llaves: ${array[índice]}

Ejemplo de uso de arrays:

$ cat array1.sh
#!/bin/bash
# Los elementos del array no necesitan ser consecutivos
array[11]=23
array[13]=37
array[5]=$((${array[11]}+ ${array[13]}))
echo "array[5]=${array[5]}"
# Otra forma de inicializar un array
array2=( cero uno dos tres cuatro )
echo "array2[3]=${array2[3]}"
echo "Elementos en array2=${#array2[@]}"
# Una tercera forma
array3=( [8]=ocho [10]=10 [13]=trece )
echo "array3[10]=${array3[10]}"
echo "array3=${array3[@]}"
echo "array3=${array3[@]:10}"
$ bash array1.sh
array[5]=60
array2[3]=tres
Elementos en array2=5
array3[10]=10
array3=ocho 10 trece
array3=10 trece