Rodrigo Gallardo-- lgallardo@computacion.cs.cinvestav.mx
Gunnar Wolf -- gwolf@gwolf.cx
14 de febrero, 2002
Asumimos que el asistente al tutorial tiene conocimientos básicos de uso de un sistema Unix, si bien puede nunca haberse internado al maravilloso mundo de la programación.
Para poder comprender la funcion del shell es necesario comprender el proceso historico del computo, desde las primeras computadoras hasta los sistemas Unix modernos.
Las primeras computadoras, en los 40s y 50s, no tenian contemplada la ejecucion de mas que un solo programa -- Eran programadas en un principio fijando a mano interruptores, y fue un muy gran avance para su usabilidad la introduccion de las primeras lectoras de tarjetas perforadas, pues, ademas de reducir sensiblemente el tiempo muerto en que un programa tenia que ser introducido, permitia guardarlo para uso futuro de una manera conveniente.
Tras la aparicion de las lectoras de tarjetas, fue solo cuestion de tiempo el que se serializaran procesos en lotes -- Cada usuario dejaba listo su programa en la lectora, la cual lo alimentaba a la computadora, esperaba a que ejecutara e imprimiera resultados, y continuaba con el siguiente proceso. Al automatizarse la carga, los tiempos muertos para la computadora se redujeron al minimo.
Ahora, el uso del procesador se podia aprovechar aun mas: El tiempo en que la lectora alimentaba a la memoria central o en que se imprimian los resultados era, a todas luces, desperdiciado. Ademas, habia trabajos de alta prioridad, que debian esperar su turno como cualquier otro pese a su importancia.
Si bien antes de la concurrencia ya existian sistemas operativos pequenos y limitados encargados de una abstraccion basica del hardware, su rol como asignadores de recursos nace cuando hay varios programas simultaneos en ejecucion. Y al haber ya un complejo ambiente en el que diferentes usuarios desde diferentes consolas requieren cargar y ejecutar diferentes programas a la vez, nace la necesidad real de un programa base que permita al usuario especificar a la computadora que quiere hacer -- un shell.
Un shell basico es simplemente un lanzaprogramas. Acepta ordenes del usuario, las cuales se traducen directamente en el nombre de un programa a ejecutar. Claro, es necesario proveer al usuario tambien de los comandos basicos para manejar los archivos en la computadora, ya que un sistema con concurrencia, para ser eficiente, requiere tener espacio de almacenamiento permanente para los programas --un disco-- y, claro, proveer los mecanismos para administrarlo -- crear, copiar, eliminar, compilar, etc.
Ha pasado ya mucho tiempo desde aquellos primitivos shells. En un sistema Unix moderno, el shell es ya un entorno completo de programacion, proporcionando al usuario todas las herramientas necesarias para automatizar la administracion de sistemas, facilitar diversas labores cotidianas, e incluso jugar con un ambiente de desarrollo agradable.
En Unix hay muchos diferentes shells, y los usuarios de cada uno de ellos lo defienden como el mejor con fervor religioso (lo cual no debe de sorprender a nadie Las principales familias son el Korn Shell, el C Shell y el Bourne Shell. Cada una de estas familias tiene varias implementaciones, y para todos hay cuando menos una implementacion libre. En este tutorial nos enfocamos al Bourne Shell, recomendando el uso de bash (Bourne Again Shell), por ser el que mas gente encuentra como primera experiencia en un sistema Unix al ser el predeterminado de casi cualquier Linux, y por existir en todos los demas Unixes.
Usar el shell como un lanzaprogramas es muy simple: Cada que le damos una linea, ejecuta el comando cuyo nombre le escribimos. Podemos indicar la ruta completa al archivo, o ejecutar los archivos que aparezcan en el path de ejecucion (ver seccion 2.3.2).
En el shell -al igual que en cualquier programa de consola de Unix-- tenemos tres flujos o descriptores de archivo abiertos por default: La entrada estandar (STDIN), la salida estandar (STDOUT) y el error estandar (STDERR). El primero puede ser utilizado para leer de el, y los otros dos para enviar datos hacia ellos. Tipicamente, STDIN viene del teclado de la terminal actualmente en uso, y tanto STDOUT como STDERR van hacia su pantalla. STDOUT muestra los datos normales o esperados durante la ejecucion, y STDERR se utiliza para enviar informacion de depuracion y errores. Cualquier programa iniciado desde el shell, a menos que se lo indiquemos explicitamente, hereda estos tres descriptores de archivo, permitiendole interactuar con el usuario.
Si bien su principal mision es ser un lanzaprogramas, un shell tiene una funcionalidad mucho mayor. En Unix, muchas veces se utiliza al shell para interconectar programas independientes --recuerden la filosofia Unix, que nos da una gran cantidad de herramientas que hacen solamente una cosa simple, pero permiten interactuar con otros programas similares creando asi construcciones complejas y utiles. El shell ademas da facilidades al usuario como la expansion, el globbing y los aliases.
Muchas veces necesito pasar a un proceso la salida de otro. Por ejemplo, para contar la cantidad de lineas en un archivo sin repeticion, primero ordeno el archivo (con sort ), despues elimino lineas duplicadas (con uniq ) y por ultimo cuento las lineas (con wc ). Si bien podria hacerlo de esta manera:
sort archivo > /tmp/ordenado
uniq /tmp/ordenado > /tmp>unico
wc /tmp/unicoesto claramente no es optimo. Puedo, mejor, entubar la salida estandar de un proceso y enviarlo al siguiente utilizando los pipes, simbolizados con el caracter |, de esta manera:
sort archivo | uniq | wcLo cual, ademas de mas compacto, es mas facil de leer y entender.
Si quiero redireccionar el error estandar en vez de la salida estandar, puedo usar &|.
Muchas veces iniciamos procesos, como la descompresion de un .tar con muchos archivos o la transferencia de un archivo remoto, que pueden tomar mucho tiempo y que no nos interesa la respuesta que puedan enviar a la consola, sino que su resultado final. Aprovechando que Unix es un sistema multitareas, podemos enviar cualquier comando que ejecutemos a un segundo plano agregando & al final de la linea de comando, de la siguiente manera:
$ wget http://iso.softwarelibre.org.mx/debian-2.2r5-1.iso &Podemos lograr este mismo efecto si, una vez lanzado el comando, lo suspendemos con Z y lo enviamos a ejecucion en segundo plano con bg :
$ wget http://iso.sofwarelibre.org.mx/debian-2.2r5-1.iso
Z
[1]+ Stopped wget http://iso.softwarelibre.org.mx/debian-2.2r5-1.iso
$ bg
[1]+ Stopped wget http://iso.softwarelibre.org.mx/debian-2.2r5-1.iso &
$
El shell es un gran aliado cuando se trata de escribir menos. Hay varios mecanismos para ahorrarnos teclazos:
Globbing es probablemente el mas comun, el que todos conocemos -- La expansion de nombres usando el caracter * , que significa ``todo lo que puedas acomodar ahi''. Por ejemplo, si en un directorio tengo los archivos salida.tmp, asdf, otroarchivo y prueba, y en ese directorio corro cat *, el shell lo interpretara como si hubiera escrito cat asdf otroarchivo prueba salida.tmp (en orden alfabetico). Si le pongo cat o*, procesara unicamente los archivos que inician con o, y dara por tanto cat otroarchivo.
La expansion trabaja con argumentos mucho mas claramente definidos que el globbing. La expansion esta claramente hecha para ahorrar teclazos y hacer mas inteligente al shell.
Cuando especificamos una lista de valores separada por comas entre llaves, el shell la expande, convirtiendola en la cadena con cada uno de los argumentos. Por ejemplo,
echo un/path/{algo,muy,demasiado}/largoNos produce la siguiente salida:
un/path/algo/largo un/path/muy/largo un/path/demasiado/largoAhora, tenemos que acordarnos de un par de reglas del juego:
echo un texto {muy,algo} confuso
obtendremos como resultado:
un texto muy algo confuso
echo ``un texto {muy,algo} confuso''
pues nos dará por resultado:
un texto {muy,algo} confuso
echo un\ texto\ {muy,algo}\ confuso
y obtendremos:
un texto muy confuso un texto algo confuso
echo {Un,Otro}\ texto\ {muy,algo}\ confuso.
nos da:
Un texto muy confuso. Un texto algo confuso. Otro texto muy confuso. Otro texto algo confuso.
Podemos indicarle al shell que cada que le demos determinada cadena la substituya por otra. Para esto utilizamos el comando interno alias .
Cuando el shell ejecuta cualquier comando, revisa si la primera palabra de cada comando que le demos, y si la encuentra en su tabla de aliases (y el operador no requiere que se inteprete literalmente usando comillas) la substituye antes de continuar procesando la línea.
Para crear un alias podemos hacerlo de la siguiente manera:
alias cosa='ls -l'con lo que cada que indiquemos el comando cosa el shell ejecutará ls -l . Ahora, si le pedimos echo cosa , como no es la primera palabra, nos va a regresar a secas cosa .
Ahora, si indicamos lo siguiente:
echo `cosa`nos va a dar el resultado de ejecutar ls -l. Recuerda que lo que encerremos en comillas inversas (ver 5.2) es ejecutado en un sub-shell, y aquel sub-shell ve a cosa como la primera palabra del comando.
Al tener los aliases precedencia aún sobre los comandos internos del shell, son muy útiles para ahorrarnos teclazos. Por ejemplo, mucha gente tiene los siguiente aliases definidos:
alias ls='ls -color'con lo que sin tener que agregarle opciones, siempre le pasaremos -color' al ls , y siempre eliminaremos y moveremos requiriendo confirmar.
alias rm='rm -i'
alias mv='mv -i'
Para consultar qué aliases tenemos definidos, damos alias sin argumentos.
Para eliminar un alias, damos unalias.
En Unix tenemos muchos diferentes shells. ¿Por qué recomendamos elegir a los derivados del Bourne?
Prácticamente cualquier lenguaje de programación nos proporciona variables con las que podemos trabajar. En shell manejarlas sigue unas reglas un tanto particulares.
En shell, los nombres de variables pueden contener letras, números y guiones bajos. Si bien ninguna regla lo marca, es una convención muy común que los nombres de las variables vayan completamente en mayúsculas. No es necesario declarar de qué tipo será cada variable (al shell le da igual si las variables guardan cadenas o números), pero sí tenemos que comprender bien cómo manejarlas, pues es una fuente muy frecuente de errores el referirnos a una variable cuando requerimos su contenido, o a la inversa.
Para asignar un valor a una variable lo hacemos de esta manera:
VARIABLE=valorCuando queramos utilizar el valor de la variable podemos hacerlo anteponiendo a su nombre un signo $ :
echo $VARIABLESiempre que queramos imprimir, comparar, hacer cuentas o en general utilizar el valor contenido en la variable, lo haremos refiriéndonos a su valor con $ . Siempre que queramos indicar al shell que haga algo utilizando la variable, lo haremos refiriéndonos a su nombre, sin $ .
Como en cualquier lenguaje de programación, un 'script' de shell tiene que poder decidir que acciones tomar según el resultao operaciones anteriores. Además, es necesario automatizar el repetir acciones, ya sea un número fijo de veces, o hasta que se cumpla alguna condición.
Puesto que la función primaria del shell es ejecutar a otros programas, resulta natural desear controlar la ejecución de un script de acuerdo al resultado de la ejecución de estos. Para lograr eso, cada programa que se ejecuta en un sistema UNIX devuelve al programa que lo ejecutó un número, que representa el resultado obtenido. Si el programa se ejecutó sin errores, devuelve un cero. Si hubo algún error, devuelve un número distinto de cero. Este número depende del error especifico, y por lo tanto varía de programa a programa. Siempre que el shell necesita tomar una decisión basada en el resultado de otro programa, considera como 'cierto' a un valor 0, y como 'falso' a cualquier otro.
Los operadores booleanos permiten combinar los resultados de varias pruebas. Funcionan de forma identica a los de C. En particular, && y || evaluan sus argumentos de izquierda a derecha, deteniendose en cuanto se sabe el resultado total. Esto permite efectuar combinaciones de control sencillas. Por ejemplo, cuando se compila e instala un paquete de software, es común dar el comando make seguido de make install, si no hubieron errores. Es posible automatizar esta secuencia, dando el comando make&&make install que efectuara al segundo solo si el primero termina sin errores.
El operador ! invierte el sentido del valor de retorno de un programa.
Estos operadores se usan para encadenar pruebas, en particular aquellas que involucran el estado de retorno de un programa.
Para efectuar otras pruebas el shell provee el operador test. Este permite realizar una serie de pruebas acerca del sistema de archivos, así como pruebas que dependan del valor de las variables del shell. Estos operadores son:
De archivo:
Verdadero si el ARCHIVO es un dispositivo de bloque.
-c ARCHIVO
Verdadero si el ARCHIVO es un dispositivo de caracteres.
-d ARCHIVO
Verdadero si el ARCHIVO es un directorio.
-e ARCHIVO
Verdadero si el ARCHIVO existe.
-f ARCHIVO
Verdadero si ARCHIVO existe y es un archivo normal.
-g ARCHIVO
Verdadero si el ARCHIVO tiene encendido el bit sgid.
-h ARCHIVO
-L ARCHIVO
Verdadero si el ARCHIVO es un vínculo simbolico.
-k ARCHIVO
Verdadero si el ARCHIVO tiene encendido el bit 'sticky'.
-p ARCHIVO
Verdadero si el ARCHIVO es un 'named pipe'.
-r ARCHIVO
Verdadero si el ARCHIVO es legible por este usuario.
-s ARCHIVO
Verdadero si el ARCHIVO existe y es no vacio.
-S ARCHIVO
Verdadero si el ARCHIVO es un 'socket'.
-t FD
Verdadero si el descriptor FD esta abierto a una terminal.
-u ARCHIVO
Verdadero si el ARCHIVO tiene prendido el bit 'suid'.
-w ARCHIVO
Verdadero si el usuario tiene permiso de escribir en el ARCHIVO.
-x ARCHIVO
Verdadero si el usuario tiene permiso de ejecutar el ARCHIVO.
-O ARCHIVO
Verdadero si el usuario es el dueño del ARCHIVO.
-G ARCHIVO
Verdadero si el ARCHIVO pertenece al grupo del usuario.
-N ARCHIVO
Verdadero si el ARCHIVO ha sido modificado desde la última lectura.
ARCHIVO1 -nt ARCHIVO2
Verdadero si el ARCHIVO1 es más nuevo que el ARCHIVO2
(de acuerdo a la fecha de modificación).
ARCHIVO1 -ot ARCHIVO2
Verdadero si el ARCHIVO1 es más viejo que el ARCHIVO2.
ARCHIVO1 -ef ARCHIVO2
Verdadero si el ARCHIVO1 es un vínculo duro al ARCHIVO2.
Verdadero si la CADENA es vacia.
-n CADENA
CADENA
Verdadero si la CADENA es no vacia.
CADENA1 = CADENA2
Verdadero si las cadenas son iguales.
CADENA1 != CADENA2
Verdadero si las cadenas son distintas.
CADENA1 < CADENA2
Verdadero si la CADENA1 va antes que la CADENA2 en orden lexicográfico.
CADENA1 > CADENA2
Verdadero si la CADENA1 va despues que la CADENA2 en orden lexicográfico.
Verdadero si la opcion del shell OPCION esta activada.
! EXPR
Verdadero si la EXPR es falsa.
EXPR1 -a EXPR2
Verdadero si ambas expresiones son verdaderas.
EXPR1 -o EXPR2
Verdadero si alguna expresión es verdadera.
arg1 OP arg2
Pruebas aritméticas. OP es uno de -eq, -ne, -lt, -le, -gt, o -ge.
Existe otra sintaxis para el operador test. En ves de usar esta palabra clave, se puede encerrar la prueba entre un par [...]. Es importante que ambos parentesis queden aislados, para que el shell los reconozca como unidades independientes. Una prueba de este estilo se puede encadenar con cualquier otra por medio de los operadores de la sección anterior. Por ejemplo, si queremos asegurarnos que un programa existe antes de intentar ejecutarlo, podemos usar la siguiente construcción:
La primera manera de controlar el flujo de un script de shell, es mediante la ejecución secuancial, es decir, el efectuar acciones una tras otra. Una lista es una secuencia de ``tubos'', separados por alguno de los operadores &&, ||, &, ; y terminados por ;, &o un fin de linea. De entre estos &&, y || tienen mayor precedencia, seguidos por & y ;. Los dos primeros tienen el efecto que se describio en la sección anterior.
Cuando dos secuencias están separadas por ; el efecto es que se efectuan ambas una tras la otra, sin importar el resultado de las anteriores. El resultado de la lista es el resultado de la última.
Cuando una secuencia está terminada por & el shell la ejecuta en el fondo, sin esperar a que termine. El resultado de la lista es 0 (verdadero).
Presentamos a continuación las sentencias de control de flujo del shell. Estas nos permiten elegir ejecutar una u otra opción dentro de un script, de acuerdo al resultado de alguna prueba.
La sentencia if nos permite elegir entre dos alterntativas de acuerdo al resultado verdadero o falso de una prueba. Tiene la sintaxis:
./configura
./corre
La sentencia case nos permite elegir una entre varias alternativas, dependiendo del valor de una expresión. La sintaxis es:
Si bien la sentencia case no proporciona una acción por defecto, es fácil proporcionar una, poniendo al final de la lista un patrón que ajuste contra cualquier cosa, es decir *).
/usr/local/bin/root-shell;;
/usr/local/bin/exit;;
Además de permitir elegir entre varias opciones, el shell nos permite repetir una secuencia de acciones un cierto número de veces, ya sea fijo o determinado por el cumplimiento de una condición.
Estas dos sentencias nos permiten efectuar repetidamente una acción, hasta que alguna condición dada se cumpla. La sintaxis es:
until lista do lista done
La sentencia for nos permite hacer ciclos sobre listas de valores definidos antes de entrar al ciclo. Puesto que el ciclo no se limita a una cantidad de iteraciones, sino que la lista puede ser generada por otro comando, tiene una gran cantidad de usos. La sintaxis es:
El siguiente ciclo entra a cada subdirectorio del directorio actual, y borra todos los archivos *.tmp dentro del mismo.
rm *.tmp
cd ..
Como en todo lenguaje de programación, es conveniente encapsular acciones complejas que se repiten varias veces. Para esto, el shell permite definir funciones, que encierran varias acciones y les dan un nombre por el que pueden ser invocadas. La sintaxis para hacer esto es:
if busca_gunnar then
Por supuesto, es deseable que la ejecución de una funcion, o de un script, varien de acuerdo a parametros que se proporcionen al momento de ser ejecutados. Para esto, el shell reserva algunos nombres de variables especiales, cuyos valores son ajustados de forma automática.
El primer tipo de variables de este estilo, son los parametros posicionales. Estos son los parametros que se dan el script o funcion al momento de ser llamados. Se utilizan las variables especiales $digitos, por ejemplo $1, $2, etc. Estas variables reciben el valor del parametro pasado en la posición que su número indica. Para referir a los parametros mas allá del $9, se debe encerrar al número entre llaves: ${10}, de lo contrario, el shell lo interpreta como el parametro $1, seguido de la cadena '0'. No es posible asignar valores a estos parametros.
if busca_alguien gunnar || busca_alguien rodrigo; then
rm *.tmp
cd ..
borra_en_dir trabajo tmp casa
Además de los parametros posicionales, hay otras variables especiales que el shell mantiene. Los valores de estas son asignados automáticamente, y no es posible modificarlos. Describimos a continuación sus funciones.
Describimos a continuación algunas de las caracteristicas más ``esotericas'' del shell. Si bien estas no son necesarias para el trabajo diario con el mismo, resulta útil conocerlas, puesto que permiten realizar cosas que son difíciles o imposibles de otra manera.
En ocasiones queremos ejecutar un comando con una entrada fija, pero no queremos poner ésta en un archivo desde el cual redireccionar la entrada. Ponerla en un archivo nos obliga a mantenerla en una ruta fija, confiendo en que nadie lo borre o modifique por no saber para que se usa (incluso nosotros mismos). Para evitar eso, podemos poner esta entrada en el texto mismo del script, y pedir al shell que alimente al comando con esta entrada. Para eso, decimos
Aqui ponemos el texto
tantas lineas como queramos
algo
Este es el texto
del correo, que puedo escribir sin preocuparme del formato
EOF
Otra caracteristica complicada del shell es la sustitución de comillas inversas. Cuando escribimos una cadena dentro de estas, el shell la interpreta como un comando. Este comando es ejecutado, se captura su salida estandar, se separa en palabras, y estas son sustituidas en vez de la cadena completa. Es posible tener comillas dobles o sencillas anidadas dentro de estas, y a la vez es posible anidar estas dentro de comillas dobles. En este caso, la salida no es separada en palabras, sino que se deja como una sola. Esta caracteristica permite capturar la salida de un comando para usarla como argumentos de otro, o para iterar sobre la misma usando for.
sed -e 's/call_me/call_him/' $file >$file.new
mv $file.new $file
El shell nos permite efectuar algunas redirecciones más complejas que las mostradas al inicio. Por ejemplo, es posible redirigir descriptores de archivo especificos. Si decimos n>archivo, el archivo será abierto para escritura, en el descriptor de archivo n. Analogamente, es posible hacer esto para lectura, o para escribir al final. Por supuesto, cualquier descriptor fuera de 0, 1 y 2 es no estandar, de modo que el programa que ejecutemos debe estar esperando que dichos descriptores esten abiertos y usarlos, o no pasará nada.
Pero de cualquier forma es útil esta caracteristica. Supongamos que queremos desacernos de la salida estandar, y guardar los mensajes de error en un archivo. Entonces podemos decir comando >/dev/null 2>/tmp/log, lo cual logra nuestro proposito. Comunmente, queremos procesar la salida de error, junto con la normal. Para esto, podemos 'duplicar' un descriptor de archivo, diciendo, por ejemplo comando 2>&1, que manda la salida de error a donde va la estandar. Si queremos procesar la salida de error en una tubería, y deshacernos de la salida normal, primero duplicamos la normal sobre la de error, y luego redirigimos la normal a /dev/null: comando 2>&1 >/dev/null. Notese que esto funciona por que la primera redirección copia el descriptor uno sobre el dos, no lo liga. Además, el orden importa. Si decimos comando >/dev/null 2>&1 primero se redirige la salida a /dev/null, y luego esa salida redirigida se copia sobre la salida de error, de modo que ambas se van al mismo lugar. Esta última operación es tan comun, que hay una sintaxis especial para ella: comando SPMamp;>archivo redirige tanto la salida estandar como la salida de error al archivo nombrado.
En ocasiones queremos ejecutar un proceso complejo o tardado, con un for o un while, por ejemplo, y queremos dejarlo trabajando en el fondo como una unidad. Para hacer esto, podemos encerrar el comando completo entre parentesis. Entonces, el shell tratará al comando como una unidad, suspendiendolo o reanudandolo junto. Esto resulta también util cuando el proceso a ejecutarse cambia el entorno del shell. Como el proceso es ejecutado por un subshell, cualquier modificación al entorno solo afecta al del subshell, y no al original.
La sustitución de procesos es una caracteristica que no está disponible en todos los sistemas. Es necesario que el sistema soporte los 'named pipes' o el sistema de archivos /dev/fd. Si existe el soporte, bash puede efectuar una sustitución del estilo <(lista) o >(lista). La lista de procesos se efectua con su salida o su entrada, respectivamente, conectada a un 'named pipe', o a un archivo en /dev/fd. El nombre de este archivo se sustituye en vez de la lista, como un argumento al comando. El comando puede abrir el archivo y usarlo para entrada o salida, segun el caso.
Si bien no es fácil ver el uso de esta caracteristica, puede resultar muy poderosa en algunos casos. Por ejemplo, el programa diff compara el contenido de dos archivos. Usando este metodo, podemos usarlo para comparar la salida de dos comandos distintos, sin necesidad de grabar la salida de estos a archivos. Uniendo esto con un programa como lynx, podemos por ejemplo comparar dos versiones de una página web, guardadas en distintos servidores, sin tener que guardarlas en ningun lado:
This document was generated using the LaTeX2HTML translator Version 2K.1beta (1.48)
Copyright © 1993, 1994, 1995, 1996,
Nikos Drakos,
Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999,
Ross Moore,
Mathematics Department, Macquarie University, Sydney.
The command line arguments were:
latex2html -no_subdir -split 0 -show_section_numbers /tmp/lyx_tmpdir15038mWGPv5/lyx_tmpbuf0/tut_shell.tex
The translation was initiated by Gunnar Wolf on 2003-10-15