Departamento de Ingeniería Telemática (DIT)
Francisco José Fernández Jiménez
©2018
2 Estructura básica de shell-scripts. Invocación
3.1 Funcionamiento general del shell
3.2 Entrecomillado y carácter de escape
3.3.4 Exportación de variables
3.4 Expansiones y sustituciones
3.4.2 Expansión de parámetros y variables
3.5.6 Comandos compuestos o estructuras de control
3.5.6.1 Secuencial (agrupación de comandos)
3.5.6.2 Condicional: if-elif-else
3.5.6.4 Bucles incondicionales: for
3.5.6.5 Bucles condicionales: while y until
3.5.6.6 Ruptura de sentencias de control
3.6 Uso de comandos y aplicaciones
3.6.1.1 Salida del proceso shell actual, exit
3.6.1.2 Entrada estándar a un shell-script, read
3.6.1.3 Construcción de comandos en tiempo de ejecución: eval
6 Anexo: Descriptores de ficheros y redirecciones
6.1 Asociación para el Proceso Shell actual
6.2 Asociación para un comando (proceso hijo) invocado desde el shell
El intérprete de comandos o shell es un programa que permite a los usuarios interactuar con el sistema, procesando las órdenes que se le indican. Los comandos invocables desde el shell pueden clasificarse en internos (corresponden en realidad a órdenes interpretadas por el propio shell) y externos (corresponden a ficheros ejecutables externos al shell). Además de comandos, los shells ofrecen otros elementos para mejorar su funcionalidad, tales como variables, funciones o estructuras de control. El conjunto de comandos internos y elementos disponibles, así como su sintaxis, dependerá del shell concreto empleado.
Además de utilizar el shell desde la línea de comandos (basada en el prompt como la indicación del shell para anunciar que espera una orden del usuario), puede emplearse para la interpretación de shell-scripts. Un shell-script o "guión de órdenes" es un fichero de texto que contiene un conjunto de comandos y órdenes interpretables por el shell.
En los S.O.’s Unix existen múltiples implementaciones de shell (en Windows, el equivalente serían los programas "command.com" o "cmd.exe"). Atendiendo al shell del que proceden y a su similitud sintáctica (incluyendo sus comandos internos), los shells de Unix pueden clasificarse en dos grandes familias (existen algunos shell adicionales, de uso residual y dentro de los shells de una misma familia también existen diferencias, pero gran parte de su sintaxis es común):
Bourne Shell ha llegado a convertirse en un estándar de facto de tal modo que todos los sistemas Unix tienen, al menos, una implementación del Bourne Shell (o un shell compatible con él), ubicada en /bin/sh. En el caso concreto de los S.O.’s UNIX Linux, no existe ninguna implementación del Bourne Shell, manteniéndose la entrada /bin/sh (así como su manual man sh) como un enlace simbólico a una implementación de shell compatible. En concreto:
Para intentar homogeneizar esta diversidad de shells, el IEEE definió un estándar de "intérprete de comandos" bajo la especificación POSIX 1003.2 (también recogida como ISO 9945.2). La creación de dicho estándar se basó en la sintaxis que presentaban múltiples shells de la familia Bourne shell (el propio Bourne Shell de Unix Versión 7, implementaciones en UNIX System V y BSD, así como ksh). Esto ha llevado a que la gran mayoría de los shells derivados del Bourne Shell, tales como bash, dash o ksh, den soporte a este estándar POSIX (mientras que los derivados del csh no). En concreto:
TAREAS |
|
El objetivo es introducir el uso del lenguaje de programación de shell-scripts. Consecuentemente, nos centraremos en la sintaxis de shell propuesta por el estándar POSIX IEEE 1003.2 lo que, conforme a lo antes indicado, se traduce en que todo lo visto aquí podrá ser utilizado en dash, bash y cualquier otro shell que satisfaga dicho estándar. Sobre la base de este documento podrá profundizarse analizando las funcionalidades adicionales añadidas por cada shell, siendo de especial interés, dado su uso, las aportadas por bash.
Descarga de los shell-scripts de esta página Para que no tenga que escribir los scripts que se muestran en esta página a mano, puede descargarse dichos scripts así como la solución a los ejercicios propuestos en el fichero comprimido shellscripts.tar.gz. Descomprímalo con el comando: tar xfvz ./shellscripts.tar.gz
En el directorio scripts están los ejemplos y en el directorio aplicacion los ejercicios propuestos resueltos. |
Descarga en PDF (versión original) Puede descargarse también la versión original de este documento en formato PDF: FAST_t3-practica.pdf. |
En su forma más básica, un shell-script puede ser un simple fichero de texto que contenga uno o varios comandos. Para ayudar a la identificación del contenido a partir del nombre del archivo, es habitual que los shell scripts tengan la extensión ".sh", por lo que seguiremos este criterio (pero recuerde que es algo meramente informativo y opcional). Por ejemplo, el siguiente fichero sería un shell-script:
script.sh |
echo "Contenido carpeta personal:" ls ~/ |
TAREAS |
|
Además de comandos, los shell-scripts pueden contener otros elementos, aportados por el shell para mejorar la funcionalidad de los scripts. De forma resumida, la estructura básica de un shell-script es la siguiente:
script_ejemplo.sh |
|
#!/bin/dash # Esto no se interpreta echo Hola ps w echo "Proceso lee el script: $$" |
<-- Shebang <-- Comentarios <-- Contenido |
Como contenido del script pueden utilizarse múltiples elementos (comandos, variables, funciones, estructuras de control, comentarios,...) que se analizarán en el siguiente apartado.
El "shebang" permite especificar el intérprete de comandos con el que deseamos que sea interpretado el resto del script cuando se usa invocación implícita (ver más adelante). La sintaxis de esta línea es la secuencia #! seguida del ejecutable del shell deseado, sobre lo que deben realizarse la siguientes advertencias:
Sintaxis estricta La sintaxis de los shell-scripts se caracteriza por ser bastante estricta en su escritura, especialmente en lo que se refiere a la inserción u omisión de espacios en blanco entre las palabras especiales. Tenga esto muy en cuenta a la hora de escribir los scripts que se proponen. |
La utilización del shebang está condicionada por la forma en que sea invocado el shell-script, existiendo 3 opciones:
En los casos en los que se crean subshells, salvo que se fuerce lo contrario (con su -c por ejemplo), el subshell pertenecerá al mismo usuario al que pertenecía el shell padre que lo ha creado. El usuario al que pertenece el proceso shell que interpreta un script condiciona las operaciones que se podrán hacer desde dentro del script (por ejemplo, si el shell pertenece al usuario dit , el script no podrá modificar el fichero /etc/passwd , mientras que si el shell pertenece al superusuario root , sí podrá hacerlo). Tenga en cuenta este aspecto para determinar qué operaciones puede realizar dentro de un script, y con qué usuario debe invocarlo.
TAREAS |
|
Conforme se ha indicado en la introducción, si bien tanto bash como dash siguen el estándar POSIX, especialmente bash añade múltiples extensiones particulares, no soportadas por otros shells como dash. Consecuentemente, cada vez que diseñemos un script deberemos tener en cuenta el shell o shells que lo soportan, asegurando que sea invocado por uno de ellos. Para que se haga una idea de la importancia de este aspecto, considere los dos scripts siguientes, basados en el uso de la estructura for (que se usará más adelante):
script_estandar.sh |
#!/bin/sh for VAR in 0 1 2 3 do echo $VAR done |
script_bash.sh |
#!/bin/bash for ((VAR=0 ; VAR<4 ; VAR++ )) do echo $VAR done |
Ambos scripts realizan la misma funcionalidad, pero script_estandar.sh está escrito bajo la sintaxis POSIX, mientras que script_bash.sh utiliza una sintaxis no estándar soportada por bash.
TAREAS |
|
En este apartado se describe el lenguaje de comandos shell definido en el estándar POSIX. Veremos el funcionamiento general del shell, así como su sintaxis. Puede encontrar la descripción detallada del estándar POSIX en el siguiente enlace: POSIX Shell Command Language
Aquí se resumen las características más utilizadas. Se recomienda acudir al estándar para obtener una descripción más detallada y exhaustiva.
El lenguaje shell es un lenguaje interpretado, en el que se leen líneas de texto (terminadas en \n), se analizan y se procesan. Las líneas a interpretar son leídas de:
Con las líneas leídas, el shell realiza los siguientes pasos (en este orden):
Advertencia A lo largo de la memoria se utilizará la palabra "ejecutar" para referirse a la ejecución de un programa binario (un programa compilado, por ejemplo), al inicio de la interpretación de un script o a la invocación de una función o comando interno. Se considerarán, por tanto, elementos ejecutables, programas binarios y scripts (con el permiso de ejecución), comandos internos y funciones. |
Cuando se escriben comandos desde el teclado y se intenta introducir un elemento que está formado por más de una línea, una vez que teclee la primera línea y pulse Intro, el shell mostrará el indicador secundario de petición de orden > (en lugar del prompt), solicitándole que continúe escribiendo el elemento. Cuando el intérprete dé por concluida la introducción del elemento, la interpretará, volviendo a mostrar el prompt de la línea de comandos. Si utiliza el cursor ↑ para intentar ver el comando introducido, en general verá cómo el shell ha rescrito la entrada para aplicarle la sintaxis con la que todo el elemento es escrito en una sola línea. Si desea cancelar la introducción de una línea (o líneas) sin necesidad de borrar lo que lleva escrito, puede pulsar Ctrl-C.
TAREAS |
Escriba el contenido del script script_estandar.sh visto en el apartado 2 directamente en una consola de comandos. |
El shell tiene una lista de caracteres que trata de manera especial (operadores) y una serie de palabras reservadas (palabras que tienen un significado especial para el Shell). Puede ver un listado de caracteres especiales y palabras reservadas en los apartados 2.2 y 2.4 del estándar.
Cuando queremos utilizar un carácter especial del shell o una palabra reservada del lenguaje sin que sea interpretada como tal o prevenir una expansión o sustitución indeseada (las expansiones y sustituciones se verán en un apartado posterior) es necesario indicárselo al shell mediante las comillas (simples o dobles) o el carácter de escape. Por ejemplo, para escribir el nombre de un fichero que contenga espacios, para pasar el símbolo < como argumento a un programa.
El entrecomillado de una cadena vacía ( '' o ") genera una palabra vacía (palabra que no tiene ningún carácter).
Vea el apartado 2.2 del estándar para obtener información detallada.
TAREAS |
|
Como en cualquier lenguaje de programación, en el lenguaje shell se pueden crear y utilizar variables, que aquí se llaman parámetros. Existen varios tipos de parámetros:
A continuación se detallan cada uno de estos tipos. Vea el apartado 2.5 del estándar para obtener más información.
El shell permite realizar las siguientes operaciones básicas con las variables:
Sólo Definición |
VAR="" VAR= |
Definición y/o Inicialización/Modificación |
VAR=valor |
Expansión (Acceso a Valor) |
$VAR ${VAR} |
Eliminación de la variable |
unset VAR |
Sobre ello deben realizarse las siguientes observaciones respecto a:
No es necesario definir las variables previamente a su uso, ya que se crean al asignarles la primera vez un valor. Al definir una variable sin inicialización, su valor por omisión es la cadena nula, esto es, las siguientes entradas son equivalentes:
VAR=
VAR=""
Con el comando set (sin argumentos) puede ver todas las variables (y funciones) definidas en el shell actual.
TAREAS |
Mire el contenido del script script_variables.sh, que deberá contener lo siguiente:
Compruebe que dispone del permiso de ejecución general. Invóquelo y analice su funcionamiento. |
Existe un conjunto de variables que afectan al funcionamiento del shell. Muchas ya han sido analizadas en temas anteriores, por ejemplo: HOME, PATH, LANG,... Puede volver a ver una descripción de ellas en el apartado 2.5.3 del estándar.
Aquí vamos a destacar la variable IFS (Input Field Separators). El valor de esta variable es una lista de caracteres que se emplearán en el proceso de división de campos realizado tras el proceso de expansión (que se verá más adelante, en el apartado 3.4) y por el comando read (ver el apartado 3.8.1 de comandos internos). El valor por defecto es <espacio><tab><nueva-línea>. Podrá ver un ejemplo de uso de esta variable cuando realice los ejercicios propuestos (ver solución a script_user.sh).
Son los parámetros de la línea de comandos con la que se ha invocado al script (equivalen a la variable argv de C). Están denotados por un número y para obtener su valor se emplea $X o ${X} para los parámetros del 1 al 9 y ${X} para parámetros mayores (números de más de un dígito). Se pueden modificar con los comando set (los crea) y shift (los desplaza de posición).
Son parámetros identificados por un carácter especial creados por el Shell y cuyo valor no puede ser modificado directamente. En esta tabla se muestran los parámetros especiales definidos en el estándar:
Parámetro especial |
Valor |
$* |
Se expande a todos los parámetros posicionales desde el 1. Si se usa dentro de comillas dobles, se expande como una única palabra formada por los parámetros posicionales separados por el primer carácter de la variable IFS (si la variable IFS no está definida, se usa el espacio como separador y si está definida a la cadena nula, los campos se concatenan). |
$@ |
Se expande a todos los parámetros posicionales desde el 1, como campos separados, incluso aunque se use dentro de comillas dobles. |
$0 |
Nombre del shell o shell-script que se está ejecutando. |
$- (guion) |
Opciones actuales del shell (modificables con el comando set). Consulte las opciones disponibles con el comando man dash . |
$# |
Nº de argumentos pasados al script (no incluye el nombre del script). |
$? |
Valor devuelto por el último comando, script, función o sentencia de control invocado. Recuerde que, en general, cualquier comando devuelve un valor. Usualmente, cuando un comando encuentra un error devuelve un valor distinto de cero. |
$$ |
PID del proceso shell que está interpretando el script. |
$! |
PID del último proceso puesto en segundo plano. |
TAREAS |
Mire el contenido del script script_var-shell.sh, que deberá contener lo siguiente:
Compruebe que dispone del permiso de ejecución. Invóquelo el comando y analice la salida: ./script_var-shell.sh arg1 arg2 |
Cuando un proceso (proceso padre, como por ejemplo el shell) ejecuta otro proceso (proceso hijo, otro programa o script), el proceso padre, además de los parámetros habituales ( argc y argv en C), le pasa un conjunto de variables de entorno al proceso hijo (cada lenguaje de programación tiene su método propio para obtenerlas y modificarlas). Las variables de entorno pasadas pueden ser utilizadas por el proceso hijo para modificar su comportamiento. Por ejemplo, un programa en C puede usar la función getenv declarada en biblioteca estándar stdlib.h para obtener dichas variables (puede obtener más información ejecutando man getenv).
El comando interno del shell export permite que una variable (previamente definida o no) sea configurada para que su valor sea copiado a los procesos hijos que sean creados desde el shell actual (por ejemplo otros shell). Presenta la sintaxis:
export VAR
export VAR=valor
En este último caso, sí es posible añadir un espacio antes o después del signo =.
Debe advertirse que "exportación" significa "paso de parámetros por valor", esto es, en el proceso hijo se creará una variable de igual nombre que en el shell padre, y con igual valor, pero serán variables independientes (esto es, la modificación de valor de la variable en el proceso hijo no afectará al valor de la variable en el shell padre). El proceso hijo no puede crear ni modificar variables del proceso padre.
TAREAS |
Mire el contenido de los siguientes scripts en su sistema:
Compruebe que dispone del permiso de ejecución. Ejecute el comando: ./script_padre.sh Analice el resultado. |
No debe confundirse la exportación con la invocación de shell-scripts mediante el mecanismo implícito basado en .. En este caso no hay ninguna copia de variables por valor, simplemente el script invocado es interpretado por el mismo shell.
TAREAS |
Mire el contenido de los siguientes scripts en su sistema:
Compruebe que dispone del permiso de ejecución general. Ejecute el siguiente commando y analice el resultado: ./script1.sh |
En los S.O.'s Linux suele ser habitual encontrar scripts que se dedican exclusivamente a contener la inicialización de un conjunto de variables, o la definición de un conjunto de funciones. Otros scripts del sistema hacen uso del mecanismo de invocación implícito basado en ., para cargar o importar las variables o funciones definidas en dichos scripts.
cont_func.sh |
#!/bin/sh fun1(){...} fun2(){...} #... |
cont_var.sh |
#!/bin/sh VARa=1 VARb=2 #... |
script_sistema.sh |
#!/bin/sh . /dir/cont_var.sh . /dir2/cont_fun.sh fun1 $VARb #... |
TAREAS |
Por ejemplo, visualice el contenido del script del sistema encargado del arranque de los servicios /etc/init.d/rc. Observe cómo contiene las líneas: . /etc/default/rcS . /lib/lsb/init-functions Visualice el contenido de esos archivos rcS e init-functions y compruebe cómo sólo contienen definiciones de variables y funciones (la sintaxis para la definición de las funciones se analizará más adelante), respectivamente. De hecho, todos los ficheros de la carpeta /etc/default/ son scripts dedicados exclusivamente a contener la inicialización de variables, siendo importadas desde otros scripts del sistema. Por ejemplo, observe cómo:
|
Como se vio en el apartado de funcionamiento general del shell (apartado 3.1), en un paso previo a la ejecución del elemento ejecutable se realizan una serie de expansiones y sustituciones. En este apartado se describe cuáles son y cómo se realizan. Puede ver una descripción detallada en el apartado 2.6 del estándar.
Existen los siguientes tipos de expansiones y sustituciones (que desarrollaremos más adelante):
Aparte de estas expansiones está el concepto de alias (ver apartado 2.3.1 del estándar) que se utiliza para crear sinónimos de comandos y sólo se realiza en el elemento ejecutable de la línea antes de cualquier otra expansión o sustitución. Su utilización es muy limitada y puede conseguirse un comportamiento similar usando funciones que permiten además parámetros (las funciones se verán más adelante). Puede ver los alias definidos usando el comando alias. Puede ver ejemplos de definición de alias en el fichero ~/.bashrc.
El orden de ejecución de estas expansiones es el siguiente:
Puede utilizar las expansiones en cualquier ubicación donde se pueda usar una cadena de texto (incluido el nombre de comandos), exceptuando las palabras reservadas del lenguaje (if, else, …).
Las apariciones de la virgulilla (o tilde de la ñ) dentro de una línea, que no se encuentren entrecomilladas, se expanden de la siguiente manera:
Variables |
Valor |
~ |
Se expande al valor de la variable HOME |
~login |
Si "login" es un nombre de usuario del sistema, se expande a la ruta absoluta del directorio de inicio de sesión de ese usuario. Si no, no se expande. |
TAREAS |
Comente la línea del script script_var-shell.sh, visto en el apartado 3.3, donde aparece la palabra "firefox". Ejecute los siguientes comandos observando el valor de las variables posicionales: ./script_var-shell.sh ~ ~root ./script_var-shell.sh ~noexiste ~dit |
El formato general para incluir una expansión de variables o parámetros, como se ha visto en apartados anteriores es:
${PAR}
Las llaves pueden omitirse, salvo cuando se trate de un parámetro posicional con más de un dígito o cuando se quiera separar el nombre de la variable de otros caracteres. Por ejemplo:
echo $PAR #puede omitirse
echo ${10} #no puede omitirse
${PAR}TEXTO #no se omite
Si la expansión de parámetros ocurre dentro de comillas dobles, sobre el resultado no se realizará la expansión de ruta ni la división de campos (pasos b y c). En el caso del parámetro especial @, se hace la división de campos siempre.
Aparte de este formato general, se pueden utilizar otros formatos que permiten establecer valores por defecto, comprobar si la variable está definida, eliminar sufijos y prefijos, contar el número de caracteres, etc. Puede ver la lista de formatos junto con ejemplos en el apartado 2.6.2 del estándar. Por ejemplo, los siguientes:
${PAR:-alternativo} |
Valor de la variable. Si la variable no tiene ningún valor, la construcción se sustituye por el valor alternativo. |
${PAR:=alternativo} |
Ídem al anterior, pero asignando el valor alternativo a la variable. |
${PAR%sufijo} |
Elimina el sufijo más pequeño del valor de la variable. sufijo es un patrón como los utilizados en la expansión de ruta. Si en vez de % se pone %% se elimina el sufijo más grande. |
${PAR#prefijo} |
Elimina el prefijo más pequeño del valor de la variable. prefijo es un patrón como los utilizados en la expansión de ruta. Si en vez de # se pone ## se elimina el prefijo más grande. |
Los siguientes scripts muestran un posible ejemplo de ambas construcciones:
script_expansion1.sh |
#!/bin/sh VAR=1 echo $VAR unset VAR echo ${VAR:-2} echo $VAR FICH=fichero.c echo ${FICH%.c}.o |
script_expansion2.sh |
#!/bin/sh VAR=1 echo $VAR unset VAR echo ${VAR:=2} echo $VAR FICH=/usr/bin/prueba echo ${FICH##*/} |
TAREAS |
Mire el contenido de los scripts anteriores en su sistema, invóquelos y analice los resultados. |
Permite que la salida estándar de un programa se utilice como parte de la línea que se va a interpretar.
Existen dos opciones, con el mismo funcionamiento:
$(comando)
`comando`
En el segundo caso se está utilizando la tilde francesa o acento grave, que no debe confundirse con las comillas simples. Para escribirla, hay que pulsar la tecla correspondiente a ` y pulsar espacio.
El shell ejecutará comando, capturará su salida estándar y sustituirá $(comando) por la salida capturada.
Por ejemplo, para almacenar en una variable el nombre de todos los ficheros con extensión .sh del directorio actual, podría escribir:
VAR=`ls *.sh`
O, por ejemplo, para matar el proceso con nombre firefox-bin, podría usar:
kill -9 $(pidof firefox-bin)
El formato para realizar una expansión aritmética es el siguiente:
$((expresión))
Permite evaluar las cadenas indicadas en la expresión como enteros, admitiendo gran parte de los operadores usados en el lenguaje C, pudiendo usar paréntesis como parte de la expresión y el signo - para números negativos (a las cadenas que contengan letras se les asigna el valor 0). Tras la evaluación aritmética, el resultado vuelve a ser convertido a una cadena. La conversión de un número a un carácter puede realizarse con $'\xxx' (en bash) o con '\xxx' (en dash), ambos con comillas simples, pero ello no está recogido en el estándar POSIX.
Si se usan variables en la expresión, no es necesario que vayan precedidas por el carácter $ si ya contienen un valor entero válido (sí es necesario para los parámetros posicionales y especiales).
Puede ver más detalles en el apartado 2.6.4 del estándar.
TAREAS |
Mire el contenido del script script_expansion3.sh, que deberá contener lo siguiente:
Compruebe que dispone del permiso de ejecución. Invóquelo mediante el comando: ./script_expansion3.sh Analice el resultado. |
Los campos que incluyan los caracteres *, ?y [ (asterisco, interrogación y apertura de corchetes) no entrecomillados serán sustituidos por la lista de ficheros que cumplan ese patrón. Si no hay ningún fichero con ese patrón no se sustituye nada. El uso de estos caracteres ya se ha utilizado en otros cursos. Puede ver más detalles en el apartado 2.13 del estándar.
TAREAS |
Utilice el script script_var-shell.sh, visto en el apartado 3.3, pero modifíquelo eliminando las líneas con la palabra "firefox", para evitar que moleste en las siguientes pruebas. Ejecute los siguientes comandos observando el valor de la variable especial @: ./script_var-shell.sh s*_for?.sh ./script_var-shell.sh s*_for*.sh ./script_var-shell.sh s*_exp*.sh ./script_var-shell.sh s*_exp*[12].sh ./script_var-shell.sh s*_e*.sh |
Un comando puede ser clasificado en las siguientes tipos de comandos (de menor a mayor nivel):
Cada uno de estos tipos se forma mediante la composición de elementos de los tipos inferior. Pero también se permite anidar distintos tipos de comandos (no todas las combinaciones son posibles como se verá más adelante), por lo que podríamos encontrarnos: tuberías de comandos compuestos, comandos compuestos formados por otros comandos compuestos, etc.
En general, el valor devuelto por un comando compuesto (tipo b y superiores) será el valor devuelto por el último comando simple ejecutado.
A continuación describiremos cada uno de los tipos. Puede ver más detalles en el apartado 2.9 del estándar.
Un comando simple está formado por (todos los elementos son opcionales) una secuencia de asignación de variables y redirecciones (en cualquier orden) seguida de palabras (elemento ejecutable y sus argumentos) y redirecciones. A continuación se muestra la estructura genérica de un comando simple (los corchetes [ ] indican qué elementos son opcionales, que es la notación usada en el resto de este documento):
[VAR=v] [redir] [ejecutable argumentos] [redir]
Pudiendo ser el ejecutable programas "ejecutables" (comandos internos y ejecutables externos) e "interpretables" (funciones).
Es decir, en un mismo comando simple se puede hacer simultáneamente la asignación de variables y la ejecución de un programa. Cuando un comando simple no incluye un programa a ejecutar, la asignación de variables afecta a todo el shell, de lo contrario la asignación sólo afecta al programa que se va a ejecutar.
Si en un mismo comando simple hubiera que hacer expansiones en las palabras y en la asignación de variables, primero se hace las expansiones de las palabras.
Ejemplos de comandos simples:
VAR=x
El anterior comando asigna el valor x a la variable VAR y afecta a todo el proceso shell actual.
VAR=x programa
Asigna el valor x a la variable VAR y afecta solo al programa.
VAR=y OTRA=z
VAR=x programa $VAR
echo $VAR
Se asigna el valor y a la variable VAR y el valor z a la variable OTRA, que afectan a todo el shell. A continuación, asigna el valor x a la variable VAR y afecta sólo al programa, al cual se le pasa como primer argumento y. A continuación se imprime y por pantalla.
VAR=x > fichero programa
VAR=x programa > fichero #equivalente
Ambas líneas, asignan el valor x a la variable VAR que afecta solo al programa. Se ejecuta el programa y la salida estándar se redirige al archivo fichero. La redirección se realiza independientemente de que aparezca antes o después del programa. Si hubiera varias redirecciones se realizan en el orden de aparición en la línea, de izquierda a derecha.
Una tubería es una secuencia de uno o más comandos (simples o compuestos, pero no ningún tipo de lista) separados por el operador |. La salida estándar de un comando se conecta a la entrada estándar del siguiente comando (cada comando se ejecuta en otro subshell simultáneamente). Esta conexión se hace previamente a cualquier redirección. El formato es:
[ ! ] comando1 [ | comando2 … ]
Opcionalmente, se puede añadir delante el carácter ! que hace la negación lógica del valor devuelto por el último comando, de tal manera que el valor devuelto por la tubería sería 1 si el último comando devuelve 0, o 0 en caso contrario.
Una lista AND es una secuencia de tuberías (tenga en cuenta que una tubería puede ser sólo un comando simple) separadas por el operador &&. El formato es:
tuberia1 [ && tuberia2 … ]
Se van ejecutando las tuberías de izquierda a derecha hasta que una de ellas devuelva un valor distinto de cero. No se realiza ninguna expansión en una tubería hasta que el shell no determine que tiene que ejecutar dicha tubería (dependerá del resultado de la tubería anterior).
Una lista OR es una secuencia de tuberías separadas por el operador ||. El formato es:
tuberia1 [ || tuberia2 … ]
Se van ejecutando las tuberías de izquierda a derecha hasta que una de ellas devuelva un valor cero. No se realiza ninguna expansión en una tubería hasta que el shell no determine que tiene que ejecutar dicha tubería.
Una lista AND-OR es el resultado de combinar listas AND y/o OR en una misma línea. Los operadores && y || se evalúan con la misma prioridad de izquierda a derecha. Ejemplo:
tuberia1 || tuberia2 && tuberia3
Las listas son secuencias de una o más listas AND-OR separadas por los operadores ; o &. Los operadores ; y & no pueden aparecer seguidos (por ejemplo, daría error prog1 & ; prog2)
Según el operador las listas pueden ser secuenciales, asíncronas o mixtas (combinación de ambas).
Se utiliza como separador el operador ;. Se van ejecutando los distintos comandos secuencialmente (no se ejecuta un comando hasta que haya terminado el anterior). Cada lista AND-OR debe estar terminada por el operador ; a excepción de la última donde es opcional. El formato es:
listaAND-OR1 [ ; listaAND-OR2 … ] [ ; ]
Se utiliza como separador el operador &. Se van ejecutando los distintos comandos sin esperar a que el comando anterior termine (ejecución en segundo plano). El formato es:
listaAND-OR1 & [ listaAND-OR2 & ]
En este caso, a menos que se haga una redirección explícita de la entrada estándar, si un programa en segundo plano lee de la entrada estándar recibirá un error de fin de fichero (EOF).
Son combinaciones de listas secuenciales y asíncronas. Por ejemplo:
#asíncrona y secuencial lANDOR1 & lANDOR2 [ ; ]
#secuencial y asíncrona lANDOR1 ; lANDOR2 &
#asíncrona y secuencial lANDOR1 & lANDOR2 & lANDOR3 ; lANDOR4
#secuencial, asíncrona, secuencial lANDOR1 ; lANDOR2 & lANDOR3
No es más que una secuencia de listas (apartado 3.5.4), separadas por el carácter de nueva línea (intros), terminada por el operador ;, el operador &, el carácter de nueva línea (intro) o un comando compuesto. La utilidad de este tipo de listas se verá sobre todo cuando se expliquen los comandos compuestos.
TAREAS |
Mire el contenido del script script_operadores.sh, que deberá contener lo siguiente:
Compruebe que dispone del permiso de ejecución. Invóquelo y analice su funcionamiento. Desde la línea de comandos, cree listas y tuberías de todos los tipos vistos usando combinaciones de los comandos ls, echo, cat y ps. |
Los comandos compuestos son lo que en otros lenguajes de programación se conocen como estructuras de control. Cada uno de estos comandos compuestos (o estructuras de control) están delimitados por una palabra reservada u operador de control al principio y otro al final (terminador). Si se hace una redirección a continuación del terminador, en la misma línea, esa redirección se aplicará a todos los comandos que se encuentre en ese comando compuesto, a menos que se haga otra redirección explícita en un comando en concreto.
A continuación se hace un repaso por todas las estructuras de control disponibles en el estándar POSIX. Puede consultar el apartado 2.9.4 del estándar para más información.
La agrupación de comandos permite mejorar la legibilidad del código, aplicar una redirección a un conjunto de comandos y crear un subshell entre otras cosas.
Existen dos formas de agrupar comandos, con los siguientes formatos:
Se ejecuta la lista compuesta en un subshell. Los cambios que se produzcan en este subshell no afectarán al shell actual. Si la lista compuesta está terminada por el carácter de nueva línea, este carácter puede omitirse.
Se ejecuta la lista compuesta en el shell actual. Recuerde que las listas compuestas están terminadas por los operadores ;, & o nueva línea (el último comando debe estar separado de la llave de cierre por esos operadores).
En ambos casos, se permite añadir una redirección al final (detrás del ) o }) que afectará a todos los comandos del grupo.
Presenta la siguiente sintaxis:
if lista-compuestaA1 then lista-compuestaB1 elif lista-compuestaA2 then lista-compuestaB2 ... else lista-compuestaN fi |
Las entradas elif tienen el significado de un else seguido de un nuevo if. Puede haber tantas entradas elif como se desee. Tanto las entradas elif como la entrada else son opcionales.
En esta estructura, lo primero que se hace es ejecutar la lista-compuestaA1, si el valor devuelto es 0 (ADVERTENCIA: 0 significa verdadero aquí), se ejecutaría lista-compuestaB1 y terminaría la estructura if. Si el valor devuelto no es 0 se comprueba el siguiente elif. Si ninguna de las listas-compuestas A devuelve 0 se ejecutaría el bloque del else. En otras palabras, las listas compuestas B solo se ejecutan si se ha comprobado su respectiva lista-compuesta A y ha devuelto 0.
A veces, para mejorar la legibilidad, las listas-compuestas se encierran entre llaves (agrupación de comandos) pero es opcional. Asimismo, then estará en una línea u otra dependiendo del operador (&, ; o nueva línea) utilizado para terminar la lista compuesta. Si desea dejar una lista-compuesta vacía (no quiere realizar ninguna operación en un determinado caso), puede utilizar el comando : (comando nulo).
Por ejemplo, si tenemos un programa llamado condicion que devuelve 0 si algo es verdadero y 1 si es falso, los siguientes ejemplos son equivalentes:
if condicion; then { comando1; comando2; } fi | if condicion; then comando1; comando2; fi |
if condicion then comando1; comando2; fi | if condicion then comando1 comando2 fi |
if condicion; then comando1; comando2; fi | |
if condicion; then { comando1; comando2; } fi |
Recuerde que si usa las llaves, debe separarlas del resto de elementos. Por ejemplo:
if condicion; then {comando1; comando2;} fi
if condicion; then{ comando1; comando2;} fi
if condicion; then { comando1; comando2} fi
darán error de sintaxis.
Respecto a la condición; que puede usarse, basta cualquier lista compuesta que devuelva un valor (por ejemplo, pueden usarse los comandos internos true o false). El valor de una lista compuesta es el valor del último comando simple ejecutado en la lista compuesta.
Un programa habitual que se utiliza como condición es el programa test. El comando test se puede ejecutar de dos formas (ambas equivalentes):
test expresion
[ expression ] #los [] deben estar separados
En la segunda forma los corchetes no son operadores ni indican que la expresión sea opcional, sino que realmente son el nombre del programa.
Puede ver la descripción del comando test según el estándar.
Como expresiones más habituales pueden usarse las siguientes:
Tipo |
Expresión |
Verdadera sí (devuelve 0) |
Enteros (n1 y n2 se convierten a enteros) |
n1 -eq n2 |
n1 = n2 |
n1 -ne n2 |
n1 ≠ n1 |
|
n1 -gt n2 |
n1 > n2 |
|
n1 –ge n2 |
n1 ≥ n2 |
|
n1 -lt n2 |
n1 < n2 |
|
n1 -le n2 |
n1 ≤ n2 |
|
Cadenas |
"$VAR" = "cad" |
$VAR vale "cad". Es conveniente, pero no necesario, poner la variable entre comillas por si tuviera espacios o estuviese vacía, para que al expandirse no dé error de sintaxis. |
"$VAR" != "cad" |
$VAR vale algo distinto de "cad". |
|
-z "$VAR" "$VAR" |
$VAR está vacía. Equivale a "$VAR" = "" |
|
-n "$VAR" |
$VAR no está vacía. Equivale a "$VAR" != "" o ! -z |
|
Ficheros |
-e "$FILE" |
$FILE existe. Si se indica un enlace simbólico, será cierta sólo si existe el enlace simbólico y el fichero apuntado. Es conveniente que esté entre comillas por el mismo motivo anterior. |
-f "$FILE" |
$FILE existe y es regular. Si se indica un enlace simbólico, el tipo es el del fichero apuntado. |
|
-h "$FILE" |
$FILE existe y es un enlace simbólico |
|
-d "$DIR" |
$DIR existe y es un fichero de tipo directorio |
|
-p "$FILE" |
$FILE existe y es un fichero especial tubería (pipe) |
|
-b "$FILE" |
$FILE existe y es un fichero especial de bloques |
|
-c "$FILE" |
$FILE existe y es un fichero especial de caracteres |
|
-r "$FILE" |
$FILE existe y puede leerse |
|
-w "$FILE" |
$FILE existe y puede modificarse |
|
-x "$FILE" |
$FILE existe y puede ejecutarse |
|
-s "$FILE" |
$FILE existe y su tamaño es mayor de cero bytes |
Cualquiera de las condiciones anteriores puede ser precedida por el operador negación !, en cuyo caso la condición será cierta si no se satisface la comparación indicada. Por ejemplo, ! -d $DIR se cumplirá si $DIR NO es un directorio.
Asimismo, se permite crear condiciones múltiples mediante los operadores:
condicion1 -a condicion2 |
AND: Verdadero si ambas condiciones son verdaderas |
condicion1 -o condicion2 |
OR: Verdadero si se cumple alguna de las dos condiciones |
Recuerde las restricciones de sintaxis del shell en lo que respecta a los espacios, especialmente importantes en la escritura de las condiciones. Por ejemplo, la siguiente entrada dará error de sintaxis (el espacio tras ; sí puede omitirse):
if[ condicion ]; then
Y la siguiente dará error porque buscaría el comando [comando] (incluyendo los corchetes como parte del nombre del comando), que en general no encontrará (mostrando un mensaje de orden no encontrada).
if [comando]; then
TAREAS |
|
Presenta la siguiente sintaxis:
case cadena_texto in patron1) lista-compuesta1;; patron2) lista-compuesta2;; ... * ) lista-defecto [;;] #coincide con todo esac |
cadena_texto debe aparecer obligatoriamente en la misma línea que la palabra reservada case (la palabra reservada in puede estar en la siguiente línea). En esta estructura, primero se expande cadena_texto (si es necesario) y busca el primer patrón que encaje con dicho valor. Cuando lo encuentre, ejecuta la lista-compuesta correspondiente y finaliza la estructura. Los patronN se interpretan como cadenas de caracteres y si es necesario se expanden (por ejemplo, pueden contener variables). Admite los mismos caracteres que los usados para la expansión de ruta (*, ? y []). Asimismo, pueden usarse patrones múltiples mediante el operador | y opcionalmente pueden comenzar con el paréntesis:
patronA | patronB)
(patronA | patronB)
(patronC)
El doble punto y coma ;; permite determinar el final de los elementos a interpretar cuando se cumpla su patrón asociado. Por ese motivo, el ;; del último patrón puede omitirse. Es posible añadir espacios entre los patrones y los paréntesis abiertos ) que marcan el final del patrón. Conforme a esto, serían sintaxis válidas alternativas las siguientes:
case cadena_texto in patron1) cmd1; cmd2;; esac | case cadena_texto in patron1 ) cmd1 cmd2 esac |
case cadena_texto in patron1) cmd1; cmd2;; esac | |
case cadena_texto in (patron1) cmd1; cmd2; esac |
TAREAS |
|
Presenta la siguiente sintaxis:
for VAR in lista_valores; do lista-compuesta done |
El nombre de la variable VAR debe aparecer obligatoriamente junto con la palabra reservada for en la misma línea. lista_valores debe estar obligatoriamente en la misma línea que la palabra reservada in. El punto y coma ; puede sustituirse por un salto de línea, y viceversa. Así, por ejemplo, serían sintaxis válidas las siguientes:
for VAR in lista_valores; do lista-compuesta done | |
for VAR in lista_valores do lista-compuesta done | for VAR in lista_valores do lista-compuesta done |
lista_valores se corresponde con un conjunto de valores (tomándose cada valor como una cadena de caracteres que puede ser objeto de expansión y como caracteres de separación los caracteres definidos en la variable IFS). La estructura for define la variable VAR (si no ha sido previamente definida). Para cada uno de los valores del resultado de expandir lista_valores, la estructura inicializa la variable VAR con dicho valor y realiza una iteración (ejecutando lista-compuesta, en la cual suele ser habitual acceder al valor de la variable VAR).
Es posible omitir in lista_valores. Si se omite equivale a haber escrito: in "$@"
TAREAS |
|
Suele ser habitual el uso del comando externo seq para generar una lista de valores. Si bien este comando no está recogido en el estándar POSIX, es habitual su presencia en la mayoría de los sistemas UNIX. El comando seq presenta la sintaxis:
seq valor_inicial valor_final
siendo ambos valores números enteros. La salida del comando es la secuencia de números enteros entre ambos valores extremos indicados.
TAREAS |
|
Presentan la siguiente sintaxis:
while lista-comp-condicion do lista-compuesta done |
until lista-comp-condicion do lista-compuesta done |
La lista-comp-condicion es una lista compuesta que se rige por las mismas directrices indicadas en la estructura if. La estructura:
Así, por ejemplo, serían válidas y equivalentes las sintaxis siguientes, si la condición del until es la condición del while negada:
while lista-comp-condW do cmd1 cmd2 done | until lista-comp-condU do cmd1 cmd2 done |
while lista-comp-condW ; do cmd1; cmd2; done | |
while lista-comp-condW ; do { cmd1; cmd2; } done |
TAREAS |
|
Igual que en otros lenguajes de programación, como en el lenguaje C, es posible romper el funcionamiento normal de las estructuras repetitivas ( for, while y until). Sin embargo, hacerlo supone hacer código no estructurado. Por coherencia con lo visto en otras asignaturas, no se aconseja su uso.
En shell script esto se realiza con dos comandos internos: continue y break
Ambos son comandos internos de la shell con la siguiente sintaxis y funcionalidad:
continue [n]
El parámetro opcional n es un número entero positivo que permite especificar la estructura de control en la que se desea detener la iteración. Si se tienen varias estructuras de control anidadas, la estructura actual en la que se encuentra el continue corresponde a la estructura 1; la estructura superior que engloba a ésta sería la estructura 2, y así sucesivamente. Así, el valor de n referencia a la estructura de control en la que deseamos detener la iteración actual y continuar con la siguiente (por omisión, "n=1").
break [n]
El parámetro opcional n es un número entero positivo que permite indicar si se desean cancelar varias estructuras de control anidadas (por omisión, "n=1", que referencia a la estructura actual en la que se encuentra el break).
Presentan la siguiente sintaxis:
Definición |
fnombre() comando-compuesto [redir] |
Invocación |
fnombre [arg1 arg2 … ] |
El paréntesis siempre debe estar vacío (sólo indica que se está definiendo una función). Pueden insertarse espacios antes, entre y después del paréntesis. El comando compuesto puede ser cualquier de los que se han visto (agrupación de comandos, estructuras condicionales, estructuras repetitivas). Opcionalmente pueden aplicarse redirecciones a la función (afecta a los comandos que contiene, salvo que contengan una redirección explícita). A continuación se muestran ejemplos básicos de definición de funciones:
fnombre(){ comando1 comando2 } | fnombre(){ comando1; comando2; } |
fnombre() { comando1; comando2; } |
En lo que se refiere al nombrado de las funciones, se aplican los mismos criterios antes expuestos para el nombrado de las variables.
El estándar permite que dentro de una función se invoque a otra. Los argumentos pasados a la función en su invocación son accesibles desde el cuerpo de la función mediante los parámetros posicionales $1, $2,..., $9, ${10},... Por tanto, dentro de la función, los parámetros posicionales no corresponden a los argumentos usados en la invocación del script.
Al igual que las variables, las funciones son:
Dentro del cuerpo de la función suele ser habitual el uso del comando return, el cual provoca la salida inmediata de la función con el valor de retorno (número) indicado:
return [n]
Si no se indica ningún valor de retorno, la función devuelve el valor del último comando ejecutado. Como siempre, el valor devuelto por la función puede obtenerse con la variable $?. return también puede utilizarse para terminar un script invocado implícitamente con ..
TAREAS |
Mire el contenido del siguiente script en su sistema, compruebe que tiene el permiso de ejecución, invóquelo con 2 números enteros como argumentos y analice su funcionamiento :
|
Con objeto de alcanzar una mayor homogeneización entre los sistemas, el estándar POSIX recoge una lista de comandos que deben ser implementados en cualquier sistema, clasificándolos según sean ordenes internas del shell (built-in) o aplicaciones externas.
Los comandos internos corresponden a órdenes interpretadas por el propio shell (luego no existe ningún fichero ejecutable asociado al comando). Se distinguen dos tipos de comandos internos:
En la siguiente tabla se recogen los comandos especiales definidos en el estándar.
Comando interno especial |
Descripción |
break,continue, export, return, unset, . |
Se han descrito anteriormente. |
: |
Comando nulo. Se suele utilizar en estructuras de control que requieren un comando para ser sintácticamente correctas, pero no se quiere hacer nada. |
eval |
Permite crear comandos a partir de sus argumentos (ver más adelante) |
exec |
Ejecuta comandos (sustituyendo al shell actual) y abre, cierra y copia descriptores de fichero |
exit |
Provoca que el shell termine (ver más adelante) |
readonly |
Permite hacer que una variable sea de sólo lectura (no admite asignaciones) |
set |
Establece opciones del proceso shell actual y modifica los parámetros posicionales. |
shift |
Elimina el número indicado de parámetros posicionales empezando desde el 1, desplazando el resto de parámetros a posiciones inferiores. |
times |
Muestra los tiempos de procesamiento del shell y sus procesos hijos. |
trap |
Permite atrapar o ignorar señales del sistema. |
Puede obtener más información en el estándar.
Como comandos internos regulares, el estándar define los siguientes:
Básicos regulares |
bg, cd, false, fg, jobs, kill, pwd, read, true, wait |
Para Profundizar regulares |
alias, command, fc, getopts, newgrp, umask, unalias |
Consulte el estándar para más información.
Entre dichos comandos (especiales y regulares), por su utilidad en los shell-scripts deben destacarse los siguientes:
La sintaxis de este comando es:
exit [n]
exit provoca la eliminación inmediata del proceso correspondiente al shell que está leyendo el script. El parámetro opcional es un número entero que corresponde al valor devuelto por el script. Si no se indica ningún parámetro, el valor devuelto por el script será el del último comando ejecutado.
TAREAS |
|
El comando readread lee una línea de la entrada estándar (teclado) y la guarda en variables. Solo funciona en shell interactivos (leyendo la entrada del teclado), de lo contrario no hace nada. En su forma más básica, presenta la siguiente sintaxis:
read VAR1 [VAR2 …]
Este comando espera a que el usuario introduzca una línea de texto incluyendo espacios (la entrada termina cuando el usuario pulsa la tecla "Intro"; la pulsación "Intro" no forma parte del valor asignado a la cadena). Esta línea se divide en campos (según la variable IFS). Tras ello, el comando define las variables dadas como argumentos, inicializándolas con los campos obtenidos en la división. Si hay más campos que variables, los campos restantes se asignan a la última variable. Si hay más variables que campos, las variables sobrantes reciben como valor la cadena vacía "".
Consulte la página del estándar para obtener más información.
Algunos intérpretes de comandos como bash añaden otras opciones a este comando, como la posibilidad de imprimir un mensaje usando la opción -p (vea la ayuda de read en bash con el comando man bash).
TAREAS |
Cree el script script_read.sh; (éste, como es breve, no se le proporciona) que contenga lo siguiente:
Asígnele el permiso de ejecución general, invóquelo y analice su funcionamiento. |
El comando read también puede ser útil, por ejemplo, para detener la interpretación del script hasta que el usuario pulse una tecla:
TAREAS |
Cree el script script_read_pause.sh que contenga lo siguiente:
Asígnele el permiso de ejecución general, invóquelo y analice su funcionamiento. |
Resulta habitual el uso de estructuras while, combinadas con case y read, para crear menús interactivos, permitiendo mantenerse dentro del menú.
TAREAS |
|
El comando eval construye un comando mediante la concatenación de sus argumentos (pueden ser variables, etc.) separados por espacios. Dicho comando construido es leído por el shell e interpretado. La sintaxis del comando es:
eval [argumentos …]
Un posible uso es la creación de referencias indirectas a variables (parecido a usar punteros en lenguaje de programación C). En la tarea siguiente se muestra esto.
TAREAS |
Cree el script script_eval.sh que contenga lo siguiente:
Asígnele el permiso de ejecución, invóquelo y analice su funcionamiento. |
Los comandos externos corresponden a ficheros ejecutables externos al shell. Cualquier posible aplicación pertenecería a esta categoría de comandos ( ps, firefox, emacs,...). El estándar POSIX recoge una lista de comandos externos que aconsejablemente deberían estar en un sistema UNIX, clasificándolos en obligatorios u opcionales según se exija o no su implementación para satisfacer el estándar. Entre los comandos externos obligatorios, el estándar define los siguientes:
Básicos |
cat, chmod, chown, cmp, cp, date, dirname, echo, expr, printf |
Para Profundizar |
awk, basename, chgrp |
TAREAS |
Busque información sobre el comando echo y printf. Ejecute los siguientes comandos y analice su funcionamiento: echo Uno echo –n Uno; echo Dos echo –e "Uno\nDos" NOMBRE=Ana printf "Hola %s. Adios %s\n" $NOMBRE $NOMBRE |
Si bien la programación de shell-scripts no se puede depurar fácilmente, los shells suelen ofrecer algunos mecanismos en este aspecto. En concreto, tanto "bash" como "dash" ofrecen los siguientes argumentos, que pueden usarse simultáneamente:
-x |
Traza: expande cada orden simple, e imprime por pantalla la orden con sus argumentos, y a continuación su salida. |
-v |
Verbose: Imprime en pantalla cada elemento completo del script (estructura de control, ...) y a continuación su salida. |
También es posible depurar sólo parte del script insertando en él los siguientes comandos (pueden usarse igualmente en la propia línea de comandos):
set -x set –xv |
Activa las trazas/verbose. Ubicarlo justo antes del trozo del script que se desea depurar. |
set +x set +xv |
Desactiva las trazas/verbose. Ubicarlo justo después del trozo del script que se desea depurar. |
TAREAS |
|
A continuación se le propone la creación de una serie de scripts. Para ello, será necesario tanto usar los conocimientos expuestos en este documento, como otros relativos a la administración de Linux:
Repita lo anterior pero utilizando funciones en vez de alias.
El script debe comprobar si los argumentos pasados son correctos, así como la existencia del usuario indicado. Como código de error podrá devolver 0 (éxito), 1 (sintaxis de invocación errónea), 2 (usuario no existe).
Se solicitan dos posibles soluciones:
VAR="nombre=v1&edad=v2&tlf=v3"
Escriba un shell-script que analice el valor de dicha variable y para cada uno de los parámetros extraiga su valor y lo imprima por pantalla. Por ejemplo, que la salida sea:Cadena analizada: nombre=v1&edad=v2&tlf=v3 nombre: v1, edad: v2, tlf: v3.
Se solicitan dos posibles soluciones:Para facilitar el acceso a los distintos recursos del sistema (zona de almacenamiento en disco, teclado, pantalla, socket,...), el kernel de Linux vincula cada recurso con un fichero, real (espacio en disco) o virtual (en memoria, como /dev y /proc). Ejemplos habituales son:
Recurso |
Fichero |
Directorio |
/carpeta |
Pantalla |
/dev/ttyXX |
Sockets TCP |
/proc/net/tcp |
Fichero regular |
/dir/file |
Teclado (según USB...) |
/dev/uinput |
Proceso con PID xxxx |
/proc/xxxx |
Disco duro |
/dev/sda |
Ratón |
/dev/psaux |
Caché ARP |
/proc/net |
Cuando un proceso necesita acceder a un recurso, debe realizar las siguientes operaciones:
|
kernel |
|
proceso |
|
Recurso |
⇔ |
Fichero |
⇔ |
Descriptor |
Un descriptor no es más que un número entero "n", con las siguientes características:
Proceso con PID xxxx |
|
Fichero |
Descriptor |
Fichero_A |
0 |
Fichero_B |
1 |
Fichero_C |
2 |
... |
... |
Al trabajar con un intérprete de comandos encontramos que, de manera habitual, los comandos suelen imprimir su información en pantalla y obtenerla del teclado. Este funcionamiento se debe a los dos motivos siguientes:
Esta configuración "estándar" es la empleada por los comandos POSIX. Así, por ejemplo, los comandos "echo cadena" o "cat fichero" están preparados para imprimir (enviar la información) en el recurso asociado al descriptor 1 (salida estándar), o el comando cat (sin argumentos) está diseñado para obtener la información del recurso asociado al descriptor 0 (entrada estándar).
Por omisión, cuando un proceso padre crea un proceso hijo, el proceso hijo sólo dispone de los descriptores "0, 1, 2", cada uno asociado al mismo recurso que en el proceso padre (el proceso hijo hereda la asociación descriptor-fichero para esos tres descriptores, el resto de descriptores no se "heredan"). Todo ello lleva a que en una consola de comandos, las órdenes "echo cadena" o "cat fichero" suelan imprimir por pantalla (recurso del descriptor 1), o el comando cat (sin argumentos) obtenga la información del teclado (recurso del descriptor 0), al heredar esa asociación descriptor-fichero del shell (proceso padre).
Sobre este comportamiento habitual, los intérpretes de comandos de Linux permiten:
Para ello, la sintaxis a aplicar varía según el proceso sobre el que queramos establecer la nueva asociación fichero-descriptor sea el propio proceso shell que estamos utilizando, o sobre un comando (proceso hijo) invocado desde este shell (en programación C, por ejemplo, se realizaría con la función "open", entre otras).
Para ello se emplea el comando "exec", mediante las siguientes sintaxis:
Sintaxis |
Funcionalidad |
exec n< fichero |
Asocia el descriptor de entrada "n" con "fichero" (archivo regular). Para abreviar, en todo el texto se usará el calificativo “descriptor de entrada” para indicar que en ese instante (puede modificarse) el descriptor está asociado con un fichero abierto para “lectura”. Al igual para salida (escritura) y entrada/salida. |
exec n> fichero |
Asocia el descriptor de salida "n" con "fichero" |
exec n<> fichero |
Asocia el descriptor de entrada y salida "n" con "fichero" |
exec n<&m |
Asocia el descriptor "n" con el mismo fichero al que actualmente (en el momento de ejecutar este comando) está asociado el descriptor "m" (duplicado de descriptor). |
exec n<&- |
Cierra el descriptor "n" (elimina su asociación con el fichero al que actualmente esté vinculado) |
Por ejemplo, el siguiente comando permite asociar el archivo /tmp/fichero con el descriptor "4":
exec 4< /tmp/fichero
A partir de ello, cada vez que queramos que un comando realice operaciones de lectura/escritura sobre ese archivo, podremos aplicar los operadores de redirección (detallados más abajo) con dicho descriptor (en lugar de usar el nombre del archivo). Por ejemplo, para que el comando ls; envíe su información a ese archivo:
ls >&4
Ello se consigue mediante la técnica de "redirección", basada en la siguiente sintaxis (es importante que no haya espacios entre n y op) :
comando [n]op fichero/descriptor
donde:
Al aplicar esa redirección sobre el comando, para el descriptor n, el comando (el proceso hijo que se crea) no heredará (aún en el caso de ser n=0, 1 o 2) la asociación descriptor-fichero del proceso shell padre, sino que asociará a dicho descriptor n el "fichero" explícitamente indicado.
A continuación se resumen los operadores de redirección definidos por el estándar POSIX, agrupadas según operen sobre descriptores para entrada (lectura de fichero) o salida (escritura en fichero):
Redirección |
Funcionalidad: El shell invoca comando como proceso hijo, configurándolo para que: |
cmd [n]< fich |
Asocie el descriptor de entrada (recuérde que, para abreviar, se está usando el calificativo "descriptor de entrada" para indicar que en ese instante el descriptor está asociado con un fichero abierto para lectura, pero puede cambiar) n; con fich (archivo regular). Por omisión n=0, esto es, cmd < fich hace que para el proceso cmd, el descriptor 0 quede asociado a fich para lectura. |
cmd [n]<&m |
Asocie el descriptor de entrada n con el mismo fichero al que actualmente está asociado el descriptor m. Dicho fichero estará así referenciado desde dos descriptores (duplicado de descriptores). m es obligatorio. |
cmd [n]<&- |
Cierre el descriptor de entrada n (elimina su asociación con el fichero al que actualmente esté vinculado). Si el descriptor no está asociado con ningún fichero, dará error. |
cmd [n]<< delim |
Asocie el descriptor de entrada n con el recurso especial Here-Document (texto empotrado). Tras ejecutar la orden, aparecerá en consola el carácter >; el texto introducido a partir de entonces será guardado en el recurso Here-Document, terminando cuando se introduzca una línea que sólo contenga (incluidos espacios en blanco) la cadena delim (delimitador) y se pulse nueva línea; en dicho momento, comando será ejecutado (pudiendo acceder al contenido del recurso Here-Document a través del descriptor n). Si al escribir la orden, la cadena delim se introduce entre comillas, el shell las eliminará, de modo que para terminar de escribir en el recurso Here-Document, habrá que escribir la cadena delim sin comillas. |
cmd <<- delim |
Ídem <<, pero las tabulaciones añadidas al principio de línea será omitidas, no insertándose en el recurso Here-Document. |
Redirección |
Funcionalidad: El shell invoca comando como proceso hijo, configurándolo para que: |
cmd [n]> fich |
Asocie el descriptor de salida n con fich (archivo regular). Por omisión n=1, esto es, cmd > fich hace que para el proceso cmd, el descriptor 1 quede asociado a fich. Si fich no existe, será creado; si existe, será limpiado previamente. Salvo que se haya activado en el shell la opción “no sobrescribir” con el comando set -C (recuerde que el comando set permite modificar el comportamiento del shell, incluyendo opciones y el valor de sus variables) en cuyo caso dará error. |
cmd [n]>| fich |
Ídem >, sin depender de la opción "no sobrescribir" (set -C). |
cmd [n]>> fich |
Ídem >, sin limpiar previamente el fichero si existe (insertando al final del contenido existente). |
cmd [n]>& m |
Asocie el descriptor de salida n con el mismo fichero al que está asociado el descriptor m. Dicho fichero estará así referenciado desde dos descriptores (duplicado de descriptores). m es obligatorio. |
cmd [n]>& - |
Cierre el descriptor de salida n (elimina su asociación con el fichero al que actualmente esté vinculado). Si el descriptor no está asociado con ningún fichero, dará error. |
Redirección |
Funcionalidad: El shell invoca comando como proceso hijo, configurándolo para que: |
cmd [n]<>fichero |
Asocie el descriptor n de entrada y salida con fichero (archivo regular, será creado si no existe). |
Además de las anteriores, existe un tipo especial de redirección denominado tubería o "pipeline", en la cual el "recurso" sería esa "tubería" que conecta dos descriptores. Esta redirección se basa en la siguiente sintaxis:
comando1 | comando2
bajo la cual, el descriptor 0 de comando2 se asociaría con el descriptor 1 de comando1 (esto es, lo que comando1 envíe a su salida estándar será redirigido a la entrada estándar de comando2). Pueden usarse dos o más comandos separados por |.
A continuación se proponen varios ejemplos de redirección y su explicación:
cat < fichero |
Por omisión cat tiene el recurso "teclado" asociado al descriptor 0 (POSIX). Con este comando, cat es invocado para que su descriptor 0 quede asociado al recurso fichero, de modo que cat toma la entrada de dicho fichero. |
ls / > fichero |
ls es invocado para que su descriptor 1 esté asociado a fichero (y no a la "pantalla" a que está asociado en el shell desde el que es invocado), esto es, ls envía su salida a fichero. |
cat << fin |
Toda la información escrita tras invocar el comando es guardada en el recurso especial Here-Document, pasándose al comando cat cuando se introduzca la línea fin y se pulse nueva línea. |
ls | more |
El descriptor de salida de ls se conecta con el de entrada de more, de modo que toda la salida ls de entrega a more como entrada. |
Adicionalmente a lo anterior, deben realizarse las siguientes aclaraciones sobre las redirecciones:
echo "Hola" > /tmp/fichero
/bin/bash < /tmp/fichero
con el segundo comando se estará abriendo un nuevo proceso shell (subshell), el cual tendrá asociado el descriptor 0 con el archivo /tmp/fichero. Si en dicho subshell abierto ejecutamos ahora:
cat
este comando hereda la configuración del subshell, teniendo igualmente asociado el descriptor 0 con el archivo /tmp/fichero. El comando cat, al ser invocado sin argumentos, sigue el estándar POSIX, imprimiendo en el recurso asociado al descriptor 2 (la pantalla en este caso) la información obtenida del recurso asociado al descriptor 0, que en este caso será el archivo fichero; (y no el teclado). Consecuentemente, el resultado es que dicho comando cat imprime directamente por pantalla el contenido de fichero. El orden de lectura de los comandos es importante. Por ejemplo, (cat << fin) < fichero no hará que fichero se pase a cat, dado que < fichero no se aplicará hasta que no haya terminado el comando cat. Por ejemplo:
cat ["Ctrl-D" para terminar, que equivale a enviar EOF]
cat << fin [Teclear "fin" y Enter para terminar]
ls -l /usr > /dev/null 2>&1
TAREAS |
Ejecute y analice el funcionamiento de las redirecciones empleadas en los siguientes comandos:
|
Por último, advertir que la explicación anterior corresponde al estándar POSIX. Algunos intérpretes de comandos como Bash soportan otros operadores (además de los POSIX). A continuación se resumen algunos de los ofrecidos por Bash:
Redirección Bash ( NO POSIX!!) |
Descripción |
Equivalente POSIX |
cmd &> fich |
Asocia los descriptores 1 y 2 con el recurso "fichero" |
cmd > fich 2>&1 |
cmd >& fich |