Aprendiendo_Korn_Shell/Secciones/Capitulo5.tex

1264 lines
88 KiB
TeX

Si es programador, puede que haya leído el último capítulo -- con su afirmación al principio de que el shell de Korn tiene un conjunto avanzado de capacidades de programación -- y se haya preguntado dónde están muchas características de los lenguajes convencionales. Tal vez el <<agujero>> más obvio en nuestra cobertura hasta ahora se refiere a las construcciones de \emph{control de flujo} como \texttt{if, for, while,} etcétera.
El control de flujo da a un programador el poder de especificar que sólo se ejecuten ciertas partes de un programa, o que ciertas partes se ejecuten repetidamente, según condiciones como los valores de las variables, si los comandos se ejecutan correctamente o no, y otras. Llamamos a esto la capacidad de controlar el flujo de ejecución de un programa.
Casi todos los scripts o funciones de shell mostrados hasta ahora no han tenido control de flujo -- ¡sólo han sido listas de comandos a ejecutar! Sin embargo, el shell de Korn, como los shells C y Bourne, tiene todas las capacidades de control de flujo que cabría esperar y más; las examinaremos en este capítulo. Las usaremos para mejorar las soluciones a algunas de las tareas de programación que vimos en el último capítulo y para resolver tareas que introducimos aquí.
Aunque hemos intentado explicar el control de flujo para que los no programadores puedan entenderlo, también simpatizamos con los programadores que temen tener que pasar por otra explicación tabula rasa. Por esta razón, algunas de nuestras discusiones relacionan los mecanismos de control de flujo del shell de Korn con aquellos que los programadores ya deberían conocer. Por lo tanto, estará en una mejor posición para entender este capítulo si ya tiene un conocimiento básico de los conceptos de control de flujo.
El shell de Korn soporta las siguientes construcciones de control de flujo:
\begin{itemize}
\item{\texttt{if/else} Ejecuta una lista de sentencias si una determinada condición es/no es verdadera.}
\item{\texttt{for} Ejecuta una lista de sentencias un número fijo de veces.}
\item{\texttt{while} Ejecuta una lista de sentencias repetidamente mientras se cumple una determinada condición.}
\item{\texttt{until} Ejecuta una lista de sentencias repetidamente hasta que se cumpla una determinada condición.}
\item{\texttt{case} Ejecuta una de varias listas de sentencias en función del valor de una variable.}
\end{itemize}
Además, el shell de Korn proporciona un nuevo tipo de construcción de control de flujo:
\begin{itemize}
\item{\texttt{select} Permitir al usuario seleccionar una de una lista de posibilidades de un menú.}
\end{itemize}
Cubriremos cada uno de ellos, pero le advertimos: la sintaxis es inusual.
\section{if/else}
El tipo más simple de construcción de control de flujo es el condicional, encarnado en la sentencia \emph{if} del shell de Korn. Se usa un condicional cuando se quiere elegir si hacer o no hacer algo, o elegir entre un pequeño número de cosas a hacer, de acuerdo con la verdad o falsedad de las condiciones. Las condiciones comprueban los valores de las variables del shell, las características de los archivos, si los comandos se ejecutan correctamente o no, y otros factores. El shell tiene un gran conjunto de pruebas incorporadas que son relevantes para la tarea de programación del shell.
La construcción \emph{if} tiene la siguiente sintaxis:
\begin{lstlisting}[language=bash]
if <condicion>
then
<declaraciones>
[elif <condiciones>
then <declaraciones...>
[else
<declaraciones>
fi
\end{lstlisting}
La forma más sencilla (sin las partes \texttt{elif} y \texttt{else}, también conocidas como cláusulas) ejecuta las \emph{sentencias} sólo si la \emph{condición} es verdadera. Si añade una cláusula \texttt{else}, podrá ejecutar un conjunto de sentencias si la condición es verdadera u otro conjunto de sentencias si la condición es falsa. Puede utilizar tantas cláusulas \texttt{elif} (contracción de \emph{else if}) como desee; introducen más condiciones y, por lo tanto, más opciones para el conjunto de sentencias a ejecutar. Si utiliza una o más cláusulas \texttt{elif}, puede considerar la cláusula \texttt{else} como la parte <<si todo lo demás falla>>.
\subsection{Estado de salida y retorno}
Quizá el único aspecto de esta sintaxis que difiere de la de lenguajes convencionales como C y Pascal es que la <<condición>> es en realidad una lista de sentencias en lugar de la expresión booleana (verdadero o falso) más habitual. ¿Cómo se determina la veracidad o falsedad de la condición? Tiene que ver con un concepto general de Unix que aún no hemos tratado: el \emph{estado de salida} de los comandos.
Cada comando Unix, ya sea que provenga del código fuente en C, algún otro lenguaje, o un script/función del shell, devuelve un código entero al proceso que lo llama -- el shell en este caso -- cuando termina. Esto se denomina estado de salida. 0 es \emph{usualmente} el estado de salida <<OK>>, mientras que cualquier otro (1 a 255) \emph{usualmente} denota un error.\footnote{Dado que se trata de una <<convención>> y no de una <<ley>>, existen excepciones. Por ejemplo, \emph{diff}(1) (encontrar diferencias entre dos archivos) devuelve 0 para <<sin diferencias>>, 1 para <<diferencias encontradas>> o 2 para un error como un argumento de nombre de archivo no válido.} La forma en que \emph{ksh} maneja los estados de salida de los comandos incorporados se describe con más detalle más adelante en esta sección.
\texttt{if} comprueba el estado de salida de la \emph{última} sentencia de la lista que sigue a la palabra clave \texttt{if}.\footnote{Los programadores de LISP encontrarán esta idea familiar.} (Si el estado es 0, la condición se evalúa como verdadera; si es cualquier otra cosa, la condición se considera falsa. Lo mismo ocurre con cada condición adjunta a una sentencia \texttt{elif} (si la hay).
Esto nos permite escribir código de la forma:
\begin{lstlisting}[language=bash]
if <comando> es ejecutado con exito
then
procesamiento normal
else
procesamiento de errores
fi
\end{lstlisting}
\newpage
Más específicamente, ahora podemos mejorar la función \emph{pushd} que vimos en el último capítulo:
\begin{lstlisting}[language=bash]
function pushd { # empuja el directorio actual a la pila
dirname=$1
cd ${dirname:?"missing directory name."}
DIRSTACK="$dirname $DIRSTACK"
print "$DIRSTACK"
}
\end{lstlisting}
Esta función ahora verifica si el argumento es un nombre de directorio válido antes de intentar cambiar al directorio y agregarlo a la pila. Si el nombre de directorio no es válido, imprime un mensaje de error.
Sin embargo, la función reacciona de manera engañosa cuando se proporciona un argumento que no es un directorio válido. En caso de que no lo hayas entendido al leer el último capítulo, aquí está lo que sucede: el comando \emph{cd} falla, dejándote en el mismo directorio en el que estabas. Esto también es apropiado. Pero luego, la tercera línea de código empuja el directorio incorrecto a la pila de todos modos, y la última línea imprime un mensaje que te hace creer que la operación fue exitosa.
Necesitamos evitar que el directorio incorrecto se agregue a la pila e imprimir un mensaje de error. Aquí tienes cómo podemos hacer esto:
\begin{lstlisting}[language=bash]
function pushd { # empujar el directorio actual a la pila
dirname=$1
if cd ${dirname:?"missing directory name."} # si el cd tuvo exito
then
DIRSTACK="$dirname $DIRSTACK"
print "$DIRSTACK"
else
print still in $PWD.
fi
}
\end{lstlisting}
La llamada a \emph{cd} está ahora dentro de una construcción \texttt{if}. Si \emph{cd} tiene éxito, devuelve 0; las dos siguientes líneas de código se ejecutan, terminando la operación \emph{pushd}. Pero si \emph{cd} falla, devuelve con estado de salida 1, y \emph{pushd} imprime un mensaje diciendo que no ha ido a ninguna parte.
Normalmente puedes confiar en que los comandos integrados y las utilidades estándar de Unix devuelvan los estados de salida apropiados, pero ¿qué pasa con tus propios scripts y funciones de shell? Por ejemplo, nos gustaría que \emph{pushd} devolviera un estado apropiado para poder usarlo también en una sentencia \texttt{if}:
\begin{lstlisting}[language=bash]
if pushd some-directory
then
what we need to do
else
handle problem case
fi
\end{lstlisting}
El problema es que el estado de salida se restablece con cada comando, por lo que <<desaparece>> si no se guarda inmediatamente. En esta función, el estado de salida del \emph{cd} incorporado desaparece cuando se ejecuta la sentencia \emph{print} (y establece su propio estado de salida).
Por lo tanto, necesitamos guardar el estado que \emph{cd} establece y usarlo como estado de salida de toda la función. Dos características del shell que aún no hemos visto nos facilitan el camino. La primera es la variable especial del shell \texttt{?}, cuyo valor (\texttt{\$?}) es el estado de salida del último comando que se ejecutó. Por ejemplo:
\begin{lstlisting}[language=bash]
cd baddir
print $?
\end{lstlisting}
hace que el shell imprima 1, mientras que:
\begin{lstlisting}[language=bash]
cd goodir
print $?
\end{lstlisting}
hace que el shell imprima 0.
\subsubsection{Return}
La segunda característica que necesitamos es la sentencia \emph{return N}, que hace que el script o función que lo rodea salga con el estado de salida \emph{N}. \emph{N} es en realidad opcional; por defecto es el valor de salida del último comando que se ejecutó. Los scripts que terminan sin una sentencia \emph{return} (es decir, todos los que hemos visto hasta ahora) devuelven lo que haya devuelto la última sentencia. Si utiliza \emph{return} dentro de una función, simplemente sale de la función. (Por el contrario, la sentencia \emph{exit N} sale de todo el script, sin importar lo profundo que esté anidado en funciones).
\newpage
Volviendo a nuestro ejemplo: guardamos el estado de salida en ambas ramas del \texttt{if}, para poder utilizarlo cuando hayamos terminado:
\begin{lstlisting}[language=bash]
function pushd { # push current directory onto stack
dirname=$1
if cd ${dirname:?"missing directory name."} # if cd was successful
then
es=$?
DIRSTACK="$dirname $DIRSTACK"
print $DIRSTACK
else
es=$?
print still in $PWD.
fi
return $es
}
\end{lstlisting}
La asignación \texttt{es=\$?} guarda el estado de salida de \emph{cd} en la variable es; la última línea lo devuelve como estado de salida de la función.
Los estados de salida no son muy útiles para otra cosa que no sea su propósito. En particular, puede verse tentado a utilizarlos como <<valores de retorno>> de funciones, como haría con funciones en C o Pascal. Esto no funcionará; en su lugar debe utilizar variables o sustitución de comandos para simular este efecto.
\subsubsection{Ejemplo avanzado: anular el comando integrado}
Utilizando el estado de salida y el comando \emph{return}, y aprovechando el orden de búsqueda de comandos del shell, podemos escribir una función \emph{cd} que anule el comando incorporado \emph{command}.
Supongamos que queremos que nuestra función \emph{cd} imprima automáticamente los directorios antiguo y nuevo. He aquí una versión para poner en su \emph{.profile} o archivo de entorno:
\begin{lstlisting}[language=bash]
function cd {
command cd "$@"
es=$?
print "$OLDPWD -> $PWD"
return $es
}
\end{lstlisting}
Esta función se basa en el orden de búsqueda de los comandos enumerados en el último capítulo. \emph{cd} es un comando incorporado no especial, lo que significa que se encuentra \emph{después} de las funciones. Por lo tanto, podemos nombrar nuestra función \emph{cd}, y el shell la encontrará primero.
¿Pero cómo llegamos al <<verdadero>> comando \emph{cd}? Lo necesitamos para cambiar de directorio. La respuesta es el comando incorporado llamado, curiosamente, \emph{command}. Su trabajo es hacer exactamente lo que necesitamos: omitir cualquier función nombrada por el primer argumento, en su lugar encontrar el comando incorporado o externo y ejecutarlo con los argumentos suministrados. En el shell de Korn, el uso de \emph{command} seguido de uno de los comandos incorporados especiales evita que los errores en ese comando aborten el script. (Esto resulta ser un mandato de POSIX).
\textbf{ADVERTENCIA:} El comando incorporado \emph{command} no es especial. Si defines una función llamada \emph{command}, ya no hay forma de llegar al real (excepto eliminando la función, por supuesto).
De todos modos, volvamos al ejemplo. La primera línea utiliza \emph{command} para ejecutar \emph{cd}. Luego guarda el estado de salida en \texttt{es}, como hicimos antes, para que pueda ser devuelto al programa que llama o al shell interactivo. Finalmente, imprime el mensaje deseado y devuelve el estado de salida guardado. Veremos una <<envoltura>> más sustancial para \emph{cd} en el \hyperref[sec:Chapter7]{Capítulo 7}.
\subsubsection{Estado de salida de canalización (pipeline)}
El estado de salida de un único comando es un simple número, cuyo valor, como hemos visto, está disponible en la variable especial \texttt{\$?} ¿Pero qué pasa con una tubería? Después de todo, puedes conectar un número arbitrario de comandos mediante tuberías. ¿El estado de salida de una tubería es el del primer comando, el del último, o algún comando intermedio? Por defecto, es el estado de salida del \emph{último} comando de la tubería. (Esto es requerido por POSIX).
La ventaja de este comportamiento es que está bien definido. Si un proceso falla, se sabe que fue el último comando el que falló. Pero si algún proceso intermedio en el pipeline falló, usted no lo sabe. La opción \emph{set -o pipefail} le permite cambiar este comportamiento.\footnote{Esta opción está disponible a partir de \emph{ksh93g}.} Al activar esta opción, el estado de salida de la canalización cambia al del último comando que falló. Si ningún comando falla, el estado de salida es 0. Esto todavía no le dice qué comando en una tubería falló, pero al menos se puede decir que algo salió mal en alguna parte y tratar de tomar medidas correctivas.
\subsubsection{Interpretación de los valores de estado de salida}
Para \emph{ksh93}, los valores de estado de salida para los comandos incorporados y varios casos excepcionales se han regularizado de la siguiente manera:
\begin{table}[h]
\center
\caption{Valores de estado de salida}
\begin{tabular}{m{2cm}|m{13cm}} \hline
\textbf{Valor} & \textbf{Significado} \\ \hline
1-125 & Comando finalizado con fallo \\
2 & Uso no válido, con mensaje de uso (comandos integrados) \\
126 & Comando encontrado, pero el archivo no es ejecutable \\
127 & Comando no encontrado \\
128-255 & Comando externo finalizado con fallo \\
\ge{} 256 & Comando muerto con una señal; restar 256 para obtener el número de señal \\
\end{tabular}
\end{table}
Las señales son una función más avanzada; se describen en el \hyperref[sec:Chapter8]{Capítulo 8}.
\subsection{Combinaciones de estado de salida}\label{sec:5.1.2}
Una de las partes más oscuras de la sintaxis del shell de Korn le permite combinar estados de salida de forma lógica, de modo que pueda probar más de una cosa a la vez.
La sintaxis \emph{statement1 \&\& statement2} significa, <<ejecuta statement1, y si su estado de salida es 0, ejecuta statement2>>. La sintaxis \emph{statement1 || statement2} es la inversa: significa <<ejecuta la sentencia1, y si su estado de salida no es 0, ejecuta la sentencia2>>.
A primera vista, parecen construcciones <<if/then>> y <<if not/then>>, respectivamente. Pero en realidad están pensadas para su uso dentro de las condiciones de las construcciones \texttt{if}, como comprenderán fácilmente los programadores de C.
Es mucho más útil pensar en estas construcciones como <<y>> y <<o>>, respectivamente. Piensa en esto:
\begin{lstlisting}[language=bash]
if statement1 && statement2
then
...
fi
\end{lstlisting}
En este caso, se ejecuta la \emph{statement1}. Si devuelve un estado 0, entonces presumiblemente se ejecutó sin error. A continuación se ejecuta la \emph{statement2}. La cláusula \texttt{then} se ejecuta si \emph{statement2} devuelve un estado 0. Por el contrario, si \emph{statement1} falla (devuelve un estado de salida distinto de cero), \emph{statement2} ni siquiera se ejecuta; la <<última sentencia>> de la condición era \emph{statement1}, que falló -- por lo que la cláusula \texttt{then} no se ejecuta. En conjunto, es justo concluir que la cláusula \texttt{then} se ejecuta si tanto \emph{statement1} como \emph{statement2} han tenido éxito.
Del mismo modo, considere esto:
\begin{lstlisting}[language=bash]
if statement1 || statement2
then
...
fi
\end{lstlisting}
Si la \emph{statement1} tiene éxito, la \emph{statement2} no se ejecuta. Esto convierte a la \emph{statement1} en la última sentencia, lo que significa que la cláusula \texttt{then} se ejecuta. Por otro lado, si la \emph{statement1} falla, se ejecuta la \emph{statement2}, y si la cláusula \texttt{then} se ejecuta o no depende del éxito de la \emph{statement2}. El resultado es que la cláusula \texttt{then} se ejecuta si la \emph{statement1} o la \emph{statement2} tienen éxito.
Como ejemplo sencillo, supongamos que necesitamos escribir un script que compruebe la presencia de dos palabras en un fichero y que simplemente imprima un mensaje diciendo si alguna de las palabras está en el fichero o no. Podemos utilizar \emph{grep} para esto: devuelve el estado de salida 0 si encuentra la cadena dada en su entrada, distinto de cero si no:
\begin{lstlisting}[language=bash]
filename=$1
word1=$2
word2=$3
if grep $word1 $filename > /dev/null || grep $word2 $filename > /dev/null
then
print "$word1 or $word2 is in $filename."
fi
\end{lstlisting}
Para asegurarnos de que todo lo que obtenemos es el estado de salida, hemos redirigido la salida de ambas invocaciones de \emph{grep} al archivo especial \emph{/dev/null}, que se conoce coloquialmente como el <<cubo de bits>>. Cualquier salida dirigida a \emph{/dev/null} desaparece efectivamente. Sin esta redirección, la salida incluiría las líneas coincidentes que contienen las palabras, así como nuestro mensaje. (Algunas versiones de \emph{grep} admiten una opción \texttt{-s} para <<silencioso>>, es decir, sin salida. POSIX \emph{grep} utiliza \texttt{-q}, que significa <<silencioso>>. La solución más portable es redirigir la salida a \emph{/dev/null}, como hemos hecho aquí).
La cláusula \texttt{then} de este código se ejecuta si cualquiera de las dos sentencias \emph{grep} tiene éxito. Ahora supongamos que queremos que el script diga si el fichero de entrada contiene o no ambas palabras. He aquí cómo hacerlo:
\begin{lstlisting}[language=bash]
filename=$1
word1=$2
word2=$3 if grep $word1 $filename > /dev/null && grep $word2 $filename > /dev/null
then
print "$word1 and $word2 are both in $filename."
fi
\end{lstlisting}
Una nota menor: cuando se usan con comandos, \texttt{\&\&} y \texttt{||} tienen la misma precedencia. Sin embargo, cuando se usan dentro de \texttt{[[...]]} (de lo que hablaremos en breve), \texttt{\&\&} tiene mayor precedencia que \texttt{||}.
Veremos más ejemplos de estos operadores lógicos más adelante en este capítulo y en el código para el depurador \emph{kshdb} en el \hyperref[sec:Chapter9]{Capítulo 9}.
\subsection{Inversión del sentido de prueba}
A veces, la forma más natural de expresar una condición es en negativo. (<<Si Dave no está ahí, entonces...>>) Supongamos que necesitamos saber que ninguna de las dos palabras está en un archivo fuente. En la mayoría de los scripts, cuando este es el caso, verá código como este:
\begin{lstlisting}[language=bash]
if grep $word1 $filename > /dev/null || grep $word2 $filename > /dev/null
then
: # do nothing
else
print "$word1 and $word2 are both absent from $filename."
fi
\end{lstlisting}
El comando \emph{:} no hace nada. El significado, entonces, es <<si palabra1 o palabra2 están presentes en nombre de fichero, no hagas nada; si no, imprime un mensaje>>. El shell de Korn le permite hacer esto de forma más elegante utilizando la palabra clave ! (introducida en POSIX): t
\begin{lstlisting}[language=bash]
filename=$1
word1=$2
word2=$3
if ! grep $word1 $filename > /dev/null &&
! grep $word2 $filename > /dev/null
then
print "$word1 and $word2 are both absent from $filename."
fi
\end{lstlisting}
\subsection{Pruebas de estado}
Los estados de salida son las únicas cosas que una construcción \texttt{if} puede comprobar. Pero eso no significa que sólo pueda comprobar si los comandos se ejecutaron correctamente o no. El shell proporciona una forma de probar una variedad de condiciones con la construcción \texttt{[[...]]}.
\footnote{El shell de Korn también acepta los comandos \texttt{[...]} y \emph{test}. (Hay comandos incorporados en todas las versiones de \emph{ksh}; se comportan como las versiones externas originales). La construcción \texttt{[[...]]} tiene muchas más opciones y está mejor integrada en el lenguaje del shell de Korn: específicamente, la división de palabras y la expansión de comodines no se hacen dentro de \texttt{[[} y \texttt{]]}, haciendo las citas menos necesarias. Además, siempre puede distinguir los operadores de los operandos, ya que los operadores no pueden ser el resultado de la expansión.}
Puede utilizar la construcción para comprobar muchos atributos diferentes de un archivo (si existe, qué tipo de archivo es, cuáles son sus permisos y propiedad, etc.), comparar dos archivos para ver cuál es más nuevo, hacer comparaciones y coincidencias de patrones en cadenas, y mucho más.
\texttt{[[ condición ]]} es en realidad una sentencia como cualquier otra, salvo que lo único que hace es devolver un estado de salida que indica si la condición es verdadera. Por lo tanto, encaja dentro de la sintaxis de las sentencias \emph{if} de la construcción \emph{if}.
\subsubsection{Comparación de cadenas}
Los corchetes dobles (\texttt{[[...]]}) rodean expresiones que incluyen varios tipos de \emph{operadores}. Empezaremos con los operadores de comparación de cadenas, que se enumeran en la Tabla \ref{Tab: 5.2}. (Observe que no hay operadores para <<mayor o igual>> o <<menor o igual>>. (Observe que no hay operadores para <<mayor que o igual>> o <<menor que o igual>>). En la tabla, \emph{str} se refiere a una expresión con un valor de cadena, y \emph{pat} se refiere a un patrón que puede contener comodines (igual que los patrones en los operadores de manejo de cadenas que vimos en el capítulo anterior). Tenga en cuenta que estos operadores comparan los valores lexicográficos de las cadenas, por lo que ``10'' < ``2''.
\begin{table}[h]
\center
\caption{Operadores de comparación de cadenas}
\label{Tab: 5.2}
\begin{tabular}{m{2cm}|m{9cm}} \hline
\textbf{Operador} & \textbf{Verdadero si...} \\ \hline
\emph{str} & \emph{str} no es nulo \\
\emph{str == pat} & \emph{str} coincide con \emph{pat} \\
\emph{str = pat} & \emph{str} coincide con \emph{pat} (obsoleto) \\
\emph{str != pat} & \emph{str} no coincide con \emph{pat} \\
\emph{str1 < str2} & \emph{str1} es menor que \emph{str2} \\
\emph{str1 > str2} & \emph{str1} es mayor que \emph{str2} \\
\emph{-n str} & \emph{str} no es nulo (tiene una longitud mayor que 0) \\
\emph{-z str} & \emph{str} es nulo (tiene longitud 0) \\
\end{tabular}
\end{table}
\newpage
Podemos utilizar uno de estos operadores para mejorar nuestra función \emph{popd}, que reacciona mal si se intenta hacer pop y la pila está vacía. Recordemos que el código para \emph{popd} es:
\begin{lstlisting}[language=bash]
function popd { # cd to top, pop it off stack
top=${DIRSTACK%% *}
DIRSTACK=${DIRSTACK#* }
cd $top
print "$PWD"
}
\end{lstlisting}
Si la pila está vacía, \texttt{\$DIRSTACK} es la cadena nula, al igual que la expresión \texttt{\${DIRSTACK\%\% *}}. Esto significa que cambiará a su directorio personal; en su lugar, queremos que \emph{popd} imprima un mensaje de error y no haga nada.
Para ello, debemos comprobar si la pila está vacía, es decir, si \texttt{\$DIRSTACK} es nulo o no. He aquí una forma de hacerlo:
\begin{lstlisting}[language=bash]
function popd { # pop directory off the stack, cd there
if [[ -n $DIRSTACK ]];
then top=${DIRSTACK%% *}
DIRSTACK=${DIRSTACK#* }
cd $top
print "$PWD"
else
print "stack empty, still in $PWD."
return 1
fi
}
\end{lstlisting}
Observe que en lugar de poner \texttt{then} en una línea separada, lo ponemos en la misma línea que el \texttt{if} después de un punto y coma, que es el carácter separador de sentencias estándar del shell. (Hay una sutileza aquí. El shell sólo reconoce palabras clave como \texttt{if} y \texttt{then} cuando están al principio de una sentencia. Esto es para que pueda escribir, por ejemplo, \texttt{print if then else is neat} sin obtener errores de sintaxis. Las nuevas líneas y el punto y coma separan las sentencias. Así, el \texttt{then} en la misma línea que el \texttt{if} es reconocido correctamente después de un punto y coma, mientras que sin el punto y coma, no lo sería).
Podríamos haber utilizado operadores distintos de \emph{-n}. Por ejemplo, podríamos haber utilizado \emph{-z} e intercambiado el código en las cláusulas \texttt{then} y \texttt{else}. También podríamos haber utilizado:
\begin{lstlisting}[language=bash]
if [[ $DIRSTACK == "" ]]; then
...
\end{lstlisting}
\begin{mybox}
\textbf{[[...]] vs el comando \emph{test} y [...]} \\
Escribimos nuestra prueba \texttt{[[ \$DIRSTACK == '''' ]]}. Este no es el uso correcto para la sintaxis más antigua \texttt{[...]} o \emph{test}.
En esta sintaxis, que el shell de Korn todavía soporta, y que es todo lo que tiene en el shell de Bourne, si \texttt{\$DIRSTACK} se evalúa como una cadena nula, el shell se quejará de que falta un argumento. Esto lleva al requisito de encerrar ambas cadenas entre comillas dobles (\texttt{[ ''\$DIRSTACK>> = '''' ]}), que es la forma más legible de hacerlo, o al truco común de añadir un carácter extra delante de las cadenas, como así: \texttt{[ x\$DIRSTACK = x ]}. Esto último funciona, ya que si \texttt{\$DIRSTACK} es nulo, el comando \texttt{[...]} sólo ve los dos caracteres \texttt{x}, pero no es muy obvio lo que está pasando, especialmente para el novato.
Tenga en cuenta también que el operador preferido del shell de Korn es \texttt{==}, mientras que test requiere un único carácter \texttt{=}.
\end{mybox}
Mientras limpiamos el código que escribimos en el último capítulo, corrijamos el manejo de errores en el script principal (\hyperref[box:4-1]{Tarea 4-1}). El código de ese script es:
\begin{lstlisting}[language=bash]
filename=${1:?"filename missing."}
howmany=${2:-10}
sort -nr $filename | head -$howmany
\end{lstlisting}
Recuerda que si omites el primer argumento (el nombre de archivo), el shell imprime el mensaje \texttt{highest: 1: filename missing.} Podemos mejorarlo sustituyendo un mensaje de <<uso>> más estándar:
\begin{lstlisting}[language=bash]
if [[ -z $1 ]]; then
print 'usage: highest filename [N]'
else
filename=$1
howmany=${2:-10}
sort -nr $filename | head -$howmany
fi
\end{lstlisting}
Se considera un estilo de programación mejor encerrar todo el código en el \texttt{if-then-else}, pero ese código puede volverse confuso si estás escribiendo un script largo en el que necesitas verificar errores y salir en varios puntos del camino. Por lo tanto, un estilo más común para la programación en shell es el siguiente:
\begin{lstlisting}[language=bash]
if [[ -z $1 ]]; then
print 'usage: highest filename [-N]'
exit 1
fi
filename=$1
howmany=${2:-10}
sort -nr $filename | head -$howmany
\end{lstlisting}
La instrucción \texttt{exit} informa a cualquier programa llamante que necesita saber si se ejecutó correctamente o no. (También puedes usar \texttt{return}, pero consideramos que \texttt{return} debería reservarse para su uso en funciones).
Como ejemplo de los operadores \texttt{==} y \texttt{!=}, podemos agregar a nuestra solución para la \hyperref[box:4-2]{Tarea 4-2}, el script principal de un compilador de C. Recuerda que se nos proporciona un nombre de archivo que termina en \texttt{.c} (el archivo de código fuente), y necesitamos construir un nombre de archivo que sea el mismo pero que termine en \texttt{.o} (el archivo de código objeto). Las modificaciones que haremos tienen que ver con otros tipos de archivos que se pueden pasar a un compilador de C.
\subsubsection{Acerca de los compiladores C}
Antes de llegar al código shell, es necesario entender algunas cosas sobre los compiladores de C. Ya sabemos que traducen el código fuente de C a código objeto. En realidad, forman parte de sistemas de compilación que también realizan otras tareas. El término <<compilador>> se utiliza a menudo en lugar de <<sistema de compilación>>, así que lo usaremos en ambos sentidos.
Aquí nos interesan dos tareas que realizan los compiladores aparte de compilar código C: pueden traducir código de lenguaje ensamblador a código objeto y pueden enlazar archivos de código objeto para formar un programa ejecutable.
El lenguaje ensamblador trabaja a un nivel muy cercano al del ordenador: cada sentencia en ensamblador puede traducirse directamente en una sentencia de código objeto, a diferencia de C u otros lenguajes de alto nivel, en los que una sola sentencia fuente puede traducirse en docenas de instrucciones de código objeto. Traducir un archivo de código en lenguaje ensamblador a código objeto se denomina, como es lógico, ensamblar el código.
Aunque mucha gente considera que el lenguaje ensamblador es algo pintorescamente anticuado -como una máquina de escribir en la era del tratamiento de textos WYSIWYG y la autoedición-, algunos programadores siguen necesitándolo cuando tratan detalles precisos del hardware informático. No es raro que un programa conste de varios archivos de código en un lenguaje de alto nivel (como C o C++) y algunas rutinas de bajo nivel en lenguaje ensamblador.
La otra tarea de la que nos ocuparemos se llama enlazado. La mayoría de los programas del mundo real, a diferencia de los asignados a una clase de programación de primer curso, constan de varios archivos de código fuente, posiblemente escritos por varios programadores diferentes. Estos archivos se compilan en código objeto; después, el código objeto debe combinarse para formar el programa final ejecutable. La tarea de combinar suele denominarse <<enlazar>>: cada componente del código objeto suele contener referencias a otros componentes, y estas referencias deben resolverse o <<enlazarse>> entre sí.
Los sistemas de compilación C son capaces de ensamblar archivos de lenguaje ensamblador en código objeto y enlazar archivos de código objeto en ejecutables. En concreto, un compilador llama a un ensamblador independiente para que se ocupe del código ensamblador y a un enlazador (también conocido como <<cargador>>, <<cargador de enlace>> o <<editor de enlace>>) para que se ocupe de los archivos de código objeto. Estas herramientas separadas se conocen en el mundo Unix como \emph{as} y \emph{ld}, respectivamente. El compilador de C propiamente dicho se invoca con el comando \emph{cc}.
Podemos expresar todos estos pasos en términos de los sufijos de los archivos pasados como argumentos al compilador de C. Básicamente, el compilador hace lo siguiente:
\begin{enumerate}
\item{Si el argumento termina en \texttt{.c} es un archivo fuente C; compilar en un archivo de código objeto \texttt{.o}.}
\item{Si el argumento termina en \texttt{.s}, es lenguaje ensamblador; ensamble en un archivo \texttt{.o}.}
\item{Si el argumento termina en \texttt{.o}, no haga nada; guárdelo para el paso de vinculación posterior.}
\item{Si el argumento termina en algún otro sufijo, imprime un mensaje de error y sale.\footnote{A efectos de este ejemplo. Sabemos que esto no es estrictamente cierto en la vida real.}}
\item{Enlaza todos los archivos de código objeto \texttt{.o} en un archivo ejecutable llamado \texttt{a.out}. Este archivo suele renombrarse a algo más descriptivo.}
\end{enumerate}
El paso 3 permite reutilizar archivos de código objeto ya compilados (o ensamblados) para crear otros ejecutables. Por ejemplo, un archivo de código objeto que implemente una interfaz para una unidad de CD-ROM podría ser útil en cualquier programa que lea CD-ROMs.
La Figura \ref{fig:05} debería hacer más claro el proceso de compilación; muestra cómo el compilador procesa los archivos fuente C \texttt{a.c} y \texttt{b.c}, el archivo de lenguaje ensamblador \texttt{c.s}, y el archivo de código objeto ya compilado \texttt{d.o}. En otras palabras, muestra cómo el compilador maneja el comando \texttt{cc a.c b.c c.s d.o}.
\begin{figure}[h!]
\centering
\includegraphics[scale=1]{fig_05}
\caption{\small{Archivos producidos por un compilador C}}
\label{fig:05}
\end{figure}
He aquí cómo empezaríamos a implementar este comportamiento en un script de shell. Supongamos que la variable \texttt{filename} contiene el argumento en cuestión, y que \emph{ccom} es el nombre del programa que realmente compila un fichero fuente C en código objeto. Supongamos además que \emph{ccom} y as (ensamblador) toman como argumentos los nombres de los ficheros fuente y objeto:
\begin{lstlisting}[language=bash]
if [[ $filename == *.c ]]; then
objname=${filename%.c}.o
ccom "$filename" "$objname"
elif [[ $filename == *.s ]]; then
objname=${filename%.s}.o
as "$filename" "$objname"
elif [[ $filename != *.o ]]; then
print "error: $filename is not a source or object file."
exit 1
fi
further processing ...
\end{lstlisting}
Recuerde del capítulo anterior que la expresión \texttt{\${filename\%.c}.o} elimina \texttt{.c} del nombre de archivo y añade \texttt{.o}; \texttt{\${filename\%.s}.o} hace lo mismo con los archivos que terminan en \texttt{.s}.
El <<procesamiento posterior>> es el paso de enlace, que veremos cuando completemos este ejemplo más adelante en el capítulo.
\subsubsection{Comprobación de atributos de archivo}
El otro tipo de operador que puede utilizarse en expresiones condicionales comprueba si un fichero tiene determinadas propiedades. Existen 24 operadores de este tipo. Aquí cubrimos los de interés más general; el resto se refieren a arcanos como sticky bits, sockets y descriptores de fichero, y por tanto son de interés sólo para programadores de sistemas. Consulte el \hyperref[sec:ApendiceB]{Apéndice B} para ver la lista completa. La Tabla \ref{Tab:5-2} enumera las que nos interesan ahora.
\begin{table}[h]
\center
\caption{Operadores de atributos de archivos}
\label{Tab:5-2}
\begin{tabular}{m{4cm}|m{11cm}} \hline
\textbf{Operador} & \textbf{Verdadero si...} \\ \hline
\texttt{-e \emph{file}} & \emph{file} existe \\
\texttt{-d \emph{file}} & \emph{file} es un directorio \\
\texttt{-f \emph{file}} & \emph{file} es un archivo regular (por ejemplo, no un directorio u otro tipo especial de archivo) \\
\texttt{-L \emph{file}} & \emph{file} es un enlace simbólico \\
\texttt{-r \emph{file}} & Tiene permisos de lectura en \emph{file} \\
\texttt{-s \emph{file}} & \emph{file} existe y no está vacío \\
\texttt{-w \emph{file}} & Tiene permisos de escritura en \emph{file} \\
\texttt{-x \emph{file}} & Tienes permiso de ejecución sobre \emph{file} o permiso de búsqueda de directorio si es un directorio \\
\texttt{-O \emph{file}} & Su propio archivo (el UID efectivo coincide con el del archivo) \\
\texttt{-G \emph{file}} & Su ID de grupo efectivo es el mismo que el del fichero \\
\texttt{\emph{file1} -nt \emph{file2}} & \emph{file1} es más nuevo que \emph{file2}\tablefootnote{En concreto, los operadores \texttt{-nt} y \texttt{-ot} comparan los \emph{tiempos de modificación} de dos archivos.} \\
\texttt{\emph{file1} -ot \emph{file2}} & \emph{file1} es más antiguo que \emph{file2} \\
\texttt{\emph{file1} -ef \emph{file2}} & \emph{file1} y \emph{file2} son el mismo archivo \\
\end{tabular}
\end{table}
Antes de llegar a un ejemplo, debes saber que las expresiones condicionales dentro de [[ y ]] también pueden combinarse usando los operadores lógicos \&\& y ||, tal como vimos con los comandos de shell simples en la \hyperref[sec:5.1.2]{Sección 5.1.2}, anteriormente en este capítulo. También es posible combinar comandos del shell con expresiones condicionales utilizando operadores lógicos, como en este caso:
\begin{lstlisting}[language=bash]
if command && [[ condition ]]; then
...
\end{lstlisting}
El \hyperref[sec:Chapter7]{Capítulo 7} contiene un ejemplo de esta combinación.
También puede negar el valor verdadero de una expresión condicional precediéndola de un signo de exclamación (!), de modo que \texttt{! expr} se evalúe como verdadero sólo si \texttt{expr} es falso. Además, puede hacer expresiones lógicas complejas de operadores condicionales agrupándolos con paréntesis. (Resulta que esto también es cierto fuera de la construcción [[...]]). Como veremos en el \hyperref[sec:Chapter8]{Capítulo 8}, la construcción \emph{(statement list)} ejecuta la lista de sentencias en un subshell, cuyo estado de salida es el de la última sentencia de la lista).
Así es como usaríamos dos de los operadores de fichero para embellecer (una vez más) nuestra función \emph{pushd}. En lugar de hacer que \emph{cd} determine si el argumento dado es un directorio válido -- es decir, devolviendo con un estado de salida malo si no lo es -- podemos hacer la comprobación nosotros mismos. Aquí está el código:
\begin{lstlisting}[language=bash]
function pushd { # empujar el directorio actual a la pila
dirname=$1
if [[ -d $dirname && -x $dirname ]]; then
cd "$dirname"
DIRSTACK="$dirname DIRSTACK"
print "$DIRSTACK"
else
print "still in $PWD."
return 1
fi
}
\end{lstlisting}
La expresión condicional evalúa a verdadero sólo si el argumento \texttt{\$1} es un directorio (\emph{-d}) y el usuario tiene permiso para cambiar a él (\emph{-x}).\footnote{Recuerde que el mismo indicador de permiso que determina el permiso de ejecución en un archivo normal determina el permiso de búsqueda en un directorio. Por eso el operador -x comprueba ambas cosas dependiendo del tipo de fichero.} Note que esta condicional también maneja el caso donde el argumento falta: \texttt{\$dirname} es nulo, y como la cadena nula no es un nombre de directorio válido, la condicional fallará.
La \hyperref[box:5-1]{Tarea 5-1} presenta un ejemplo más completo del uso de los operadores de archivo.
\begin{mybox}[Tarea 5-1]\label{box:5-1}
Escribe un script que imprima esencialmente la misma información que \texttt{ls -l} pero de una forma más fácil de usar.
\end{mybox}
Aunque esta tarea requiere un código relativamente prolijo, es una aplicación directa de muchos de los operadores de ficheros:
\begin{lstlisting}[language=bash]
if [[ ! -e $1 ]]; then
print "file $1 does not exist."
return 1
fi
fi [[ [[ -d $1 ]]; then
print -n "$1 is a directory that you may "
if [[ ! -x $1 ]]; then
print -n "not "
fi
print "search."
elif [[ -f $1 ]]; then
print "$1 is a regular file."
else
print "$1 is a special type of file."
fi
if [[ -O $1 ]]; then
print 'you own the file.'
else
print 'you do not own the file.'
fi
if [[ -r $1 ]]; then
print 'you have read permission on the file.'
fi
if [[ -w $1 ]]; then
print 'you have write permission on the file.'
fi
if [[ -x $1 && ! -d $1 ]]; then
print 'you have execute permission on the file.'
fi
\end{lstlisting}
Llamaremos a este script \emph{fileinfo}. Así es como funciona:
\begin{itemize}
\item{La primera condicional comprueba si el archivo dado como argumento no existe (el signo de exclamación es el operador <<not>>; los espacios que lo rodean son obligatorios). Si el fichero no existe, el script imprime un mensaje de error y sale con el estado de error.}
\item{La segunda condicional comprueba si el fichero es un directorio. Si es así, la primera imprime parte de un mensaje; recuerde que la opción \emph{-n} indica a \emph{print} que no imprima una nueva línea al final. La condicional interna comprueba si no tienes permiso de búsqueda en el directorio. Si no tiene permiso de búsqueda, se añade la palabra <<not>> al mensaje parcial. A continuación, el mensaje se completa con <<search>> y una nueva línea. }
\item{La cláusula \texttt{elif} comprueba si el fichero es un fichero normal; en caso afirmativo, imprime un mensaje.}
\item{La cláusula \emph{else} tiene en cuenta los distintos tipos de ficheros especiales de los sistemas Unix recientes, como sockets, dispositivos, ficheros FIFO, etc. Asumimos que el usuario ocasional no está interesado en sus detalles.}
\item{La siguiente condicional comprueba si eres el propietario del fichero (es decir, si su ID de propietario es el mismo que tu ID de usuario efectivo). Si es así, imprime un mensaje diciendo que usted es el propietario. (Los ID reales y efectivos de Usuario y Grupo se explican en el \hyperref[sec:Chapter10]{Capítulo 10}.) }
\item{Las dos condiciones siguientes comprueban sus permisos de lectura y escritura sobre el fichero.}
\item{La última condicional comprueba si puedes ejecutar el fichero. Comprueba si tiene permiso de ejecución y si el fichero no es un directorio. (Si el archivo fuera un directorio, el permiso de ejecución significaría realmente el permiso de búsqueda de directorio).}
\end{itemize}
Como ejemplo de la salida de \emph{fileinfo}, suponga que hace un \texttt{ls -l} de su directorio actual y contiene estas líneas:
\begin{lstlisting}[language=bash]
-rwxr-xr-x 1 billr other 594 May 28 09:49 bob
-rw-r-r- 1 billr other 42715 Apr 21 23:39 custom.tbl
drwxr-xr-x 2 billr other 64 Jan 12 13:42 exp
-r-r-r- 1 root other 557 Mar 28 12:41 lpst
\end{lstlisting}
\emph{custom.tbl} y \emph{lpst} son archivos de texto normales, \emph{exp} es un directorio y \emph{bob} es un script de shell. Escribir \texttt{fileinfo bob} produce esta salida:
\begin{lstlisting}[language=bash]
bob is a regular file.
you own the file.
you have read permission on the file.
you have write permission on the file.
you have execute permission on the file.
\end{lstlisting}
Al escribir \emph{fileinfo} \texttt{custom.tbl} se obtiene lo siguiente:
\begin{lstlisting}[language=bash]
custom.tbl is a regular file.
you own the file.
you have read permission on the file.
you have write permission on the file.
\end{lstlisting}
Escribiendo \texttt{fileinfo exp} se obtiene esto:
\begin{lstlisting}[language=bash]
exp is a directory that you may search.
you own the file.
you have read permission on the file.
you have write permission on the file.
\end{lstlisting}
Por último, al escribir \texttt{fileinfo lpst} se obtiene lo siguiente:
\begin{lstlisting}[language=bash]
lpst is a regular file.
you do not own the file.
you have read permission on the file.
\end{lstlisting}
\subsubsection{Condicionales aritmétricos}
El shell también proporciona un conjunto de pruebas \emph{aritméticas}. Éstas son diferentes de las comparaciones de \emph{cadenas de caracteres} como < y >, que comparan valores \emph{lexicográficos} de cadenas, no valores numéricos. Por ejemplo, <<6>> es mayor que <<57>> lexicográficamente, al igual que <<p>> es mayor que <<ox>>, pero, por supuesto, ocurre lo contrario cuando se comparan como números.
En la Tabla \ref{Tab:5-3} se resumen los operadores de comparación aritmética. Los programadores de Fortran encontrarán su sintaxis ligeramente familiar.
\begin{table}[h]
\center
\caption{Operadores aritméticos de prueba}
\label{Tab:5-3}
\begin{tabular}{m{2cm}|m{5cm}|m{2cm}|m{5cm}} \hline
\textbf{Prueba} & \textbf{Comparación} & \textbf{Prueba} & \textbf{Comparación} \\ \hline
\texttt{-lt} & Menor que & \texttt{-gt} & Mayor que \\
\texttt{-le} & Menor o igual que & \texttt{-ge} & Mayor o igual que \\
\texttt{-eq} & Igual & \texttt{-ne} & No igual \\
\end{tabular}
\end{table}
Le resultarán muy útiles en el contexto de las variables numéricas que veremos en el próximo capítulo. Son necesarias si desea combinar pruebas numéricas con otros tipos de pruebas dentro de la misma expresión condicional.
Sin embargo, el shell tiene una sintaxis separada para expresiones condicionales que involucran sólo números. (Esta sintaxis se trata en el \hyperref[sec:Chapter6]{Capítulo 6}.) Es considerablemente más eficiente, así como más general, por lo que debería utilizarla con preferencia a los operadores de prueba aritmética enumerados anteriormente.
De hecho, parte de la documentación de \emph{ksh93} considera obsoletas estas condicionales numéricas. Por lo tanto, si necesita combinar [[...]] y pruebas numéricas, hágalo utilizando los operadores \texttt{!}, \texttt{\&\&} y \texttt{||} del intérprete de comandos fuera de [[...]], en lugar de dentro de ellos. De nuevo, cubriremos los condicionales numéricos del shell en el próximo capítulo.
\section{for}
La mejora más obvia que podríamos hacer al script anterior es la capacidad de informar sobre varios archivos en lugar de sólo uno. Pruebas como \emph{-e} y \emph{-d} sólo toman argumentos individuales, por lo que necesitamos una manera de llamar al código una vez para cada archivo dado en la línea de comandos.
La forma de hacer esto -- de hecho, la forma de hacer muchas cosas con el shell de Korn -- es con una construcción de bucle. La más simple y ampliamente aplicable de las construcciones de bucle del shell es el bucle \texttt{for}. Usaremos \texttt{for} para mejorar \emph{fileinfo} pronto.
El bucle for le permite repetir una sección de código un número fijo de veces. Durante cada vez que se repite el código (conocido como iteración), una variable especial llamada \emph{variable de bucle} se establece a un valor diferente; de esta forma cada iteración puede hacer algo ligeramente diferente.
El bucle \emph{fo}r es algo, pero no totalmente, similar a sus homólogos en lenguajes convencionales como C y Pascal. La principal diferencia es que el bucle \emph{for} del shell no le permite especificar un número de veces para iterar o un rango de valores sobre los que iterar; en su lugar, sólo le permite dar una lista fija de valores. En otras palabras, con el bucle \emph{for} normal, no puedes hacer nada como este código tipo Pascal, que ejecuta las sentencias 10 veces:
\begin{lstlisting}[language=bash]
for x := 1 to 10 do
begin
statements ...
end
\end{lstlisting}
(Para ello necesitas el bucle aritmético \emph{for}, que veremos en el \hyperref[sec:Chapter6]{Capítulo 6}).
Sin embargo, el bucle \emph{for} es ideal para trabajar con argumentos en la línea de órdenes y con conjuntos de ficheros (por ejemplo, todos los ficheros de un directorio determinado). Veremos un ejemplo de cada uno de ellos. Pero primero, aquí está la sintaxis para la construcción \emph{for}:
\begin{lstlisting}[language=bash]
for name [in list]
do
statements that can use $name ...
done
\end{lstlisting}
\emph{list} es una lista de nombres. (Si se omite en \emph{list}, la lista por defecto es "\$@", es decir, la lista entrecomillada de argumentos de la línea de comandos, pero siempre proporcionamos en \emph{list} en aras de la claridad). En nuestras soluciones a la siguiente tarea, mostramos dos formas sencillas de especificar listas.
En \emph{ksh93} hay una interesante interacción entre el bucle \emph{for} y las variables \emph{nameref} (ver \hyperref[sec:Chapter4]{Capítulo 4}). Si la variable de control es un \emph{nameref}, entonces cada elemento de la lista de nombres puede ser una variable shell diferente, y el shell asigna el \emph{nameref} a cada variable sucesivamente. Por ejemplo:
\begin{lstlisting}[language=bash]
$ first="I am first" # Initialize test variables
$ second="I am in the middle"
$ third="I am last"
$ nameref refvar=first # Create nameref
$ for refvar in first second third ; do # Loop over variables
> print "refvar -> ${!refvar}, value: $refvar" # Print referenced var, value
> done
refvar -> first, value: I am first
refvar -> second, value: I am in the middle
refvar -> third, value: I am last
$ print ${!refvar}, $refvar # Show final state
third, I am last
\end{lstlisting}
El bucle \emph{for} es fundamental para resolver la \hyperref[box:5-2]{Tarea 5-2}.
\begin{mybox}[Tarea 5-2]\label{box:5-2}
Usted trabaja en un entorno con varios ordenadores en una red local. Escribe un script de shell que te diga quién ha iniciado sesión en cada máquina de la red.
\end{mybox}
El comando \emph{finger(1)} se puede utilizar (entre otras cosas) para encontrar los nombres de los usuarios que han iniciado sesión en un sistema remoto; el comando \texttt{finger \emph{@systemname}} hace esto. Su salida depende de la versión de Unix, pero se parece a esto:
\begin{lstlisting}[language=bash]
[motet.early.com]
Trying 127.146.63.17...
-User- -Full name- -What- Idle TTY -Console Location-
hildy Hildegard von Bingen ksh 2d5h p1 jem.cal (Telnet)
mikes Michael Schultheiss csh 1:21 r4 ncd2.cal (X display 0)
orlando Orlando di Lasso csh 28 r7 maccala (Telnet)
marin Marin Marais mush 1:02 pb mussell.cal (Telnet)
johnd John Dowland tcsh 17 p0 nugget.west.nobis. (X Window)
\end{lstlisting}
En esta salida, \emph{motet.early.com} es el nombre de red completo de la máquina remota.
Suponga que los sistemas de su red se llaman \emph{fred, bob, dave} y \emph{pete}. Entonces el siguiente código haría el truco:
\begin{lstlisting}[language=bash]
for sys in fred bob dave pete
do
finger @$sys
print
done
\end{lstlisting}
Esto funciona independientemente del sistema en el que esté conectado. Imprime una salida para cada máquina similar a la anterior, con líneas en blanco entre ellas.
Una solución ligeramente mejor sería almacenar los nombres de los sistemas en una variable de entorno. De esta forma, si se añaden sistemas a su red y necesita una lista de sus nombres en más de un script, sólo tendrá que cambiarlos en un lugar. Si el valor de una variable son varias palabras separadas por espacios (o TABS), \emph{for} lo tratará como una lista de palabras.
Aquí está la solución mejorada. Primero, ponga líneas en su archivo \emph{.profile} o de entorno que definan la variable \texttt{SYSNAMES} y conviértala en una variable de entorno:
\begin{lstlisting}[language=bash]
SYSNAMES="fred bob dave pete"
export SYSNAMES
\end{lstlisting}
Entonces, el script puede tener este aspecto:
\begin{lstlisting}[language=bash]
for sys in $SYSNAMES
do
finger @$sys
print
done
\end{lstlisting}
Lo anterior ilustra un uso simple de \emph{for}, pero es mucho más común usar \emph{for} para iterar a través de una lista de argumentos de línea de comandos. Para mostrar esto, podemos mejorar el script \emph{fileinfo} anterior para que acepte múltiples argumentos. Primero, escribimos un poco de código <<envolvente>> que hace la iteración:
\begin{lstlisting}[language=bash]
for filename in "$@" ; do
finfo $filename
print
done
\end{lstlisting}
A continuación, convertimos el script original en una función llamada \emph{finfo}\footnote{Una función puede tener el mismo nombre que un script; sin embargo, esto no es una buena práctica de programación.}:
\begin{lstlisting}[language=bash]
function finfo {
if [[ ! -e $1 ]]; then
print "file $1 does not exist."
return 1
fi
...
}
\end{lstlisting}
El script completo consiste en el código del bucle \emph{for} y la función anterior. Debido a que la función debe ser definida antes de que pueda ser usada, la definición de la función debe ir primero, o bien debe estar en un directorio listado tanto en \texttt{PATH} como en \texttt{FPATH}.
El script \emph{fileinfo} funciona de la siguiente manera: en la sentencia \emph{for}, <<\$@>> es una lista de todos los parámetros posicionales. Para cada argumento, el cuerpo del bucle se ejecuta con \texttt{filename} fijado a ese argumento. En otras palabras, la función \emph{fileinfo} es llamada una vez por cada valor de \texttt{\$filename} como su primer argumento (\$1). La llamada a \emph{print} después de la llamada a \emph{fileinfo} simplemente imprime una línea en blanco entre los conjuntos de información sobre cada fichero.
Dado un directorio con los mismos ficheros que en el ejemplo anterior, al teclear \texttt{fileinfo *} se obtendría la siguiente salida:
\begin{lstlisting}[language=bash]
bob is a regular file. you own the file.
you have read permission on the file.
you have write permission on the file.
you have execute permission on the file.
custom.tbl is a regular file.
you own the file.
you have read permission on the file.
you have write permission on the file.
exp is a directory that you may search.
you own the file.
you have read permission on the file.
you have write permission on the file.
lpst is a regular file.
you do not own the file.
you have read permission on the file.
\end{lstlisting}
La \hyperref[box:5-3]{Tarea 5-3} es una tarea de programación que explota el otro uso principal de \emph{for}.
\begin{mybox}[Tarea 5-3]\label{box:5-3}
Su sistema Unix tiene la capacidad de transferir archivos desde un sistema MS-DOS, pero deja intactos los nombres de archivo MS-DOS. Escribe una secuencia de comandos que traduzca los nombres de archivo de un directorio determinado del formato MS-DOS a un formato más compatible con Unix.
\end{mybox}
Los nombres de archivo en el antiguo sistema MS-DOS de Microsoft tienen el formato \emph{FILENAME.EXT. FILENAME} puede tener hasta ocho caracteres; \emph{EXT} es una extensión que puede tener hasta tres caracteres. Las letras son todas mayúsculas. Queremos hacer lo siguiente:
\begin{enumerate}
\item{Traducir letras de mayúsculas a minúsculas.}
\item{Si la extensión es nula, elimine el punto.}
\end{enumerate}
La primera herramienta que necesitaremos para este trabajo es la utilidad Unix \emph{tr(1)}, que traduce caracteres de uno en uno
\footnote{Como veremos en el \hyperref[sec:Chapter6]{Capítulo 6}, es posible hacer la traducción de mayúsculas y minúsculas dentro del shell, sin usar un programa externo. Sin embargo, ignoraremos ese hecho por ahora.}. Dados los argumentos \emph{charset1} y \emph{charset2}, traduce los caracteres de la entrada estándar que son miembros de \emph{charset1} a los caracteres correspondientes de \emph{charset2}. Los dos conjuntos son rangos de caracteres encerrados entre corchetes (\texttt{[...]} en forma de expresión regular estándar a la manera de \emph{grep, awk, ed,} etc.). Más concretamente, \texttt{tr [A-Z] [a-z]} toma su entrada estándar, convierte las mayúsculas en minúsculas y escribe el texto convertido en la salida estándar
\footnote{Los sistemas modernos compatibles con POSIX soportan locales, que son formas de utilizar conjuntos de caracteres no ASCII de forma portable. En un sistema así, la invocación correcta de \emph{tr} es \texttt{tr [:upper:] [:lower:]}. Sin embargo, la mayoría de los usuarios veteranos de Unix tienden a olvidar esto.}.
Esto se encarga del primer paso en el proceso de traducción. Podemos usar un operador de cadena del shell de Korn para manejar el segundo. Aquí está el código para un script que llamaremos \emph{dosmv}:
\begin{lstlisting}[language=bash]
for filename in ${1:+$1/}* ; do
newfilename=$(print $filename | tr '[A-Z]' '[a-z]')
newfilename=${newfilename%.}
print "$filename -> $newfilename"
mv $filename $newfilename
done
\end{lstlisting}
El * en la construcción \emph{for} \emph{no} es lo mismo que \texttt{\$*}. Es un comodín, es decir, todos los archivos de un directorio.
Este script acepta un nombre de directorio como argumento, siendo por defecto el directorio actual. La expresión \texttt{\$\{1:+\$1/\}} se evalúa como el argumento (\$1) con una barra añadida si se proporciona el argumento, o la cadena nula si no se proporciona. Por lo tanto, la expresión \texttt{\$\{1:+\$1/\}*} completa equivale a todos los archivos del directorio indicado, o a todos los archivos del directorio actual si no se proporciona ningún argumento.
Por lo tanto, \texttt{filename} toma el valor de cada \emph{filename} de la lista. \texttt{filename} se convierte en \texttt{newfilename} en dos pasos. (Podríamos haberlo hecho en uno, pero la legibilidad se habría resentido.) El primer paso utiliza \emph{tr} en un canal dentro de una construcción de sustitución de comandos. Nuestro viejo amigo \emph{print} convierte el valor de \texttt{filename} en la entrada estándar de \emph{tr}. La salida de \emph{tr} se convierte en el valor de la expresión de sustitución de comandos, que se asigna a \texttt{newfilename}. Así, si \texttt{\$filename} fuera \texttt{DOSFILE.TXT}, \texttt{filename} se convertiría en \texttt{dosfile.txt}.
El segundo paso utiliza uno de los operadores de coincidencia de patrones del intérprete de comandos, el que elimina la coincidencia más corta que encuentra al final de la cadena. El patrón aquí es \texttt{.}, que significa un punto al final de la cadena
\footnote{Los expertos en expresiones regulares de Unix deben recordar que se trata de la sintaxis de comodines del shell, en la que los puntos no son operadores y, por lo tanto, no es necesario ocultarlos.}. Esto significa que la expresión \texttt{\$\{newfilename\%.\}} eliminará un punto de \texttt{\$newfilename} sólo si está al final de la cadena; de lo contrario, la expresión dejará \texttt{\$newfilename} intacto. Por ejemplo, si \texttt{\$newfilename} es \texttt{dosfile.txt}, no se modificará, pero si es \texttt{dosfile.}, la expresión lo cambiará a \texttt{dosfile} sin el punto final. En cualquier caso, el nuevo valor se asigna de nuevo a \texttt{newfilename}.
La última sentencia en el cuerpo del bucle \emph{for} realiza el renombrado de ficheros con el comando estándar de Unix \emph{mv(1)}. Antes de eso, un comando \emph{print} simplemente informa al usuario de lo que está ocurriendo.
Hay un pequeño problema con esta solución: si hay ficheros en el directorio dado que no son ficheros MS-DOS (en particular, si hay ficheros cuyos nombres no contienen letras mayúsculas o no contienen un punto), entonces la conversión no hará nada con esos nombres de fichero y \emph{mv} será llamado con dos argumentos idénticos. \emph{mv} se quejará con el mensaje: \texttt{mv: \emph{filename} and \emph{filename} are identical}. La solución es muy sencilla: compruebe si los nombres de archivo son idénticos:
\begin{lstlisting}[language=bash]
for filename in ${1:+$1/}* ; do
newfilename=$(print $filename | tr '[A-Z]' '[a-z]')
newfilename=${newfilename%.}
# subtlety: quote value of $newfilename to do string comparison,
# not regular expression match
if [[ $filename != "$newfilename" ]]; then
print "$filename -> $newfilename"
mv $filename $newfilename
fi
done
\end{lstlisting}
Si está familiarizado con un sistema operativo distinto de MS-DOS y Unix, puede poner a prueba su destreza en la escritura de scripts en este punto escribiendo un script que traduzca los nombres de archivo del formato de ese sistema al formato Unix. Utilice el script anterior como guía.
En concreto, si conoce el sistema operativo OpenVMS de Compaq (nee DEC), aquí tiene un reto de programación:
\begin{enumerate}
\item{Escriba un script llamado \emph{vmsmv} que sea similar a dosmv pero que funcione con nombres de archivo OpenVMS en lugar de con nombres de archivo MS-DOS. Recuerde que los nombres de archivo OpenVMS terminan con punto y coma y números de versión.}
\item{Modifica tu script para que si hay varias versiones del mismo fichero, renombre sólo la última versión (con el número de versión más alto).}
\item{Modifíquelo aún más para que su script borre las versiones antiguas de los archivos. }
\end{enumerate}
La primera de ellas es una modificación relativamente sencilla de dosmv. La número 2 es difícil; aquí tienes una pista de estrategia:
\begin{itemize}
\item{Desarrolle una expresión regular que coincida con los nombres de archivo OpenVMS (de todos modos, la necesitará para el número 1).}
\item{Obtenga una lista de los nombres base (sin los números de versión) de los archivos del directorio indicado pasando \emph{ls} por \emph{grep} (con la expresión regular anterior), \emph{cut} y \emph{sort -u}. Utilice \emph{cut} con punto y coma como <<separador de campos>>. Utilice cut con un punto y coma como "separador de campos"; asegúrese de entrecomillar el punto y coma para que el shell no lo trate como un separador de sentencias. \emph{sort -u} elimina los duplicados tras la ordenación. Utilice la sustitución de comandos para guardar la lista resultante en una variable.}
\item{Utiliza un bucle \emph{for} en la lista de nombres base. Para cada nombre, obtenga el número de versión más alto del archivo (sólo el número, no el nombre completo). Haga esto con otra tubería: pipe \emph{ls} a través de \emph{cut}, \emph{sort -n}, y \emph{tail -1}. \emph{sort -n} ordena en orden numérico (no lexicográfico); \emph{tail -N} muestra las últimas \emph{N} líneas de su entrada. De nuevo, utilice la sustitución de comandos para capturar la salida de este proceso en una variable. }
\item{Añada el número de versión más alto al nombre base; éste es el archivo que hay que renombrar en formato Unix. }
\end{itemize}
Una vez que hayas completado el número 2, puedes hacer el número 3 añadiendo una sola línea de código a tu script; a ver si averiguas cómo.
Finalmente, \emph{ksh93} proporciona el bucle \emph{for} aritmético, que es mucho más cercano en sintaxis y estilo al bucle \emph{for} de C. Lo presentamos en el próximo capítulo, después de discutir las capacidades aritméticas generales del shell. Lo presentamos en el próximo capítulo, después de discutir las capacidades aritméticas generales del shell.
\section{case}
La siguiente construcción de control de flujo a cubrir es \texttt{case}. Mientras que la sentencia \texttt{case} en Pascal y la sentencia \texttt{witch} similar en C pueden usarse para probar valores simples como enteros y caracteres, la construcción \texttt{case} del shell de Korn le permite probar cadenas contra patrones que pueden contener caracteres comodín. Al igual que sus equivalentes en lenguajes convencionales, \texttt{case} permite expresar una serie de sentencias del tipo \texttt{if-then-else} de forma concisa.
La sintaxis de \texttt{case} es la siguiente
\begin{lstlisting}[language=bash]
case expression in
pattern1 )
statements ;;
pattern2 )
statements ;;
...
esac
\end{lstlisting}
Cualquiera de los \emph{patrones} puede ser en realidad varios patrones separados por caracteres <<barra OR>> (\texttt{|}, que es lo mismo que el símbolo de la tubería, pero en este contexto significa <<o>>). Si la \emph{expresión} coincide con uno de los patrones, se ejecutan las sentencias correspondientes. Si hay varios patrones separados por barras OR, la expresión puede coincidir con cualquiera de ellos para que se ejecuten las sentencias asociadas. Los patrones se comprueban en orden hasta que se encuentra una coincidencia; si no se encuentra ninguna, no ocurre nada.
Esta sintaxis bastante desgarbada debería quedar más clara con un ejemplo. Una opción obvia es revisar nuestra solución a la \hyperref[box:4-2]{Tarea 4-2}, el front-end para el compilador C. Anteriormente en este capítulo, escribimos algo de código que procesaba archivos de entrada según sus sufijos (.c, .s, o .o para C, ensamblador, o código objeto, respectivamente).
Podemos mejorar esta solución de dos maneras. En primer lugar, podemos utilizar \emph{for} para permitir que se procesen varios archivos a la vez; en segundo lugar, podemos utilizar case para agilizar el código:
\begin{lstlisting}[language=bash]
for filename in "$@"; do
case $filename in
*.c )
objname=${filename%.c}.o
ccom "$filename" "$objname" ;;
*.s )
objname=${filename%.s}.o
as "$filename" "$objname" ;; *.o ) ;;
* )
print "error: $filename is not a source or object file."
exit 1 ;;
esac
done
\end{lstlisting}
La construcción \texttt{case} en este código maneja cuatro casos. Los dos primeros son similares a los casos \texttt{if} y \texttt{elif} anteriores en este capítulo; llaman al compilador o al ensamblador si el nombre de archivo termina en \texttt{.c} o \texttt{.s}, respectivamente.
Después, el código es un poco diferente. Recordemos que si el nombre del fichero termina en \texttt{.o} no hay que hacer nada (suponiendo que los ficheros relevantes se enlazarán más tarde). Manejamos esto con el caso \texttt{*.o )}, que no tiene declaraciones. No hay nada malo con un <<caso>> para el cual el script no hace nada.
Si el nombre del archivo no termina en \texttt{.o}, se produce un error. Esto se trata en el último caso, que es \texttt{*}. Esto captura cualquier cosa que no coincida con los otros casos. (De hecho, un caso \texttt{*} es análogo a un \emph{case} por defecto en C y un caso de lo contrario en algunos lenguajes derivados de Pascal).
El bucle \emph{for} que lo rodea procesa correctamente todos los argumentos de la línea de comandos. Esto nos lleva a otra mejora: ahora que sabemos cómo procesar todos los argumentos, deberíamos ser capaces de escribir el código que pasa todos los ficheros objeto al enlazador (el programa
\emph{ld}) al final. Podemos hacerlo construyendo una cadena de nombres de ficheros objeto, separados por espacios, y pasársela al enlazador cuando hayamos procesado todos los ficheros de entrada. Inicializamos la cadena a \emph{null} y añadimos un nombre de fichero objeto cada vez que se crea uno, es decir, durante cada iteración del bucle \emph{for}. El código para esto es simple, requiriendo sólo adiciones menores:
\begin{lstlisting}[language=bash]
objfiles=""
for filename in "$@"; do
case $filename in
*.c )
objname=${filename%.c}.o
ccom "$filename" "$objname" ;;
*.s )
objname=${filename%.s}.o
as "$filename" "$objname" ;;
*.o )
objname=$filename ;;
* )
print "error: $filename is not a source or object file."
exit 1 ;;
esac
objfiles+=" $objname"
done
ld $objfiles
\end{lstlisting}
La primera línea en esta versión del script inicializa la variable \texttt{objfiles} a \emph{null}
\footnote{Esto no es estrictamente necesario, porque se asume que todas las variables son nulas si no se inicializan explícitamente (a menos que la opción \emph{nounset} esté activada). Sólo hace que el código sea más fácil de leer.}.
Añadimos una línea de código en el caso \texttt{*.o} para establecer \texttt{objname} igual a \texttt{\$filename}, porque ya sabemos que es un archivo objeto. Así, el valor de \texttt{objname} se establece en todos los casos -- excepto en el caso de error, en el que la rutina imprime un mensaje y se retira.
La última línea de código en el cuerpo del bucle \emph{for} añade un espacio y el último \texttt{\$objname} a \texttt{objfiles}. Llamando a este script con los mismos argumentos que en la Figura \ref{fig:05} resultaría en \texttt{\$objfiles} igual a \texttt{" a.o b.o c.o d.o"} cuando el bucle for termina (el espacio inicial no importa). Esta lista de nombres de archivos de objetos se le da a \emph{ld} como un único argumento, pero el shell lo divide en múltiples nombres de archivo correctamente.
La \hyperref[box:5-4]{Tarea 5-4} es una nueva tarea cuya solución inicial utiliza case.
\begin{mybox}[Tarea 5-4]\label{box:5-4}
Eres un administrador de sistemas,\footnote{Nuestras condolencias.} y necesitas configurar el sistema para que las variables de entorno TERM de los usuarios reflejen correctamente en qué tipo de terminal están. Escribe algún código que haga esto.
\end{mybox}
El código para la solución de esta tarea debe ir en el archivo \emph{/etc/profile}, que es el archivo maestro de inicio que se ejecuta para cada usuario antes de su \emph{.profile}.
Por el momento, asumimos que tienes una configuración tradicional de tipo mainframe, en la que los terminales están cableados al ordenador. Esto significa que puedes determinar qué terminal (física) está siendo utilizada por la línea (o \emph{tty}) en la que se encuentra. Normalmente tiene un nombre como \emph{/dev/ttyNN}, donde \emph{NN} es el número de línea. Puedes encontrar tu tty con el comando \emph{tty(1)}, que lo imprime en la salida estándar.
Supongamos que tu sistema tiene diez líneas más una línea de consola de sistema (\emph{/dev/console}), con los siguientes terminales:
\begin{itemize}
\item{Las líneas tty01, tty03 y tty04 son Givalt GL35a (nombre terminfo <<gl35a>>).}
\item{La línea tty07 es una Tsoris T-2000 (<<t2000>>). }
\item{La línea tty08 y la consola son Shande 531s (<<s531>>).}
\item{El resto son Vey VT99 (<<vt99>>).}
\end{itemize}
Aquí está el código que hace el trabajo:
\begin{lstlisting}[language=bash]
case $(tty) in
/dev/tty0[134] ) TERM=gl35a ;;
/dev/tty07 ) TERM=t2000 ;;
/dev/tty08 | /dev/console ) TERM=s531 ;;
* ) TERM=vt99 ;;
esac
\end{lstlisting}
El valor que case comprueba es el resultado de la sustitución del comando. Por lo demás, la única novedad de este código es la barra OR después de /dev/tty08. Esto significa que \emph{/dev/tty08} y \emph{/dev/console} son patrones alternativos para el caso que establece TERM en <<s531>>.
Tenga en cuenta que no es posible colocar patrones alternativos en líneas separadas a menos que utilice caracteres de continuación de barra invertida al final de todas las líneas excepto de la última. En otras palabras, la línea
\begin{lstlisting}[language=bash]
/dev/tty08 | /dev/console ) TERM=s531 ;;
\end{lstlisting}
podría cambiarse por el ligeramente más legible:
\begin{lstlisting}[language=bash]
/dev/tty08 | \
/dev/console ) TERM=s531 ;;
\end{lstlisting}
La barra invertida debe estar al final de la línea. Si la omite, o si hay caracteres (incluso espacios) después de ella, el shell se queja con un mensaje de error de sintaxis.
En realidad, este problema se resuelve mejor utilizando un archivo que contenga una tabla de líneas y tipos de terminales. Veremos cómo hacerlo así en el \hyperref[sec:Chapter7]{Capítulo 7}.
Cuando aparecía un caso dentro de la construcción de sustitución de comandos \$(...), \emph{ksh88} tenía un problema: la ) que delimita cada patrón del código a ejecutar terminaba el \$(...). Para evitarlo, era necesario poner una ( delante del patrón:
\begin{lstlisting}[language=bash]
result=$(case $input in
( dave ) print Dave! ;; # Open paren required in ksh88
( bob ) print Bob! ;;
esac)
\end{lstlisting}
\emph{ksh93} todavía acepta esta sintaxis, pero ya no la requiere.
\subsection{Fusionando <<cases>>}
A veces, al escribir una construcción de tipo case, hay instancias en las que un caso es un subconjunto de lo que debería hacerse para otro. El lenguaje C maneja esto permitiendo que un caso en un switch <<continue>> en el código de otro. Un hecho poco conocido es que el shell de Korn (pero no el shell de Bourne) tiene una funcionalidad similar.
Por ejemplo, supongamos que nuestro compilador de C sólo genera código ensamblador, y que depende de nuestro script front-end convertir el código ensamblador en código objeto. En este caso, queremos pasar del caso \texttt{*.c} al caso \texttt{*.s}. Esto se hace usando \texttt{;\&} para terminar el cuerpo del caso que hace la caída:
\begin{lstlisting}[language=bash]
objfiles=""
for filename in "$@"; do
case $filename in
*.c )
asmname=${filename%.c}.s
ccom "$filename" "$asmname"
filename=$asmname ;& # Continuación de ejecución
*.s )
objname=${filename%.s}.o
as "$filename" "$objname" ;;
*.o )
objname=$filename ;;
* )
print "error: $filename is not a source or object file."
exit 1 ;;
esac
objfiles+=" $objname"
done
ld $objfiles
\end{lstlisting}
Antes de caer, el caso \texttt{*c} tiene que restablecer el valor de \texttt{filename} para que el caso \texttt{*.s} funcione correctamente. Suele ser una muy buena idea añadir un comentario indicando que la <<continuación de ejecución>> es intencional, aunque es más obvio en shell que en C. Volveremos a este ejemplo una vez más en el \hyperref[sec:Chapter6]{Capítulo 6} cuando discutamos cómo manejar las opciones \emph{dash} en la línea de comandos.
\section{select}
Casi todas las construcciones de control de flujo que hemos visto hasta ahora también están disponibles en el shell de Bourne, y el shell C tiene equivalentes con diferente sintaxis. Nuestra siguiente construcción, \texttt{select}, es exclusiva del shell de Korn; además, no tiene análogos en los lenguajes de programación convencionales.
\texttt{select} le permite generar menús sencillos fácilmente. Tiene una sintaxis concisa, pero hace bastante trabajo. La sintaxis es:
\begin{lstlisting}[language=bash]
select name [in list]
do
declaraciones que puede usar $name ...
done
\end{lstlisting}
Es la misma sintaxis que el bucle \emph{for} normal, excepto por la palabra clave \texttt{select}. Y al igual que \texttt{for}, puede omitir en \texttt{list}, y por defecto será <<\$@>>, es decir, la lista de argumentos de línea de comandos entrecomillados.
Esto es lo que hace \texttt{select}:
\begin{itemize}
\item{Genera un menú de cada elemento de la lista, formateado con números para cada opción.}
\item{Solicita al usuario un número (con el valor de PS3)}
\item{Almacena la opción seleccionada en el nombre de la variable y el número seleccionado en la variable incorporada \texttt{REPLY}}
\item{Ejecuta las sentencias del cuerpo}
\item{Repite el proceso para siempre (pero vea más abajo cómo salir)}
\end{itemize}
Una vez más, un ejemplo ayudará a aclarar este proceso. Supongamos que necesita escribir el código para la \hyperref[box:5-4]{Tarea 5-4}, pero su vida no es tan simple. No tiene terminales conectadas a su ordenador; en su lugar, sus usuarios se comunican a través de un servidor de terminales, o se conectan remotamente, vía \emph{telnet} o \emph{ssh}. Esto significa, entre otras cosas, que el número de tty no determina el tipo de terminal.
Por lo tanto, no tiene otra opción que preguntar al usuario por su tipo de terminal en el momento del login. Para hacer esto, puedes poner el siguiente código en \emph{/etc/profile} (asume que tienes la misma elección de tipos de terminal):
\begin{lstlisting}[language=bash]
PS3='terminal? '
select term in gl35a t2000 s531 vt99; do
if [[ -n $term ]]; then
TERM=$term
print TERM is $TERM
export TERM
break
else
print 'invalid.'
fi
done
\end{lstlisting}
Cuando ejecutes este código, verás este menú:
\begin{lstlisting}[language=bash]
1) gl35a
2) t2000
3) s531
4) vt99
terminal?
\end{lstlisting}
La variable de shell incorporada \texttt{PS3} contiene la cadena de consulta que utiliza \texttt{select}; su valor por defecto es el poco útil <<\#? >>. Así que la primera línea del código anterior la establece en un valor más relevante
\footnote{En cuanto a \texttt{PS1}, \emph{ksh} realiza la sustitución de parámetros, comandos y aritmética en el valor antes de imprimirlo.}.
La sentencia \texttt{select} construye el menú a partir de la lista de opciones. Si el usuario introduce un número válido (de 1 a 4), la variable \emph{term} se establece en el valor correspondiente; de lo contrario, es nula. (Si el usuario sólo pulsa INTRO, el shell vuelve a imprimir el menú).
El código en el cuerpo del bucle comprueba si \texttt{term} no es nulo. Si lo es, asigna \texttt{\$term} a la variable de entorno \texttt{TERM}, exporta \texttt{TERM}, e imprime un mensaje de confirmación; entonces la sentencia \emph{break} sale del bucle \texttt{select}. Si \texttt{term} es nulo, el código imprime un mensaje de error y repite el prompt (pero no el menú).
La sentencia \texttt{break} es la forma habitual de salir de un bucle \texttt{select}. En realidad (al igual que su análogo en C), se puede utilizar para salir de cualquier estructura de control circundante que hayamos visto hasta ahora (excepto \texttt{case}, donde el doble punto y coma actúa como \emph{break}), así como de los \texttt{while} y \texttt{until} que veremos próximamente. No hemos introducido \emph{break} hasta ahora porque algunas personas consideran que es un mal estilo de codificación utilizarlo para salir de un bucle. Sin embargo, es necesario para salir de \texttt{select} cuando el usuario hace una elección válida. \footnote{Un usuario también puede teclear CTRL-D -- para fin de entrada -- para salir de un bucle de \texttt{selección}. Esto le da al usuario una forma uniforme de salir, pero no ayuda mucho al programador del shell.}
Perfeccionemos nuestra solución haciendo que el menú sea más fácil de usar, para que el usuario no tenga que conocer el nombre \emph{terminfo} de su terminal. Para ello, utilizaremos cadenas de caracteres entre comillas como elementos del menú y, a continuación, utilizaremos mayúsculas y minúsculas para determinar el nombre \emph{terminfo}:
\begin{lstlisting}[language=bash]
print 'Select your terminal type:'
PS3='terminal? '
select term in \
'Givalt GL35a' \
'Tsoris T-2000' \
'Shande 531' \
'Vey VT99'
do
case $REPLY in
1 ) TERM=gl35a ;;
2 ) TERM=t2000 ;;
3 ) TERM=s531 ;;
4 ) TERM=vt99 ;;
* ) print 'invalid.' ;;
esac
if [[ -n $term ]]; then
print TERM is $TERM
export TERM
break
fi
done
\end{lstlisting}
Este código se asemeja un poco más a una rutina de menú en un programa convencional, aunque \texttt{select} sigue proporcionando la atajo de convertir las opciones del menú en números. Enumeramos cada una de las opciones del menú en líneas separadas por razones de legibilidad, pero una vez más necesitamos caracteres de continuación para evitar que el shell se queje de la sintaxis.
Esto es lo que verá el usuario cuando se ejecute este código:
\begin{lstlisting}[language=bash]
Select your terminal type:
1) Givalt GL35a
2) Tsoris T-2000
3) Shande 531
4) Vey VT99 terminal?
\end{lstlisting}
Este es un poco más informativo que la salida del código anterior.
Cuando se ingresa al cuerpo del bucle \texttt{select}, \texttt{\$term} es igual a una de las cuatro cadenas (o es nulo si el usuario hizo una elección no válida), mientras que la variable integrada \texttt{REPLY} contiene el número que el usuario seleccionó. Necesitamos una declaración case para asignar el valor correcto a \texttt{TERM}; utilizamos el valor de \texttt{REPLY} como selector de caso.
Una vez que se completa la declaración \texttt{case}, la instrucción \texttt{if} verifica si se hizo una elección válida, como en la solución anterior. Si la elección fue válida, \texttt{TERM} ya ha sido asignado, por lo que el código simplemente imprime un mensaje de confirmación, exporta \texttt{TERM} y sale del bucle \texttt{select}. Si no fue válido, el bucle \texttt{select} repite el mensaje y vuelve a pasar por el proceso.
Dentro de un bucle \texttt{select}, si \texttt{REPLY} se establece en una cadena nula, el shell vuelve a imprimir el menú. Esto sucede, como se mencionó, cuando el usuario presiona ENTER. Pero también puedes establecer explícitamente \texttt{REPLY} en la cadena nula para forzar al shell a volver a imprimir el menú.
La variable \texttt{TMOUT} (tiempo de espera) puede afectar la instrucción \texttt{select}. Antes del bucle \texttt{select}, configúrala en algún número de segundos \emph{N} y, si no se ingresa nada en ese lapso de tiempo, el \texttt{select} se cerrará. Como se explicará más adelante, \texttt{TMOUT} también afecta al comando \texttt{read} y al mecanismo interactivo de solicitud de entrada del shell.
\section{while y until}
Las dos estructuras de control de flujo restantes que proporciona el shell de Korn son \texttt{while} y \texttt{until}. Estas son similares; ambas permiten que se ejecute repetidamente una sección de código mientras (o hasta que) se cumpla cierta condición. También se asemejan a construcciones análogas en Pascal (\texttt{while}/\texttt{do} y \texttt{repeat}/\texttt{until}) y en C (\texttt{while} y \texttt{do}/\texttt{until}).
\texttt{while} y \texttt{until} son más útiles cuando se combinan con características que veremos en el próximo capítulo, como aritmética entera, entrada/salida de variables y procesamiento de línea de comandos. Aún así, podemos mostrar un ejemplo útil incluso con las herramientas que hemos cubierto hasta ahora.
La sintaxis para \texttt{while} es:
\begin{lstlisting}[language=bash]
while
condición
do
declaraciones...
done
\end{lstlisting}
Para \texttt{until}, simplemente sustituye \texttt{until} por \texttt{while} en el ejemplo anterior. Al igual que con \texttt{if}, la \emph{condición} es realmente una lista de \emph{declaraciones} que se ejecutan; el estado de salida de la última se utiliza como el valor de la condición. Puedes usar una condicional con \texttt{[[} y \texttt{]]} aquí, al igual que con \texttt{if}.
\textbf{NOTA:} La \emph{única} diferencia entre \texttt{while} y \texttt{until} es la forma en que se maneja la condición. En \texttt{while}, el bucle se ejecuta mientras la condición sea verdadera; en \texttt{until}, se ejecuta mientras la condición sea falsa. Hasta aquí, todo es familiar. \textbf{PERO:} la condición de \texttt{until} se verifica en la parte \emph{superior} del bucle, no en la parte \emph{inferior} como en construcciones análogas en C y Pascal.
El resultado es que puedes convertir cualquier \texttt{until} en un \texttt{while} simplemente negando la condición. El único lugar donde \texttt{until} podría ser mejor es algo como esto:
\begin{lstlisting}[language=bash]
until
comando
; do
declaraciones...
done
\end{lstlisting}
El significado de esto es esencialmente <<Realizar \emph{declaraciones} hasta que el \emph{comando} se ejecute correctamente>>. En nuestra opinión, esta no es una contingencia probable. Por lo tanto, usaremos \texttt{while} en el resto de este libro.
La \hyperref[box:5-5]{Tarea 5-5} es un buen candidato para \texttt{while}.
\begin{mybox}[Tarea 5-5]\label{box:5-5}
Implementa una versión simplificada del comando \texttt{whence} incorporado en el shell.
\end{mybox}
Con <<simplificada>>, nos referimos a que implementaremos solo la parte que verifica todos los directorios en tu \textbf{PATH} para el comando que proporcionas como argumento (no implementaremos la verificación de alias, comandos incorporados, etc.).
Podemos hacer esto seleccionando los directorios en \textbf{PATH} uno por uno, utilizando uno de los operadores de coincidencia de patrones del shell, y viendo si hay un archivo con el nombre dado en el directorio en el que tienes permisos para ejecutarlo. Aquí está el código:
\begin{lstlisting}[language=bash]
path=$PATH
dir=${path%%:*}
while [[ -n $path ]]; do
if [[ -x $dir/$1 &&! -d $dir/$1 ]]; then
print "$dir/$1"
return
fi
path=${path#*:}
dir=${path%%:*}
done
return 1
\end{lstlisting}
La primera línea de este código guarda \texttt{\$PATH} en \textbf{path}, nuestra propia copia temporal. Añadimos dos puntos al final para que cada directorio en \textbf{\$path} termine en dos puntos (en \texttt{\$PATH}, los dos puntos se usan solo \emph{entre} directorios); el código subsiguiente depende de que esto sea así.
La siguiente línea selecciona el primer directorio de \textbf{\$path} utilizando el operador que elimina la coincidencia más larga con el patrón dado. En este caso, eliminamos la coincidencia más larga con el patrón \texttt{:*}, es decir, dos puntos seguidos de cualquier cosa. Esto nos da el primer directorio en \textbf{\$path}, que almacenamos en la variable \texttt{dir}.
La condición en el bucle \texttt{while} verifica si \texttt{\$path} no es nulo. Si no es nulo, construye la ruta completa \texttt{\$dir/\$1} y verifica si hay un archivo con ese nombre para el cual tienes permisos de ejecución (y que no sea un directorio). Si es así, imprime la ruta completa y sale de la rutina con un estado de salida de 0 (<<OK>>).
Si no se encuentra un archivo, entonces se ejecuta este código:
\begin{lstlisting}[language=bash]
path=${path#*:}
dir=${path%%:*}
\end{lstlisting}
El primero de estos utiliza otro operador de cadena del shell: este elimina la coincidencia más corta con el patrón dado desde el principio de la cadena. En este punto, este tipo de operador debería resultarte familiar. Esta línea elimina el directorio frontal de \texttt{\$path} y asigna el resultado de nuevo a \textbf{path}. La segunda línea es igual que antes del \texttt{while:} encuentra el (nuevo) directorio frontal en \texttt{\$path} y lo asigna a \textbf{dir}. Esto configura el bucle para otra iteración.
Así, el código recorre todos los directorios en \texttt{PATH}. Sale cuando encuentra un archivo ejecutable coincidente o cuando ha <<consumido>> todo el \texttt{PATH}. Si no se encuentra ningún archivo ejecutable coincidente, no imprime nada y sale con un estado de error.
Podemos mejorar este script un poco aprovechando la utilidad UNIX \emph{file} (1). \emph{file} examina archivos dados como argumentos y determina qué tipo son, basándose en el \emph{número mágico} del archivo y diversas heurísticas (conjeturas educadas). Un número mágico es un campo en el encabezado de un archivo ejecutable que el enlazador establece para identificar qué tipo de ejecutable es.
Si el \emph{nombre de archivo} es un programa ejecutable (compilado desde C u otro lenguaje), entonces escribir \texttt{file \emph{filename}} produce una salida similar a esto:
\begin{lstlisting}[language=bash]
filename
: ELF32-bit LSB executable 80386 Version 1
\end{lstlisting}
Sin embargo, si \texttt{filename} no es un programa ejecutable, examinará las primeras líneas e intentará adivinar qué tipo de información contiene el archivo. Si el archivo contiene texto (en lugar de datos binarios), \texttt{file} buscará indicaciones de que es inglés, comandos de shell, C, Fortran, entrada \emph{troff(1)} y varias otras cosas. A veces, \texttt{file} se equivoca, pero generalmente acierta.
Supongamos que \emph{fred} es un archivo ejecutable en el directorio \texttt{/usr/bin}, y que \emph{bob} es un script de shell en \texttt{/usr/local/bin}. Escribir \texttt{file /usr/bin/fred} produce esta salida:
\begin{lstlisting}[language=bash]
/usr/bin/fred: ELF 32-bit LSB executable 80386 Version 1
\end{lstlisting}
Escribir \texttt{file /usr/local/bin/bob} tiene este resultado:
\begin{lstlisting}[language=bash]
/usr/local/bin/bob: commands text
\end{lstlisting}
Podemos simplemente sustituir \texttt{file} por \texttt{print} para imprimir un mensaje más informativo en nuestro script:
\begin{lstlisting}[language=bash]
path=$PATH
dir=${path%%:*}
while [[ -n $path ]]; do
if [[ -x $dir/$1 &&! -d $dir/$1 ]]; then
file $dir/$1
exit
fi
path=${path#*:}
dir=${path%%:*}
done
exit 1
\end{lstlisting}
Observa que al mover la declaración \texttt{dir=\$\{path\%\%:*\}} al principio del cuerpo del bucle, solo es necesario hacerla una vez.
Finalmente, solo para mostrar cuán pequeña es la diferencia entre \texttt{while} y \texttt{until}, señalamos que la línea:
\begin{lstlisting}[language=bash]
until [[ ! -n $path ]]; do
\end{lstlisting}
puede usarse en lugar de:
\begin{lstlisting}[language=bash]
while [[ -n $path ]]; do
\end{lstlisting}
con resultados idénticos.
Veremos ejemplos adicionales de \emph{while} en el próximo capítulo.
\subsection{break y continue}
Anteriormente en este capítulo, vimos la declaración \texttt{break} utilizada con la construcción \texttt{select} para salir de un bucle. \texttt{break} se puede usar con cualquier construcción de bucle: \texttt{for, select, while} y \texttt{until}.
La declaración \texttt{continue} está relacionada; su función es omitir cualquier declaración restante en el cuerpo del bucle y comenzar la próxima iteración.
Tanto las declaraciones \texttt{break} como \texttt{continue} aceptan un argumento numérico opcional (que puede ser una expresión numérica). Esto indica cuántos bucles circundantes deben romperse o continuarse. Por ejemplo:
\begin{lstlisting}[language=bash]
while condition1; do # Bucle externo
...
while condition2; do # Bucle interno
...
break 2 # Sale del bucle externo
done
done
... # La ejecución continúa aquí después de break
\end{lstlisting}
Los programadores notarán que las declaraciones \texttt{break} y \texttt{continue}, especialmente con la capacidad de salir o continuar en varios niveles de bucle, compensan de manera muy limpia la falta de una palabra clave \texttt{goto} en el lenguaje de shell.