Shell

La shell es el intérprete de órdenes que utiliza Linux, bueno Unix en general. No hay una sola shell, sino que cada usuario puede elegir la que quiera en cada momento. Además las shells funcionan como lenguajes de programación de alto nivel, lo que se estuaciará en un capítulo posterior.
En principio hay dos familias de shell, las shell de Bourne y las shell C. Dentro de cada una de ellas hay diferentes vesiones, como por ejemplo ksh (Korn shell) o bash (Bourne Again Shell),. Nosotros en Linux vamos a usar esta última, aunque gran parte de las cosas son aplicables a otras shell.

Definiciones

Variables de shell

Una variable de shell es una zona de almacenamiento de información con un nombre que la identifica. Para no liarnos mucho, es similar a un variable en lenguaje C. Podemos asignar un valor a una variable mediante el operador “=”. Si la variable no existe el operador “=” la crea. No se debe dejar espacio entre el nombre de la variable y el operador de asignación (=).
Para usar el valor de una variable es necesario anteponerle el símbolo “$”.
Ejemplos:
$ AA=Hola que tal 
$ echo AA
$ echo $AA
Como norma de estilo, se suelen usar letras mayúscular para definir los nombres de las variables.

Variables de entorno

Son variables que tienen un significado propio para la shell o algún otro programa. Entre ellas podemos citar:
PATH
Indica la ruta de búsqueda de programas ejecutables. Está constituida por una lista de directorios separados por “:“. El directorio actual, de forma predeterminada, no viene incluida en PATH.
PS1
Especifica el indicador del sistema. Lo habitual es que PS1 sea “$” para usuarios normales y “#” para root.
PS2
Especifica el indicador secundario del sistema. Aparece cuando no se ha completado una orden.
LANG
Especifica el lenguaje que se aplica al usuario. Para español se utiliza “es”.
LC_ALL
Contiene el idioma y se utoiliza para usar los valores locales como mensajes del sistema, símbolo monetario, formato de fecha, formato de números decimales y otras características.
TERM
Indica el tipo de teminal que se está utilizando. Por ejemplo, si estamos en la consola el valor de TERM será “linux”, para usar las carácterísticas del teclado. Si entramos por telnet desde w9x entonces probablemente tengamos que poner vt100.
EDITOR
Especifica el editor por omisión del sistema. Lo habitual en los sistema Unix es que el editor por omisión sea “vi”.
DISPLAY
Especifica qué equipo muestra la salida que se efectúa en modo gráfico. Ese equipo deberá tener un servidor gráfico.
LD_LIBRARY_PATH
Esta variable se utiliza para definir rutas alternativas de búsqueda de para bibliotecas de funciones del sistema.
PWD
Contiene el directorio de trabajo efectivo.
Con la orden env (environment) podemos comprobar el valor de las variables de entorno del sistema. Para modificarlas basta asignarle un nuevo valor.
Ejemplo:
$ env
PWD=/home/usuario/public_html/docs/shell
LTDL_LIBRARY_PATH=/home/usuario/.kde/lib:/usr/lib 
HOSTNAME=www.bdat.com 
LD_LIBRARY_PATH=/home/usuario/.kde/lib:/usr/lib 
QTDIR=/usr/lib/qt-2.2.2 
CLASSPATH=./:/usr/lib/jdk118/lib/classes.zip 
LESSOPEN=|/usr/bin/lesspipe.sh %s  
PS1=[\u@\h\W]$ 
KDEDIR=/usr 
BROWSER=/usr/bin/netscape 
USER=usuario
MACHTYPE=i386-redhat-linux-gnu 
LC_ALL=es_ES 
MAIL=/var/spool/mail/usuario 
EDITOR=joe 
LANG=es
COLORTERM=
DISPLAY=:0
LOGNAME=usuario 
SHLVL=4 
LC_CTYPE=iso-8859-1 
SESSION_MANAGER=local/www.bdat.com:/tmp/.ICE-unix/965 
SHELL=/bin/bash 
HOSTTYPE=i386 
OSTYPE=linux-gnu 
HISTSIZE=100 
HOME=/home/usuario 
TERM=xterm 
PATH=/usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/usr/lib/jdk118/bin:./:/sbin:/usr/sbin:/usr/local/sbin:/opt/mozilla 
GROFF_TYPESETTER=latin1 
LESSCHARSET=latin1 

Ficheros ejecutables

Un fichero ejecutable es un fichero que tenga asignado el permiso de ejecución.
Los ficheros ejecutables pueden ser de dos tipos:
  • Binarios, cuando son resultado de la compilación de un programa, por ejemplo en C.
  • Programas de shell, llamados scripts o guiones. Son ficheros de texto que contienen órdenes de shell y son interpretados por una shell.

Shell y subshell

Podemos abrir una nueva shell simplemente ejecutando el fichero binario que contiene la shell.
También, cuando ejecutamos un script se abre una nueva shell que es la encargada de ir interpretando las diferentes órdenes y de mantener el valor de las variables definidas. Esta subshell tiene como tiempo de vida el tiempo de ejecución del script. Además esta subshell hereda el valor de parte de las variables de entorno, pero en su propio espacio de memoria, por lo que las modificaciones de cualquier variable de la nueva shell no tendrá repercusión en la shell padre.

Cerrar una shell

Para cerrar una shell usamos la orden exit. Esta orden termina la ejecución de la shell y vuelve a la shell padre. Si ejecutamos la orden exit sobre la shell inicial que abrimos al entrar al sistema (llamada login shell) entonces terminamos la sesión de trabajo.

Variables exportadas

En el apartado anterior, veíamos que una subshell hereda parte de las variables definidas en la la shell padre. Para que las shell hijas de una dterminada shell hereden una variable es necesario indicarlo explícitamente con la orden export. Por ejemplo, si queremos que el valor de la variable EDITOR pase a las subshell que sean hijas de la shell activa, tendremos que poner
$ export EDITOR
En la shell bash, podemos realizar en una sola operación la asignación de una valor y exportarla, por ejemplo
$ export EDITOR=vi
No todas las shell permiten esta operación, en algunas hay que hacerlo en dos operaciones:
$ export 
EDITOR=vi

Ejemplos:

Asignar la palabra contenido a una variable llamada AA
$ AA=contenido
Mostrar el contenido de la variable en pantalla.
$ echo $AA
Abrir una shell hija de la anterior.
$ /bin/bash
Mostrar de nuevo el contenido de la variable
$ echo $AA
Salir de la subshell activa
$ exit
Mostrar de nuevo el contenido de la variable
$ echo $AA
Exportar la variable
$ export AA
Abrir una shell hija de la anterior.
$ /bin/bash
Mostrar de nuevo el contenido de la variable
$ echo $AA

Las comillas en la shell

En las operaciones con la shell distinguimos tres tipos de comillas con distintas funcionalidades: las comillas dobles, las comillas simples y la comilla invertica, el acento grave francés. A continuación describimos las funcdiones:
'
Engloban un literal. La shell no trata de interpretar nada de lo que haya comprendido entre estas comillas.
La shell expande las variables que haya especificadas sustituyendo su nombre por el contenido. Para imprimir un $ es necesario protegerlo con una \.
̀ La shell intenta ejecutar lo que haya comprendido entre estas comillas.
Ejemplo:
Asignamos una valor a una variable:
$ AA=”ls -la”
y observamos la diferencia entre:
echo '$AA'
echo “$AA”
y
echo  ̀$AA ̀

Personalización de la shell

Cada vez que se inicia una shell, se lee un fichero de configuración. No es un fichero complejo, es simplemente un fichero con ódenes que se se ejecutan automáticamente cada vez que se inicia una nueva shell.
Diferentes shell utilizan diferentes ficheros de configuración. Las shell C suelen llamar a este fichero .login. Las shell de Bourne suelen llamar a este fichero .profile. En la shell bash, además del .profile, tenemos también el fichero .bashrc.
En este fichero vamos a ejecutar las órdenes y asiganar valores a variables necesarios para adaptar la shell a nuestras necesidades. En general, todo aquéllo que queramos que se ejecute cada vez que entremos al sistema. Por ejemplo, nos puede interesar añadir a este fichero una línea como:
$ export LANG=es
para hacer que nuestro idioma predeterminado sea el español.
También podemos poner . (directorio efectivo) en la ruta de búsqueda de programas ejecutables:
$ export PATH=$PATH:.
La shell bash también usa el fichero .inputrc para configurar el teclado.

Intoducción

El editor vi (visual) es el programa de edición común a todos los sistemas Unix. Aún cuando es posible utilizar otros editores, es necesario conocer su funcionamiento en determinadas situaciones críticas.

Modos de trabajo

El editor vi tiene dos modos de trabajo:
  • Modo inserción: En este modo se puede introducir texto en el fichero que estamos editando. Para salir del modo inserción tenemos que pulsar la tecla Esc.
  • Modo orden: En este modo vi acepta órdenes. Las órdenes son letras con algún posible arguemento.

Terminales

Para el correcto funcionamiento del vi tenemos que asegurarnos que las características de la terminal son las adecuadas. Por ejemplo, si trabajamos con un terminal vt100 deberíamos poner:
$ TERM=vt100
$ export TERM
La línea inferior de la pantalla la usa vi como línea de estado, para escribir algunos mensajes y para escribir las órdenes.

Salir de vi

Para salir de vi hay que estar en modo orden, por lo que se tendrá que pulsar Esc.
orden
acción
ZZ
sale de vi grabando el fichero
:q!
sale de vi sin grabar los cambios
:wq
sale de vi grabando los cambios

Introducir texto (modo inserción)

orden
acción
i
inserta texto en la posición del cursor
a
inserta texto después de la posición del cursor
A
inserta texto al final de la línea
o
Crea una nueva línea bajo la actual
O
Crea una nueva línea sobre la actual

Borrar

orden
acción
x
borra el carácter sobre el cursor
d0
borra hasta princio de línea
dw
borra hasta el final de la palabra.
dnw
borra hasta el final de la palabra n
db
borra hasta el principio de la palabra
dd
borr la línea actual

Desplazamientos

orden
acción
->,espacio
espacio a la derecha
<-,h
espacio a la izquierda
w
palabra a la derecha
b
palabra a la izquierda
$
fin de línea
0
principio de línea
return
línea siguiente
j
línea de abajo
k
línea de arriba

Búsquedas y sustituciones

orden
acción
/expreg
busca expreg hacia adelante
?expreg
busca expreg hacia atrás
/
repite la última búsqueda hacia adelante
?
repite la última búsqueda hacia atrás
s/buscado/sustitución[/g]
sustituye la primera aparición de la palabra buscado reemplazándola por la parabra sustitición. Si añadimos /g al final, la sustitución es global en todo el documento.

Otras órdenes

orden
acción
H
principio de la pantalla
M
mitad de la pantalla
L
final de la pantalla
nG
A la línea n
(
principio de frase
)
fin de frase
{
principio de párrafo
}
fin de párrafo
r
sustituye el carácter del cursor
R
sustituye caracteres
D
borra hasta el final de línea
:set
number numera las líneas
:set
ai establece el sangrado automático
:!orden
ejecuta el orden de shell
:w
graba el fichero sin salir de vi.
Ejecución y agrupación de órdenes
Una vez vistas gran parte de las órdenes de usuario, pasamos a ver una de las principales características de un sistema Unix que es la facilidad para agrupar órdenes de distintas formas para realizar tareas complejas. Por un lado en la propia línea de órdenes se pueden especificar órdenes y unir su ejecución de diversas formas.
Hasta ahora hemos visto como combinar órdenes con tubería, como redirigir las salida y como gestionar procesos en primer y segundo planos. Ahora vamos a ver otra serie de mecanismos para ejecutar órdenes y programas y verificar el estado de conclusión de la orden.

Código de terminación de una orden

Cuando una orden termina le devuelve al sistema un código de finalización, un valor cero en caso de terminar correctamente o un valor uno si se ha producido un error. Este valor se almacena en la variable $?.
Por ejemplo
[pfabrega@port /home]$ ls -la /home
total 32
drwxr-xr-x    5 root     root         4096 sep 20 14:16 .
drwxr-xr-x   18 root     root         4096 sep 20 03:23 ..
drwxr-xr-x    4 root     root         4096 sep 28 21:16 httpd
drwxr-xr-x    2 root     root        16384 sep 19 18:25 lost+found
drwx--x--x   20 pfabrega pfabrega     4096 nov 24 19:07 pfabrega
[pfabrega@port /home]$ echo $?
0
[pfabrega@port /home]$ ls -la asdfg
ls: asdfg: No existe el fichero o el directorio
[pfabrega@port /home]$ echo $?
1
[pfabrega@port /home]$
Podemos observar como en la primera ejecución ls -la devuelve un valor 0 de terminación correcta y en la segunda devuelve un error y n código 1.

Ejecución consecutiva

Podemos agrupar varias órdenes en una misma línea de ordenes separándolas por “;”
La agrupación de órdenes separadas por “;” es útil cuando tenemos que repetir una misma secuencia de órdenes varias veces.
La ejecución de cada una de las órdenes se realiza cuando ha concluido la anterior, e independiente de que el resultado haya sido correcto o no.
Esto nos puede resultar útil para demorar la ejecución de una o varias órdenes un determinado tiempo, por ejemplo
$ sleep 300 ; ps axu
y la orden ps axu se ejecutaría a los 300 segundos.

Ejecución condicional

Otra situación algo más elaborada que la anterior es ejecutar una orden condicionada a la terminación correcta o no de una orden previa.
Esta funcionalidad nos la proporcionan los operadores “&&” y “||”.

Operador &&

El primer operador, “&& ” separa dos órdenes de forma que la que tiene a la derecha sólo se ejecuta cuando la de la izquierda termina correctamente, es decir
orden1 && orden2
orden2 sólo se ejecutará si orden1 terminó sin ningún error.
Por ejemplo, queremos ejecutar la orden cat fichero sólo si existe fichero; entonces tendremos que buscar una orden que termine con un error si no existe fichero, por ejemplo ls fichero y condicionar la ejecución de cat fichero a esta:
$ ls fichero && cat fichero
Otro ejemplo, para compilar los controladores de dispositivos de linux e instalarlos, lo podemos hacer como:
make module && make modules_install
es decir instalará los controladores sólo si ha conseguido compilarlos correctamente.

Operador ||

El segundo operador, “|| ” tiene un comportamiento similar al anterior, separa dos órdenes de forma que la que tiene a la derecha sólo se ejecuta cuando la de la izquierda termina incorrectamente, es decir
orden1 || orden2
orden2 sólo se ejecutará si orden1 terminó con algún error.
Por ejemplo si no existe fichero queremos crearlo vacía y si existe no hacemos nada. Igual que en el ejemplo anterior, buscamos una orden que termine con un error si no existe fichero y condicionamos la ejecución de la orden touch fichero al error de la orden previa:
$ ls fichero || touch fichero
También, al igual que en el ejempo anterior podríamos hacer que si el proceso make modules falla, se borraran todos los ficheros temporales que se crean:
make modules || rm -r *.o

Ejecución simultánea

Otra posibilidad de ejecución también posible es lanzar varios procesos simutáneamente en segundo plano; basta escribir uno a continuación de otro en la línea de órdenes separados por “&”. Este es el símbolo que se utiliza para indicar que el proceso se tiene que ejecutar en segundo plano, pero también actúa como separador para la ejecución de distintas órdenes.
por ejemplo :
[pfabrega@port pfabrega]$ sleep 10 & sleep 20 & sleep 15 &
[pfabrega@port pfabrega]$ ps axu
USER       PID %CPU %MEM   VSZ  RSS TTY      STAT START   TIME COMMAND
root         1  0.1  0.2  1408  540 ?        S    22:19   0:04 init [3]
...
pfabrega  1263  0.3  0.2  1624  516 pts/4    S    23:24   0:00 sleep 10
pfabrega  1264  0.0  0.2  1624  516 pts/4    S    23:24   0:00 sleep 20
pfabrega  1265  0.0  0.2  1624  516 pts/4    S    23:24   0:00 sleep 15
pfabrega  1266  0.0  0.3  2732  848 pts/4    R    23:24   0:00 ps axu

Agrupando con paréntesis

Podemos organizar la ejecución de varias órdenes agrupándolas convenientemente mediante paréntesis para modificar el orden predeterminado de ejecución. En primer lugar actúan los ; después los & y la ejecución es de izquierda a derecha. Para las ejecuciones condicionales se tiene en cuenta el valor de la variable $? que se fija por la última orden que se ejecuta.
Para alterar esta forma de ejecución podemos utilizar los paréntesis. En realidad los paréntesis fuerzan una nueva subshell para ejecutar las órdenes correspondientes.
Esta característica puede ser interesante en diferentes situaciones:
Quemos enviar una secuencia de órdene s a segundo pláno
(mkdir copiaseg; cp -r ./original/* ./copiaseg; rm -r ./original~; rm -r ./original.tmp) &

Resultado de la ejecución de una orden

Es habitual necesitar almacenar el resultado de la ejecución de una orden en una variable en lugar de que se dirija a la salida estándar o simplemente ejecutar como orden el resultado de otra orden.

Comillas invertidas ̀ ̀

Las comillas invertidas consideran una orden lo que tengan dentro y lo ejecutan devolviendo el resultado como líneas de texto a la shell. Por ejemplo:
$ A="ls /bin"
$ `echo $A`
arch        consolechars   ed          igawk     mount          rpm        tar
ash         cp             egrep       ipcalc    mt             rvi        tcsh
ash.static  cpio           ex          kill      mv             rview      touch
awk         csh            false       ln        netstat        sed        true
basename    date           fgrep       loadkeys  nice           setserial  umount
bash        dd             gawk        login     nisdomainname  sfxload    uname
bash2       df             gawk-3.0.6  ls        ping           sh         usleep
ssh         dmesg          grep        mail      ps             sleep      vi
cat         dnsdomainname  gtar        mkdir     pwd            sort       view
chgrp       doexec         gunzip      mknod     red            stty       ypdomainname
chmod       domainname     gzip        mktemp    rm             su         zcat
chown       echo           hostname    more      rmdir          sync
También podríamos haber puesto:
 
$ A="ls /bin" $ B=`$A` $ echo $B arch ash ash.static awk basename bash bash2 bsh cat chgrp chmod chown consolechars cp cpio csh date dd df dmesg dnsdomainname doexec domainname echo ed egrep ex false fgrep gawk gawk-3.0.6 grep gtar gunzip gzip hostname igawk ipcalc kill ln loadkeys login ls mail mkdir mknod mktemp more mount mt mv netstat nice nisdomainname ping ps pwd red rm rmdir rpm rvi rview sed setserial sfxload sh sleep sort stty su sync tar tcsh touch true umount uname usleep vi view ypdomainname zcat

El operador $()

La shell bash proporciona el operador $() similar a las comillas invertidas. Ejecuta como orden los que haya entre paréntesis y devuelve su resultado. El mecanismo de funcionamiento es idéntico, con la ventaja de poder anidar operadores.

Programas de shell

Además de las anteriores posibilidades también se pueden agrupar una serie de órdenes en un fichero de texto que se ejecutarán consecutivamente siguiendo el flujo determinado por órdenes de control similares a cualquier lenguaje de programación. Estos ficheros se conocen como scripts, guiones o simplemente programas de shell. A las órdenes agrupadas en ficheros también se le aplican todas las características descritas anteriormente. No olvidemos que para un sistema unix, una línea leída de un fichero es idéntica a una línea leída desde el teclado, una serie de caracteres terminado por un carácter de retorno de carro.
Cualquier forma de ejecución que se pueda dar en la línea de órdenes también se puede incluir en un fichero de texto, con lo que facilitamos su repetición. Y si por último añadimos las estructuras que controlan el flujo de ejecución y ciertas condiciones lógicas, tenemos un perfecto lenguaje de programación para administrar el sistema con toda facilidad. Un administrador que sabe cual es su trabajo cotidiano, realizar copias de seguridad, dar de alta o baja usuarios, comprobar que los servicios están activos, analizar log de incidencias, configurar cortafuegos, lanzar o parar servicios, modificar configuraciones, etc., normalmente se creará sus script personalizados. Algunos los utilizará cuando sea necesario y para otros programará el sistema para que se ejecuten periódicamente.
La programación en shell es imprescindible para poder administrar un sistema Unix de forma cómoda y eficiente.
Vemos un primer ejemplo:
  #!/bin/bash  
  echo Hola Mundo 
Puestas estas dos línea en un fichero de texto con permiso de ejecución, al ejecutarlo escribiría en pantalla ”Hola Mundo”. La primera línea, como veremos con posterioridad, indica qué shell es la que interpreta el programa de shell.

subshell

Para la ejecución de programas de shell, la shell se encarga de interpretar unas órdenes en unos casos o de lanzar el programa adecuado en otros casos. En general, cuando lanzamos la ejecución de un conjunto de órdenes agrupadas en un programa de shell, se abre una nueva shell (subshell hija de la anterior) que es la encargada de interpretar las órdenes del fichero. Una vez concluida la ejecución esta subshell muere y volvemos a la shell inicial. Esto es importante tenerlo presente para saber el comportamiento de los programas. Por ejemplo, los cambios hechos en las variables de shell dentro de un programa no se conservan una vez concluida la ejecución.
Vamos a ilustrar este comportamiento con nuestro primer ejemplo de programa de shell:
Creamos un programa en un fichero de texto, lo ejecutamos, comprobamos que se crea una nueva shell y que los cambios en las variables hechos dentro del programa no se mantienen una vez concluido. Editamos un fichero llamado ”pruebashell” con el siguiente contenido:
echo “******** GUION ********”
echo “el valor previo de VAR es ** $VAR **”
VAR=”valor asignado dentro del guion”
echo “Ahora VAR vale ** $VAR **”
ps
echo “******** FIN DEL GUION ********”
Con este guion mostramos el valor previo de una variable llamada VAR, le asignamos un valor nuevo y también los mostramos. Para verificar que se lanza una nueva shell mostramos la lista de procesos con la orden ps.
Una vez editado el fichero tendremos que asignarle el permiso de ejecución
$ chmod u+x pruebashell
después asignamos un una valor a la variable VAR para comprobar como cambia. Además tendremos que exportarla par que la shell hija pueda heredarla:
$ export VAR=”valor previo”
Ahora mostramos la lista de procesos para ver cuantas shell tenemos abiertas:
$ ps 
o bien
$ ps |wc -l
y a continuación ejecutamos el guion “pruebashell”:
$ ./pruebashell
y volvemos a mostrar el contenido de la variable:
$ echo $VAR
Podremos observar como aparece una shell más. Si la variable VAR está exportada veremos como muestra el valor que asignamos antes de ejecutar el guion y como muestra el que le asignamos dentro del guion. Y al final, al mostrar la variable VAR, observamos como nos muestra el valor que tenía antes de ejecutar el guion; el guion no ha modificado la variable.
Este mismo comportamiento se puede aplicar a la orden cd. Veamos el siguiente ejemplo, un simple script que cambia de directorio.
Editamos el fichero llamado “cambia” con el siguiente contenido:
echo “cambiando de directorio”
cd /tmp
echo “estamos en:“
pwd
Es decir, el script simplemente cambia al directorio /tmp.
Una vez editado le asignomos el permiso de ejecución
$ chmod u+x cambia
Ahora mostramos nuestro directorio activo
$ pwd
ejecutamos el script
$ ./cambia
y volvemos a comproba nuesto directorio activo
$ pwd
y observamos como estamos situados en el mismo directorio que antes de la ejecución del guion.
¿Por qué ocurre todo esto?, Porque todos los cambios se realizan en la subshell que ha interpretado el guion.

Comentarios y continuaciones de línea

Para añadir un comentario a un programa de shell se utiliza el carácter #. Cuando la shell interprete el programa ignorará todo lo que haya desde este carácter hasta el final de la línea.
Por otro lado si nos vemos obligados a partir un línea en dos o más, tendremos que finalizar cada línea de texto inconclusa con el carácter \. De esta forma la shell las verá todas ellas como si se tratara de una única línea.

Parámetros posicionales

En un programa de shell definen unas variables especiales, identificadas por números, que toman los valores de los argumentos que se indican en la línea de órdenes al ejecutarlo. Tras el nombre de un script se pueden añadir valores, cadenas de texto o números separados por espacios, es decir, parámetros posicionales del programa de shell, a los que se puede acceder utilizando estas variables.
La variable $0 contiene el parámetro 0 que es el nombre del programa de shell.
Las variables $1, $2, $3, $4, ... hacen referencia a los argumentos primero, segundo, tercero, cuarto, ... que se le hayan pasado al programa en el momento de la llamada de ejecución.
Por ejemplo, si tenemos un programa de shell llamado parametros con el siguiente contenido:
echo $0 
echo $1 
echo $2
echo $3 
al ejecutarlo
$ parametros primero segundo tercero
prametros
primero
segundo
tercero

Modificación de los parámetros posicionales

Durante la ejecución de un programa de shell podría interesarnos modificar el valor de los parámetros posicionales. Esto no lo podemos hacer directamente, las variables 1, 2, ... no están definidas como tales. Para realizar estos cambios tenemos que utilizar la orden set. Esta orden asigna los valores de los parámetros posicionales a la shell activa de la misma forma que se hace en la línea de órdenes al ejecutara un programa; hay que tener en cuenta que no los asigna individualmente, sino en conjunto.
Por ejemplo
$ set primero segundo tercero
$ echo $1 
primero
$ echo $2 
segundo
$ echo $3
tercero

La sentencia shift

La sentencia shift efectúa un desplazamiento de los parámetros posicionales hacia la izquierda un número especificado de posiciones. La sintaxis para la sentencia shift es:
$ shift n
donde n es el número de posiciones a desplazar. El valor predeterminado para n es 1. Hay que observar que al desplazar los parámetros hacia la izquierda de pierden los primeros valores, tantos como hayamos desplazado, al superponerse los que tiene a la derecha.
Por ejemplo:
$ set uno dos tres cuatro
$ echo $1
uno
$ shift
$ echo $1
dos
$ shift
$ echo $1
tres
$ shift
$ echo $1
cuatro

Operador {}

Hemos visto la forma de acceder a los diez primeros parámetros posicionales, pero para acceder a parámetros de más de dos dígitos tendremos que usar una pareja { } para englobar el número.
$ echo ${10}
$echo ${12}
El operador { } también se usa para delimitar el nombre de las variables si se quiere utilizarla incluida dentro de un texto sin separaciones:
$DIR=principal
$ DIRUNO=directorio
$ UNO=subdirectorio
$ echo $DIRUNO
directorio
$ echo ${DIR}UNO
principalUNO

Variables predefinidas

Además de las variables de shell propias del entorno, las definidas por el usuario y los parámetros posicionales en un shell existen otra serie de variables cuyo nombre está formado por un carácter especial, precedido por el habitual símbolo $.

Variable $*

*
La variable $* contiene una cadena de caracteres con todos los parámetros posicionales de la shell activa excepto el nombre del programa de shell. Cuando se utiliza entre comillas dobles se expande a una sola cadena y cada uno de los componentes está separado de los otros por el valor del carácter separador del sistema indicado en la variable IFS. Es decir si IFS tiene un valor “s” entonces "$*" es equivalente  a  "$1s$2s...". Si IFS no está definida, los parámetros se separan por espacios en blanco. Si IFS está definida pero tiene un contenido nulo los parámetros se unen sin separación.

Variable $@

@
La variable $@ contiene una cadena de caracteres con todos los parámetros posicionales de la shell activa excepto el nombre del programa de shell. La diferencia con $* se produce cuando se expande entre comillas dobles; $@ entre comillas dobles se expande en tantas cadenas de caracteres como parámetros posicionales haya. Es decir “$@” equivale a "$1" "$2" ...

Variable $#

#
Contiene el número de parámetros posicionales excluido el nombre del probrama de shell. Se suele utilizar en un guion de shell para verificar que el número de argumentos es el correcto.

Variable $?

?
Contiene el estado de ejecución de la última orden, 1 para una terminación con error o 0 para una terminación correcta. Se utiliza de forma interna por los operadores || y && que vimos con anterioridad, se utilizar por la orden test que vermos más adelante y también la podremos usar explícitamente.

Variable $$

$
contiene el PID de la shell. En un subshell obtenida por una ejecución con (), se expande al PID de la shell actual, no al de la subshell. Se puede utilizar para crear ficheros con nombre único, por ejemplo $$.tmp, para datos temporales.

Variable $!

!
Contiene el PID de la orden más recientemente ejecutada en segundo plano. Esta variable no puede ayudar a controlar desde un guion de shell los diferentes procesos que hayamos lanzado en segundo plano.

Ejemplos

Vemos algunos ejemplos a continuación:
$ set a b c d e
$ echo $# 
5
$ echo $*
a b c d e
$ set "a      b" c d
$ echo $# 
3 
$ echo $*
a     b c d
Otro ejemplo, si tenemos el script llamado ejvar1 con el siguiente contenido
ps
echo “ el PID es $$”
al ejecutarlo
$ ./ejvar1
PID TTY          TIME CMD
930 pts/3    00:00:00 bash
1011 pts/3    00:00:00 bash
1012 pts/3    00:00:00 ps
el PID es 1011 
y vemos como muestra el PID de la shell que ejecuta el script.
Como el PID del prodceso es único en el sistema, este valor puede utilizarse para construir nombres de ficheros temporales únicos para un proceso. Estos ficheros normalmente se suelen situar en el directorio temporal /tmp.
Por ejemplo:
miproctemp=/tmp/miproc.$$
. . . .
rm -f $miproctemp

Uso de valores predeterminados de variables

Además de las asignaciones de valores a variables vista con anterioridad, que consistía en utilizar el operador de asignación (=), podemos asignarle valores dependiendo del estado de la variable.

Uso de variable no definida o con valor nulo

Cuando una variable no está definida, o lo está pero contiene un valor nulo, se puede hacer que se use un valor predeterminado mediante la siguiente la expresión:
$ {variable:-valorpredeterminado}
Esta expresión devuelve el contenido de variable si está definida y tiene un valor no nulo. Por ejemplo si la variable resultado inicialmente no esta definida:
$ echo ${resultado}
$ echo "E1 resultado es: {resultado:-0}"
E1 resultado es: O
$ resultado=1
$ echo "E1 resultado es: ${resultado:-0}"
E1 resultado es: 1
A los parámetros posicionales podemos acceder como:
{$1: -o}

Uso de variable no definida

En algunas ocasiones interesa utilizar un valor predeterminado sólo en el caso de que la variable no esté definida. La expresión que se utiliza para ello es algo diferente de la anterior:
${variable- valorpredeterminado}
Consultar el ejmplo anterior.

Uso de variable definida o con valor nulo

Existe una expresión opuesta a la anterior. En este caso, si la variable está definida y contiene un valor no nulo, entonces en vez de usarse dicho valor, se utiliza el que se especifica. En caso contrario, el valor de la expresión es la cadena nula. La expresión para esto es:
$ {variable: +valorpredeterminado}
Por ejemplo:
$ resultado=10
$ echo ${resultado:+5}
5
$ resultado=
$ echo ${resultado:+30}
$

Uso de variable no definida

En algunas ocasiones puede también interesar que el comportamiento anterior sólo suceda cuando la variable no esté definida. La expresión para ello es:
$ {variable+valorpredeterminado}

Asignación de valores predeterminados de variables

Anteriormente veíamos la forma de utilizar un valor predeterminado de las variable en ciertos casos. Ahora, además de usar el valor predeterminado, queremos asignarlo a la variable.

Asignación a variable o definida o con valor nulo

En este caso no sólo utilizamos un valor predeterminado, sino que en la misma operación lo asignamos. La expresión que se utiliza para ello es la siguiente:
${variable:=valorpredeterminado}
Si el contenido de variable es no nulo, esta expresión devuelve dicho valor. Si el valor es nulo o la variable no está definida entonces el valor de la expresión es valorpredeterminado, el cual será también asignado a la variable variable. Veamos un ejemplo en el que se supone que la variable resultado no está definida:
 
$ echo ${resultado}
$ echo "El resultado es: ${resultado:=0}"
E1 resultado es: 0
$ echo ${resultado}
0
A los parámetros posicionales no se le pueden asignar valores utilizando este mecanismo.

Asignación a variable no definida

Análogo al caso anterior para el caso de que la variable no esté definida. La expresión ahora es:
${variable=valorpredeterminado}

Mostrar un mensaje de error asociado a una variable

Ahora lo que pretendemos es terminar un script con un mensaje de error asociado al contenido de una variable.

Variable no definida o con valor nulo

En otras ocasiones no interesa utilizar ningún valor por defecto, sino comprobar que la variable está definida y contiene un valor no nulo. En este último caso interesa avisar con un mensaje y que el programa de shell termine. La expresión para hacer esto es:
$ {variable : ?Mensaje }
Por ejemplo:
$ res=${resultado:? "variable no válida''}
resultado variable no valida
En el caso de que la variable resultado no esté definida o contenga un valor nulo, se mostrará el mensaje especificado en pantalla, y si esta instrucción se ejecuta desde un programa de shell, éste finalizará.

Variable no definida

Análogo al caso anterior para el caso de que la variable no esté definida. La expresión para ello es:
${variable?mensaje}

Otras operaciones con variables

Ciertas shell propporcionan unas facilidades que pueden ser útiles para ahorrar código en la programación de guiones de shell, como son la eliminación o extracción de subcadenas de una variable.

Subcadenas de una variable

${variable:inicio:longitud}
Extrae una subcadena de variable, partiendo de inicio y de tamaño indicado por longitud. Si se omite longitud toma hasta el fin de la cadena original.
$ A=abcdef
$ echo ${A:3:2}
de
$ echo ${A:1:4}
bcde
$ echo ${A:2}
cdef

Cortar texto al principio de una variable

${variable#texto}
Corta texto de variable si variable comienza por texto. Si variable no comienza por texto variable se usa inalterada. El siguiente ejemplo muestra el mecanismo de funcionamiento:
$ A=abcdef
$ echo ${A#ab}
cdef
$ echo ${A#$B}
cdef
$ B=abc
$ echo ${A#$B}
def
$ echo ${A#cd}
abcdef

Cortar texto al final de una variable

${variable%texto}
Corta texto de variable si variable termina por texto. Si variable no termina por texto variable se usa inalterada.
Vemos un ejemplo:
$ PS1=$
$PS1="$ "
$ A=abcdef
$ echo ${A%def}
abc
$ B=cdef
$ echo ${A%$B}
ab

Reemplazar texto en una variable

${variable/texto1/texto2}
${variable//texto1/texto2}
Sustituye texto1 por texto2 en variable. En la primera forma, sólo se reemplaza la primera aparición. La segunda forma hace que se sustituyan todas las apariciones de texto1 por texto2.
$ A=abcdef
$ echo ${A/abc/x}
xdef
$ echo ${A/de/x}
abcxf

Evaluación aritmética

En habitual tener que efectuar evaluaciones de expresiones aritméticas enteras durante la ejecución de un script de shell; por ejemplo para tener contadores o acumuladores o en otros casos.
Hasta ahora habíamos visto que esto lo podíamos hacer con expr, pero hay otra forma más cómoda: let
La sintaxis de let es la siguiente:
let variable=expresión aritmética
por ejemplo
let A=A+1
En algunas shell incluso podremos omitir la palabra let, aunque por motivos de compatibilidad esto no es aconsejable.
Para evaluar expresiones reales, es decir con coma decimal, tendremos que usar otros mecanismos y utilidades que pueda proporcionar el sistema. En linux disponemos de la orden bc.

Selección de la shell de ejecución

Si queremos que nuestro programa sea interpretado por una shell concreta lo podemos indicar en la primera línea del fichero de la siguiente forma
#!/ruta/shell
Por ejemplo, si queremos que sea la shell bash la que interprete nuestro script tendríamos que comenzarlo por
#!/bin/bash
En ciertas ocasiones es interesante forzar que sea una shell concreta la que interprete el script, por ejemplo si el script es simple podemos seleccoinar una shell que ocupe pocos recursos como sh. Si por el contrario el script hace uso de características avanzadas puede que nos interese seleccionar bash como shell.
Esta característica se puede usar para ciertos trucos de programación si sustituimos un shell concreto por una orden, dicha orden se ejecutará sobre el contenido del fichero. Por ejemplo, si quisieramos crear un fichero de autolectura en el momento de ejecución, podríamos usar como shell /usr/bin/less, por ejemplo:
#!/usr/bin/less
Cita de prensa tomada de la publicación de “El Byte sindicalista”:
Queda descartada la huelga de bytes en los sistemas Linux. Parece que sus condiciones de trabajo en este sistema son más que aceptables y no plantean ni quejas ni plantes. El último “core” ocurrió hace ya varios años y desde entonces la confictividad laboral con los bytes no ha existido en los sistemas Linux en cualquiera de sus sabores. Los representantes de los bytes reconocen que en los sistema Linux tienen unas condiciones sanitarias óptimas, el riesgo de contaminación con bytes enfermos es inexistente salvo en casos de pruebas de laboratorio. También hemos observado que los bytes están muy satisfechos con los medios de transporte que disponen en los sistemas Linux, medios cómodos y eficientes.
No se puede decir lo mismo en otros entornos de trabajo donde los bytes se quejan continuamente de sus condiciones laborales: falta permanente de espacio, tanto en la RAM como cuando duermen en su disco duro en unos incómodos sistemas de ficheros. También se reflejan las quejas de los excesivos puntos control sanitario que tienen que tienen que sufrir en sus desplazamientos y operaciones. Estas quejas se traducen en conflictos laborales periódicos de diversa intensidad. Son muy habituales las huelgas de celo cuando el sistema lleva cierto tiempo trabajando lo que ralentiza el sistema productivo global. Cuando las condiciones se hacen insoportables se producen manifestaciones y revueltas acompañadas de pancartas azules para exigir mejoras laborales.
Apoyemos a los bytes en sus reivindicaciones laborales, porque mejorando las condiciones en que se desenvuelve la actividad de los bytes mejoraremos las condiciones de todos.
No a la explotación del byte
 
El Byte sindicalista, junio 2004
Y al ejecutar el fichero que contuviera este script, simplemente lo mostraría en la pantalla permitiendo desplazarnos por el texto usando la orden less que hemos reflejado en el encabezado del propio script.

Lectura desde la entrada estándar: read

La orden read permite leer valores desde la entrada estándar y asignarlos a variables de shell.
La forma más simple de leer una variable es la siguiente:
read variable
Después de esto, el programa quedará esperando hasta que se le proporcione una cadena de caracteres terminada por un salto de línea. El valor que se le asigna a variable es esta cadena (sin el salto de línea final).
La instrucción read también puede leer simultáneamente varias variables:
read varl var2 var3 var4 
La cadena suministrada por el usuario empieza por el primer carácter tecleado hasta el salto de línea final. Esta línea se supone dividida en campos por el separador de campos definido por la variable de shell IFS (Internal Field Separator) que de forma predeterminada es una secuencia de espacios y tabuladores.
El primer campo tecleado por el usuario será asignado a var1, el segundo a var2, etc. Si el número de campos es mayor que el de variables, entonces la última variable contiene los campos que sobran. Si por el contrario el número de campos es mayor que el de variables las variables que sobran tendrán un valor nulo.
La segunda característica es la posibilidad incluir un mensaje informativo previo a la lectura. Si en la primera variable de esta instrucción aparece un carácter "?". todo lo que quede hasta el final de este primer argumento de read, se considerará el mensaje que se quiere enviar a la salida estándar.
Ejemplo:
read nombre?"Nombre y dos apellidos? " apl ap2

Evaluación de expresiones: test

La orden test evalúa una expresión para obtener una condición lógica que posteriormente se utilizará para determinar el flujo de ejecución del programa de shell con las sentencias correspondientes. La sintaxis de la instrucción es:
test expr
Para construir la expresión disponemos una serie de facilidades proporcionadas por la shell. Estas expresiones evalúan una determinada condición y devuelven una condición que puede ser verdadera o falsa. El valor de la condición actualiza la variable $? con los valores cero o uno para los resultados verdadero o falso.
Pasamos a describir a continuación estas condiciones, y tenemos que tener en cuenta que es importante respetar todos los espacios que aquí aparecen.
expresión
significado
-f fichero
el fichero existe y es un fichero regular
-r fichero
el fichero existe y es de lectura
-w fichero
el fichero existe y es de escritura
-x fichero
el fichero existe y es ejecutable
-h fichero
el fichero existe y es un enlace simbólico.
-d fichero
el fichero existe y es un directorio
-p fichero
el fichero existe y es una tubería con nombre
-c fichero
el fichero existe y es un dispositivo de carácter
-b fichero
el fichero existe y es un dispositivo de bloques
-u fichero
el fichero existe y tiene activo el bit SUID
-g fichero
el fichero existe y tiene activo el bit SGID
-s fichero
el fichero existe y su longitud es mayor que 0
-z s1
la longitud de la cadena s1 es cero
-n s1
la longitud de la cadena s1 es distinta de cero
s1=s2
la cadena s1 y la s2 son iguales
s1!=s2
la cadena s1 y la s2 son distintas
n1 -eq n2
los enteros n1 y n2 son iguales.
n1 -ne n2
los enteros n1 y n2 no son iguales.
n1 -gt n2
n1 es estrictamente mayor que n2.
n1 -ge n2
n1 es mayor o igual que n2.
nl -lt n2
n1 es menor estricto que n2.
nl -le n2
n1 es menor o igual que n2.
Estas expresiones las podemos combinar con:
Operador
Significado
!
operador unario de negación
-a
operador AND binario
-o
operador OR binario
Ejemplos:
$ test -f /etc/profile
$ echo $?
$ test -f /etc/profile -a -w /etc/profile
$ echo $?
$ test 0 -lt 0 -o -n "No nula''
$ echo $?
La orden test se puede sustituir por unos corchetes abiertos y cerrados. Es necesario dejar espacios antes y después de los corchetes; este suele ser unos de los errores más frecuentes.

Estructura de control

La programación en shell dispone de las sentencias de control del flujo de instrucciones necesarias para poder controlar perfectamente la ejecución de las órdenes necesrias.

Sentencia if

La shell dispone de la sentencia if de bifurcación del flujo de ejecución de un programa similar a cualquier otro lenguaje de programación. La forma más simple de esta sentencia es:
if   lista_órdenes
then
  lista_órdenes
fi
fi, que es if alrevés, indica donde termina el if.
En la parte reservada para la condición de la sentencia if, aparece una lista de órdenes separados por ";". Cada uno de estos mandatos es ejecutado en el orden en el que aparecen. La condición para evaluar por if tomará el valor de salida del último mandato ejecutado. Si la última orden ha terminado correctamente, sin condición de error, se ejecutará la lista de órdenes que hay tras then. Si esta órden ha fallado debido a un error, la ejecución continúa tras el if.
Como condición se puede poner cualquier mandato que interese, pero lo más habitual es utilizar diferentes formas de la orden test. Por ejemplo:
if [ -f mifichero ] 
then 
  echo "mifichero existe"
fi
Pero también podemos poner:
if grep body index.html
then
  echo “he encontrado la cadena body en index.html”
fi
Como en cualquier lenguaje de programación también podemos definir las acciones que se tieenen que ejecutar en el caso de que la condición resulte falsa:
if lista_órdenes
then
  lista_órdenes
else
  lista_órdenes
fi
Por ejemplo:
if [ -f "$1" ] 
then
  pr $1
else
  echo "$1 no es un fichero regular"
fi
Cuando queremos comprobar una condición cuando entramos en el else, es decir, si tenemos else if es posible utilizar elif. Vemos un ejemplo:
if [ -f "$1" ]
then
  cat $1 
elif [ -d "$1" ] 
then
  ls $1/*
else
  echo "$1 no es ni fichero ni directorio"
fi

Sentencia while

La sentencia while tiene la siguiente sintaxis:
while lista_órdenes
do
  lista órdenes
done
La lista de órdenes que se especifican en el interior del bucle while se ejecutará mientras que lista_órdenes devuelva un valor verdadero, lo que significa que la última orden de esta lista termina correctamente.
Vemos un ejemplo:
I=0
while [ ${resp:=s} = s ]
do
  I= ̀expr $I + 1 ̀
  echo $I
  read resp?"Quiere usted continuar(s/n)? "
done

Sentencia until

La sentencia until similar a while, es otro bucle que se ejecutará hasta que se cumpla la condición, es decir, hasta que la lista de órdenes termina correctamente. Su formato es el siguiente:
until lista_órdenes
do
  lista órdenes
done

Sentencia for

La sentencia for repite una serie de órdenes a la vez que una variable de control va tomando los sucesivos valores indicado por una lista de cadenas de texto. Para cada iteración la variable de control toma el valor de uno de los elementos de la lista. La sintaxis de for es la siguientes
 
for variabl ein lista 
do
  lista mandatos
done
 
lista es una serie de cadenas de texto separadas por espacios y tabuladores. En cada iteración del bucle la variable de control variable toma el valor del siguiente campo y se ejecuta la secuencia de mandatos lista_mandatos.
Ejemplo:
for i in $*
do 
  echo $i
done
y mostraríamos todos los parámetros posicionales.
for i in *
do 
  echo $i
done
y mostraríamos la lista de ficheros del directorio activo.

Sentencias break y continue

La sentencia break se utiliza para terminar la ejecución de un bucle while o for. Es decir el control continuará por la siguiente instrucción del bucle. Si existen varios bucles anidados, break terminará la ejecución del último que se ha abierto.
Es posible salir de n niveles de bucle mediante la instrucción break n.
La instrucción continue reinicia el bucle con la próxima iteración dejando de ejecutar cualquier orden posterior en la iteración en curso.

Sentencia case

La sentencia case proporciona un if múltiple similar a la sentencia switch de C. El formato básico de esta sentencia es el siguiente:
case variable in 
  patrón1)
    lista_órdenes1
  ;; 
  patrón2) 
    lista_órdenes2
  ;;
...
  patrónN) 
    lista_órdenesN;;
esac
La shell comprueba si variable coincide con alguno de los patrones especificados. La comprobación se realiza en orden, es decir empezando por patrón1 terminando por patrónN. En el momento en que se detecte que la cadena cumple algún patrón, se ejecutará la secuencia de mandatos correspondiente hasta llegar a ";;". Estos dos puntos y comas fuerzan a salir de la sentencia case y a continuar por la siguiente sentencia después de esac (esac es case alrevés).
Las reglas para componer patrones son las mismas que para formar nombres de ficheros, así por ejemplo, el carácter "*" es cumplido por cualquier cadena, por lo que suele colocarse este patrón en el último lugar, actuando como acción predeterminada para el caso de que no se cumpla ninguna de las anteriores. Ejemplo:
case "$1" in
  start)
        echo -n "Ha seleccionado start "
        ;;
  stop)
        echo -n "Ha seleccionado stop "
        ;;
  status)
        echo -n "Ha seleccionado stop "
        ;;
  restart)
        echo -n " Ha seleccionado restart "
        ;;
  *)
        echo "No es una opción válida"
        exit 1
esac 

Terminar un programa de shell (exit)

Como hemos visto, todas las órdenes órdenes tienen un estado de finalización, que por convenio es 0 cuando la ejecución terminó correctamente, y un valor distinto de 0 cuando lo incorrectamente o con error.
Si un programa de shell termina sin errores devolverá un valor cero, pero también es posible devolver explícitamente un valor mediante la sentencia exit. La ejecución de esta sentencia finaliza en ese instante el programa de shell, devolviendo el valor que se le pose como argumento, o el estado de la última orden ejecutada si no se le pasa ningún valor.
Ejemplo:
if grep "$1" /var/log/messages
then
  exit 0
else
  exit 10
fi

Opciones en un programa de shell: getopts

En los sistema Unix es habitual poner las opciones para ejecutar una orden siguiendo unas normas:
  • Las opciones están formadas por una letra precedida de un guión.
  • El orden de las opciones es indiferente, pero suelen preceder a los ntes de los argumentos propiamente.
  • Cada opción puede llevar uno o más argumentos.
  • Las opciones sin argumentos pueden agruparse después de un único guión
Para facilitar el análisis de las opciones la shell dispone de la orden getopts. La sintaxis de getopts es la siguiente:
getopts cadenaopciones variable [args ...]
En cadenaopciones se sitúan las opciones válidas del programa. Cada letra en esta cadena significa una opción válida. El carácter ":" después de una letra indica que esa opción lleva un argumento asociado o grupo de argumentos separados por una secuencia de espacios y tabuladores.
Esta orden se combina con la sentencia while para iterar por cada opción que aparezca en la línea de órdenes en la llamada al programa. En variable se almacena el valor de cada una de las opciones en las sucesivas llamadas. En la variable OPTIND se guarda el índice del siguiente argumento que se va a procesar. En cada llamada a un programa de shell esta variable se inicializa a 1.
Además:
  • Cuando a una opción le acompaña un argumento, getopts lo sitúa en la variable OPTARG.
  • Si en la línea de mandatos apar ece una opción no válida entonces asignará "?" a variable.
Veamos un ejemplo:
while getopts ab:cd opcion 
do
        case $opcion in
                a) 
                  echo "Opción a"
                ;;
                b) 
                  echo "Opción b con argumento OPTARG"
                ;;
                c) 
                  echo "Opción c"
                ;;
                d) 
                  echo ”Opcion d”
                ?) 
                  echo "Uso: $0 -acd -b opcion "
        esac
done
shift `expr $OPTIND - 1`
echo "resto de argumentos: $*"
Observamos como hemos desplazados todos los argumentos con shift para enerlos disponibles con los parámetros posicionales y descartando las opciones previamente analizadas.

Evaluación de variables: eval

La orden eval toma una serie de cadenas como argumentos:
eval [arg1 [arg2] ...]
los expande siguiendo las normas de expansión de la shell, separándolos por espacios y trata de ejecutar la cadena resultante como si fuera cualquier orden.
Esta instrucción se debería utilizar cuando:
Pretendemos examinar el resultado de una expansión realizada por la shell.
Para encontrar el valor de una variable cuyo nombre es el valor de otra variable.
Ejecutar una línea que se ha leído o compuesto internamente en el programa de shell.

Funciones

Ciertas shell como bash permiten la declaración de funciones para agrupar bloques código como en un lenguaje de programación convencional.
La forma de declarar un función es
function mi_funcion 
{ 
  código de la función
} 
Para realizar la llamada a la función sólo tenemos que usar su nombre. Además las funciones pueden tener argumentos en su llamada. No es necesario declarar los parámetros en la declaración de la función, basta usar las variables $1, $2, etc. dentro de la definición de las instrucciones y serán los parametros en su orden correspondiente. Para llamar a una función con argumentos no se usan los habituales paréntesis.
Ejemplos:
           #!/bin/bash    
           function terminar { 
               exit 0
           } 
           function saludo { 
               echo ¡Hola Mundo! 
           } 
           saludo 
           terminar 
Otro ejemplo:
              #!/bin/bash    
               function terminar { 
                  exit 0
               }   
               function saludo { 
                    echo $1  
                }   
                saludo Hola 
                saludo Mundo 
                terminar 
En este último ejemplo podemos observar el uso de una función con argumentos, tanto en la delaración como en la llamada.

Trucos de programación en shell

Vemos a continuación ua serie de ejemplo genéricos para resolver cuestiones que se presentan en la programación de shell con cierta frecuencia

Script con número variable de argumentos:

Este programa de shell pretende ejecutar una serie de instrucciones para cada uno de los argumentos que se le pasen desde la línea de órdenes.
for i in $*
do
  instrucciones
done
Por ejemplo, hacemos un script que mueva todos los ficheros pasado como argumento al directorio ./papelera:
for i in $*
do
  if [ -f $i ]
  then
    mv $i ./papelera
  fi
done

Script para aplicar una serie de órdenes a cada fichero de un directorio

Muy similar al ejemplo anterior, pero sustituimos $* por simplemente * que equivale a todos los ficheros del directorio activo.
for i in *
do
  instrucciones
done

Leer un fichero de texto línea a línea

Es muy habitual tener que procesar todas las línea de un fichero de texto para realizar diferentes operaciones. Vemos una primera forma:
while read LINEA
do
  instucciones por línea
done <fichero
En este caso estamos redirigiendo la entrada estándar de la orden read, que es el teclado, por un fichero. Al igual que en el caso del teclado, la lectura se realizará hasta que se encuentre un salto de línea. Observamos como la redirección se realiza tras el final de la sentencia while.
Otra forma posible para hacer esto mismo sería:
cat fichero|while read LINEA
do
  instrucciones por línea
done
Este método difiere ligeramente del anterior, ya que al utilizar una tubería creamos una nueva shell con lo cual puede ocurrir que no se conserven ciertos valores de las variables de shell.

Cambiar una secuencia de espacios por un separador de campos

La salida de ciertas órdenes y ciertos ficheros separan los datos por espacios en blanco. Por ejemplo las órdenes ps o ls -la, o ifconfig.
Si queremos utilizar parte de los datos de las salidas de estas órdenes tendremos que contar las columnas en las que aparece cada dato y cortarlos con cut usando la opción -c. Pero otra opción sería sustituir toda una serie de espacios en blanco por un separador, por ejemplo ”:” o ”;”.
Por ejemplo vamos a ver como sustituir los espacios de la orden ifconfig por ”;”.
La salida normal sería:
$ /sbin/ifconfig
eth0      Link encap:Ethernet  HWaddr 00:90:F5:08:37:E4
          inet addr:192.168.1.5  Bcast:192.168.1.255  Mask:255.255.255.0
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:5103 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:100
          Interrupt:10 Base address:0x3200
 
lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:16436  Metric:1
          RX packets:1726 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1726 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
Ahora usamos sed para sustituir cualquier secuencia de espacios en blanco ([ ][ ]*) por un separador ”;”:
$ /sbin/ifconfig |sed "s/[ ][ ]*/;/g"
eth0;Link;encap:Ethernet;HWaddr;00:90:F5:08:37:E4;
;inet;addr:192.168.1.5;Bcast:192.168.1.255;Mask:255.255.255.0
;UP;BROADCAST;MULTICAST;MTU:1500;Metric:1
;RX;packets:0;errors:0;dropped:0;overruns:0;frame:0
;TX;packets:5241;errors:0;dropped:0;overruns:0;carrier:0
;collisions:0;txqueuelen:100;
;Interrupt:10;Base;address:0x3200;
lo;Link;encap:Local;Loopback;
;inet;addr:127.0.0.1;Mask:255.0.0.0
;UP;LOOPBACK;RUNNING;MTU:16436;Metric:1
;RX;packets:1773;errors:0;dropped:0;overruns:0;frame:0
;TX;packets:1773;errors:0;dropped:0;overruns:0;carrier:0
;collisions:0;txqueuelen:0;
Ahora podríamos cortar de forma exacta el campo que nos interese, por ejemplo:
$ /sbin/ifconfig | sed "s/[ ][ ]*/:/g" | grep inet | cut -f4 -d:
192.168.1.5
127.0.0.1
Vemos paso a paso la anterior orden compuesta:
  • Primero ejecutamos la orden ifconfig
  • Sustituimos los espacios en blanco por ”:”
  • Buscamos la línea que contenga la palabra inet
  • Cortamos el campo 4 usando ”:” como separador.

Prácticas

Ejercicios propuestos

¿Que salida ocasionaría cada una de las siguientes órdenes si la ejecutamos consecutivamente?
$ set a b c d e f g h i j k l m n
$ echo $10
$ echo $15
$ echo $*
$ echo $#
$ echo $?
$ echo ${11}
Explica que realizaría cada una de las siguientes órdenes ejecutadas en secuencia
$ A=\$B
$ B=ls
$ echo $A
$ eval $A
Mostrar el último parámetro posicional (Pista: $#)
Asignar el úlimo parámetro posicional a la variable ULT
Realizar un programa que escriba los 20 primeros números enteros.
Realizar un programa que numere las líneas de un fichero
Realizar un programa que tomando como base el contenido de un directorio escriba cada elemento contenido indicando si es fichero o directorio.
Realizar un programa que muestre todos los ficheros ejecutables del directorio activo.
Modificar el programa anterior para que indique el tipo de cada elemento contenido en el directorio activo: fichero, directorio, ejecutable,...

Ejercicios resueltos sobre ficheros y directorios

Guion de shell que genere un fichero llamado listaetc que contenga los ficheros con permiso de lectura que haya en el directorio /etc:
for F in /etc/*
do
  if [ -f $F -a -r $F ]
  then
    echo $F >> listaetc
  fi 
done
Hacer un guion de shell que, partiendo del fichero generado en el ejercicio anterior, muestre todos los ficheros del directorio /etc que contengan la palagra ”procmail”:
while read LINEA
do
  if grep procmail $L >/dev/null 2>&1
  then
    echo $L
  fi
done <listaetc
Hacer un guion de shell que cuente cuantos ficheros y cuantos directorios hay en el directorio pasado como argumento:
DI=0
FI=0
for I in $1/*
do
  if [ -f $I ]
  then
    let FI=FI+1
  fi
  if [ -d $I ]
  then 
    let DI=DI+1
 fi
done
Hacer un guion de shell que compruebe si existe el directorio pasado como argumento dentro del directorio activo. En caso de que exista, que diga si no está vacío.
if [ -d $1 ]
then
  echo ”$1 existe”
  N=$(ls | wc -l)
  if [ $N -gt 0 ]
  then
    echo ”S1 no está vacio, contiene $N ficheros no ocultos”
  fi
fi
Hacer un guion de shell que copie todos los ficheros del directorio actual en un directorio llamado csg. Si el directorio no existe el guion lo debe de crear.
if [ ! -d csg ]
then
  mkdir csg
fi
cp * csg
Hacer un script que muestre el fichero del directorio activo con más líneas:
NLIN=0
for I in * 
do
  if [ -f $I ]
  then
    N=$(wc -l $I)
    if [ $N -gt $NLIN ]
    then
      NOMBRE=$I
      NLIN=$N
    fi
  fi
done
echo ”$NOMBRE tiene $NLIN lineas”