ASO-GIT
La shell es 1)una potente interfaz con el sistema operativo basada en línea de comandos y 2)un interprete de scripts. Un shell script es un programa realizado en el lenguaje de la shell. Dicho de una forma sencilla y generalizando, un shell script es un fichero de texto que contiene órdenes que también podrían ser ejecutadas desde la línea de comandos.
Los shell script son muy utilizados en la administración de los sistemas ya que permiten, con una sola orden, ejecutar múltiples comandos, generalmente para automatizar alguna tarea. Ejemplos de tareas son la realización de copias de seguridad, la comprobación del espacio libre, la rotación de ficheros de log, etc. Cuando estas tareas se realizan de forma periódica su ejecución además se suele automatizar utilizando cron.
Hay múltiples implementaciones de shell: Bourne shell (sh), Bourne-Again shell (bash), Debian Almquist shell (dash), C shell (csh), etc. En Ubuntu, sh era la shell por defecto para el arranque del sistema, aunque desde hace años esa labor ha pasado a manos de dash (para minimizar los cambios, ahora en Ubuntu /bin/sh es un enlace simbólico a dash). Para los usuarios, la shell por defecto es bash (la shell de inicio de un usuario concreto se encuentra en /etc/passwd).
Para saber qué shell se está usando (ya sea en un terminal, ya sea en un script) se puede consultar la variable de entorno $SHELL
Como se ha dicho, los shell script son ficheros de texto, por lo cual se pueden crear con cualquier editor: vi, nano, gedit, etc.
Empecemos con el siguiente ejemplo:
El símbolo #
indica el comienzo de comentario (puede ir en cualquier parte de la línea, pero si va precedido de una orden debe ir separado por un blanco).
En el ejemplo, la primera línea, aunque parece un comentario, tiene un significado especial. #!/bin/bash
se llama shebang (el nombre viene del sonido de las dos primeras letras de #, SHarp, y de ! que se denomina bang en la cultura hacker). Esta línea dice a la shell que lo ejecuta qué intérprete de comandos usar de no especificarse uno concreto para su ejecución. En este primer caso, es la bash
quien ejecutaría ese script, pero podría haber sido cualquier otro intérprete, por ejemplo:
Nota: El shebang es optativo (pero muy recomendable) y debe aparecer siempre en la primera línea. Por otro lado, Linux no usa las extensiones de los ficheros para saber su tipo, aunque es recomendable su utilización (así además evitamos que el nombre del script coincida con alguna orden de Linux). Para saber el tipo de fichero Linux utiliza los "números mágicos". En el caso de los script, que son ficheros de texto, el shebang ayuda a reconocerlos.
Ahora podemos ejecutar el primer programa, por ejemplo, especificando que sea con bash:
También podríamos haber utilizado dash:
Se puede hacer que sea la propia shell actual la que ejecute el script (con lo cual, por ejemplo, se puede hacer que el script varíe las variables de entorno de la shell actual) mediante la orden source
(que también se puede abreviar como .
):
Para hacer que el script se ejecute con la shell especificada en el shebang, el fichero tiene que tener como mínimo permisos de lectura y ejecución:
Ahora ya podemos llamarlo directamente desde la línea de comando:
Es necesario especificar la ruta del fichero porque, por razones de seguridad, Linux no busca por defecto el fichero en el directorio actual (solo lo hace en los directorios que se encuentran en la variable de entorno PATH).
Ejercicio: Explica qué ocurre aquí:
El error que aparece en la primera llamada al script es debido a que estamos intentando ejecutar un script Python con bash. En la segunda, como no especificamos ninguna shell y existe un shebang, se utiliza éste (/usr/bin/python) para su ejecución, obteniéndose el resultado esperado.
Para depurar scripts, las shell disponen de diversas opciones. Por ejemplo, bash tiene entre otras -x
(Print commands and their arguments as they are executed) y -v
(Print shell input lines as they are read). Estas opciones se pueden incluir en el shebang y/o en la ejecución explícita con bash. Ejemplo
Se utilizan los siguientes identificadores:
$0
nombre del script. Si no se quiere el path, se puede usar $(basename $0)$i
(con i entre 1 y 9), i-esimo parámetro${i}
(obligatorio para i a partir de 10, aunque se puede usar para cualquier valor de i), i-ésimo parámetro$#
Número de argumentos$*
Todos los argumentosCuando se ejecuta una orden en la shell, va a devolver un valor (exit status) que indica si el resultado de la ejecución ha sido correcto (0) o ha habido algún problema (>0). Este valor se almacena en la variable $?
como se ve en el siguiente ejemplo:
El segundo echo $?
lo que muestra es el resultado de la ejecución de la orden anterior, que es echo $?
y que se ha ejecutado correctamente, por lo cual se muestra 0.
De igual forma, los script pueden devolver un valor de salida determinado mediante la orden bash exit
(que además hace que termine la ejecución del script) seguida de un número que es el que se desea que se retorne de exit status. Si la última orden ejecutada antes de terminar el script no indica explícitamente el valor retornado (esto es, se termine en una instrucción distinta de exit
seguida del valor a retornar), el valor de salida es el exit status de dicha última orden.
Al igual que en la línea de comando, en un script se pueden definir variables. Éstas pueden ser "locales" (existen solo en la shell donde se han declarado) o "globales" o de entorno, que son además accesibles a todas las subshell que se ejecuten desde la shell en la que se han declarado.
Por ejemplo, sean los siguientes script:
Razónese el resultado obtenido por las siguientes órdenes:
Al invocar p1.sh con source
, dicho script se ejecuta en la propia shell por lo cual se puede acceder al contenido de ambas variables.
En bash se pueden hacer lecturas interactivas usando la orden interna read, que además puede escribir, con la opción -p
, un prompt o mensaje previo. Además se puede especificar la variable donde se guardará la entrada del usuario (de no especificarse, se guarda en la variable REPLY)
Por ejemplo:
Hay otras opciones interesantes, como -s
(de silence que hace que la entrada de teclado no se muestre por pantalla) y -nX
que hace que la lectura sea como máximo de X caracteres (si se llega al tope sin pulsar intro, se termina la lectura y se pasa a la siguiente orden). Un uso común se muestra a continuación:
if
En shell scripts es posible alterar el camino de ejecución en función del valor de retorno de una instrucción cualquiera. Por ejemplo:
Es importante tener esto en cuenta: lo que evalúa if
no es una condición booleana sino el resultado (exit status) de ejecutar una instrucción. Es fácil olvidarse de esto dado que en la mayoría de los lenguajes de programación, la clausula if
evalúa una expresión lógica que puede ser true
o false
.
Ejercicio: escribir un script que reciba como argumento un nombre de usuario. El script debe verificar si dicho usuario existe (si existe en el archivo /etc/passwd
) y emitir un mensaje por stdout indicando si existe o no.
Se puede hacer evaluando en un if
el resultado de hacer grep ^$USUARIO: /etc/passwd
. Nótese que se pone ^ para especificar que el nombre de usuario debe encontrarse a principio de línea y : es el separador de campos en el fichero passwd.
Si realmente necesitamos hacer comprobaciones lógicas entonces necesitamos una orden que las haga por nosotros: la orden test
test
test es una orden de la shell que evalúa expresiones booleanas simples de ficheros, números y cadenas, devolviendo (exit status) 0 en caso verdadero y 1 en caso falso.
Nota: se pueden usar la notación de corchetes, por ejemplo [ 10 -lt 5 ]
, donde el corchete de apertura equivale a test
(por eso es necesario que tras el corchete vaya un espacio en blanco).
Algunos argumentos comunes (nótese que cada uno es un argumento y que por tanto deben estar separados por espacios):
Comprobación de test
con ficheros
-d nombre
nombre es un directorio
-f nombre
nombre es un fichero regular
-r nombre
nombre existe y se puede leer
-w nombre
nombre existe y se puede escribir
-x nombre
nombre existe y es ejecutable
-s nombre
nombre existe y su tamaño no es cero
f1 -nt f2
f1 es más reciente que f2
f1 -ot f2
f1 es más antiguo que f2
Comprobaciones de test
con cadenas
s1 = s1
s1 igual a s2
s1 != s2
s1 distinta a s2
-z s
s tiene longitud 0
-n s
s tiene longitud mayor que cero
test numéricos (números enteros)
a -eq b
a igual a b
a -ne b
a distinto de b
a -gt b
a mayor que b
a -ge b
a mayor o igual a b
a -lt b
a menor que b
a -le b
a menor o igual que b
Combinación y negación de test
t1 -a t2
And: los test t1 y t2 son ciertos
t1 -o t2
Or: t1 o t2 es cierto
!t
Niega t
\( \)
Agrupa test
if
/ if else
/ if elif
La sintaxis de la orden if
es:
Es muy habitual poner el then
en la misma línea que el if
, lo cual requiere un ;
Como ya se ha visto, es posible también utilizar un if-else
:
Cuando intervienen varias condiciones, es más cómodo usar el if-elif
:
Ejemplo:
case
Cuando dependiendo del resultado de una expresión haya múltiples opciones, es mejor utilizar case
en vez de if
. Su sintaxis es:
Es importante que la última orden de cada caso termine en ;;
y por ello, para que resalte y sea fácil descubrir un olvido, se suele poner en la línea siguiente.
Ejemplo (nótese que en los casos se pueden usar patrones como los que se utilizan en la expansión de nombres de fichero):
Hay varías instrucciones en la shell que permiten realizar bucles. Se muestran a continuación mediante ejemplos.
while
until
for
La primera sintaxis del for es muy similar a C:
En su segunda sintaxis, permite el recorrido de listas. Por ejemplo:
Un ejemplo de ejecución sería:
Una de las posibles aplicaciones es el recorrido de listas de argumentos cuando dicha lista es muy larga, como por ejemplo por el resultado de la expansión del metacaracter *
. ¿Qué ocurriría si el script anterior lo invocamos haciendo ./colores.sh /etc/*
?