Guión de la sesión 4: Bash Scripts
Hola a todos, me habría encantado estar en la última clase con vosotros y despedirnos como dios manda ;)
Había pensado en hacer un video pero me iba a llevar demasiado tiempo, especialmente editarlo, así que he cambiado el formato a un tutorial que resulta ser el guión de mi propia clase :) A ver qué tal.
¡Ah! No olvidéis rellenar la encuesta: https://servicios.upm.es/encuestas
Empezamos: man bash
Primer script de Bash
- Vamos a construir un script suficientemente interesante como para ver los aspectos fundamentales de lo que queremos que aprendáis.
- La palabra script (guión en español) es la palabra que habitualmente se usa para hablar de un programa en un lenguaje interpretado.
- Los programas en los lenguajes interpretados son “textos” que son directamente leídos por un intérprete (otro programa) para ejecutar línea a línea las sentencias que van apareciendo.
- En un lenguaje compilado los programas son traducidos a código máquina que directamtente puede ser ejecutado por la propia CPU (o por otro programa que es una CPU abstracta como es el caso de la Java Virtual Machine de Java).
- Bueno, que estamos en Bash.
- El programa que interpretará nuestros scripts es
/bin/bash
, es decir el propio programa Bash que se ejecuta cuando accedéis a una máquina Linux como triqui
.
- Como ya viene siendo habitual, a mi me gusta empezar cualquier programa con un código que no haga nada pero que sea sintácticamente correcto y podamos ejecutarlo son problemas.
- Vamos con un script que escribe “Hola mundo” en la salida estándar. Creamos un fichero al que vamos a llamar
hola.sh
(por convención usamos la extensión .sh
pero técnicamente no es necesario, podríamos usar cualquier extensión sensata o directamente ninguna):
1
2
|
#!/bin/bash
echo Hola mundo
|
- Salvamos el fichero y lo ejecutamos desde nuestra línea de comandos:
1
2
|
$ ./hola.sh
bash: ./hola.sh: Permiso denegado
|
- Ya hemos aprendido algo, para poder ejecutar un script de Bash es necesario darle permisos de ejecución.
- Para ello usaremos el comando
chmod +x hola.sh
y…
1
2
3
|
$ chmod +x hola.sh
$ ./hola.sh
Hola mundo
|
- Perfecto. Analicemos el programa.
- La primera línea es una línea que caracteriza todos los scripts e indica qué intérprete se va a usar para procesar script, en nuestro caso el intérprete Bash (
/bin/bash
).
- Es la línea con la que comenzará cualquier script:
#!/bin/bash
- Si alguien quiere desgranar, el símbolo
#
es un comentario que el intérprete ignorará, y !
indica que se va a ejecutar el programa que sigue (/bin/bash
) el cuál va a ser alimentado por su entrada estándar con el contenido del propio fichero (hola.sh
).
- De alguna forma es como hacer esto:
1
2
|
$ /bin/bash < hola.sh
Hola mundo
|
- Mola.
- La segunda línea es, como cabe esperar, un comando de Unix, en este caso es la ejecución del programa
echo
pasándole como argumentos Hola
y mundo
.
- En otras palabras, las sentencias del lenguaje Bash pasan a ser todos los programas que tenga instalados en mi máquina.
- Cuidado porque esto nos da una potencia desmesurada como veremos a continuación.
Script de predicción del tiempo
- En internet hay un servicio Web para tener una predicción del tiempo (clima):
https://wttr.in/
(programa implementado en Go por Igor Chubin).
- Puedes probarlo tú mismo desde la consola, para ello necesitas tener instalado el programa
curl
(en Ubuntu lo puedes instalar usando sudo apt-get install curl
, en triqui ya está instalado).
- El programa
curl
es un cliente de HTTP que se conecta a un servidor Web y saca por la salida estándar lo que ese servicio devuelve. En nuestro caso:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
$ curl -s https://wttr.in/madrid
Weather report: madrid
\ / Sunny
.-. +13(12) °C
― ( ) ― ↙ 7 km/h
`-’ 10 km
/ \ 0.0 mm
┌─────────────┐
┌──────────────────────────────┬───────────────────────┤ Fri 15 Dec ├───────────────────────┬──────────────────────────────┐
│ Morning │ Noon └──────┬──────┘ Evening │ Night │
├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
│ \ / Sunny │ \ / Sunny │ \ / Clear │ \ / Clear │
│ .-. +7(5) °C │ .-. +11(10) °C │ .-. +10(9) °C │ .-. +8(7) °C │
│ ― ( ) ― ↓ 8-12 km/h │ ― ( ) ― ↙ 11-12 km/h │ ― ( ) ― ↙ 8-14 km/h │ ― ( ) ― ↙ 6-13 km/h │
│ `-’ 10 km │ `-’ 10 km │ `-’ 10 km │ `-’ 10 km │
│ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │
└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘
┌─────────────┐
┌──────────────────────────────┬───────────────────────┤ Sat 16 Dec ├───────────────────────┬──────────────────────────────┐
│ Morning │ Noon └──────┬──────┘ Evening │ Night │
├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
│ \ / Sunny │ \ / Sunny │ \ / Clear │ \ / Clear │
│ .-. +6(5) °C │ .-. 11 °C │ .-. +8(9) °C │ .-. 7 °C │
│ ― ( ) ― ↙ 7-11 km/h │ ― ( ) ― ↓ 5-6 km/h │ ― ( ) ― ↙ 3-7 km/h │ ― ( ) ― ↙ 5-10 km/h │
│ `-’ 10 km │ `-’ 10 km │ `-’ 10 km │ `-’ 10 km │
│ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │
└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘
┌─────────────┐
┌──────────────────────────────┬───────────────────────┤ Sun 17 Dec ├───────────────────────┬──────────────────────────────┐
│ Morning │ Noon └──────┬──────┘ Evening │ Night │
├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
│ \ / Sunny │ \ / Sunny │ \ / Clear │ \ / Clear │
│ .-. +6(5) °C │ .-. 10 °C │ .-. 8 °C │ .-. +7(6) °C │
│ ― ( ) ― ↙ 5-10 km/h │ ― ( ) ― ↙ 5 km/h │ ― ( ) ― ↙ 2-5 km/h │ ― ( ) ― ↓ 6-13 km/h │
│ `-’ 10 km │ `-’ 10 km │ `-’ 10 km │ `-’ 10 km │
│ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │ / \ 0.0 mm | 0% │
└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘
Location: Madrid, Área metropolitana de Madrid y Corredor del Henares, Comunidad de Madrid, España [40.4167047,-3.7035824]
Follow @igor_chubin for wttr.in updates
|
- Bien, la idea es hacer un script que descargue la predicción del tiempo para un LUGAR dado como argumento del script y que lo almacene en un fichero de texto cuyo mombre incluirá la fecha y el nombre del lugar.
- Además queremos que el programa responda correctamente al argumento
-h
para que de algo de ayuda al usuario.
- Además, si el fichero ya existe no deberá realizarse la descarga de datos.
- Además, si hay algún error tendremos que emitir mensajes adecuados para el usuario del script.
- Mola.
- Vayamos paso a paso.
Argumentos de un Script
- Los argumentos de un script se recogen en parámetros posicionales:
$0
, $1
, $2
, etc.
- Además hay dos parámetros especiales que expandem a cuántos argumentos hay (
$#
) y todos los argumentos separados por espacios ($*
).
- Cread el script
arg.sh
para probar lo que acabmos de decir:
1
2
3
4
5
6
|
#!/bin/bash
echo Número de argumentos: $#
echo Argumento 0: $0
echo Argumento 1: $1
echo Argumento 2: $2
echo Todos los argumentos: $*
|
- Y ahora hacemos algunas pruebas para deducir cómo funcionan los parámetros posicionales:
1
2
3
4
5
6
7
8
9
10
11
12
|
$ ./param.sh
Número de argumentos: 0
Argumento 0: ./arg.sh
Argumento 1:
Argumento 2:
Todos los argumentos:
$ ./param.sh 1 dos III 100
Número de argumentos: 4
Argumento 0: ./arg.sh
Argumento 1: 1
Argumento 2: dos
Todos los argumentos: 1 dos III 100
|
- Pues ahora ya sabemos cómo vamos a leer el argumento LUGAR o las opciones como
-h
.
Fecha de hoy
- Por otro lado vamos a necesitar extraer la fecha en la que estamos ejecutando el script para usarla como nombre del fichero.
- En Unix existe un programa que te devuelve la fecha en la salida estándar:
date
- En concreto se puede especificar un formato concreto (ver
man date
):
1
2
|
$ date +%Y-%m-%d
2023-12-15
|
- Os propongo un reto intermedio: crear un fichero vacío con nombre la fecha de hoy y extensión
.txt
.
- Os ayudo introduciendo un programa que permite crear un fichero vacío:
touch
(el programar realmente modifica la fecha de actualización de un fichero preo si el fichero no existe entonces lo crea).
- Mola.
- Pensadlo por un rato: crear un fichero vacío con nombre la fecha de hoy + extensión
.txt
.
- Vamos con ello.
- La única forma de hacerlo es usando command substitution:
$(...)
.
- La expresión
$(command)
de Bash se expande a la salida estandar del comando
.
- Cuando uso la palabra expandir quiero decir que donde tú escribas
$(command)
Bash lo va a cambiar por la salida estándar de command
.
- Por ejemplo:
1
2
|
$ echo la fecha es $(date +%Y-%m-%d)
la fecha es 2023-12-15
|
- Bien, pues ahora podemos crear ese fichero, pero lo vamos a hacer usando una variable intermedia (variable de entorno, llamada a veces parámetro en Bash, no confundir con argumento):
1
2
3
4
|
$ FECHA=$(date +%Y-%m-%d)
$ touch $FECHA.txt
$ ls -al *.txt
-rw-r--r-- 1 angel angel 0 dic 15 18:29 2023-12-15.txt
|
- Está vacío el fichero, esa era la idea. Lo borramos que no moleste:
1
2
|
$ rm $FECHA.txt
$ ls -al *.txt
|
Primera versión de tiempo.sh
- Pues vamos con nuestra primera versión del script
tiempo.sh
: consultamos el sitio Web con curl y almacenamos la salida en un fichero usando como lugar madrid
sin usar argumentos de momento:
1
2
3
4
|
#!/bin/bash
FECHA=$(date +%Y-%m-%d)
curl https://wttr.in/madrid?T > madrid-$FECHA.txt
|
1
2
3
4
|
$ ./tiempo.sh
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 6649 100 6649 0 0 16069 0 --:--:-- --:--:-- --:--:-- 16099
|
- Feo… Yo personalmente no quiero ver esa información de descarga que escribe
curl
en la salida de error.
- Tenemos dos formas de solucionarlo, una es enviando la salida de error a la basura (el fichero
/dev/null
es la trituradora), así:
1
|
curl https://wttr.in/madrid?T > madrid-$FECHA.txt 2> /dev/null
|
- La otra, más pro, leyendo el manual de
curl
y viendo que hay un flag --silent
:
1
|
curl --silent https://wttr.in/madrid?T > madrid-$FECHA.txt
|
1
2
|
$ ls -al *.txt
-rw-r--r-- 1 angel angel 6649 dic 15 18:55 madrid-2023-12-15.txt
|
- Y puedes ver el contenido. ¿Con qué comando? (respuesta:
cat madrid-2023-12-15.txt
, en tu caso con tu fecha, claro).
Segunda versión de tiempo.sh
- Admitamos el lugar como el argumento del script (es decir
$1
). El script nos quedaría así (observa que he cambiado madrid por $1
para que Bash expanda el argumento en los sitios adecuados):
1
2
3
4
|
#!/bin/bash
FECHA=$(date +%Y-%m-%d)
curl --silent https://wttr.in/$1?T > $1-$FECHA.txt
|
- Y directamente podemos probar con
barcelona
:
1
2
3
4
|
$ ./tiempo.sh barcelona
$ ls -al *.txt
-rw-r--r-- 1 angel angel 6703 dic 15 19:04 barcelona-2023-12-15.txt
-rw-r--r-- 1 angel angel 6649 dic 15 18:55 madrid-2023-1215.txt
|
- De nuevo puedes comprobar el contenido del nuevo fichero para asegurarte de que todo ha ido bien y que se ha descargado la predicción de Barcelona.
Tercera versión de tiempo.sh
- En este tercer paso vamos a procesar los argumentos para que nuestro script ayude al usuario.
- El “manual” de invocación será
tiempo.sh [ -h | LUGAR ]
: si se invoca con -h
sale una ayuda apropiada por la salida estándar, si se invoca con un lugar se conecta al servidor usando ese lugar como referencia y si se invoca sin argumentos o con más de un argumento, también sale la ayuda pero por la salida de error y el script tiene que terminar mal.
- Vamos a por ello.
- Bash tiene varias construcciones de flujo, las habituales de cualquier lenguaje de programación imperativo:
if
, case
, for
, while
, etc. (consúlta el manual!).
- Empecemos con el
if
, según la sintaxis del manual, la construcción if
se define así:
1
|
if list; then list; [ elif list; then list; ] ... [ else list; ] fi
|
- Donde pone
list
se puede entender que va un comando de Bash.
- Si el primer comando, el que va después del
if
va bien (si exit status es 0) se ejecutan los comandos del then
, si no se ejecutan los comandos del else
.
- Empecemos en pseudocódigo (lo siguiente no es sintácticamente correcto):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#!/bin/bash
if <<$# ES IGUAL A 1>>; then
if <<$1 ES IGUAL A -h>>; then
<<MENSAJE DE AYUDA EN SALIDA ESTÁNDAR>>
else
FECHA=$(date +%Y-%m-%d)
curl --silent https://wttr.in/$1?T > $1-$FECHA.txt
fi
else
<<MENSAJE DE AYUDA EN SALIDA DE ERROR>>
exit 1
fi
exit 0
|
El programa test
- Bien, fácil :)
- ¿Cómo comprobamos que
$#
ES IGUAL A 1?
- Realmente necesitamos un programa cuyo exit status sea 0 cuando esa condición sea cierta.
- Existe un programa maravilloso y extraordinariamente importante en Unix para comprobar condiciones, condiciones sobre datos y sobre ficheros.
- Ese programa se llama
test
: ya estás tardando en ejecutar man test
.
- Con test puedes comparar datos.
- Por ejemplo, según el manual,
test $# -eq 1
tiene un exit status 0 cuando el valor de $#
es igual a 1 (-eq
indica equal).
- Puedes comprobar si dos strings son iguales con, por ejemplo,
test "$1" = "-h"
(observa que comparar números es con -eq
y comparar enteros es =
).
- Luego lo vemos más tarde pero puedes comprobar si un fichero existe o no (opción
-e
), si es legible o no (opción -r
), si es ejecutable o no (opción -x
), etc.
- Pues con esto podemos refinar nuestro pseudocódigo (sigue sin ser correcto pero se acerca):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#!/bin/bash
if test $# -eq 1; then
if test "$1" = "-h">>; then
<<MENSAJE DE AYUDA EN SALIDA ESTÁNDAR>>
else
FECHA=$(date +%Y-%m-%d)
curl --silent https://wttr.in/$1?T > $1-$FECHA.txt
fi
else
<<MENSAJE DE AYUDA EN SALIDA DE ERROR>>
exit 1
fi
exit 0
|
- El mensaje de ayuda podemos ya escribirlo, serán varias líneas, algo como (observa el uso de
$0
para responder al usuario con su propia invocación del script):
1
2
3
|
echo "USO: $0 [ -h | LUGAR ]"
echo " Crea un fichero con el nombre del lugar y la fecha actual conteniendo"
echo " la predicción del tiempo según el servicio https://wttr.in"
|
- Bien, pues adelante. El script finalmente queda así:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#!/bin/bash
if test $# -eq 1; then
if test "$1" = "-h"; then
echo "USO: $0 [ -h | LUGAR ]"
echo " Crea un fichero con el nombre del lugar y la fecha actual conteniendo"
echo " la predicción del tiempo según el servicio https://wttr.in"
else
FECHA=$(date +%Y-%m-%d)
curl --silent https://wttr.in/$1?T > $1-$FECHA.txt
fi
else
echo "USO: $0 [ -h | LUGAR ]" 1>&2
echo " Crea un fichero con el nombre del lugar y la fecha actual conteniendo" 1>&2
echo " la predicción del tiempo según el servicio https://wttr.in" 1>&2
exit 1
fi
exit 0
|
- Es importante observar el uso de la redirección
1>&2
. Su significado es redirige la salida estánda a la salida de error (mira el manual de Bash para ver más opcione de redirección).
- Puedes probar el script con las siguientes llamadas y analiza los resultados (no aparecen aquí, tendrás que probarlo tú misma):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
$ ./tiempo amsterdam
$ echo $?
$ ./tiempo
$ echo $?
$ ./tiempo -h
$ echo $?
$ ./tiempo madrid barcelona
$ echo $?
$ ./tiempo > /dev/null
$ echo $?
$ ./tiempo -h > /dev/null
$ echo $?
$ ./tiempo 2> /dev/null
$ echo $?
$ ./tiempo -h 2> /dev/null
$ echo $?
|
- Observa cómo redirigimos la salida estándar y la salida de error a la basura para distinguir el uso de
-h
y el uso no esperado del comando (sin argumentos o con varios argumentos).
El programa [
- ¿Puedes probar a ejecutar
which [
?
- ¿¡Hay un programa que se llama
[
!? Ciertamente.
- Y… sorpresa, ese programa es exáctamente el mismo programa
test
- Sólo hay algo que cambia: el programa
[
comprueba que su último argumento es ]
.
- Pero entonces, si
[
es el programar test
entonces es posible escribir esta llamada:
- O esta otra llamada (ya veremos lo que significa):
- Es decir, el programa
[
está ahí para poder escribir tests de forma más bonita.
Cuarta versión de tiempo.sh
- Ya casi estamos terminando el script, sólo falta comprobar que el fichero no está ya generado. Si lo está no será necesario descargar la predicción del tiempo.
- Usaremos
test
de nuevo (test
es siempre nuestro amigo ;)).
- El siguiente comando dentro del script comprobará si el fichero existe o no:
- Vamos a refactorizar un poquito (refactorizar es cambiar el programa para que haga lo mismo pero teniendo un código mejor, en este caso más legible).
- Vamos a incluir una nueva variable
LUGAR
:
- Y una nueva variable
NOMBREFICHERO
:
1
|
NOMBREFICHERO=$LUGAR-$FECHA.txt
|
- Bien, pues adelante, el script quedaría así, de nuevo en pseudocódigo ante la duda de qué hacer si el fichero existe:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
if test $# -eq 1; then
if test "$1" = "-h"; then
echo "USO: $0 [ -h | LUGAR ]"
echo " Crea un fichero con el nombre del lugar y la fecha actual conteniendo"
echo " la predicción del tiempo según el servicio https://wttr.in"
else
LUGAR=$1
FECHA=$(date +%Y-%m-%d)
NOMBREFICHERO=$LUGAR-$FECHA.txt
if test -e $NOMBREFICHERO; then
<<NO SE QUÉ HACER AQUÍ, QUIZÁS NADA>>
else
curl --silent https://wttr.in/$LUGAR?T > $NOMBREFICHERO
fi
fi
else
echo "USO: $0 [ -h | LUGAR ]" 1>&2
echo " Crea un fichero con el nombre del lugar y la fecha actual conteniendo" 1>&2
echo " la predicción del tiempo según el servicio https://wttr.in" 1>&2
exit 1
fi
exit 0
|
- Algo que hay que saber es que si tienes una condición de test, la puedes invertir usando
!
. Esto dice la sintaxis del manual de test
:
1
2
3
4
5
6
7
|
test EXPRESSION
An omitted EXPRESSION defaults to false. Otherwise, EXPRESSION
is true or false and sets exit status. It is one of:
...
! EXPRESSION
EXPRESSION is false
...
|
- Por lo tanto, podríamos escribir la comprobación de esta forma:
1
2
3
4
5
|
if test ! -e $NOMBREFICHERO; then
curl --silent https://wttr.in/$LUGAR?T > $NOMBREFICHERO
else
echo El fichero $NOMBREFICHERO ya existe
fi
|
- Precioso, este es el script tras este cuarto paso:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#!/bin/bash
if test $# -eq 1; then
if test "$1" = "-h"; then
echo "USO: $0 [ -h | LUGAR ]"
echo " Crea un fichero con el nombre del lugar y la fecha actual conteniendo"
echo " la predicción del tiempo según el servicio https://wttr.in"
else
LUGAR=$1
FECHA=$(date +%Y-%m-%d)
NOMBREFICHERO=$LUGAR-$FECHA.txt
if test ! -e $NOMBREFICHERO; then
curl --silent https://wttr.in/$LUGAR?T > $NOMBREFICHERO
else
echo El fichero $NOMBREFICHERO ya existe
fi
fi
else
echo "USO: $0 [ -h | LUGAR ]" 1>&2
echo " Crea un fichero con el nombre del lugar y la fecha actual conteniendo" 1>&2
echo " la predicción del tiempo según el servicio https://wttr.in" 1>&2
exit 1
fi
exit 0
|
case
- Ahora toca dar un paso sencillo para aprender cómo funciona otra construcción del lenguaje Bash: vamos a convertir el
if
de segundo nivel en un case
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
#!/bin/bash
if test $# -eq 1; then
case $1 in
-h | --help)
echo "USO: $0 [ -h | LUGAR ]"
echo " Crea un fichero con el nombre del lugar y la fecha actual conteniendo"
echo " la predicción del tiempo según el servicio https://wttr.in"
;;
*)
LUGAR=$1
FECHA=$(date +%Y-%m-%d)
NOMBREFICHERO=$LUGAR-$FECHA.txt
if test ! -e $NOMBREFICHERO; then
curl --silent https://wttr.in/$LUGAR?T > $NOMBREFICHERO
else
echo El fichero $NOMBREFICHERO ya existe
fi
;;
esac
else
echo "USO: $0 [ -h | LUGAR ]" 1>&2
echo " Crea un fichero con el nombre del lugar y la fecha actual conteniendo" 1>&2
echo " la predicción del tiempo según el servicio https://wttr.in" 1>&2
exit 1
fi
|
- Aspectos interesantes de
case
: se usa la palabra reservada in
, cada caso se empieza por una alternativa de texto con el que encajar separados por |
(nada que ver los pipes) y terminada en )
, y la lista de comandos termina con un ;;
(es como un break
de Java).
- El
case
se cierra con esac
(que es case
al revés, igual que fi
es if
al revés, muy idiomático :)).
Versión final de tiempo.sh
- Nuestra última versión va a ser una refactorización para embellecer el script usando funciones de Bash.
- Vamos a factorizar el código de descarga y, más útil aún, el código que informa sobre cómo se usa el script.
- En Bash una función se define usando la siguiente sintaxis:
1
2
3
4
|
function fname
{
list;
}
|
- Donde
list
son varios comandos simples o compuestos.
- Observar que las funciones no tienen argumentos, los argumentos formales de la función van a ser
$1
, $2
, etc.
- Una vez que se define una función, ésta es indistinguible de un programa, es decir, se invoca de esta forma:
- Donde a1, a2, etc. son los argumentos con los que se invoca la función (así los argumentos formales dentro de la función
$1
, $2
, etc. se expanden a dichos valores a1, a2, etc.)
- Vamos a definir la función
descargar_wttr
:
1
2
3
4
5
6
7
8
9
10
11
12
|
function descargar_wttr
{
LUGAR=$1
FECHA=$(date +%Y-%m-%d)
NOMBREFICHERO=$LUGAR-$FECHA.txt
if test ! -e $NOMBREFICHERO; then
curl --silent https://wttr.in/$LUGAR?T > $NOMBREFICHERO
return 0;
else
return 1;
fi
}
|
- La función es silenciosa (no imprime nada) pero su exit status es 0 si todo va bien 1 si el fichero ya existe.
- Ahora podemos escribir el siguiente
if
:
1
2
3
|
if ! descargar_wrrt $1; then
echo El fichero $NOMBREFICHERO ya existe
fi
|
- Observar que la función se usa como si fuera un programa, por ejemplo podríamos escribir:
1
|
descargar_wrrt valencia
|
- En ese caso el
$1
en el cuerpo de la función expande al argumento valencia
.
- Observar el uso de el símbolo de exclamación
!
en este caso para invertir el exit status de un comando.
- La segunda función va a ser la que se encarga de informar de cómo se usa el script, vamos a llamarla
uso
:
1
2
3
4
5
|
uso() {
echo "USO: $0 [ -h | LUGAR ]"
echo " Crea un fichero con el nombre del lugar y la fecha actual conteniendo"
echo " la predicción del tiempo según el servicio https://wttr.in"
}
|
- Aquí podemos ver que la palabra reservada **
function
es opcional y cambiarla por ()
despues del nombre.
- Además, cuando no hay
return
el exit status de la función es el exit status del último comando ejecutado.
- Todo junto ahora:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
#!/bin/bash
uso() {
echo "USO: $0 [ -h | LUGAR ]"
echo " Crea un fichero con el nombre del lugar y la fecha actual conteniendo"
echo " la predicción del tiempo según el servicio https://wttr.in"
}
function descargar_wttr
{
LUGAR=$1
FECHA=$(date +%Y-%m-%d)
NOMBREFICHERO=$LUGAR-$FECHA.txt
if test ! -e $NOMBREFICHERO; then
curl --silent https://wttr.in/$LUGAR?T > $NOMBREFICHERO
return 0
else
return 1
fi
}
if test $# -eq 1; then
case $1 in
-h | --help)
uso
;;
*)
if ! descargar_wrrt $1; then
echo El fichero $NOMBREFICHERO ya existe
fi
;;
esac
else
uso 1>&2
exit 1
fi
exit 0
|
- Observa que la salida de una invocación de una función puede redirigirse al completo.
Iteraciones
- Para terminar vamos a jugar con las iteraciones con un
for
.
- Vamos a hacer dos ejercicios, el primero es imprimir todos los argumentos de un script usando un
for
.
- Y a su vez lo vamos a hacer de dos formas diferentes. La primera es con una variable que vamos a iterar entre todos los valores de
$*
(todos los argumentos):
1
2
3
4
5
|
#!/bin/bash
echo Número de argumentos: $#
for arg in $*; do
echo Argumento: $arg
done
|
- La segunda forma es usando el comando interno de Bash
shift
. Según el manual:
1
2
3
4
5
6
7
8
9
|
shift [n]
The positional parameters from n+1 ... are renamed to $1
.... Parameters represented by the numbers $# down to
$#-n+1 are unset. n must be a non-negative number less
than or equal to $#. If n is 0, no parameters are
changed. If n is not given, it is assumed to be 1. If
n is greater than $#, the positional parameters are not
changed. The return status is greater than zero if n is
greater than $# or less than zero; otherwise 0.
|
- Eso significa que si hacemos un
shift
el parámetro $2
se convierte en $1
, $3
se convierte en $2
, etc. Además el contador de parámetros $#
se decrementa.
- Vamos a usar ese comando interno y una expresión aritmética (las operaciones aritméticas en Bash se escriben entre dobles paréntesis y si se quiere expandir su resultado se usa un
$((...))
, prueba echo $((3 + 1))
).
- El script
arg.sh
queda de esta forma:
1
2
3
4
5
6
7
8
9
|
#!/bin/bash
echo Número de argumentos: $#
N=$#
while (($#)); do
echo Argumento $((N - $# + 1)): $1
shift
done
|
- Ejercicio propuesto: versión de
tiempo.sh
para permitir varios lugares como argumentos en el script y que se genere un fichero para cada uno de ellos.
Famous Last Words
- Si quieres buscar inspiración te recomiendo que eches un ojo a los scripts de arranque del sistema que hay en los directorios
/etc/init.d
o /etc/rc*
. Todos ellos están llenos de increibles ideas.
- Recuerda que los operadores de composición
&&
y ||
son muy útils para evitar escribir código más verboso con if
s anidados. Por ejemplo, este código:
1
|
grep -zqs '^container=' /proc/1/environ && exit 0
|
- Es equivalente a este otro:
1
2
3
|
if grep -zqs '^container=' /proc/1/environ; then
exit 0
fi
|
1
|
[ -x "$DAEMON" ] || exit 0
|
1
2
3
4
5
|
if [ -x "$DAEMON" ]; then
; # No hacer nada
else
exit 0
fi
|