Aprendiendo_Korn_Shell/Secciones/Capitulo6.tex

1271 lines
87 KiB
TeX

Deberías tener un sólido conocimiento de las técnicas de programación de shell ahora que has pasado por los capítulos anteriores. Lo que has aprendido hasta este punto te permite escribir muchos scripts y funciones de shell no triviales y útiles.
Sin embargo, es posible que hayas notado algunas lagunas restantes en el conocimiento que necesitas para escribir código de shell que se comporte como los comandos UNIX a los que estás acostumbrado. En particular, si eres un usuario experimentado de UNIX, puede que hayas notado que ninguno de los scripts de ejemplo mostrados hasta ahora tiene la capacidad de manejar \emph{opciones} (precedidas por un guion ( - )) en la línea de comandos. Y si programas en un lenguaje convencional como C o Pascal, habrás notado que el único tipo de datos que hemos visto en las variables de shell son cadenas de caracteres; no hemos visto cómo hacer operaciones aritméticas, por ejemplo.
Estas capacidades son ciertamente cruciales para la capacidad del shell de funcionar como un lenguaje de programación UNIX útil. En este capítulo, mostraremos cómo el shell de Korn admite estas y otras características relacionadas.
\section{Opciones de línea de comandos}
Ya hemos visto muchos ejemplos de los \emph{parámetros posicionales} (variables llamadas \textbf{1, 2, 3,} etc.) que el shell utiliza para almacenar los argumentos de línea de comandos de un script o función de shell cuando se ejecuta. También hemos visto variables relacionadas como * (para la cadena de todos los argumentos) y \# (para el número de argumentos).
De hecho, estas variables contienen toda la información de la línea de comandos del usuario. Pero considera qué sucede cuando hay opciones involucradas. Los comandos típicos de UNIX tienen la forma \texttt{comando [- opciones] args}, lo que significa que puede haber 0 o más opciones. Si un script de shell procesa el comando \textbf{fred bob pete}, entonces \texttt{\$1} es <<bob>> y \texttt{\$2} es <<pete>>. Pero si el comando es \textbf{fred -o bob pete}, entonces \texttt{\$1} es \textbf{-o}, \texttt{\$2} es <<bob>>, y \texttt{\$3} es <<pete>>.
Podrías pensar que podrías escribir código como este para manejarlo:
\begin{lstlisting}[language=bash]
if [[ $1 = -o ]]; then
codigo que procesa la opcion -o
1=$2
2=$3
fi
procesamiento normal de $1 y $2...
\end{lstlisting}
Pero este código tiene varios problemas. En primer lugar, las asignaciones como \texttt{1=\$2} son ilegales porque los parámetros posicionales son de solo lectura. Incluso si fueran legales, otro problema es que este tipo de código impone limitaciones en la cantidad de argumentos que el script puede manejar, lo cual es muy imprudente. Además, si este comando tuviera varias opciones posibles, el código para manejarlas se volvería muy desordenado muy rápidamente.
\subsection{shift}
Afortunadamente, el shell proporciona una solución a este problema. El comando \texttt{shift} realiza la función de:
\begin{lstlisting}[language=bash]
1=$2
2=$3
...
\end{lstlisting}
para cada argumento, independientemente de cuántos haya. Si proporcionas un argumento numérico\footnote{En realidad, el argumento puede ser una expresión numérica; el shell la evalúa automáticamente.}
a \texttt{shift}, desplazará los argumentos esa cantidad de veces; por ejemplo, \texttt{shift 3} tiene este efecto:
\begin{lstlisting}[language=bash]
1=$4
2=$5
...
\end{lstlisting}
Esto conduce inmediatamente a un código que maneja una única opción (llamémosla \texttt{-o} ) y arbitrariamente muchos argumentos:
\begin{lstlisting}[language=bash]
if [[ $1 = -o ]]; then
procesar la opción -o
shift
fi
procesamiento normal de argumentos...
\end{lstlisting}
Después de la construcción \texttt{if}, \texttt{$1, $2,} etc., se establecen en los argumentos correctos, y \# también se ajusta automáticamente.
Podemos usar \texttt{shift} junto con las características de programación que hemos visto hasta ahora para implementar esquemas de opciones simples. Sin embargo, necesitaremos ayuda adicional cuando las cosas se vuelvan más complejas. El comando integrado \texttt{getopts}, que presentaremos más adelante, proporciona esta ayuda.
\texttt{shift} por sí mismo nos da suficiente poder para implementar la opción \texttt{-N} en el script más \emph{avanzado} que vimos en el \hyperref[sec:Chapter4]{Capítulo 4}, Programación Básica de Shell (Tarea 4-1). Recuerda que este script toma un archivo de entrada que enumera artistas y la cantidad de álbumes que tienes de ellos. Ordena la lista e imprime los N números más altos, en orden descendente. El código que realiza el procesamiento real de datos es:
\begin{lstlisting}[language=bash]
filename=$1
howmany=${2:-10}
sort -nr $filename | head -$howmany
\end{lstlisting}
Nuestra sintaxis original para llamar a este script era \texttt{highest filename [- N ]}, donde \emph{N} se predetermina a 10 si se omite. Cambiemos esto a una sintaxis UNIX más convencional, en la que las opciones se proporcionan antes de los argumentos: \texttt{highest [- N ] filename}. Así es como escribiríamos el script con esta sintaxis:
\begin{lstlisting}[language=bash]
if [[ $1 = -+([0-9]) ]]; then
howmany=$1
shift
elif [[ $1 = -* ]]; then
print 'usage: highest [-N] filename'
return 1
else
howmany="-10"
fi
filename=$1
sort -nr $filename | head $howmany
\end{lstlisting}
En este código, se considera que la opción se ha proporcionado si \texttt{\$1} coincide con el patrón \texttt{-+([0-9])}. Esto utiliza uno de los operadores de expresiones regulares del shell de Korn, que vimos en el \hyperref[sec:Chapter4]{Capítulo 4}. Observa que no rodeamos el patrón con comillas (ni siquiera comillas dobles); si lo hiciéramos, el shell lo interpretaría literalmente, no como un patrón. Este patrón significa <<un guion seguido por uno o más dígitos>>. Si \texttt{\$1} coincide, entonces lo asignamos a la variable \textbf{howmany}.
Si \texttt{\$1} no coincide, comprobamos si es una opción en absoluto, es decir, si coincide con el patrón \texttt{-*}. Si es así, entonces es inválido; imprimimos un mensaje de error y salimos con un estado de error. Si llegamos al caso final (\texttt{else}), asumimos que \texttt{\$1} es un nombre de archivo y lo tratamos como tal en el código siguiente. El resto del script procesa los datos como antes.
Podemos extender lo que hemos aprendido hasta ahora a una técnica general para manejar múltiples opciones. Para concretar, supongamos que nuestro script se llama \emph{bob} y queremos manejar las opciones \texttt{-a, -b} y \texttt{-c}:
\begin{lstlisting}[language=bash]
while [[ $1 = -* ]]; do
case $1 in
-a )
procesar opción -a ;;
-b )
procesar opción -b ;;
-c )
procesar opción -c ;;
*)
print 'uso: bob [-a] [-b] [-c] args...'
return 1
esac
shift
done
procesamiento normal de argumentos...
\end{lstlisting}
Este código verifica repetidamente \texttt{\$1} siempre que comience con un guion (-). Luego, la construcción \texttt{case} ejecuta el código apropiado según la opción \texttt{\$1}. Si la opción es inválida, es decir, si comienza con un guion pero no es \texttt{-a, -b} o \texttt{-c}, el script imprime un mensaje de uso y retorna con un estado de error. Después de procesar cada opción, los argumentos se desplazan. El resultado es que los parámetros posicionales se establecen en los argumentos reales cuando el bucle \texttt{while} termina.
Observa que este código es capaz de manejar opciones de longitud arbitraria, no solo una letra (por ejemplo, \texttt{-fred} en lugar de \texttt{-a}).
\subsection{Opciones con argumentos}
Necesitamos agregar un ingrediente más para que el procesamiento de opciones sea realmente útil. Recuerda que muchos comandos tienen opciones que toman sus \emph{propios} argumentos. Por ejemplo, el comando \texttt{cut}, en el que confiamos mucho en el \hyperref[sec:Chapter4]{Capítulo 4}, acepta la opción \texttt{-d} con un argumento que determina el delimitador de campo (si no es el TAB predeterminado). Para manejar este tipo de opción, simplemente usamos otro \texttt{shift} cuando estamos procesando la opción.
Supongamos que, en nuestro script \emph{bob}, la opción \texttt{-b} requiere su propio argumento. Aquí está el código modificado que lo procesará:
\begin{lstlisting}[language=bash]
while [[ $1 = -* ]]; do
case $1 in
-a)
procesar opción -a ;;
-b)
procesar opción -b $2 es el argumento de la opción
shift ;;
-c)
procesar opción -c ;;
*)
print 'uso: bob [-a] [-b barg] [-c] args...'
return 1
esac
shift
done
procesamiento normal de argumentos...
\end{lstlisting}
\subsection{getopts}
Hasta ahora, tenemos una forma completa, aunque aún limitada, de manejar las opciones de la línea de comandos. El código anterior no permite a un usuario combinar argumentos con un solo guion, por ejemplo, \texttt{-abc} en lugar de \texttt{-a -b -c}. Tampoco permite especificar argumentos para opciones sin un espacio entre ellos, por ejemplo, \texttt{-barg} además de \texttt{-b arg}\footnote{Aunque la mayoría de los comandos de UNIX permiten esto, en realidad va en contra de las Reglas Estándar de Sintaxis de Comandos en \emph{intro} (1) del Manual del Usuario.}
El shell proporciona una forma incorporada de manejar múltiples opciones complejas sin estas restricciones. El comando incorporado \texttt{getopts}\footnote{\texttt{getopts} reemplaza al comando externo \emph{getopt(1)}, utilizado en la programación del shell de Bourne; \texttt{getopts} está mejor integrado en la sintaxis del shell y se ejecuta de manera más eficiente. Los programadores en C reconocerán \texttt{getopts} como muy similar a la rutina de la biblioteca estándar \emph{getopt(3)}.}
se puede utilizar como condición del \texttt{while} en un bucle de procesamiento de opciones. Dada una especificación de qué opciones son válidas y cuáles requieren sus propios argumentos, configura el cuerpo del bucle para procesar cada opción por turno.
La función \texttt{getopts} toma al menos dos argumentos. El primero es una cadena que puede contener letras y dos puntos. Cada letra es una opción válida; si una letra va seguida de dos puntos, la opción requiere un argumento. Si la letra va seguida de un numeral (\texttt{\#}), la opción requiere un argumento numérico. Los dos puntos (\texttt{:}) o el numeral (\texttt{\#})pueden ir seguidos de \texttt{[descripción]}, es decir, una cadena descriptiva encerrada entre corchetes que se utiliza al generar mensajes de error de uso. Si añades un espacio con texto más descriptivo a la lista de caracteres de opción, ese texto también se imprime en los mensajes de error.
\texttt{getopts} selecciona opciones de la línea de comandos y asigna cada una (sin el guion inicial) a una variable cuyo nombre es el segundo argumento de \texttt{getopts}. Mientras haya opciones por procesar, \texttt{getopts} devuelve un estado de salida 0; cuando las opciones se agotan, devuelve un estado de salida 1, lo que hace que el bucle \emph{while} salga.
De forma predeterminada, \texttt{getopts} recorre \texttt{"\$@"}, la lista entre comillas de los argumentos de la línea de comandos. Sin embargo, puedes proporcionar argumentos adicionales a \texttt{getopts}, en cuyo caso los utiliza en su lugar.
\texttt{getopts} realiza algunas otras acciones que facilitan el procesamiento de opciones; las veremos a medida que examinemos cómo utilizar \texttt{getopts} en el ejemplo anterior.
\begin{lstlisting}[language=bash]
while getopts ":ab:c" opt; do
case $opt in
a)
procesar opción -a ;;
b)
procesar opción -b $OPTARG es el argumento de la opción ;;
c)
procesar opción -c ;;
\?)
print 'uso: bob [-a] [-b barg] [-c] args...'
return 1
esac
done
shift $(($OPTIND - 1))
procesamiento normal de argumentos...
\end{lstlisting}
La llamada a \texttt{getopts} en la condición del bucle \texttt{while} configura el bucle para aceptar las opciones \texttt{-a, -b} y \texttt{-c}, y especifica que \texttt{-b} toma un argumento. (Explicaremos el \textbf{``:''} que inicia la cadena de opciones en un momento.) Cada vez que se ejecuta el cuerpo del bucle, tiene la opción más reciente disponible, sin un guion (-), en la variable \texttt{opt}.
Si el usuario escribe una opción no válida, \texttt{getopts} normalmente imprime un mensaje de error (de la forma \texttt{cmd: -o: unknown option}) y establece \texttt{opt} en \texttt{?}. \texttt{getopts} termina de procesar todas sus opciones y, si se encontró un error, el shell sale. Sin embargo, ahora aquí hay un truco oscuro: si comienzas la cadena de letras de opción con dos puntos, \texttt{getopts} no imprimirá el mensaje y el shell no saldrá. Esto te permite manejar los mensajes de error por tu cuenta.
Puedes proporcionar el colon inicial y proporcionar tu propio mensaje de error en un \texttt{case} que maneje \texttt{?} y salga manualmente, como se indicó anteriormente, o puedes proporcionar texto descriptivo dentro de la llamada a \texttt{getopts} y dejar que el shell maneje la impresión del mensaje de error. En este último caso, el shell también saldrá automáticamente al encontrar una opción no válida.
Hemos modificado el código en la construcción \texttt{case} para reflejar lo que hace \texttt{getopts}. Pero observa que ya no hay más declaraciones \texttt{shift} dentro del bucle \texttt{while}: \texttt{getopts} no depende de \texttt{shifts} para realizar un seguimiento de dónde está. No es necesario desplazar los argumentos hasta que \texttt{getopts} haya terminado, es decir, hasta que el bucle \texttt{while} salga.
Si una opción tiene un argumento, \texttt{getopts} lo almacena en la variable \texttt{OPTARG}, que se puede usar en el código que procesa la opción.
La única declaración \texttt{shift} restante es después del bucle \texttt{while}. \texttt{getopts} almacena en la variable \texttt{OPTIND} el número del próximo argumento que se va a procesar; en este caso, ese es el número del primer argumento (no de opción) de la línea de comandos. Por ejemplo, si la línea de comandos fuera \texttt{bob -ab pete}, entonces \texttt{\$OPTIND} sería <<2>>. Si fuera \texttt{bob -a -b pete}, entonces \texttt{\$OPTIND} sería <<3>>. \texttt{OPTIND} se reinicializa a 1 cada vez que ejecutas una función, lo que te permite usar \texttt{getopts} dentro del cuerpo de una función.
La expresión \texttt{\$((\$OPTIND - 1))} es una expresión aritmética (como veremos más adelante en este capítulo) igual a \texttt{\$OPTIND} menos 1. Este valor se utiliza como argumento para \texttt{shift}. El resultado es que la cantidad correcta de argumentos se desplaza fuera del camino, dejando los argumentos <<reales>> como \texttt{\$1, \$2,} etc.
Antes de continuar, ahora es un buen momento para resumir todo lo que hace \texttt{getopts} (incluyendo algunos puntos que aún no se han mencionado):
\begin{enumerate}
\item Si se le proporciona la opción \texttt{-a} y un argumento, \texttt{getopts} usa ese argumento como el nombre del programa en cualquier mensaje de error, en lugar del predeterminado, que es el nombre del script. Esto es muy útil si estás usando \texttt{getopts} dentro de una función, donde \texttt{\$0} es el nombre de la función. En ese caso, es menos confuso si el mensaje de error utiliza el nombre del script en lugar del nombre de la función.
\item Su primer argumento (no de opción) es una cadena que contiene todas las letras de opción válidas. Si una opción requiere un argumento, un dos puntos sigue a su letra en la cadena. Un dos puntos inicial hace que \texttt{getopts} no imprima un mensaje de error cuando el usuario da una opción no válida.
\texttt Su segundo argumento es el nombre de una variable que contiene cada letra de opción (sin ningún guion inicial) a medida que se procesa. Al encontrar un error, esta variable contendrá un carácter ? literal.
\item Seguir una letra de opción con un \# en lugar de dos puntos indica que la opción toma un argumento numérico.
\item Cuando una opción toma un argumento (la letra de opción va seguida de dos puntos o de un símbolo \#), agregar un signo de interrogación indica que el argumento de la opción es opcional (es decir, no es obligatorio).
\item Si se proporcionan argumentos adicionales en la línea de comandos de \texttt{getopts} después de la cadena de opciones y el nombre de la variable, se utilizan en lugar de \texttt{\$@}.
\item Si una opción toma un argumento, el argumento se almacena en la variable \texttt{OPTARG}.
\item La variable \texttt{OPTIND} contiene un número igual al próximo argumento de la línea de comandos que se va a procesar. Después de que \texttt{getopts} haya terminado, es igual al número del primer argumento <<real>>.
\item Si el primer carácter en la cadena de opciones es + (o el segundo carácter después de dos puntos iniciales), entonces las opciones también pueden comenzar con +. En este caso, la variable de opción tendrá un valor que comienza con +.
\end{enumerate}
\texttt{getopts} puede hacer mucho, mucho más de lo que se ha descrito aquí. Consulta el \hyperref[sec:ApendiceB]{Apéndice B}, que proporciona la historia completa.
Las ventajas de \texttt{getopts} son que minimiza el código adicional necesario para procesar opciones y admite completamente la sintaxis estándar de opciones UNIX (según se especifica en \emph{intro(1)} del Manual del Usuario).
Como ejemplo más concreto, volvamos a nuestro front-end del compilador de C (\hyperref[box:4-2]{Tarea 4-2}). Hasta ahora, le hemos dado a nuestro script la capacidad de procesar archivos fuente de C (que terminan en .c), archivos de código ensamblador (.s) y archivos de código objeto (.o). Aquí está la última versión del script:
\begin{lstlisting}[language=bash]
objfiles=""
for filename in "$@"; do
case $filename in
*.c)
objname=${filename%.c}.o
compile $filename $objname ;;
*.s)
objname=${filename%.s}.o
assemble $filename $objname ;;
*.o)
objname=$filename ;;
*)
print "error: $filename is not a source or object file."
return 1 ;;
esac
objfiles="$objfiles $objname"
done
ld $objfiles
\end{lstlisting}
Ahora podemos darle al script la capacidad de manejar opciones. Para saber qué opciones necesitaremos, tendremos que discutir más a fondo lo que hacen los compiladores.
\subsubsection{Más sobre compiladores C}
El compilador de C en un sistema UNIX moderno típico (ANSI C en System V Release 4) tiene aproximadamente 30 opciones diferentes de línea de comandos, pero nos limitaremos a las más ampliamente utilizadas.
Esto es lo que implementaremos. Todos los compiladores proporcionan la capacidad de eliminar el paso final de enlace, es decir, la llamada al enlazador \emph{ld}. Esto es útil para compilar código C en archivos de código objeto que se vincularán más tarde y para aprovechar la verificación de errores del compilador por separado antes de intentar vincular. La opción \texttt{-c} suprime el paso de enlace, produciendo solo los archivos de código objeto compilados.
Los compiladores de C también son capaces de incluir mucha información adicional en un archivo de código objeto que puede ser utilizada por un depurador (aunque es ignorada por el enlazador y el programa en ejecución). Si no sabes qué es un depurador, consulta el \hyperref[sec:Chapter9]{Capítulo 9}, Depuración de Programas de Shell. El depurador necesita mucha información sobre el código C original para poder hacer su trabajo; la opción \texttt{-g} indica al compilador que incluya esta información en su salida de código objeto.
Si aún no estás familiarizado con los compiladores de C de UNIX, es posible que te haya parecido extraño ver en el último capítulo que el enlazador coloca su salida (el programa ejecutable) en un archivo llamado \texttt{a.out}. Esta convención es un vestigio histórico que a nadie le ha importado cambiar. Aunque ciertamente es posible cambiar el nombre del ejecutable con el comando \emph{mv}, el compilador de C proporciona la opción \texttt{-o} \emph{filename}, que utiliza \texttt{filename} en lugar de \texttt{a.out}.
Otra opción que admitiremos aquí tiene que ver con las \emph{bibliotecas}. Una biblioteca es una colección de código objeto, parte del cual se incluirá en el ejecutable en tiempo de enlace. (Esto contrasta con un archivo de código objeto precompilado, que se vincula por completo). Cada biblioteca incluye una gran cantidad de código objeto que respalda un cierto tipo de interfaz o actividad; los sistemas UNIX típicos tienen bibliotecas para cosas como redes, funciones matemáticas y gráficos.
Las bibliotecas son extremadamente útiles como bloques de construcción que ayudan a los programadores a escribir programas complejos sin tener que <<reinventar la rueda>> cada vez. La opción \texttt{-l} \emph{name} del compilador de C indica al enlazador que incluya el código necesario de la biblioteca \emph{name}.\footnote{En realidad, esto es un archivo llamado \emph{lib} \texttt{name .a} en un directorio de bibliotecas estándar como \texttt{/lib}.}
en el ejecutable que construye. Una biblioteca particular llamada c (el archivo \texttt{libc.a}) siempre está incluida. Esto se conoce como la biblioteca de tiempo de ejecución de C; contiene código para la capacidad de entrada y salida estándar de C, entre otras cosas.
Finalmente, es posible que un buen compilador de C realice ciertas cosas que hagan que su código objeto de salida sea más pequeño y eficiente. Colectivamente, estas cosas se llaman \emph{optimización}. Puedes pensar en un \emph{optimizador} como un paso adicional en el proceso de compilación que analiza el código objeto de salida y lo cambia para mejor. La opción \texttt{-O} invoca al optimizador.
La Tabla \ref{Tab:6.1} resume las opciones que incorporaremos en nuestro front-end del compilador de C.
\begin{table}[h]
\center
\caption{Opciones Populares del Compilador de C}
\label{Tab:6.1}
\begin{tabular}{|m{2cm}|m{13cm}|} \hline
\textbf{Opción} & \textbf{Significado} \\\hline
\textbf{-c} & Producir solo código objeto; no invocar al enlazador \\
\textbf{-g} & Incluir información de depuración en archivos de código objeto \\
\textbf{-l} \emph{lib} & Incluir la biblioteca \emph{lib} al enlazar \\
\textbf{-o} \emph{exefile} & Producir el archivo ejecutable \emph{exefile} en lugar del predeterminado \texttt{a.out} \\
\textbf{-O} & Invocar al optimizador \\\hline
\end{tabular}
\end{table}
También debes tener en cuenta esta información sobre las opciones:
\begin{itemize}
\item Las opciones \textbf{-o} y \textbf{-l lib} se pasan simplemente al enlazador \emph{(ld)}, que las procesa por sí mismo.
\item En la mayoría de los sistemas, \texttt{ld} requiere que las opciones de biblioteca vayan después de los ficheros objeto en la línea de órdenes. (Además, el orden de las bibliotecas en la línea de órdenes es importante. Si una rutina en \texttt{libA.a} hace referencia a otra rutina de \texttt{libB.a}, entonces \texttt{libA.a} debe aparecer primero en la línea de comandos (\texttt{-lA -lB}). Esto implica que la biblioteca C (\texttt{libc.a}) debe cargarse en último lugar, ya que las rutinas de otras bibliotecas casi siempre dependen de las rutinas estándar de la biblioteca C.
\item La opción \textbf{-l} \emph{lib} se puede usar varias veces para vincular varias bibliotecas.
\item La opción \textbf{-g} se pasa al comando \texttt{ccom} (el programa que realiza la compilación real de C).
\item Supondremos que el optimizador es un programa independiente llamado \emph{optimize} que acepta un archivo de código objeto como argumento y lo optimiza <<en su lugar>>, es decir, sin producir un archivo de salida separado.
\end{itemize}
Para nuestro front-end, hemos elegido dejar que el shell se encargue de imprimir el mensaje de uso. Aquí está el código para el script \emph{occ} que incluye el procesamiento de opciones:
\begin{lstlisting}[language=bash]
# inicializar variables relacionadas con las opciones
do_link=true
debug=""
link_libs="-l c"
exefile=""
opt=false
# procesar opciones de línea de comandos
while getopts ":cgl:o:O" opt; do
case $opt in
c )
do_link=false ;;
g )
debug="-g" ;;
l )
link_libs="$link_libs -l $OPTARG" ;;
o )
exefile="-o $OPTARG" ;;
O )
opt=true ;;
\? )
print 'uso: occ [-cgO] [-l lib] [-o file] files...'
return 1 ;;
esac
done
shift $(($OPTIND - 1))
# procesar los archivos de entrada
objfiles=""
for filename in "$@"; do
case $filename in
*.c )
objname=${filename%.c}.o
ccom $debug $filename $objname
if [[ $opt = true ]]; then
optimize $objname
fi ;;
*.s )
objname=${filename%.s}.o
as $filename $objname ;;
*.o )
objname=$filename ;;
* )
print "error: $filename no es un archivo fuente u objeto."
return 1 ;;
esac
objfiles="$objfiles $objname"
done
if [[ $do_link = true ]]; then
ld $exefile $link_libs $objfiles
fi
\end{lstlisting}
Examinemos la parte del código que procesa las opciones. Las primeras líneas inicializan variables que utilizaremos más adelante para almacenar el estado de cada una de las opciones. Utilizamos <<true>> y <<false>> como valores de verdad para mayor legibilidad; son simplemente cadenas y, de lo contrario, no tienen un significado especial. Las inicializaciones reflejan estas suposiciones:
\begin{enumerate}
\item Querremos realizar el enlace.
\item No querremos que el compilador genere información de depuración que consuma espacio.
\item La única biblioteca de código objeto que necesitaremos es \emph{c}, la biblioteca estándar de tiempo de ejecución de C que se enlaza automáticamente.
\emph El archivo ejecutable que crea el enlazador será el archivo predeterminado del enlazador, \emph{a.out}.
\emph No querremos invocar al optimizador.
\end{enumerate}
Las construcciones \texttt{while, getopts} y \texttt{case} procesan las opciones de la misma manera que en el ejemplo anterior. Esto es lo que hace el código que maneja cada opción:
\begin{itemize}
\item Si se proporciona la opción \texttt{-c}, se establece la bandera \texttt{do\_link} en <<false>>, lo que hará que la condición \texttt{if} al final del script sea falsa, lo que significa que el enlazador no se ejecutará.
\item Si se proporciona \texttt{-g}, la variable \texttt{debug} se establece en <<\emph{-g}>>. Esto se pasa en la línea de comandos al compilador.
\item Cada \texttt{-l} \emph{lib} que se proporciona se agrega a la variable \texttt{link\_libs}, de modo que cuando el bucle \texttt{while} sale, \texttt{\$link\_libs} es toda la cadena de opciones \texttt{-l}. Esta cadena se pasa al enlazador.
\item Si se proporciona \texttt{-o} \emph{file}, la variable \texttt{exefile} se establece en <<\emph{-o file}>>. Esta cadena se pasa al enlazador.
\item Si se especifica \texttt{-O}, se establecerá la bandera \texttt{opt}. Esta especificación hace que la condición condicional \texttt{if [[ \$opt = true ]]} sea verdadera, lo que significa que se ejecutará el optimizador.
\end{itemize}
El resto del código es una modificación del bucle \texttt{for} que ya hemos visto; las modificaciones son resultados directos del procesamiento de opciones anterior y deberían ser autoexplicativas.
\section{Variables Enteras y Aritmética}
La expresión \texttt{\$((\$OPTIND - 1))} en el último ejemplo da una pista de cómo el shell puede realizar aritmética entera. Como podrías imaginar, el shell interpreta las palabras rodeadas por \texttt{\$(( y ))} como expresiones aritméticas. Las variables en expresiones aritméticas no necesitan ir precedidas por signos de dólar, aunque no está mal hacerlo.
Las expresiones aritméticas se evalúan dentro de comillas dobles, al igual que las tildes, variables y sustituciones de comandos. \emph{Finalmente}, estamos en posición de establecer la regla definitiva sobre citar cadenas: cuando haya dudas, encierra una cadena entre comillas simples, a menos que contenga tildes o cualquier expresión que involucre un signo de dólar, en cuyo caso deberías usar comillas dobles.
Por ejemplo, el comando \emph{date (1)} en versiones derivadas de System V de UNIX acepta argumentos que le indican cómo formatear su salida. El argumento \texttt{+\%j} le indica que imprima el día del año, es decir, el número de días desde el 31 de diciembre del año anterior.
Podemos usar \texttt{+\%j} para imprimir un pequeño mensaje de anticipación a las vacaciones:
\begin{lstlisting}[language=bash]
print "Solo quedan $(( (365-$(date +%j)) / 7 )) semanas hasta el Año Nuevo!"
\end{lstlisting}
Mostraremos dónde encaja esto en el esquema general del procesamiento de la línea de comandos en el \hyperref[sec:Chapter7]{Capítulo 7}.
La función de expresiones aritméticas está integrada en la sintaxis del shell de Korn y estaba disponible en el shell de Bourne (en la mayoría de las versiones) solo a través del comando externo \emph{expr(1)}. Así que es otro ejemplo de una característica deseable proporcionada por un comando externo (es decir, un truco sintáctico) que se integra mejor en el shell. \texttt{[[...]]} y \emph{getopts} son también ejemplos de esta tendencia de diseño.
Mientras que \texttt{expr} y \emph{ksh88} estaban limitados a la aritmética entera, \emph{ksh93} admite la aritmética de punto flotante. Como veremos en breve, puedes realizar prácticamente cualquier cálculo en el shell de Korn que podrías hacer en C o en la mayoría de los otros lenguajes de programación.
Los operadores aritméticos del shell de Korn son equivalentes a sus contrapartes en el lenguaje C. La precedencia y la asociatividad son las mismas que en C. (Más detalles sobre la compatibilidad del shell de Korn con el lenguaje C se pueden encontrar en el \hyperref[sec:ApendiceB]{Apéndice B}; dichos detalles son de interés principalmente para personas que ya están familiarizadas con C.) La Tabla \ref{Tab:6-2} muestra los operadores aritméticos que se admiten, en orden de mayor a menor precedencia. Aunque algunos de ellos son (o contienen) caracteres especiales, no es necesario escaparlos con barra invertida, porque están dentro de la sintaxis \texttt{\$((...))}.
\begin{table}[h]
\small
\center
\caption{Operadores Aritméticos}
\label{Tab:6-2}
\begin{tabular}{|m{5cm}|m{6cm}|m{4cm}|} \hline
\textbf{Operador} & \textbf{Significado} & \textbf{Asociatividad} \\\hline
\texttt{++ --} & Incremento y decremento, prefijo y postfijo & De izquierda a derecha \\\hline
\texttt{+ - ! \~{}} & Más y menos unario; negación lógica y bit a bit & De derecha a izquierda \\\hline
\texttt{**} & Exponenciación \tablefootnote{\emph{ksh93m} y más reciente. El operador ** no está en lenguaje C.} & De derecha a izquierda \\\hline
\texttt{* / \%} & Multiplicación, división y resta & De izquierda a derecha \\\hline
\texttt{+ -} & Adición y sustracción & De izquierda a derecha \\\hline
\texttt{<< >>} & Desplazamiento de bits a izquierda y derecha & De izquierda a derecha \\\hline
\texttt{< <= > >=} & Comparaciones & De izquierda a derecha \\\hline
\texttt{== =!} & Iguales y no iguales & De izquierda a derecha \\\hline
\texttt{\&} & AND a nivel de bits & De izquierda a derecha \\\hline
\texttt{\^{}} & OR exclusivo a nivel de bits & De izquierda a derecha \\\hline
\texttt{|} & OR a nivel de bits & De izquierda a derecha \\\hline
\texttt{\&\&} & AND lógico (cortocircuito) & De izquierda a derecha \\\hline
\texttt{||} & OR lógico (cortocircuito) & De izquierda a derecha \\\hline
\texttt{?:} & Expresión condicional & De derecha a izquierda \\\hline
\texttt{= =+ -= *= /= \%= \&= \^{}= <<= >>=} & Operadores de asignación & De derecha a izquierda \\\hline
\texttt{,} & Evaluación secuencial & De izquierda a derecha \\\hline
\end{tabular}
\end{table}
Los paréntesis se pueden utilizar para agrupar subexpresiones. La sintaxis de la expresión aritmética (como en C) admite operadores relacionales como <<valores de verdad>> de 1 para verdadero y 0 para falso.
Por ejemplo, \texttt{\$((3 > 2))} tiene el valor 1; \texttt{\$(( (3 > 2) || (4 <= 1) ))} también tiene el valor 1, ya que al menos una de las dos subexpresiones es verdadera.
Si estás familiarizado con C, C++ o Java, los operadores enumerados en la Tabla \ref{Tab:6-2} te resultarán familiares. Pero si no lo estás, algunos de ellos merecen una pequeña explicación.
Las formas de asignación de los operadores regulares son una forma conveniente de actualizar una variable de manera más compacta. Por ejemplo, en Pascal o Fortran podrías escribir \texttt{x = x + 2} para sumar 2 a x. El \texttt{+=} te permite hacerlo de manera más concisa: \texttt{\$((x += 2))} suma 2 a x y almacena el resultado nuevamente en x. (Compara esto con la reciente adición del operador \texttt{+=} a \emph{ksh93} para la concatenación de cadenas).
Dado que sumar y restar 1 son operaciones tan frecuentes, los operadores \texttt{++} y \texttt{--} proporcionan una forma aún más abreviada de realizarlas. Como podrías imaginar, \texttt{++} suma 1, mientras que \texttt{--} resta 1. Estos son operadores unarios. Echemos un vistazo rápido a cómo funcionan.
\begin{lstlisting}[language=bash]
$ i=5
$ print $((i++)) $i
5 6
$ print $((++i)) $i
7 7
\end{lstlisting}
¿Qué está sucediendo aquí? En ambos casos, el valor de i se incrementa en uno. Pero el valor devuelto por el operador depende de su ubicación con respecto a la variable en la que se está operando. Un operador de sufijo (que ocurre después de la variable) devuelve el valor \emph{antiguo} de la variable como resultado de la expresión y luego incrementa la variable. Por el contrario, un operador de \texttt{prefijo}, que viene antes de la variable, incrementa la variable primero y luego devuelve el nuevo valor. El operador \texttt{--} funciona de la misma manera que \texttt{++}, pero disminuye la variable en uno en lugar de incrementarla.
El shell también admite números en base \texttt{N}, donde \texttt{N} puede ser hasta 64. La notación \texttt{B\#N} significa <<N base B>>. Por supuesto, si omites el \texttt{B\#}, la base predetermina a 10. Los dígitos son \texttt{0-9, a-z (10-35), A-Z (36-61), @ (62)} y \texttt{\_(63)}. (Cuando la base es menor o igual a 36, puedes usar letras en mayúsculas y minúsculas). Por ejemplo:
\begin{lstlisting}[language=bash]
$ print el número ksh 43#G es $((43#G))
el número ksh 43#G es 42
\end{lstlisting}
Curiosamente, puedes usar variables de shell para contener subexpresiones, y el shell sustituye el valor de la variable al realizar la aritmética. Por ejemplo:
\begin{lstlisting}[language=bash]
$ almost_the_answer=10+20
$ print $almost_the_answer
10+20
$ print $(( almost_the_answer + 12 ))
42
\end{lstlisting}
\subsection{Funciones Aritméticas Incorporadas}
El shell proporciona varias funciones aritméticas y trigonométricas incorporadas para su uso con \texttt{\$((...))}. Se llaman utilizando la sintaxis de llamada a función en C. Las funciones trigonométricas esperan que los argumentos estén en radianes, no en grados. (Hay 2 \pi radianes en un círculo). Por ejemplo, recordando los días de la escuela secundaria, recuerda que 45 grados es \pi divido por 4. Digamos que necesitamos el coseno de 45 grados:
\begin{lstlisting}[language=bash]
$ pi=3.1415927 # Valor aproximado para pi
$ print el coseno de pi/4 es $(( cos(pi / 4) ))
el coseno de pi/4 es 0.707106772982
\end{lstlisting}
Una mejor aproximación de \pi se puede obtener utilizando la función atan:
\begin{lstlisting}[language=bash]
pi=$(( 4. * atan(1.) )) # Un valor mejor para pi
\end{lstlisting}
La Tabla \ref{Tab:6-3} enumera las funciones aritméticas incorporadas.
\begin{table}[h]
\caption{Funciones aritméticas incorporadas}
\label{Tab:6-3}
\begin{tabular}{|m{2cm}|m{5.5cm}|m{2cm}|m{5.5cm}|} \hline
\textbf{Función} & \textbf{Retorna} &\textbf{Función} & \textbf{Retorna}\\ \hline
abs & Valor absoluto & hypot & Distancia euclidiana \\\hline
acos & Arco coseno & int & Parte entera \\\hline
asin & Arco seno & log & Logaritmo natural \\\hline
atan & Arco tangente & pow & Exponeciación ($x^y$) \\\hline
atan2 & Arco tangente de dos variables & sin & Seno \\\hline
cos & Coseno & sinh & Seno hiperbólico \\\hline
cosh & Coseno hiperbólico & sqrt & Raíz cuadrada \\\hline
exp & Exponencial ($e^x$) & tan & Tangente \\\hline
fmod & Resto en coma flotante & tanh & Tangente hiperbólica \\\hline
\end{tabular}
\textbf{NOTA:} \texttt{hypot pow atan2} y \texttt{fmod} fueron agregados a partir de \emph{ksh93e}.
\end{table}
\subsection{Condiciones Aritméticas}
Otro constructo, estrechamente relacionado con \texttt{\$((...))}, es \texttt{((...))} (sin el signo de dólar inicial). Usamos esto para evaluar pruebas de condiciones aritméticas, al igual que \texttt{[[...]]} se utiliza para pruebas de cadenas, atributos de archivos y otros tipos de pruebas.
\texttt{((...))} evalúa operadores relacionales de manera diferente a \texttt{\$((...))} para que puedas usarlo en las construcciones \texttt{if} y \texttt{while}. En lugar de producir un resultado textual, simplemente establece su estado de salida según la verdad de la expresión: 0 si es verdadera, 1 en caso contrario. Entonces, por ejemplo, \texttt{((3 > 2))} produce un estado de salida 0, al igual que \texttt{(( (3 > 2) || (4 <= 1) ))}, pero \texttt{(( (3 > 2) \&\& (4 <= 1) ))} tiene un estado de salida 1 ya que la segunda subexpresión no es verdadera.
También puedes usar valores numéricos como valores de verdad dentro de este constructo. Es similar al concepto análogo en C, lo que significa que es un tanto contraintuitivo para los programadores que no son de C: un valor de 0 significa \emph{falso} (es decir, devuelve un estado de salida 1), y un valor no igual a 0 significa \emph{verdadero} (devuelve un estado de salida 0), por ejemplo, \texttt{(( 14 ))} es verdadero. Consulta el código del depurador \emph{kshdb} en el \hyperref[sec:Chapter9]{Capítulo 9} para ver dos ejemplos más de esto.
\subsection{Variables y Asignación Aritmética}
El constructo \texttt{(( ... ))} también se puede utilizar para definir variables enteras y asignarles valores. La declaración:
\begin{lstlisting}[language=bash]
(( var=expression ))
\end{lstlisting}
crea la variable entera intvar (si aún no existe) y le asigna el resultado de la \emph{expresión}.
La sintaxis de doble paréntesis es la recomendada. Sin embargo, si prefiere utilizar un comando para realizar operaciones aritméticas, el shell le proporciona uno: el comando incorporado \texttt{let}. La sintaxis es:
\begin{lstlisting}[language=bash]
let var=expression
\end{lstlisting}
No es necesario (porque es redundante) rodear la expresión con \texttt{\$(( y ))} en una declaración \texttt{let}. Como con cualquier asignación de variables, no debe haber espacio a ambos lados del signo igual (=). Es una buena práctica rodear las expresiones con comillas, ya que muchos caracteres son tratados como especiales por el shell (por ejemplo, * , \# , y paréntesis); además, debes poner entre comillas expresiones que incluyan espacios en blanco (espacios o TAB). Consulta la Tabla \ref{Tab:6.4} para ejemplos.
\begin{lstlisting}[language=bash]
let "x = 3.1415927" "y = 1.41421"
\end{lstlisting}
Mientras que \emph{ksh88} solo te permitía usar variables enteras, \emph{ksh93} ya no tiene esta restricción y las variables también pueden ser de punto flotante. (Un \emph{número entero} es lo que se llamaba un <<número entero>> en la escuela, un número que no tiene una parte fraccionaria, como 17 o 42. Los números de punto flotante, en cambio, pueden tener partes fraccionarias, como 3.1415927). El shell busca un punto decimal para determinar que un valor es de punto flotante. Sin él, los valores se tratan como enteros. Esto es principalmente un problema para la división: la división entera truncará cualquier parte fraccionaria. El operador \% requiere un divisor entero.
El shell proporciona dos alias integrados para declarar variables numéricas: \texttt{integer} para variables enteras y \texttt{float} para variables de punto flotante. (Ambos son alias para el comando \texttt{typeset} con diferentes opciones. Se proporcionan más detalles en la \hyperref[sec:6.5.3]{Sección 6.5.3}, más adelante en este capítulo).
Finalmente, todas las asignaciones tanto a variables enteras como de punto flotante se evalúan automáticamente como expresiones aritméticas. Esto significa que no es necesario utilizar el comando \texttt{let}:
\begin{lstlisting}[language=bash]
$ integer the_answer
$ the_answer=12+30
$ print $the_answer
42
\end{lstlisting}
\begin{table}[h]
\center
\caption{Asignaciones de Expresiones Enteras de Muestra}
\label{Tab:6.4}
\begin{tabular}{m{3cm}|m{3cm}} \hline
\textbf{Asignación} & \textbf{Valor} \\
\textbf{let x=} & \textbf{\$x} \\\hline
\texttt{x=1+4} & 5 \\
\texttt{'x = 1 + 4'} & 5 \\
\texttt{'x = 1.234 + 3'} & 4.234 \\
\texttt{'x = (2+3) * 5'} & 25 \\
\texttt{'x = 2 + 3 * 5'} & 17 \\
\texttt{'x = 17 / 3'} & 5 \\
\texttt{'x = 17 / 3.0'} & 5.66666666667 \\
\texttt{'17 \% 3'} & 2 \\
\texttt{'1$<<$4'} & 16 \\
\texttt{'48$>>$3'} & 6 \\
\texttt{'17 \& 3'} & 1 \\
\texttt{'17 \& 3'} & 19 \\
\texttt{'17 \^{} 3'} & 18 \\
\end{tabular}
\end{table}
La \hyperref[box:6-1]{Tarea 6-1} es una pequeña tarea que hace uso de la aritmética.
\begin{mybox}[Tarea 6-1]\label{box:6-1}
Escribe un script llamado \emph{pages} que, dado el nombre de un archivo de texto, indique cuántas páginas de salida contiene. Supón que hay 66 líneas por página, pero proporciona una opción que permita al usuario anular eso.
\end{mybox}
Haremos que nuestra opción sea \emph{-N}, al estilo de \emph{head}. La sintaxis para esta única opción es tan simple que no necesitamos molestarnos con \texttt{getopts}. Aquí está el código:
\begin{lstlisting}[language=bash]
if [[ $1 = -+([0-9]) ]]; then
let page_lines=${1#-}
shift
else
let page_lines=66
fi
let file_lines="$(wc -l < $1)"
let pages=file_lines/page_lines
if (( file_lines % page_lines > 0 )); then
let pages=pages+1
fi
print "$1 tiene $pages páginas de texto."
\end{lstlisting}
Observa que utilizamos la condición entera \texttt{(( file\_lines \% page\_lines > 0 ))} en lugar de la forma \texttt{[[ ... ]]}.
En el corazón de este código se encuentra la utilidad de UNIX \emph{wc(1)}, que cuenta el número de líneas, palabras y caracteres (bytes) en su entrada. De forma predeterminada, su salida se ve algo así:
\begin{lstlisting}[language=bash]
8 34 161 bob
\end{lstlisting}
La salida de \emph{wc} significa que el archivo bob tiene 8 líneas, 34 palabras y 161 caracteres. \emph{wc} reconoce las opciones \texttt{-l, -w} y \texttt{-c}, que le indican que imprima solo el número de líneas, palabras o caracteres, respectivamente.
Normalmente, \emph{wc} imprime el nombre de su archivo de entrada (dado como argumento). Como solo queremos el número de líneas, tenemos que hacer dos cosas. Primero, le proporcionamos la entrada mediante la redirección de archivos, como en \texttt{wc -l < bob} en lugar de \texttt{wc -l bob}. Esto produce el número de líneas precedido por uno o más espacios.
Desafortunadamente, ese espacio complica las cosas: la declaración \texttt{let file\_lines=\$(wc -l < \$1)} se convierte en \texttt{let file\_lines= N} después de la sustitución de comandos; el espacio después del signo igual es un error. Eso nos lleva a la segunda modificación, las comillas alrededor de la expresión de sustitución de comandos. La declaración \texttt{let file\_lines=" N"} es perfectamente legal, y \texttt{let} sabe cómo eliminar el espacio inicial.
La primera cláusula \texttt{if} en el script \emph{pages} verifica si se proporcionó una opción y, si es así, elimina el guion (-) y lo asigna a la variable \texttt{page\_lines}. \emph{wc} en la expresión de sustitución de comandos devuelve el número de líneas en el archivo cuyo nombre se proporciona como argumento.
El siguiente grupo de líneas calcula el número de páginas y, si hay un resto después de la división, suma 1. Finalmente, se imprime el mensaje correspondiente.
Como un ejemplo más grande de aritmética entera, completaremos nuestra emulación de las funciones \texttt{pushd} y \texttt{popd} del shell C (\hyperref[box:4-7]{Tarea 4-7}). Recuerda que estas funciones operan en \texttt{DIRSTACK}, una pila de directorios representada como una cadena con los nombres de directorios separados por espacios. \texttt{pushd} y \texttt{popd} del shell C toman tipos adicionales de argumentos, que son:
\begin{itemize}
\item \texttt{pushd +n} toma el \emph{n-ésimo} directorio en la pila (comenzando con 0), lo rota hacia arriba y hace \texttt{cd} a él.
\item \texttt{pushd} sin argumentos, en lugar de quejarse, intercambia los dos directorios principales en la pila y hace \texttt{cd} al nuevo directorio superior.
\item \texttt{popd +n} toma el \emph{n-ésimo} directorio en la pila y simplemente lo elimina.
\end{itemize}
La característica más útil de estas funciones es la capacidad de acceder al n-ésimo directorio en la pila. Aquí tienes las versiones más recientes de ambas funciones:
\begin{lstlisting}[language=bash]
function pushd { # apila el directorio actual en la pila
dirname=$1
if [[ -d $dirname && -x $dirname ]]; then
cd $dirname
DIRSTACK="$dirname ${DIRSTACK:-$PWD}"
print "$DIRSTACK"
else
print "aún en $PWD."
fi
}
function popd { # desapila el directorio de la pila, realiza cd al nuevo tope
if [[ -n $DIRSTACK ]]; then
DIRSTACK=${DIRSTACK#* }
cd ${DIRSTACK%% *}
print "$PWD"
else
print "pila vacía, aún en $PWD."
fi
}
\end{lstlisting}
Para acceder al \emph{n-ésimo} directorio, utilizamos un bucle \texttt{while} que transfiere el directorio superior a una copia temporal de la pila \emph{n} veces. Pondremos el bucle en una función llamada \texttt{getNdirs} que se ve así:
\begin{lstlisting}[language=bash]
function getNdirs {
stackfront=''
let count=0
while (( count < $1 )); do
stackfront="$stackfront ${DIRSTACK%% *}"
DIRSTACK=${DIRSTACK#* }
let count=count+1
done
}
\end{lstlisting}
El argumento pasado a \texttt{getNdirs} es el \emph{n} en cuestión. La variable \texttt{stackfront} es la copia temporal que contendrá los primeros n directorios cuando el bucle esté completo. \texttt{stackfront} comienza como nulo; \texttt{count}, que cuenta el número de iteraciones del bucle, comienza como 0.
La primera línea del cuerpo del bucle agrega la parte superior de la pila (\texttt{\$\{DIRSTACK\%\% *\}}) a \texttt{stackfront}; la segunda línea elimina la parte superior de la pila. La última línea incrementa el contador para la próxima iteración. Todo el bucle se ejecuta \emph{N} veces, para valores de \texttt{count} de 0 a \emph{N}-1.
Cuando el bucle termina, el último directorio en \texttt{\$stackfront} es el \emph{n-ésimo} directorio. La expresión \texttt{\$\{stackfront\#\#*\}} extrae este directorio. Además, \texttt{DIRSTACK} ahora contiene la <<parte posterior>> de la pila, es decir, la pila sin los primeros \emph{n} directorios. Con esto en mente, ahora podemos escribir el código para las versiones mejoradas de \texttt{pushd} y \texttt{popd}:
\begin{lstlisting}[language=bash]
function pushd {
if [[ $1 = ++([0-9]) ]]; then
# caso de pushd +n: rotar el n-ésimo directorio hacia arriba
let num=${1#+}
getNdirs
$num newtop=${stackfront##* }
stackfront=${stackfront%$newtop}
DIRSTACK="$newtop $stackfront $DIRSTACK"
cd $newtop
elif [[ -z $1 ]]; then
# caso de pushd sin argumentos; intercambiar los dos directorios superiores
firstdir=${DIRSTACK%% *}
DIRSTACK=${DIRSTACK#* }
seconddir=${DIRSTACK%% *}
DIRSTACK=${DIRSTACK#* }
DIRSTACK="$seconddir $firstdir $DIRSTACK"
cd $seconddir
else
cd $dirname
# caso normal de pushd con nombre de directorio
dirname=$1
if [[ -d $dirname && -x $dirname ]]; then
DIRSTACK="$dirname ${DIRSTACK:-$PWD}"
print "$DIRSTACK"
else
print "aún en "$PWD."
fi
fi
}
function popd {
# sacar el directorio de la pila, cd al nuevo superior
if [[ $1 = ++([0-9]) ]]; then
# caso de popd +n: eliminar el n-ésimo directorio de la pila
let num={$1#+}
getNdirs $num
stackfront=${stackfront% *}
DIRSTACK="$stackfront $DIRSTACK"
else
# caso normal de popd sin argumento
if [[ -n $DIRSTACK ]]; then
DIRSTACK=${DIRSTACK#* }
cd ${DIRSTACK%% *}
print "$PWD"
else
print "pila vacía, aún en $PWD."
fi
fi
}
\end{lstlisting}
Las funciones han crecido bastante; veámoslas por separado. La instrucción \texttt{if} al principio de \texttt{pushd} verifica si el primer argumento es una opción en la forma de + \emph{N}. Si es así, se ejecuta el primer bloque de código. El primer \texttt{let} simplemente elimina el signo más (+) del argumento y asigna el resultado, como un número entero, a la variable \texttt{num}. Esto, a su vez, se pasa a la función \texttt{getNdirs}.
Las siguientes dos instrucciones de asignación establecen \texttt{newtop} como el directorio \emph{N -ésimo}, es decir, el último directorio en \texttt{\$stackfront}, y eliminan ese directorio de \texttt{stackfront}. Las dos últimas líneas en esta parte de \texttt{pushd} vuelven a armar la pila en el orden apropiado y realizan \texttt{cd} al nuevo directorio superior.
La cláusula \texttt{elif} verifica si no hay argumento, en cuyo caso \texttt{pushd} debería intercambiar los dos directorios superiores en la pila. Las primeras cuatro líneas de esta cláusula asignan los dos directorios superiores a \texttt{firstdir} y \texttt{seconddir}, y los eliminan de la pila. Luego, como se mencionó anteriormente, el código vuelve a armar la pila en el nuevo orden y realiza \texttt{cd} al nuevo directorio superior.
La cláusula \texttt{else} corresponde al caso habitual, donde el usuario proporciona un nombre de directorio como argumento.
\texttt{popd} funciona de manera similar. La cláusula \texttt{if} verifica la opción + \emph{N}, que en este caso significa eliminar el \emph{N -ésimo} directorio. Un \texttt{let} extrae el \emph{N} como un número entero; la función \texttt{getNdirs} coloca los primeros \emph{N} directorios en \texttt{stackfront}. \\
Luego, la línea \texttt{stackfront=\$\{stackfront\%*\}} elimina el último directorio (el \emph{N -ésimo} directorio) de \texttt{stackfront}. Finalmente, se vuelve a armar la pila con el \emph{N -ésimo} directorio faltante.
La cláusula \texttt{else} cubre el caso habitual, donde el usuario no proporciona un argumento.
Antes de dejar este tema, aquí tienes algunos ejercicios que deberían poner a prueba tu comprensión de este código:
\begin{enumerate}
\item Agrega código a \texttt{pushd} para que salga con un mensaje de error si el usuario no proporciona un argumento y la pila contiene menos de dos directorios.
\item Verifica que cuando el usuario especifica \emph{+ N} y \emph{N} supera el número de directorios en la pila, tanto \texttt{pushd} como \texttt{popd} utilicen el último directorio como el \emph{N -ésimo} directorio.
\item Modifica la función \texttt{getNdirs} para que verifique la condición anterior y salga con un mensaje de error apropiado si es verdadero.
\item Cambia \texttt{getNdirs} para que use \texttt{cut} (con sustitución de comandos), en lugar del bucle \texttt{while}, para extraer los primeros \emph{N} directorios. Esto utiliza menos código pero se ejecuta más lentamente debido a los procesos adicionales generados.
\end{enumerate}
\section{for aritmétrico}
El bucle \texttt{for} descrito en el \hyperref[sec:Chapter5]{Capítulo 5} ha estado en los shells Unix desde la versión 7 del Bourne Shell. Como se ha mencionado, no puede hacer bucles al estilo Pascal o C para un número fijo de iteraciones con ese bucle \texttt{for}. \emph{ksh93} introdujo el bucle aritmético \emph{for} para remediar esta situación y para acercar el shell a un lenguaje de programación tradicional (algunos dirían <<real>>).
La sintaxis se parece a las facilidades aritméticas del shell que acabamos de ver. Es casi idéntica a la sintaxis del bucle \texttt{for} de C, excepto por el conjunto extra de paréntesis:
\begin{lstlisting}[language=bash]
for ((init; condition; increment))
do
sentencias ...
done
\end{lstlisting}
Aquí, \emph{init} representa algo que debe hacerse una vez, al comienzo del bucle. La \emph{condición} se comprueba, y mientras sea verdadera, el shell ejecuta las \emph{sentencias}. Antes de volver al principio del bucle para comprobar la condición de nuevo, el shell ejecuta \emph{increment}.
Cualquier \emph{init, condition} e \emph{increment} puede ser omitida. Una condición \emph{omitida} se trata como \emph{verdadera}; es decir, el cuerpo del bucle siempre se ejecuta. (El llamado <<bucle infinito>> requiere que utilice algún otro método para salir del bucle). Utilizaremos el bucle aritmético \emph{for} para la \hyperref[box:6-2]{Tarea 6-2}, que es nuestra siguiente tarea, bastante simple.
\begin{mybox}[Tarea 6-2]\label{box:6-2}
Escribe un script simple que tome una lista de números en la línea de comandos y los sume, imprimiendo el resultado.
\end{mybox}
Aquí está el código; la explicación viene a continuación:
\begin{lstlisting}[language=bash]
sum=0
count=$#
for ((i = 1;i <= count; i++))
do
let "sum += $1"
shift
done
print $sum
\end{lstlisting}
La primera línea inicializa la variable \texttt{sum} a 0. \texttt{sum} acumula la suma de los argumentos. La segunda línea es más que nada para facilitar la lectura; \texttt{count} indica cuántos argumentos hay. La tercera línea inicia el bucle propiamente dicho. La variable \texttt{i} es la variable de control del bucle. La cláusula \emph{init} la pone a 1, la cláusula \emph{condition} la comprueba con el límite de \texttt{count}, y la cláusula \texttt{increment} le añade 1 cada vez que se pasa por el bucle. Una cosa que notará de inmediato es que dentro del encabezado del bucle \texttt{for}, no hay necesidad del \$ delante del nombre de una variable para obtener el valor de esa variable. Esto es cierto para cualquier expresión aritmética en el shell de Korn.
El cuerpo del bucle hace la suma. Simplemente deja que haga las cuentas: la suma se consigue añadiendo \texttt{\$1} al valor de \texttt{sum}. El comando \emph{shift} mueve el siguiente argumento a \texttt{\$1} para usarlo en la siguiente vuelta del bucle. Finalmente, cuando el bucle termina, el script imprime el resultado.
El bucle \emph{for} aritmético es particularmente útil para trabajar con todos los elementos de un array indexado, que veremos en la siguiente sección.
\section{Arrays (matrices)}
Hasta ahora, hemos visto tres tipos de variables: cadenas de caracteres, enteros y números de punto flotante. El cuarto tipo de variable que admite el shell de Korn es un \texttt{array}. Como probablemente sepas, un array es como una lista de cosas; puedes referirte a elementos específicos en un array con \emph{índices}, de modo que \texttt{a[i]} se refiere al \emph{i-ésimo} elemento del array a. El shell de Korn proporciona dos tipos de \emph{arrays}: \emph{arrays} indexados y \emph{arrays} asociativos.
\subsection{Matrices indexadas}
El shell de Korn proporciona una funcionalidad de arrays indexados que, aunque útil, es mucho más limitada que las características análogas en lenguajes de programación convencionales. En particular, los arrays indexados solo pueden ser unidimensionales (es decir, no hay arrays de arrays) y están limitados a 4096 elementos.\footnote{4096 es un valor mínimo en \emph{ksh93}. Las versiones recientes permiten hasta 64K elementos.}
Los índices comienzan en 0. Esto implica que el valor máximo del índice es 4095. Además, los índices pueden ser cualquier expresión aritmética: \emph{ksh} evalúa automáticamente la expresión para obtener el índice real.
Hay tres formas de asignar valores a elementos de un array. La primera es la más intuitiva: puedes usar la sintaxis estándar de asignación de variables del shell con el índice del array entre corchetes ([]). Por ejemplo:
\begin{lstlisting}[language=bash]
nicknames[2]=bob
nicknames[3]=ed
\end{lstlisting}
Estas asignaciones colocan los valores \texttt{bob} y \texttt{ed} en los elementos del array \texttt{nicknames} con índices 2 y 3, respectivamente. Al igual que con las variables regulares del shell, los valores asignados a elementos del array se tratan como cadenas de caracteres a menos que la asignación esté precedida por \texttt{let}, o el array se haya declarado numérico con una de las opciones \texttt{typeset -i, -ui, -E} o \texttt{-F}. (Estrictamente hablando, el valor asignado con \emph{let} sigue siendo una cadena; es solo que con \emph{let}, el shell evalúa la expresión aritmética que se asigna para producir esa cadena).
La segunda forma de asignar valores a un array es con una variante de la declaración set, que vimos en el \hyperref[sec:Chapter3]{Capítulo 3}. La declaración:
\begin{lstlisting}[language=bash]
set -A aname val1 val2 val3 ...
\end{lstlisting}
crea el array \texttt{aname} (si aún no existe) y asigna \emph{val1} a \texttt{aname[0]}, \emph{val2} a \texttt{aname[1]}, etc. Como podrías imaginar, esto es más conveniente para cargar un array con un conjunto inicial de valores.
La tercera forma (recomendada) es usar la forma de asignación compuesta:
\begin{lstlisting}[language=bash]
aname=(val1 val2 val3)
\end{lstlisting}
A partir de \emph{ksh93j}, puedes usar el operador \texttt{+=} para agregar valores a un array:
\begin{lstlisting}[language=bash]
aname+=(val4 val5 val6)
\end{lstlisting}
Para extraer un valor de un array, utiliza la sintaxis \texttt{\$\{aname[i]\}}. Por ejemplo, \texttt{\$\{nicknames[2]\}} tiene el valor <<bob>>. El índice \emph{i} puede ser una expresión aritmética, como se explicó anteriormente. Si usas * o @ en lugar del índice, el valor será todos los elementos, separados por espacios. Omitir el índice (\texttt[\$nicknames\}) es lo mismo que especificar el índice 0 (\texttt{\$\{nicknames[0]\}}).
Ahora llegamos al aspecto algo inusual de los arrays en el shell de Korn. Supongamos que los únicos valores asignados a \emph{nicknames} son los dos que vimos anteriormente. Si escribes \texttt{print "\$\{nicknames[*]\}"}, verás la salida:
\begin{lstlisting}[language=bash]
bob ed
\end{lstlisting}
En otras palabras, \texttt{nicknames[0]} y \texttt{nicknames[1]} no existen. Además, si escribieras:
\begin{lstlisting}[language=bash]
nicknames[9]=pete
nicknames[31]=ralph
\end{lstlisting}
y luego escribieras \emph{print "\$\{nicknames[*]\}"}, la salida se vería así:
\begin{lstlisting}[language=bash]
bob ed pete ralph
\end{lstlisting}
Esto explica por qué dijimos <<los elementos de \emph{nicknames} con índices 2 y 3>> anteriormente, en lugar de <<el segundo y tercer elemento de \emph{nicknames}>>. Cualquier elemento de array con valores no asignados simplemente no existe; si intentas acceder a sus valores, obtienes cadenas nulas.
Puedes preservar cualquier espacio en blanco que coloques en los elementos de tu array usando \texttt{"\$\{aname[@]\}"} (con las comillas dobles) en lugar de \texttt{\$\{aname[*]\}}, al igual que puedes hacerlo con \texttt{"\$@"} en lugar de \texttt{\$*} o \texttt{"\$*"}.
El shell proporciona un operador que te dice cuántos elementos tiene definidos un array: \texttt{\$\{\#aname[*]\}}. Así que \texttt{\$\{\#nicknames[*]\}} tiene el valor 4. Ten en cuenta que necesitas el \texttt{[*]} porque el nombre del array solo se interpreta como el elemento 0. Esto significa, por ejemplo, que \texttt{\$\{\#nicknames\}} es igual a la longitud de \texttt{nicknames[0]} (ver \hyperref[sec:Chapter4]{Capítulo 4}). Dado que \texttt{nicknames[0]} no existe, el valor de \texttt{\$\{\#nicknames\}} es 0, la longitud de la cadena nula.
Si piensas en un array como un mapeo de enteros a valores (es decir, ingresas un número y obtienes un valor), puedes ver por qué los arrays son estructuras de datos <<dominadas por números>>. Debido a que las tareas de programación del shell están mucho más orientadas hacia cadenas de caracteres y texto que hacia números, la facilidad de array indexado del shell no es tan ampliamente útil como podría parecer al principio.
Sin embargo, podemos encontrar cosas útiles para hacer con arrays indexados. Aquí tienes una solución más limpia para la \hyperref[box:5-4]{Tarea 5-4}, en la que un usuario puede seleccionar su tipo de terminal (variable de entorno TERM) al iniciar sesión. Recuerda que la versión <<amigable para el usuario>> de este código usaba \texttt{select} y un enunciado \texttt{case}:
\begin{lstlisting}[language=bash]
print 'Selecciona tu tipo de terminal:'
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 "inválido." ;;
esac
if [[ -n $term ]]; then
print "TERM es $TERM"
export TERM
break
fi
done
\end{lstlisting}
Podemos eliminar todo el enunciado \texttt{case} aprovechando el hecho de que el enunciado \texttt{select} almacena la elección numérica del usuario en la variable \texttt{REPLY}. Solo necesitamos una línea de código que almacene todas las posibilidades para \texttt{TERM} en un array, en un orden que corresponda a los elementos en el menú \texttt{select}. Luego podemos usar \texttt{\$REPLY} para indexar el array. El código resultante es:
\begin{lstlisting}[language=bash]
set -A termnames gl35a t2000 s531 vt99
print 'Selecciona tu tipo de terminal:'
PS3='¿Terminal? '
select term in \
'Givalt GL35a' \
'Tsoris T-2000' \
'Shande 531' \
'Vey VT99'
do
if [[ -n $term ]]; then
TERM=${termnames[REPLY-1]}
print "TERM es $TERM"
export TERM
break
fi
done
\end{lstlisting}
Este código configura el array \texttt{termnames} para que \texttt{\$\{termnames[0]\}} sea \emph{gl35a}, \texttt{\$\{termnames[1]\}} sea \emph{t2000}, etc. La línea \texttt{TERM=\$\{termnames[REPLY-1]\}} reemplaza esencialmente todo el enunciado \texttt{case} utilizando \texttt{REPLY} para indexar el array.
Observa que el shell sabe interpretar el texto en un índice de array como una expresión aritmética, como si estuviera encerrado en \texttt{((} y \texttt{))}, lo que a su vez significa que la variable no necesita ir precedida por un signo de dólar (\texttt{\$}). Debemos restar 1 al valor de \texttt{REPLY} porque los índices de array comienzan en 0, mientras que los números de elementos del menú \texttt{select} comienzan en 1.
Piensa en cómo podrías usar arrays para mantener la pila de directorios para \texttt{pushd} y \texttt{popd}. El bucle \texttt{for} aritmético también podría resultar útil.
\subsection{Matrices asociativas}
Como se mencionó en la sección anterior, las tareas de programación en shell suelen ser orientadas a cadenas de texto en lugar de a números. \emph{ksh93} introdujo \emph{arrays asociativos} en el shell para mejorar la programabilidad de la misma. Los arrays asociativos son fundamentales en la programación en lenguajes como \emph{awk, perl} y \emph{python}.
Un array asociativo es un array indexado por valores de cadena. Proporciona una asociación entre el índice de cadena y el valor del array en ese índice, lo que hace que ciertos tipos de tareas funcionen de manera mucho más natural. Le indicas al shell que un array es asociativo usando \texttt{typeset -A}:
\begin{lstlisting}[language=bash]
typeset -A person
person[firstname]="frank"
person[lastname]="jones"
\end{lstlisting}
Podemos reescribir nuestro ejemplo de terminal de la sección anterior utilizando arrays asociativos:
\begin{lstlisting}[language=bash]
typeset -A termnames # termnames es un array asociativo
termnames=([Givalt GL35a]=gl35a # Rellenar los valores
[Tsoris T-2000]=t2000
[Shande 531]=s531
[Vey VT99]=vt99)
print 'Selecciona tu tipo de terminal:'
PS3='¿Terminal? '
select term in "${!termnames[@]}" # Presentar menú de índices de array
do
if [[ -n $term ]]; then
TERM=${termnames[$term]} # Usar cadena para indexar el array
print "TERM es $TERM"
break
fi
done
\end{lstlisting}
Observa que los corchetes cuadrados en la asignación compuesta actúan como comillas dobles; aunque está bien poner entre comillas los índices de cadena, no es necesario. También observa la construcción \texttt{\$\{!termnames[@]\}}. Es un poco complicado, pero nos da todos los índices del array como cadenas separadas entre comillas que conservan cualquier espacio en blanco incrustado, al igual que \texttt{\$@} (ver la siguiente sección).
A partir de \emph{ksh93j}, al igual que para los arrays regulares, puedes usar el operador \texttt{+=} para agregar valores a un array asociativo:
\begin{lstlisting}[language=bash]
termnames+=([Boosha B-27]=boo27 [Cherpah C-42]=chc42)
\end{lstlisting}
Como nota adicional, si aplicas \texttt{typeset -A} a una variable previamente existente que no era un array, el valor actual de esa variable se colocará en el índice 0. La razón es que el shell trata \texttt{\$x} como equivalente a \texttt{\$\{x[0]\}}, de modo que si haces:
\begin{lstlisting}[language=bash]
x=fred
typeset -A x
print $x
\end{lstlisting}
todavía obtendrás \texttt{fred}.
\subsection{Operadores de nombre de matriz}
En el \hyperref[sec:Chapter4]{Capítulo 4} vimos que el shell proporciona numerosas formas de acceder y manipular los valores de las variables del shell. En particular, vimos operadores que funcionan con \emph{nombres} de variables del shell. Varios operadores adicionales se aplican a los arrays. Se describen en la Tabla \ref{Tab:6-5}.
\begin{table}[h]
\center
\caption{Operadores relacionados con nombres de arrays}
\label{Tab:6-5}
\begin{tabular}{|m{5cm}|m{10cm}|} \hline
\textbf{Operador} & \textbf{Significado} \\ \hline
\texttt{\$\{!array[subíndice]\}} & Devuelve el subíndice real utilizado para indexar el array. Los subíndices pueden provenir de expresiones aritméticas o de los valores de variables del shell. \\\hline
\texttt{\$\{!array[*]\}} & Lista de todos los subíndices del array asociativo. \\\hline
\texttt{\$\{!array[@]\}} & Lista de todos los subíndices de la matriz asociativa, pero se expande a palabras separadas cuando se utiliza dentro de comillas dobles. \\\hline
\end{tabular}
\end{table}
Puedes pensar en la construcción \texttt{\$\{!...\}} para producir el array real como conceptualmente similar a su uso con variables \texttt{nameref}. Allí, indica la variable real a la que se refiere una \texttt{nameref}. Con arrays, produce el subíndice real utilizado para acceder a un elemento en particular. Esto es valioso porque los subíndices se pueden generar dinámicamente, por ejemplo, como expresiones aritméticas o mediante las diversas operaciones de cadena disponibles en el shell. Aquí tienes un ejemplo sencillo:
\begin{lstlisting}[language=bash]
$ set -A letters a b c d e f g h i j k l m n o p q r s t u v w x y z
$ print ${letters[20+2+1]}
x
$ print ${!letters[20+2+1]}
23
\end{lstlisting}
Para recorrer todos los elementos de un array indexado, podrías usar fácilmente un bucle \texttt{for} aritmético que fuera de 0 a, por ejemplo, \texttt{\$\{\#letters[*]\}} (el número de elementos en \texttt{letters}). Los arrays asociativos son diferentes: no hay límites inferiores o superiores en los índices del array, ya que todos son cadenas. Los dos últimos operadores en la Tabla \ref{Tab:6-5} facilitan recorrer un array asociativo:
\begin{lstlisting}[language=bash]
typeset -A bob # Crear un array asociativo
... # Rellénalo
for index in "${!bob[@]}"; do # Para todos los subíndices de bob
print "bob[$index] es ${bob[$index]}" # Imprimir cada elemento
...
done
\end{lstlisting}
Analógamente a la diferencia entre \texttt{\$*} y \texttt{"\$@"}, es mejor usar la versión con \texttt{@} del operador, dentro de comillas dobles, para preservar los valores originales de cadena. (Usamos \texttt{\$\{!var[@]\}} con \texttt{select} en el último ejemplo en la sección anterior sobre arrays asociativos).
\section{typeset}
La última característica del shell de Korn que se relaciona con los tipos de valores que pueden contener las variables es el comando \texttt{typeset}. Si eres programador, podrías suponer que \texttt{typeset} se utiliza para especificar el \emph{tipo} de una variable (entero, cadena, etc.); estarías parcialmente en lo correcto. \texttt{typeset} es una colección bastante \emph{ad hoc} de cosas que puedes hacer a las variables para restringir los tipos de valores que pueden tomar. Las operaciones se especifican mediante opciones para \texttt{typeset}; la sintaxis básica es:
\begin{lstlisting}[language=bash]
typeset opción nombre_variable[=valor]
\end{lstlisting}
Aquí, \emph{opción} es una letra de opción precedida por un guion o signo más. Las opciones se pueden combinar y se pueden usar varios \emph{nombre\_variable}. Si omites \emph{nombre\_variable}, el shell imprime una lista de variables para las cuales se activa la opción dada.
Las opciones disponibles se dividen en dos categorías básicas:
\begin{itemize}
\item Operaciones de formato de cadena, como justificación a la derecha e izquierda, truncamiento y control de mayúsculas y minúsculas.
\item Funciones de tipo y atributo que son de interés principal para programadores avanzados.
\end{itemize}
\subsection{Variables locales en funciones}
\texttt{typeset} sin opciones tiene un significado importante: si se utiliza una declaración \texttt{typeset} dentro de la definición de una función, las variables involucradas se vuelven \emph{locales} a esa función (además de cualquier propiedad que puedan adquirir como resultado de las opciones de \texttt{typeset}). La capacidad de definir variables que son locales a unidades de <<subprogramas>> (procedimientos, funciones, subrutinas, etc.) es necesaria para escribir programas grandes, porque ayuda a mantener los subprogramas independientes del programa principal y entre sí.
\textbf{NOTA:} Los nombres de variables locales están restringidos a identificadores simples. Cuando se usa \texttt{typeset} con un nombre de variable compuesto (es decir, uno que contiene puntos), esa variable se vuelve automáticamente global, incluso si la declaración \texttt{typeset} ocurre dentro del cuerpo de una función.
Si solo deseas declarar una variable local en una función, utiliza \texttt{typeset} sin opciones. Por ejemplo:
\begin{lstlisting}[language=bash]
function afunc {
typeset diffvar
samevar=funcvalue
diffvar=funcvalue
print "samevar es $samevar"
print "diffvar es $diffvar"
}
samevar=globvalue
diffvar=globvalue
print "samevar es $samevar"
print "diffvar es $diffvar"
afunc
print "samevar es $samevar"
print "diffvar es $diffvar"
\end{lstlisting}
Este código imprimirá lo siguiente:
\begin{lstlisting}[language=bash]
samevar es globvalue
diffvar es globvalue
samevar es funcvalue
diffvar es funcvalue
samevar es funcvalue
diffvar es globvalue
\end{lstlisting}
La Figura \ref{fig:06} muestra esto gráficamente.
\begin{figure}[h!]
\centering
\includegraphics[scale=1]{fig_06}
\caption{\small{Variables locales en funciones}}
\label{fig:06}
\end{figure}
Verás varios ejemplos adicionales de variables locales dentro de funciones en el \hyperref[sec:Chapter9]{Capítulo 9}.
\subsection{Opciones de formato de cadena}
Ahora veamos las diversas opciones de \texttt{typeset}. La Tabla \ref{Tab:6-6} enumera las opciones de formato de cadena; las tres primeras toman un argumento numérico opcional.
\begin{table}[h]
\center
\caption{Opciones de formato de cadena de \texttt{typeset}}
\label{Tab:6-6}
\begin{tabular}{|m{2cm}|m{13cm}|} \hline
\textbf{Opción} & \textbf{Operación} \\ \hline
\texttt{-Ln} & Justificar a la izquierda. Elimina los espacios iniciales; si se da n, rellena con espacios o trunca a la derecha hasta la longitud n. \\\hline
\texttt{-Rn} & Justificar a la derecha. Elimina los espacios finales; si se da n, rellena con espacios o trunca a la izquierda hasta la longitud n. \\\hline
\texttt{-Zn} & Si se utiliza con -R, añade 0 a la izquierda en lugar de espacios si es necesario. Si se utiliza con -L, elimina los 0 iniciales. Por sí mismo, actúa igual que -RZ. \\\hline
\texttt{-l} & Convierte las letras a minúsculas. \\\hline
\texttt{-u} & Convierte las letras a mayúsculas. \\\hline
\end{tabular}
\end{table}
Aquí tienes algunos ejemplos simples. Supongamos que la variable \texttt{alpha} se asigna con las letras del alfabeto, en mayúsculas y minúsculas alternadas, rodeadas por tres espacios a cada lado:
\begin{lstlisting}[language=bash]
alpha=" aBcDeFgHiJkLmNoPqRsTuVwXyZ "
\end{lstlisting}
La Tabla \ref{Tab:6-7} muestra algunas declaraciones \texttt{typeset} y sus valores resultantes (suponiendo que cada una de las declaraciones se ejecute <<independientemente>>).
\begin{table}[h]
\center
\caption{Ejemplos de opciones de formato de cadena de \texttt{typeset}}
\label{Tab:6-7}
\begin{tabular}{|m{5cm}|m{10cm}|} \hline
\textbf{Declaración} & \textbf{Valor de \emph{v}} \\ \hline
\texttt{typeset -L v=\$alpha} & \texttt{''aBcDeFgHiJkLmNoPqRsTuVwXyZ\hspace{0.4cm}''} \\\hline
\texttt{typeset -L10 v=\$alpha} & \texttt{''aBcDeFgHiJ''} \\\hline
\texttt{typeset -R v=\$alpha} & \texttt{''\hspace{1.5cm}aBcDeFgHiJkLmNoPqRsTuVwXyZ''} \\\hline
\texttt{typeset -R16 v=\$alpha} & \texttt{''kLmNoPqRsTuVwXyZ''} \\\hline
\texttt{typeset -l v=\$alpha} & \texttt{''\hspace{0.7cm}abcdefghijklmnopqrstuvwxyz\hspace{0.7cm}''} \\\hline
\texttt{typeset -uR5 v=\$alpha} & \texttt{''VWXYZ''} \\\hline
\texttt{typeset -Z8 v="123.50"} & \texttt{''00123.50''} \\\hline
\end{tabular}
\end{table}
Cuando ejecutas \texttt{typeset} en una variable existente, su efecto es \emph{acumulativo} con cualquier \texttt{typeset} que se haya utilizado anteriormente. Esto tiene excepciones obvias:
\begin{itemize}
\item Un \texttt{typeset -u} deshace un \texttt{typeset -l}, y viceversa.
\item Un \texttt{typeset -R} deshace un \texttt{typeset -L}, y viceversa.
\item No puedes combinar \texttt{typeset -l} y \texttt{typeset -u} con algunos de los atributos numéricos, como \texttt{typeset -E}. Sin embargo, ten en cuenta que \texttt{typeset -ui} crea enteros sin signo.
\item \texttt{typeset -A} y \texttt{typeset -n} (array asociativo y referencias de nombre, respectivamente) no son acumulativos.
\end{itemize}
Puedes desactivar explícitamente las opciones de \texttt{typeset} escribiendo \texttt{typeset +o}, donde \texttt{o} es la opción que activaste anteriormente. Por supuesto, es difícil imaginar escenarios en los que desees activar y desactivar múltiples opciones de formato de \texttt{typeset} una y otra vez; generalmente, configuras una opción de \texttt{typeset} en una variable dada solo una vez.
Una aplicación evidente para las opciones \texttt{-L} y \texttt{-R} es aquella en la que necesitas una salida de ancho fijo. La fuente más ubicua de salida de ancho fijo en el sistema Unix se refleja en la \hyperref[box:6-3]{Tarea 6-3}.
\begin{mybox}[Tarea 6-3]\label{box:6-3}
Fingir que \texttt{ls} no hace una salida de varias columnas; escribe un script de shell que lo haga.
\end{mybox}
Por simplicidad, supongamos que nuestra versión de Unix es antigua y que los nombres de archivo están limitados a 14 caracteres.\footnote{No conocemos sistemas Unix modernos que aún tengan esta restricción. Pero aplicarla aquí simplifica considerablemente el problema de programación.}
Nuestra solución para esta tarea se basa en muchos de los conceptos que hemos visto anteriormente en este capítulo. También se basa en el hecho de que \texttt{set -A} (para construir arrays) se puede combinar con la sustitución de comandos de una manera interesante: cada palabra (separada por espacios, tabulaciones o saltos de línea) se convierte en un elemento del array. Por ejemplo, si el archivo \emph{bob} contiene 50 palabras, el array \emph{fred} tiene 50 elementos después de la declaración:
\begin{lstlisting}[language=bash]
set -A fred $(< bob)
\end{lstlisting}
Nuestra estrategia es obtener los nombres de todos los archivos en el directorio dado en una variable de array. Utilizamos un bucle \texttt{for} aritmético, como vimos anteriormente en este capítulo, para obtener cada nombre de archivo en una variable cuya longitud se ha establecido en 14. Imprimimos esa variable en formato de cinco columnas, con dos espacios entre cada columna (para un total de 80 columnas), usando un contador para llevar un registro de las columnas. Aquí está el código:
\begin{lstlisting}[language=bash]
set -A filenames $(ls $1)
typeset -L14 fname
let numcols=5
for ((count = 0; count < ${#filenames[*]} ; count++)); do
fname=${filenames[count]}
print -rn "$fname "
if (( (count+1) % numcols == 0 )); then
print # nueva línea
fi
done
if (( count % numcols != 0 )); then
print
fi
\end{lstlisting}
La primera línea configura el array \texttt{filenames} para contener todos los archivos en el directorio dado por el primer argumento (el directorio actual por defecto). La declaración \texttt{typeset} configura la variable \texttt{fname} para tener un ancho fijo de 14 caracteres. La siguiente línea inicializa \texttt{numcols} con el número de columnas por línea.
El bucle \emph{for} aritmético itera una vez por cada elemento en \texttt{filenames}. En el cuerpo del bucle, la primera línea asigna el próximo elemento del array a la variable de ancho fijo. La declaración \texttt{print} imprime este último seguido de dos espacios; la opción \texttt{-n} suprime la última nueva línea de \texttt{print}.
Luego está la declaración \texttt{if}, que determina cuándo comenzar la próxima línea. Verifica el resto de \texttt{(count + 1) \% numcols} - recuerda que los signos de dólar no son necesarios dentro de la construcción \texttt{\$((...))} - y si el resultado es 0, es hora de emitir una nueva línea a través de una declaración \texttt{print} sin argumentos. Observa que aunque \texttt{\$count} aumenta en 1 con cada iteración del bucle, el resto pasa por un ciclo de 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, ...
Después del bucle, una construcción \texttt{if} emite una nueva línea final si es necesario, es decir, si el \texttt{if} dentro del bucle no lo acaba de hacer.
También podemos usar opciones de \texttt{typeset} para mejorar el código de nuestro script \emph{dosmv} (\hyperref[box:5-3]{Tarea 5-3}), que traduce nombres de archivo en un directorio dado del formato MS-DOS al formato Unix. El código del script es:
\begin{lstlisting}[language=bash]
for filename in ${1:+$1/}* ; do
newfilename=$(print $filename | tr '[A-Z]' '[a-z]')
newfilename=${newfilename%.}
# sutileza: entrecomillar el valor de $newfilename para hacer una comparación de cadenas,
# no una coincidencia de expresiones regulares
if [[ $filename != "$newfilename" ]]; then
print "$filename -> $newfilename"
mv $filename $newfilename
fi
done
\end{lstlisting}
Podemos reemplazar la llamada a \texttt{tr} en el bucle \texttt{for} con una a \texttt{typeset -l} antes del bucle:
\begin{lstlisting}[language=bash]
typeset -l newfilename
for filename in ${1:+$1/}* ; do
newfilename=${filename%.}
# sutileza: entrecomillar el valor de $newfilename para hacer una comparación de cadenas,
# no una coincidencia de expresiones regulares
if [[ $filename != "$newfilename" ]]; then
print "$filename -> $newfilename"
mv $filename $newfilename
fi
done
\end{lstlisting}
De esta manera, la traducción a minúsculas se realiza automáticamente cada vez que se asigna un valor a \emph{newfilename}. No solo este código es más limpio, sino que también es más eficiente, ya que se eliminan los procesos adicionales creados por `tr` y la sustitución de comandos.
\subsection{Opciones de tipo y atributo}\label{sec:6.5.3}
Las otras opciones de \texttt{typeset} son más útiles para programadores avanzados de shell que están <<ajustando>> scripts extensos. Estas opciones se enumeran en la Tabla \ref{Tab:6-8}.
\begin{longtable}[h]{|p{2cm}|p{13cm}|}
\caption{Opciones de tipo y atributo de \texttt{typeset}}
\label{Tab:6-8}\\
\hline
\textbf{Opción} & \textbf{Operación} \\ \hline
\endfirsthead
\hline
\textbf{Opción} & \textbf{Operación} \\ \hline
\endhead
-A & Crear un array asociativo \\\hline
-En & Representa la variable internamente como un número de punto flotante de doble precisión; mejora la eficiencia de la aritmética de punto flotante. Si se indica \emph{n}, es el número de cifras significativas que se utilizarán en la salida. Los números grandes se imprimen en notación científica: \texttt{[-]d.ddde±dd}. Los números pequeños se imprimen en notación normal: \texttt{[-]ddd.ddd}. \\\hline
-Fn & Representa la variable internamente como un número de punto flotante de doble precisión; mejora la eficiencia de la aritmética de punto flotante. Si se da \emph{n}, es el número de decimales a utilizar en la salida. Todos los valores se imprimen en notación regular: \texttt{[-]ddd.ddd}. \\\hline
-H & En sistemas que no son Unix, los nombres de archivo de estilo Unix se convierten al formato apropiado para el sistema local. \\\hline
-in & Representa la variable internamente como un entero; mejora la eficiencia de la aritmética de enteros. Si se da \emph{n}, es la base utilizada para la salida. La base por defecto es 10. \\\hline
-n & Crea una variable \emph{nameref} (ver \hyperref[sec:Chapter4]{Capítulo 4}). \\\hline
-p & Cuando se utiliza por sí mismo, imprime declaraciones compuestas (\texttt{typeset}) que describen los atributos de cada una de las variables del shell que tienen atributos establecidos. Con opciones adicionales, solo imprime aquellas variables que tienen el atributo correspondiente establecido. Destinado para volcar el estado del shell en un archivo que luego puede ser utilizado por un shell diferente para recrear el estado original del shell. \\\hline
-r & Hace que la variable sea de solo lectura: prohíbe la asignación a la misma y no permite que sea \emph{desestablecida}. El comando integrado \texttt{readonly} hace lo mismo, pero \texttt{readonly} no puede ser usado para variables locales. \\\hline
-t & <<Etiqueta>> la variable. La lista de variables etiquetadas está disponible mediante \texttt{typeset +t}. Esta opción está obsoleta. \\\hline
-uin & Representa la variable internamente como un entero \emph{sin signo}. Esto se discute más adelante en el \hyperref[sec:ApendiceB]{Apéndice B}. Si se proporciona \emph{n}, es la base utilizada para la salida. La base predeterminada es 10.\tablefootnote{Esta función sólo está disponible en \emph{ksh93m} y posteriores.} \\\hline
-x & Esto hace lo mismo que el comando \texttt{export}, pero \texttt{export} no puede ser utilizado para variables locales. \\\hline
-f & Hace referencia solo a nombres de funciones; consulte la \hyperref[sec:6.5.4]{Sección 6.5.4}, más adelante en este capítulo. \\\hline
\end{longtable}
La opción \texttt{-i} es la más útil. Puedes agregarla a un script cuando hayas terminado de escribir y depurar para hacer que la aritmética se ejecute un poco más rápido, aunque la mejora de velocidad solo será evidente si tu script realiza \emph{mucha} aritmética. La palabra más legible \texttt{integer} es un alias integrado para \texttt{typeset -i}, de modo que \texttt{integer x=5} es lo mismo que \texttt{typeset -i x=5}. De manera similar, la palabra \texttt{float} es un alias predefinido para \texttt{typeset -E}.\footnote{Los programadores de C, C++ y Java pueden encontrar sorprendente la elección de la palabra \emph{float}, ya que internamente el shell usa números de punto flotante de doble precisión. Teorizamos que se eligió el nombre \emph{float} porque su significado es más obvio para el no programador que la palabra \emph{double}}.
La opción \texttt{-r} es útil para configurar <<constantes>> en scripts de shell; las constantes son como variables, excepto que no puedes cambiar sus valores una vez que se han inicializado. Las constantes te permiten dar nombres a los valores incluso si no deseas que se cambien; se considera una buena práctica de programación utilizar constantes con nombres en programas grandes.
La solución para la \hyperref[box:6-3]{Tarea 6-3} contiene un buen candidato para \texttt{typeset -r}: la variable \texttt{numcols}, que especifica el número de columnas en la salida. Dado que \texttt{numcols} es un entero, también podríamos usar la opción \texttt{-i}, es decir, reemplazar \texttt{let numcols=5} con \texttt{typeset -ri numcols=5}. Si intentáramos asignar otro valor a \texttt{numcols}, el shell respondería con el mensaje de error \texttt{ksh: numcols: is read only}.
Estas opciones también son útiles sin argumentos, es decir, para ver qué variables existen que tienen esas opciones activadas.
\subsection{Opciones de función}\label{sec:6.5.4}
La opción \texttt{-f} tiene varias subopciones, todas relacionadas con funciones. Estas se enumeran en la Tabla \ref{Tab:6-9}.
\begin{table}[h]
\center
\caption{Opciones de función de \texttt{typeset}}
\label{Tab:6-9}
\begin{tabular}{|m{2cm}|m{13cm}|} \hline
\textbf{Opción} & \textbf{Operación} \\ \hline
-f & Sin argumentos, imprime todas las definiciones de funciones. \\\hline
-f \emph{fname} & Imprime la definición de la función \emph{fname}. \\\hline
+f & Imprime todos los nombres de funciones. \\\hline
-ft & Activa el modo de rastreo para la(s) función(es) nombrada(s). (\hyperref[sec:Chapter9]{Capítulo 9}) \\\hline
+ft & Desactiva el modo de rastreo para la(s) función(es) nombrada(s). (\hyperref[sec:Chapter9]{Capítulo 9}) \\\hline
-fu & Define los nombres dados como funciones de carga automática. (\hyperref[sec:Chapter4]{Capítulo 4}) \\\hline
\end{tabular}
\end{table}
Dos de estas tienen alias integrados que son más mnemotécnicos: \texttt{functions} (nota la s) es un alias para \texttt{typeset -f} y \texttt{autoload} es un alias para \texttt{typeset -fu}.
Finalmente, si escribes \texttt{typeset} sin argumentos, verás una lista de todas las variables que tienen atributos establecidos (en un orden no discernible), precedidas por las palabras clave apropiadas para cualquier opción de \texttt{typeset} que esté activada. Por ejemplo, escribir \texttt{typeset} en un shell sin personalizar te da una lista de la mayoría de las variables integradas del shell y sus atributos que se ve así:
\begin{lstlisting}[language=bash]
export HISTFILE
integer TMOUT
export FCEDIT
export _AST_FEATURES
export TERM
HISTEDIT
PS2
PS3
integer PPID
export MAIL
export LOGNAME
export EXINIT
integer LINENO
export PATH
integer HISTCMD
export _
export OLDPWD
export PWD
float precision 3 SECONDS
export SHELL
integer RANDOM
export HOME
export VISUAL
export MANPATH
export EDITOR
export ENV
export HISTSIZE
export USER
export LANG
export MORE
integer OPTIND
integer MAILCHECK
export CDPATH
readonly namespace .sh
\end{lstlisting}
Aquí está la salida de \texttt{typeset -p}:
\begin{lstlisting}[language=bash]
typeset -x HISTFILE
typeset -i TMOUT
typeset -x FCEDIT
typeset -x _AST_FEATURES
typeset -x TERM
typeset -x ASIS_DIR
typeset HISTEDIT
typeset PS2
typeset PS3
typeset -i PPID
typeset -x MAIL
typeset -x LOGNAME
typeset -x EXINIT
typeset -i LINENO
typeset -x PATH
typeset -i HISTCMD
typeset -x _
typeset -x OLDPWD
typeset -x PWD
typeset -F 3 SECONDS
typeset -x SHELL
typeset -i RANDOM
typeset -x HOME
typeset -x VISUAL
typeset -x MANPATH
typeset -x EDITOR
typeset -x ENV
typeset -x HISTSIZE
typeset -x USER
typeset -x LANG
typeset -x MORE
typeset -i OPTIND
typeset -i MAILCHECK
typeset -x CDPATH
typeset -r .sh
\end{lstlisting}
El siguiente comando guarda los valores y atributos de todas las variables del shell para un uso posterior:
\begin{lstlisting}[language=bash]
{ set ; typeset -p ;} > varlist
\end{lstlisting}