Aprendiendo_Korn_Shell/Secciones/Capitulo7.tex

1604 lines
116 KiB
TeX

Las últimas series de capítulos se han sumergido en detalles sobre diversas técnicas de programación en el shell, enfocándose principalmente en el flujo de datos y control a través de los programas en el shell. En este capítulo, cambiaremos el enfoque hacia dos temas relacionados. El primero es el mecanismo del shell para realizar entrada y salida orientada a archivos. Presentaremos información que amplía lo que ya sabes sobre los redireccionadores básicos de E/S del shell.
En segundo lugar, nos centraremos en la E/S a nivel de línea y palabra. Este es un tema fundamentalmente diferente, ya que implica mover información entre los dominios de archivos/terminales y variables del shell. \texttt{print} y la sustitución de comandos son dos formas de hacer esto que ya hemos visto.
Nuestra discusión sobre la E/S de línea y palabra nos llevará a una explicación más detallada de cómo el shell procesa las líneas de comandos. Esta información es necesaria para que puedas entender exactamente cómo el shell maneja las \emph{citas} y para que puedas apreciar el poder de un comando avanzado llamado \texttt{eval}, que cubriremos al final del capítulo.
\section{Redireccionadores de E/S}
En el \hyperref[sec:Chapter1]{Capítulo 1} aprendiste sobre los redireccionadores básicos de E/S del shell, \texttt{<}, \texttt{>}, y \texttt{|}. Aunque estos son suficientes para cubrir el 95\% de tu vida en Unix, debes saber que el shell de Korn admite un total de 20 redireccionadores de E/S. La Tabla \ref{Tab:7-1} los enumera, incluyendo los tres que ya hemos visto. Aunque algunos de los demás son útiles, otros son principalmente para programadores de sistemas. Esperaremos hasta el próximo capítulo para discutir los últimos tres, que, junto con \texttt{>|} y \texttt{<<<}, no están presentes en la mayoría de las versiones del shell de Bourne.
\begin{table}[h]
\center
\caption{Redireccionadores de E/S}
\label{Tab:7-1}
\begin{tabular}{|m{4cm}|m{11cm}|} \hline
\textbf{Redireccionador} & \textbf{Función} \\ \hline
> file & Dirigir la salida estándar al archivo \\\hline
< file & Tomar entrada estándar del archivo \\\hline
cmd1 | cmd2 & Tubería; tomar salida estándar de cmd1 como entrada estándar a cmd2 \\\hline
$>>$ file & Dirigir la salida estándar al archivo; añadir al archivo si ya existe \\\hline
>| file & Fuerza la salida estándar a fichero incluso si \emph{noclobber} está activado \\\hline
<> file & Abrir archivo tanto para lectura como para escritura en la entrada estándar\tablefootnote{Normalmente, los archivos abiertos con \texttt{<} se abren en modo de solo lectura.} \\\hline
$<<$ label & \emph{here-document}; ver texto \\\hline
$<<-$ label & \emph{here-document} variante; ver texto \\\hline
$<<<$ label & \emph{here-document} cadena (string); ver texto \\\hline
n> file & Duplicar la entrada estándar del descriptor de archivo n \\\hline
n< file & Establecer archivo como descriptor de archivo de entrada n \\\hline
<\&n & Duplicar la entrada estándar del descriptor de archivo n \\\hline
>\&n & Duplicar salida estándar a descriptor de fichero n \\\hline
<\&n- & Mover el descriptor de fichero n a la entrada estándar \\\hline
>\&n- & Mover el descriptor de fichero n a la salida estándar \\\hline
<\&- & Cerrar la entrada estándar \\\hline
>\&- & Cerrar la salida estándar \\\hline
|\& & Proceso en segundo plano con E/S desde el shell padre \\\hline
n<\&p & Mover la entrada del coproceso al descriptor de fichero n \\\hline
n>\&p & Mover la salida del coproceso al descriptor de fichero n \\\hline
\end{tabular}
\end{table}
Observa que algunos de los redireccionadores en la Tabla \ref{Tab:7-1} contienen un dígito \emph{n} y que sus descripciones contienen el término <<descriptor de archivo>>; cubriremos eso en un momento. (De hecho, cualquier redireccionador que comience con \texttt{<} o \texttt{>} puede usarse con un descriptor de archivo; esto se omite en la tabla por simplicidad).
Los dos primeros redireccionadores nuevos, \texttt{>>} y \texttt{>|}, son variaciones simples del redireccionador de salida estándar \texttt{>}. \texttt{>>} agrega al archivo de salida (en lugar de sobrescribirlo) si ya existe; de lo contrario, actúa exactamente como \texttt{>}. Un uso común de \texttt{>>} es agregar una línea a un archivo de inicialización (como \texttt{.profile} o \texttt{.mailrc}) cuando no quieres molestias con un editor de texto. Por ejemplo:
\begin{lstlisting}[language=bash]
$ cat >> .mailrc
> alias fred frederick@longmachinename.longcompanyname.com
> ^D
$
\end{lstlisting}
Como vimos en el \hyperref[sec:Chapter1]{Capítulo 1}, \texttt{cat} sin argumentos utiliza la entrada estándar como su entrada. Esto te permite escribir la entrada y finalizarla con CTRL-D en su propia línea. La línea de \texttt{alias} se agregará al archivo \texttt{.mailrc} si ya existe; si no existe, se crea el archivo con esa única línea.
Recuerda del \hyperref[sec:Chapter3]{Capítulo 3} que puedes evitar que el shell sobrescriba un archivo con \texttt{> file} escribiendo \texttt{set -o noclobber}. El operador \texttt{>|} anula \texttt{noclobber}, es el redireccionador de <<¡Hazlo de todos modos, demonios!>>.
Los sistemas Unix te permiten abrir archivos en modo solo lectura, solo escritura y lectura/escritura. El redireccionador \texttt{<} abre el archivo de entrada en modo solo lectura; si un programa intenta escribir en la entrada estándar, recibirá un error. De manera similar, el redireccionador \texttt{>} abre el archivo de salida en modo solo escritura; intentar leer desde la salida estándar genera un error. El redireccionador \texttt{<>} abre un archivo para lectura y escritura, por defecto en la entrada estándar. Depende del programa invocado darse cuenta de esto y aprovecharlo, pero es útil en el caso en que un programa pueda querer actualizar datos en un archivo <<en su lugar>>. Este operador se utiliza principalmente para escribir clientes de red; consulta la \hyperref[sec:7.1.4]{Sección 7.1.4}, más adelante en este capítulo, para ver un ejemplo.
\subsection{Here-Documents}
El redireccionador \texttt{<< label} básicamente fuerza que la entrada a un comando sea el texto del programa del shell, que se lee hasta que hay una línea que contiene solo la \emph{etiqueta}. La entrada en el medio se llama un \emph{here-document} (documento aquí). Los \emph{here-documents} no son muy interesantes cuando se usan desde el indicador de comandos. De hecho, es lo mismo que el uso normal de la entrada estándar excepto por la etiqueta. Podríamos haber utilizado un \emph{here-document} en el ejemplo anterior de $>>$, así (EOF, por <<end of file>> o <<fin de archivo>>, es una etiqueta que se usa con frecuencia):
\begin{lstlisting}[language=bash]
$ cat >> .mailrc << EOF
> alias fred frederick@longmachinename.longcompanyname.com
> EOF
$
\end{lstlisting}
Los \emph{here-documents} están destinados a ser utilizados desde scripts de shell; te permiten especificar entrada <<por lotes>> para programas. Un uso común de los \emph{here-documents} es con editores de texto simples como \emph{ed(1)}. La \hyperref[box:7-1]{Tarea 7-1} utiliza un \emph{here-document} de esta manera.
\begin{mybox}[Tarea 7-1]\label{box:7-1}
El comando \texttt{s file} en \emph{mail(1)} guarda el mensaje actual en un \emph{archivo}. Si el mensaje proviene de una red (como Internet), tiene varias líneas de encabezado antepuestas que proporcionan información sobre la ruta de la red. Necesitas esta información porque estás intentando resolver algunos problemas de enrutamiento de red. Escribe un script de shell que extraiga solo las líneas de encabezado del archivo.
\end{mybox}
Podemos usar \emph{ed} para eliminar las líneas del cuerpo, dejando solo el encabezado. Para hacer esto, necesitamos saber algo sobre la sintaxis de los mensajes de correo, específicamente, que siempre hay una línea en blanco entre las líneas de encabezado y el texto del mensaje. El comando \texttt{ed /\^{}\$/,\$d} hace el truco: significa <<Eliminar desde la primera línea en blanco \footnote{La línea tiene que estar completamente vacía; sin espacios ni TABs. Está bien: los encabezados de los mensajes de correo están separados de sus cuerpos exactamente por este tipo de línea en blanco.}
hasta la última línea del archivo>>. También necesitamos los comandos \texttt{ed w} (escribir el archivo modificado) y \texttt{q} (salir). Aquí está el código que resuelve la tarea:
\begin{lstlisting}[language=bash]
ed $1 << \EOF
/^$/,$d
w
q
EOF
\end{lstlisting}
Normalmente, el shell realiza sustitución de parámetros (variables), sustitución de comandos y sustitución aritmética en el texto de un \emph{here-document}, lo que significa que puedes usar variables y comandos del shell para personalizar el texto. Esta evaluación se desactiva si alguna parte del delimitador está entre comillas, como se hizo en el ejemplo anterior. (Esto evita que el shell trate \texttt{\$d} como una sustitución de variable).
A menudo, sin embargo, quieres que el shell realice sus evaluaciones: quizás el uso más común de los \emph{here-documents} es proporcionar plantillas para generadores de formularios o texto de programas para generadores de programas. La \hyperref[box:7-2]{Tarea 7-2} es una tarea sencilla para administradores de sistemas que muestra cómo funciona esto.
\begin{mybox}[Tarea 7-2]\label{box:7-2}
Escribe un script que envíe un mensaje de correo a un conjunto de usuarios informando que se ha instalado una nueva versión de cierto programa en un directorio específico.
\end{mybox}
Puedes obtener una lista de todos los usuarios en el sistema de varias maneras; quizás la más fácil es usar \emph{cut} para extraer el primer campo de /etc/passwd, el archivo que contiene toda la información de la cuenta de usuario. Los campos en este archivo están separados por dos puntos (:).\footnote{Hay algunos problemas posibles con esto; por ejemplo, \texttt{/etc/passwd} generalmente contiene información sobre <<cuentas>> que no están asociadas con personas, como \texttt{uucp, lp} y \texttt{daemon}. Ignoraremos tales problemas para el propósito de este ejemplo.}
Dada una lista de usuarios, el siguiente código hace el truco:
\begin{lstlisting}[language=bash]
pgmname=$1
for user in $(cut -f1 -d: /etc/passwd); do
mail $user << EOF
Dear $user,
A new version of $pgmname has been installed in $(whence pgmname).
Regards,
Your friendly neighborhood sysadmin.
EOF
done
\end{lstlisting}
El shell sustituye los valores apropiados para el nombre del programa y su directorio.
El redireccionador \texttt{<<} tiene dos variaciones. Primero, puedes evitar que el shell realice la sustitución de parámetros, comandos y aritmética al rodear la \emph{etiqueta} con comillas simples o dobles. (En realidad, es suficiente citar solo un carácter en la \emph{etiqueta}.) Vimos esto en la solución para la \hyperref[box:7-1]{Tarea 7-1}.
La segunda variación es \texttt{<<-}, que elimina los TABs iniciales (pero no los espacios) del here-document y de la línea de etiqueta. Esto te permite sangrar el texto del here-document, haciendo que el script del shell sea más legible:
\begin{lstlisting}[language=bash]
pgmname=$1
for user in $(cut -f1 -d: /etc/passwd); do
mail $user <<- EOF
Dear $user,
A new version of $pgmname has been installed in $(whence pgmname).
Regards,
Your friendly neighborhood sysadmin.
EOF
done
\end{lstlisting}
Por supuesto, debes elegir tu \emph{etiqueta} para que no aparezca como una línea de entrada real.
\subsection{Here-Strings}
Un giro común en la programación de shell es usar \texttt{print} para generar un texto que será procesado aún más por uno o más comandos:
\begin{lstlisting}[language=bash]
# empezar con un interrogatorio suave
print -r "$name, $rank, $serial_num" | interrogate -i mild
\end{lstlisting}
Esto podría reescribirse para usar un \emph{here-document} que es ligeramente más eficiente, aunque no necesariamente más fácil de leer:
\begin{lstlisting}[language=bash]
# empezar con un interrogatorio suave
interrogate -i mild << EOF
$name, $rank, $serial_num
EOF
\end{lstlisting}
A partir de \emph{ksh93n}, \footnote{Gracias a David Korn por proporcionarme acceso previo a la versión con esta característica. ADR.}
el shell de Korn proporciona una nueva forma de here-document, usando tres signos de menor que:
\begin{lstlisting}[language=bash]
program <<< WORD
\end{lstlisting}
En esta forma, el texto de WORD (seguido de un salto de línea final) se convierte en la entrada del programa. Por ejemplo:
\begin{lstlisting}[language=bash]
# empezar con un interrogatorio suave
interrogate -i mild <<< "$name, $rank, $serial_num"
\end{lstlisting}
Esta notación se originó por primera vez en la versión de Unix del shell \texttt{rc}, donde se llama <<here string>>. Luego fue adoptada por el shell Z, \emph{zsh} (ver \hyperref[sec:ApendiceA]{Apéndice A}), del cual el shell de Korn lo tomó prestado. Esta notación es simple, fácil de usar, eficiente y visualmente distinguible de los \emph{here-documents} regulares.
\subsection{Descriptores de ficheros}
Los siguientes redireccionadores en la Tabla \ref{Tab:7-1} dependen de la noción de un \emph{descriptor de archivo}. Este es un concepto de E/S (Entrada/Salida) de bajo nivel en Unix que es vital entender al programar en C o C++. Aparece a nivel del shell cuando quieres hacer algo que no involucra la entrada estándar, la salida estándar y el error estándar. Puedes manejarte con algunos hechos básicos sobre ellos; para toda la información, consulta las entradas \emph{open(2), creat(2), read(2), write(2), dup(2), dup2(2), fcntl(2)} y \emph{close(2)} en el manual de Unix. (Como las entradas del manual están dirigidas al programador en C, su relación con los conceptos del shell no será necesariamente obvia).
Los descriptores de archivo son enteros que comienzan en 0 que indexan un array de información de archivos dentro de un proceso. Cuando un proceso se inicia, tiene tres descriptores de archivo abiertos. Estos corresponden a los tres estándares: entrada estándar (descriptor de archivo 0), salida estándar (1) y error estándar (2). Si un proceso abre archivos Unix para entrada o salida, se les asignan los siguientes descriptores de archivo disponibles, comenzando por 3.
De lejos, el uso más común de los descriptores de archivo con el shell de Korn es guardar el error estándar en un archivo. Por ejemplo, si deseas guardar los mensajes de error de un trabajo largo en un archivo para que no desplacen la pantalla, agrega \texttt{2> archivo} a tu comando. Si también deseas guardar la salida estándar, agrega \texttt{> archivo1 2> archivo2}.
Esto nos lleva a la \hyperref[box:7-3]{Tarea 7-3}.
\begin{mybox}[Tarea 7-3]\label{box:7-3}
Quieres iniciar un trabajo largo en segundo plano (para liberar tu terminal) y guardar tanto la salida estándar como el error estándar en un solo archivo de registro. Escribe una función que haga esto.
\end{mybox}
Llamaremos a esta función \texttt{start}. El código es muy conciso:
\begin{lstlisting}[language=bash]
function start {
"$@" > logfile 2>&1 &
}
\end{lstlisting}
Esta línea ejecuta cualquier comando y parámetros que sigan a \texttt{start}. (El comando no puede contener tuberías ni redireccionadores de salida). Primero envía la salida estándar del comando a \texttt{logfile}.
Luego, el redireccionador \texttt{2>\&1} dice: <<Envía el error estándar (descriptor de archivo 2) al mismo lugar que la salida estándar (descriptor de archivo 1)>>. \texttt{2>\&1} es en realidad una combinación de dos redireccionadores en la \hyperref[box:7-1]{Tabla 7-1}: \texttt{n> archivo} y \texttt{>\&n}. Dado que la salida estándar se redirige a \texttt{logfile}, el error estándar también irá allí. El \texttt{\&} final pone el trabajo en segundo plano para que recuperes tu indicador del shell.
Como una pequeña variación en este tema, podemos enviar tanto la salida estándar como el error estándar a una tubería en lugar de un archivo: \texttt{comando 2>\&1 | ...} hace esto. (Por qué esto funciona se describe en breve). Aquí hay una función que envía tanto la salida estándar como el error estándar al archivo de registro (como se mencionó anteriormente) y al terminal:
\begin{lstlisting}[language=bash]
function start {
"$@" 2>&1 | tee logfile &
}
\end{lstlisting}
El comando \emph{tee(1)} toma su entrada estándar y la copia en la salida estándar y en el archivo dado como argumento.
Estas funciones tienen una desventaja: debes permanecer conectado hasta que se complete el trabajo. Aunque siempre puedes escribir \texttt{jobs} (ver \hyperref[sec:Chapter1]{Capítulo 1}) para verificar el progreso, no puedes irte de tu oficina por el día a menos que quieras arriesgarte a una violación de seguridad o desperdiciar electricidad. Veremos cómo resolver este problema en el \hyperref[sec:Chapter8]{Capítulo 8}.
Los demás redireccionadores orientados a descriptores de archivos (por ejemplo, \texttt{<\&n}) se usan generalmente para leer entrada (o escribir salida) desde (o hacia) más de un archivo al mismo tiempo. Veremos un ejemplo más adelante en este capítulo. De lo contrario, están destinados principalmente a programadores de sistemas, al igual que \texttt{<\&-} (forzar el cierre de la entrada estándar) y \texttt{>\&-} (forzar el cierre de la salida estándar), \texttt{<\&n-} (mover el descriptor de archivo n a la entrada estándar) y \texttt{>\&n-} (mover el descriptor de archivo n a la salida estándar).
Finalmente, solo debemos señalar que \texttt{0<} es lo mismo que \texttt{<}, y \texttt{1>} es lo mismo que \texttt{>}. (De hecho, 0 es el valor predeterminado para cualquier operador que comience con \texttt{<}, y 1 es el valor predeterminado para cualquier operador que comience con \texttt{>}).
\subsubsection{Orden de redireccionamiento}
El shell procesa las redirecciones de E/S en un orden específico. Una vez que entiendas cómo funciona esto, puedes aprovecharlo, especialmente para gestionar la disposición de la salida estándar y el error estándar.
Lo primero que hace el shell es configurar la entrada y salida estándar para las canalizaciones según lo indicado por el carácter |. Después de eso, procesa el cambio de descriptores de archivo individuales. Como acabamos de ver, el idioma más común que se beneficia de esto es enviar tanto la salida estándar como el error estándar por la misma canalización a un programa de paginación, como \emph{more} o \emph{less}. \footnote{\emph{less} es un programa de paginación no estándar pero comúnmente disponible que tiene más funciones que \emph{more}.}
\begin{lstlisting}[language=bash]
$ mycommand -h fred -w wilma 2>&1 | more
\end{lstlisting}
En este ejemplo, el shell primero establece la salida estándar de \texttt{mycommand} como la tubería a \emph{more}. Luego redirige el error estándar (descriptor de archivo 2) para que sea igual a la salida estándar (descriptor de archivo 1), es decir, la tubería.
Cuando se trabaja solo con redireccionadores, se procesan de izquierda a derecha, según aparecen en la línea de comandos. Un ejemplo similar al siguiente ha estado en la página del manual del shell desde la versión original 7 de Bourne shell:
\begin{lstlisting}[language=bash]
program > file1 2>&1 # Salida estándar y error estándar a file1
program 2>&1 > file1 # Error estándar al terminal y salida estándar a file1
\end{lstlisting}
En el primer caso, la salida estándar se envía a \texttt{file1} y luego el error estándar se envía al mismo lugar que la salida estándar, es decir, \texttt{file1}. En el segundo caso, el error estándar se envía al mismo lugar que la salida estándar, que sigue siendo el terminal. La salida estándar se redirige luego a \texttt{file1}, pero solo la salida estándar. Si comprendes esto, probablemente sepas todo lo que necesitas saber sobre los descriptores de archivo.
\subsection{Nombres de archivos especiales}\label{sec:7.1.4}
Normalmente, cuando proporcionas una ruta de acceso después de un redireccionador de E/S como \texttt{<} o \texttt{>}, el shell intenta abrir un archivo real con el nombre de archivo dado. Sin embargo, hay dos tipos de rutas de acceso donde el shell trata las rutas de acceso de manera especial.
El primer tipo de ruta de acceso es \texttt{/dev/fd/N}, donde \emph{N} es el número de descriptor de archivo de un archivo que ya está abierto. Por ejemplo:
\begin{lstlisting}[language=bash]
# supongamos que el descriptor de archivo 6 ya está abierto en un archivo
print 'algo significativo' > /dev/fd/6 # igual que 1>&6
\end{lstlisting}
Esto funciona incluso en sistemas que no tienen un directorio \texttt{/dev/fd}. Este tipo de ruta de acceso también se puede utilizar con los varios operadores de prueba de atributos de archivos del comando \texttt{[[...]]}.
El segundo tipo de ruta de acceso permite el acceso a servicios de Internet a través de los protocolos TCP o UDP. Las rutas de acceso son:
\begin{description}
\item[/dev/tcp/host/port] Usando TCP, se conecta al host remoto en el \emph{puerto} remoto. El host puede darse como una dirección IP en notación decimal con puntos (1.2.3.4) o como un nombre de host (\url{www.oreilly.com}). De manera similar, el puerto para el servicio deseado puede ser un nombre simbólico (típicamente encontrado en \texttt{/etc/services}) o un número de puerto numérico.\footnote{La posibilidad de utilizar nombres de host se añadió en \emph{ksh93f}; el uso de nombres de servicio se añadió en \emph{ksh93m}.}
\item[/dev/udp/host/port] Esto es lo mismo, pero usando UDP.
\end{description}
Para usar estos archivos para E/S bidireccional, abre un nuevo descriptor de archivo usando el comando integrado exec (que se describe en el \hyperref[sec:Chapter9]{Capítulo 9}), usando el operador <<leer y escribir>>, \texttt{<>}. Luego utiliza \texttt{read -u} y \texttt{print -u} para leer y escribir en el nuevo descriptor de archivo. (El comando \emph{read} y la opción \texttt{-u} para \emph{read} y \emph{print} se describen más adelante en este capítulo).
El siguiente ejemplo, cortesía de David Korn, muestra cómo hacer esto. Implementa el programa \emph{whois(1)}, que proporciona información sobre el registro de nombres de dominio en Internet:
\begin{lstlisting}[language=bash]
host=rs.internic.net
port=43
exec 3<> /dev/tcp/$host/$port
print -u3 -f "%s\r\n" "$@"
cat <&3
\end{lstlisting}
Usando el comando integrado \texttt{exec} (ver \hyperref[sec:Chapter9]{Capítulo 9}), este programa utiliza el operador <<leer y escribir>>, \texttt{<>}, para abrir una conexión bidireccional al host \emph{rs.internic.net} en el puerto TCP 43, que proporciona el servicio \emph{whois}. (El script también podría haber usado \texttt{port=whois}). Luego utiliza el comando \texttt{print} para enviar las cadenas de argumentos al servidor \texttt{whois}. Finalmente, lee el resultado devuelto usando \emph{cat}. Aquí hay una ejecución de muestra:
\begin{lstlisting}[language=bash]
$ whois.ksh kornshell.com
Whois Server Version 1.3
Domain names in the .com, .net, and .org domains can now be registered
with many different competing registrars. Go to http://www.internic.net
for detailed information.
Domain Name: KORNSHELL.COM
Registrar: NETWORK SOLUTIONS, INC.
Whois Server: whois.networksolutions.com
Referral URL: http://www.networksolutions.com
Name Server: NS4.PAIR.COM
Name Server: NS0.NS0.COM
Updated Date: 02-dec-2001
>>> Last update of whois database: Sun, 10 Feb 2002 05:19:14 EST <<<
The Registry database contains ONLY .COM, .NET, .ORG, .EDU domains and
Registrars.
\end{lstlisting}
La programación de redes está más allá del alcance de este libro. Pero para la mayoría de las cosas, probablemente querrás usar conexiones TCP en lugar de conexiones UDP si escribes algún programa de red en \emph{ksh}.
\section{Cadenas de E/S}
Ahora volveremos al nivel de E/S de cadenas y examinaremos las declaraciones \texttt{print, printf} y \texttt{read}, que proporcionan al shell capacidades de E/S más análogas a los lenguajes de programación convencionales.
\subsection{print}
Como hemos visto innumerables veces en este libro, \texttt{print} simplemente imprime sus argumentos en la salida estándar. Deberías usarlo en lugar del comando echo, cuya funcionalidad difiere de un sistema a otro.\footnote{Específicamente, hay una diferencia entre las versiones System V y BSD. Este último acepta opciones similares a las de \texttt{print}, mientras que el primero acepta secuencias de escape al estilo del lenguaje C.}
(La versión incorporada de \emph{echo} en el shell de Korn emula lo que hace la versión estándar de \texttt{echo} del sistema). Ahora exploraremos el comando \texttt{print} con más detalle.
\subsubsection{Secuencias de escape para print}
\texttt{print} acepta varias opciones, así como varias secuencias de escape que comienzan con una barra invertida. (Debes usar una doble barra invertida si no rodeas la cadena que las contiene con comillas; de lo contrario, la propia shell <<roba>> una barra invertida antes de pasar los argumentos a \texttt{print}). Estas son similares a las secuencias de escape reconocidas por \texttt{echo} y el lenguaje C; se enumeran en la Tabla \ref{Tab:7-2}.
Estas secuencias muestran un comportamiento bastante predecible, excepto \texttt{\textbackslash{}f}. En algunas pantallas, provoca un borrado de pantalla, mientras que en otras provoca un avance de línea. Expulsa la página en la mayoría de las impresoras. \texttt{\textbackslash{}v} está algo obsoleto; generalmente provoca un avance de línea.
\begin{longtable}[h]{|m{3cm}|m{12cm}|}
\caption{Secuencias de escape de print}
\label{Tab:7-2}\\
\hline
\textbf{Secuencia} & \textbf{Caracter impreso} \\ \hline
\endfirsthead
\hline
\textbf{Secuencia} & \textbf{Caracter impreso} \\ \hline
\endhead
\textbackslash{}a & ALERT o CTRL-G \\\hline
\textbackslash{}b & BACKSPACE o CTRL-H \\\hline
\textbackslash{}c & Omitir la nueva línea final y dejar de procesar la cadena \\\hline
\textbackslash{}E & ESCAPE o CTRL-[ \\\hline
\textbackslash{}f & FORMFEED o CTRL-L \\\hline
\textbackslash{}n & Nueva línea (no al final del comando) o CTRL-J \\\hline
\textbackslash{}r & ENTER (RETURN) o CTRL-M \\\hline
\textbackslash{}t & TAB o CTRL-I \\\hline
\textbackslash{}v & VERTICAL TAB o CTRL-K \\\hline
\textbackslash{}0n & Carácter ASCII con valor octal (base-8) \emph{n}, donde \emph{n} tiene de 1 a 3 dígitos. A diferencia de C, C++ y muchos otros lenguajes, se requiere el 0 inicial. \\\hline
\textbackslash{}\textbackslash{} & Barra diagonal inversa simple \\\hline
\end{longtable}
La secuencia \texttt{\textbackslash{}0n} es aún más dependiente del dispositivo y se puede utilizar para E/S compleja, como el control del cursor y caracteres gráficos especiales.
\subsubsection{Opciones para print}
\texttt{print} también acepta algunas opciones con guión; ya hemos visto \texttt{-n} para omitir el salto de línea final. Las opciones se enumeran en la Tabla \ref{Tab:7-3}.
\begin{table}[h]
\center
\caption{Opciones de print}
\label{Tab:7-3}
\begin{tabular}{|m{2cm}|m{13cm}|} \hline
\textbf{Opción} & \textbf{Función} \\ \hline
-e & Procesar secuencias de escape en los argumentos (este es el valor por defecto). \\\hline
-f \emph{format} & Imprime como si fuera mediante \emph{printf} con el formato dado (véase la sección siguiente). \\\hline
-n & Omite la nueva línea final (igual que la secuencia de escape \textbackslash{}c). \\\hline
-p & Imprime en la tubería a la coroutine; ver \hyperref[sec:Chapter8]{Capítulo 8}. \\\hline
-r & Raw; ignora las secuencias de escape listadas arriba. \\\hline
-R & Como -r, pero además ignora cualquier otra opción excepto -n. \\\hline
-s & Imprime en un archivo de historial de comandos (véase el \hyperref[sec:Chapter2]{Capítulo 2}). \\\hline
-un & Imprime en el descriptor de archivo n. \\\hline
\end{tabular}
\end{table}
Observa que algunas de estas son redundantes: \texttt{print -n} es lo mismo que \texttt{print} con \texttt{\textbackslash{}c} al final de una línea; \texttt{print -un ...} es equivalente a \texttt{print ... >\&n} (aunque la primera es ligeramente más eficiente).
Sin embargo, \texttt{print -s} no es lo mismo que \texttt{print ... >> \$HISTFILE}. El último comando deja temporalmente inoperables los modos de edición \emph{vi} y \emph{emacs}; debes usar \texttt{print -s} si deseas imprimir en tu archivo de historial.
Imprimir en tu archivo de historial es útil si deseas editar algo que el shell expande cuando procesa una línea de comandos, por ejemplo, una variable de entorno compleja como PATH. Si ingresas el comando \texttt{print -s PATH=\$PATH}, presionas ENTER y luego presionas CTRL-P en modo emacs (o ESC k en modo \emph{vi}), verás algo como esto:
\begin{lstlisting}[language=bash, escapeinside={(*}{*)}]]
$ (*\highlight{P}*)ATH=/bin:/usr/bin:/etc:/usr/ucb:/usr/local/bin:/home/billr/bin
\end{lstlisting}
Es decir, el shell expande la variable (y cualquier otra cosa, como sustituciones de comandos, comodines, etc.) antes de escribir la línea en el archivo de historial. Tu cursor estará al final de la línea (o al principio de la línea en modo \emph{vi}), y podrás editar tu PATH sin tener que volver a escribir todo.
\subsection{printf}
Si necesitas producir informes formateados, el comando \emph{print} del shell se puede combinar con atributos de formato para variables y así producir datos de salida que se alinean razonablemente. Pero solo puedes hacer tanto con estas facilidades.
La rutina de biblioteca \emph{printf(3)} del lenguaje C proporciona potentes instalaciones de formato para un control total de la salida. Es tan útil que muchos otros lenguajes de programación derivados de Unix, como \emph{awk} y \emph{perl}, admiten instalaciones similares o idénticas. Principalmente debido a que el comportamiento de \emph{echo} en diferentes sistemas Unix no se podía reconciliar y reconociendo la utilidad de \emph{printf}, el estándar de shell POSIX exige un comando \emph{printf} a nivel de shell que proporcione la misma funcionalidad que la rutina de biblioteca \emph{printf(3)}. Esta sección describe cómo funciona el comando \emph{printf} y examina capacidades adicionales únicas de la versión de \emph{printf} del shell de Korn.
El comando \emph{printf} puede emitir una cadena simple al igual que el comando \emph{print}.
\begin{lstlisting}[language=bash]
printf "¡Hola, mundo!\n"
\end{lstlisting}
La principal diferencia que notarás de inmediato es que, a diferencia de \emph{print}, \emph{printf} no suministra automáticamente un salto de línea. Debes especificarlo explícitamente como \texttt{\textbackslash{}n}.
La sintaxis completa del comando \emph{printf} tiene dos partes:
\begin{lstlisting}[language=bash]
printf cadena-de-formato [argumentos ...]
\end{lstlisting}
La primera parte es una cadena que describe las especificaciones de formato; esto se suministra mejor como una constante de cadena entre comillas. La segunda parte es una lista de argumentos, como una lista de cadenas o valores de variables, que corresponden a las especificaciones de formato. (Si hay más argumentos que especificaciones de formato, \emph{ksh} recorre las especificaciones de formato en la cadena de formato, reutilizándolas en orden, hasta que se complete). Una especificación de formato se precede de un signo de porcentaje (\%), y el especificador es uno de los caracteres que se describen en breve. Dos de los principales especificadores de formato son \texttt{\%s} para cadenas y \texttt{\%d} para enteros decimales.
La cadena de formato combina texto para ser emitido literalmente con especificaciones que describen cómo formatear los argumentos subsiguientes en la línea de comandos de \emph{printf}. Por ejemplo:
\begin{lstlisting}[language=bash]
$ printf "¡Hola, %s!\n" Mundo
¡Hola, Mundo!
\end{lstlisting}
Dado que el comando \emph{printf} está incorporado, no estás limitado a números absolutos:
\begin{lstlisting}[language=bash]
$ printf "La respuesta es %d.\n" 12+10+20
La respuesta es 42.
\end{lstlisting}
Los especificadores permitidos se muestran en la Tabla \ref{Tab:7-4}.
\begin{table}[h]
\center
\caption{Especificadores de formato utilizados en \emph{printf}}
\label{Tab:7-4}
\begin{tabular}{|m{3cm}|m{12cm}|} \hline
\textbf{Especificador} & \textbf{Descripción} \\ \hline
\%c & Carácter ASCII (imprime el primer carácter del argumento correspondiente) \\\hline
\%d & Número entero decimal \\\hline
\%i & Número entero decimal \\\hline
\%e & Formato de coma flotante ([-]d.precisióne[+-]dd) (véase el texto siguiente para el significado de la precisión) \\\hline
\%E & Formato de coma flotante ([-]d.precisiónE[+-]dd) \\\hline
\%f & Formato de coma flotante ([-]ddd.precisión) \\\hline
\%g & Conversión \%e o \%f, la que sea más corta, sin ceros al final \\\hline
\%G & Conversión \%E o \%f, la que sea más corta, sin ceros al final. \\\hline
\%o & Valor octal sin signo \\\hline
\%s & Cadena (string) \\\hline
\%u & Valor decimal sin signo \\\hline
\%x & Número hexadecimal sin signo. Usa a-f para 10 al 15 \\\hline
\%X & Número hexadecimal sin signo. Usa A-F para 10 al 15 \\\hline
\%\% & \% literal \\\hline
\end{tabular}
\end{table}
El comando \emph{printf} se puede utilizar para especificar el ancho y la alineación de los campos de salida. Una expresión de formato puede tener tres modificadores opcionales que siguen a \% y preceden al especificador de formato:
\begin{lstlisting}[language=bash]
%flags width.precision format-specifier
\end{lstlisting}
El \emph{ancho} del campo de salida es un valor numérico. Cuando especificas un ancho de campo, el contenido del campo se justifica a la derecha de forma predeterminada. Debes especificar una bandera de <<->> para obtener justificación a la izquierda. (El resto de las banderas se discuten en breve). Por lo tanto, <<\%-20s>> imprime una cadena justificada a la izquierda en un campo de 20 caracteres de ancho. Si la cadena tiene menos de 20 caracteres, el campo se rellena con espacios en blanco para completar. En los siguientes ejemplos, se imprime un | para indicar el ancho real del campo. El primer ejemplo justifica el texto a la derecha:
\begin{lstlisting}[language=bash]
printf "|%10s|\n" hello
\end{lstlisting}
Produce:
\begin{lstlisting}[language=bash]
| hello|
\end{lstlisting}
El siguiente ejemplo justifica el texto a la izquierda:
\begin{lstlisting}[language=bash]
printf "|%-10s|\n" hello
\end{lstlisting}
Produce:
\begin{lstlisting}[language=bash]
|hello |
\end{lstlisting}
El modificador de \emph{precisión}, utilizado para valores decimales o de punto flotante, controla la cantidad de dígitos que aparecen en el resultado. Para valores de cadena, controla el número máximo de caracteres de la cadena que se imprimirán.
Puedes especificar tanto el \emph{ancho} como la \emph{precisión} dinámicamente, mediante valores en la lista de argumentos de \emph{printf}. Haces esto especificando asteriscos, en lugar de valores literales.
\begin{lstlisting}[language=bash]
$ myvar=42.123456
$ printf "|%*.*G|\n" 5 6 $myvar
|42.1235|
\end{lstlisting}
En este ejemplo, el ancho es 5, la precisión es 6 y el valor a imprimir proviene del valor de \texttt{myvar}.
La \emph{precisión} es opcional. Su significado exacto varía según la letra de control, como se muestra en la Tabla \ref{Tab:7-5}:
\begin{longtable}[h]{|p{4.5cm}|p{10.5cm}|}
\caption{Significado de la precisión}
\label{Tab:7-5}\\
\hline
\textbf{Conversión} & \textbf{Significado de la presición} \\ \hline
\endfirsthead
\hline
\textbf{Conversión} & \textbf{Significado de la presición} \\ \hline
\endhead
\%d, \%i, \%o, \%u, \%x, \%X & El número mínimo de dígitos a imprimir. Cuando el valor tiene menos dígitos, se rellena con ceros a la izquierda. La precisión por defecto es 1. \\\hline
\%e, \%E & El número mínimo de dígitos a imprimir. Cuando el valor tiene menos dígitos, se rellena con ceros después del punto decimal. La precisión por defecto es 10. Una precisión de 0 inhibe la impresión del punto decimal. \\\hline
\%f & El número de dígitos a la derecha del punto decimal. \\\hline
\%g, \%G & El número máximo de dígitos significativos. \\\hline
\%s & El número máximo de caracteres a imprimir. \\\hline
\end{longtable}
Finalmente, una o más \emph{banderas} pueden preceder al ancho del campo y la precisión. Ya hemos visto la bandera <<->> para la justificación a la izquierda. El resto de las banderas se muestran en la Tabla \ref{Tab:7-6}.
\begin{table}[h]
\center
\caption{Banderas para printf}
\label{Tab:7-6}
\begin{tabular}{|m{3cm}|m{12cm}|} \hline
\textbf{Carácter} & \textbf{Descripción} \\ \hline
- & Justifica a la izquierda el valor formateado dentro del campo. \\\hline
space & Anteponga un espacio a los valores positivos y un signo menos a los negativos. \\\hline
+ & Anteponga siempre un signo a los valores numéricos, aunque el valor sea positivo. \\\hline
\# & Utilice una forma alternativa: \%o tiene un 0 precedente; \%x y \%X están prefijados con 0x y 0X, respectivamente; \%e, \%E y \%f siempre tienen un punto decimal en el resultado; y \%g y \%G no tienen ceros finales eliminados. \\\hline
0 & Rellenar la salida con ceros, no con espacios. Esto sólo ocurre cuando la anchura del campo es mayor que el resultado convertido. En el lenguaje C, esta bandera se aplica a todos los formatos de salida, incluso a los no numéricos. Para \emph{ksh}, sólo se aplica a los formatos numéricos. \\\hline
\end{tabular}
\end{table}
Si \emph{printf} no puede realizar una conversión de formato, devuelve un estado de salida no nulo.
Similar a \emph{print}, el comando \emph{printf} incorporado interpreta secuencias de escape dentro de la cadena de formato. Sin embargo, \emph{printf} acepta un rango más amplio de secuencias de escape; son las mismas que para la cadena \texttt{\$'...'}. Estas secuencias se enumeran más adelante en la Tabla \ref{Tab:7-9}.
\subsection{Especificadores adicionales de printf en el shell de Korn}
Además de los especificadores estándar recién descritos, el shell de Korn acepta varios especificadores adicionales. Estos proporcionan funciones útiles a expensas de la no portabilidad a otras versiones del comando \emph{printf}.
\begin{description}
\item[\$b] Cuando se utiliza en lugar de \%s, expande las secuencias de escape de estilo de impresión en la cadena del argumento. Por ejemplo:
\begin{lstlisting}[language=bash]
$ printf "%s\n" 'hola\nmundo'
hola\nmundo
$ printf "%b\n" 'hola\nmundo'
hola
mundo
\end{lstlisting}
\item[\%H] Cuando se utiliza en lugar de \%s, muestra los caracteres especiales HTML y XML como sus correspondientes nombres de entidad. Por ejemplo:
\begin{lstlisting}[language=bash]
$ printf "%s\n" "Aquí hay caracteres < y > reales"
Aquí hay caracteres < y > reales
$ printf "%H\n" "Aquí hay caracteres < y > reales"
Aquí&nbsp;hay&nbsp;caracteres&nbsp;&lt;&nbsp;y&nbsp;&gt;&nbsp;reales
\end{lstlisting}
Curiosamente, los espacios se convierten en \&nbsp;, el carácter de espacio literal irrompible de HTML y XML.
\item[\%n] Esto es un préstamo de ISO C. Coloca el número de caracteres escritos hasta el momento en la variable dada. Esto es posible ya que \emph{printf} está incorporado en el shell.
\begin{lstlisting}[language=bash]
$ printf "hola, mundo\n%n" msglen
hola, mundo
$ print $msglen
13
\end{lstlisting}
\item[\%P] Cuando se utiliza en lugar de \%s, traduce la expresión regular extendida de estilo \emph{egrep} a un patrón de shell de Korn equivalente. Por ejemplo:
\begin{lstlisting}[language=bash]
$ printf "%P\n" '(.*\.o|.*\.obj|core)+'
*+(*\.o|*\.obj|core)*
\end{lstlisting}
\item[\%q] Cuando se utiliza en lugar de \%s, imprime el argumento de cadena entre comillas de forma que pueda reutilizarse posteriormente dentro de un script de shell. Por ejemplo:
\begin{lstlisting}[language=bash]
$ printf "print %q\n" "una cadena con ' y \" en ella"
print $'una cadena con \' y " en ella'
\end{lstlisting}
(La notación \texttt{\$'...'} se explica en la \hyperref[sec:7.3.3.1]{Sección 7.3.3.1}, más adelante en este capítulo).
\item[\%R] Va a la inversa de \%P, traduciendo patrones en expresiones regulares extendidas. Por ejemplo:
\begin{lstlisting}[language=bash]
$ printf "%R\n" '+(*.o|*.c)'
^(.*\.o|.*\.c)+$
\end{lstlisting}
\item[\%(date format)T] El formato de \emph{date} es una cadena de comandos de fecha similar a la de \emph{date(1)}. El argumento es una cadena que representa una fecha y una hora. \emph{ksh} convierte la cadena de fecha dada en la hora que representa y luego la reformatea según el formato de \emph{date(1)} que usted suministre. \emph{ksh} acepta una amplia variedad de formatos de fecha y hora. Por ejemplo:
\begin{lstlisting}[language=bash]
$ date
Wed Jan 30 15:46:01 IST 2002
$ printf "%(Ahora es %m/%d/%Y %H:%M:%S)T\n" "$(date)"
Ahora es 30/01/2002 15:46:07
\end{lstlisting}
Los sistemas Unix mantienen el tiempo en <<segundos desde Epoch>>. \emph{Epoch} es la medianoche del 1 de enero de 1970, UTC. Si dispone de un valor de tiempo en este formato, puede utilizarlo con el especificador de conversión \%T precediéndolo de un carácter \#, así:
\begin{lstlisting}[language=bash]
$ printf "%(It is now %m/%d/%Y %H:%M:%S)T\n" '#1012398411'
Ahora es 30/01/2002 15:46:51
\end{lstlisting}
\item[\%z] Imprime un byte cuyo valor es cero.
\end{description}
Por último, para el formato \%d, después de la precisión puede suministrar un punto adicional y un número que indique la base de salida:
\begin{lstlisting}[language=bash]
$ printf '42 es %.3.5d en base 5\n' 42
42 es 132 en base 5
\end{lstlisting}
\subsection{read}
La otra cara de las facilidades de E/S de cadena del shell es el comando \emph{read}, que te permite leer valores en variables del shell. La sintaxis básica es:
\begin{lstlisting}[language=bash]
read var1 var2 ...
\end{lstlisting}
Hay algunas opciones, que cubrimos en la \hyperref[sec:7.2.3.5]{Sección 7.2.3.5}, más adelante en este capítulo. Esta instrucción toma una línea de la entrada estándar y la descompone en palabras delimitadas por cualquiera de los caracteres en el valor de la variable \texttt{IFS} (ver \hyperref[sec:Chapter4]{Capítulo 4}; estos suelen ser un espacio, un TAB y un salto de línea). Las palabras se asignan a las variables \emph{var1, var2,} etc. Por ejemplo:
\begin{lstlisting}[language=bash]
$ read fred bob
dave pete
$ print "$fred"
dave
$ print "$bob"
pete
\end{lstlisting}
Si hay más palabras que variables, las palabras excedentes se asignan a la última variable. Si omites las variables por completo, toda la línea de entrada se asigna a la variable \texttt{REPLY}.
Puede que hayas identificado esto como el ingrediente que falta en las capacidades de programación de scripts del shell que hemos visto hasta ahora. Se asemeja a las declaraciones de entrada en lenguajes convencionales, como su homónimo en Pascal. Entonces, ¿por qué esperamos tanto para introducirlo?
En realidad, \emph{read} es una especie de escape de la filosofía tradicional de programación de scripts del shell, que dicta que la unidad de datos más importante para procesar es un \emph{archivo de texto} y que se deben utilizar utilidades de Unix como \emph{cut, grep, sort,} etc., como bloques de construcción para escribir programas.
\emph{read}, por otro lado, implica procesamiento línea por línea. Podrías usarlo para escribir un script del shell que haga lo que normalmente haría una tubería de utilidades, pero tal script inevitablemente se vería así:
\begin{lstlisting}[language=bash]
while (read a line) do
procesar la línea
imprimir la línea procesada
end
\end{lstlisting}
Este tipo de script suele ser mucho más lento que una tubería; además, tiene la misma forma que un programa que alguien podría escribir en C (o un lenguaje similar) que hace lo mismo mucho, \emph{mucho} más rápido. En otras palabras, si vas a escribirlo de esta manera línea por línea, no tiene sentido escribir un script del shell. (Los autores han pasado años sin escribir un script con \emph{read} en él).
\subsubsection{Lectura de líneas de ficheros}
Sin embargo, los scripts de shell con \emph{read} son útiles para ciertos tipos de tareas. Uno de ellos es cuando estás leyendo datos de un archivo lo suficientemente pequeño como para que la eficiencia no sea una preocupación (digamos unas pocas cientos de líneas o menos), y realmente es necesario colocar fragmentos de entrada en variables del shell.
Una tarea que ya hemos visto se ajusta a esta descripción: la \hyperref[box:5-4]{Tarea 5-4}, el script que un administrador del sistema podría usar para establecer la variable de entorno TERM de un usuario según la línea de terminal que esté utilizando. El código en el \hyperref[sec:Chapter5]{Capítulo 5} utilizó una declaración \texttt{case} para seleccionar el valor correcto para \texttt{TERM}.
Este código presumiblemente residiría en \texttt{/etc/profile}, el archivo de inicialización del sistema que el shell de Korn ejecuta antes de ejecutar el \emph{.profile} de un usuario. Si las terminales en el sistema cambian con el tiempo, como seguramente lo harán, entonces el código tendría que cambiarse. Sería mejor almacenar la información en un archivo y cambiar solo el archivo en su lugar.
Supongamos que colocamos la información en un archivo cuyo formato es típico de los archivos de <<configuración del sistema>> de Unix: cada línea contiene un nombre de dispositivo, un TAB y un valor de TERM. Si el archivo, que llamaremos \texttt{/etc/terms}, contenía los mismos datos que la declaración \texttt{case} en el \hyperref[sec:Chapter5]{Capítulo 5}, se vería así:
\begin{lstlisting}[language=bash]
console s531
tty01 gl35a
tty03 gl35a
tty04 gl35a
tty07 t2000
tty08 s531
\end{lstlisting}
Podemos usar \emph{read} para obtener los datos de este archivo, pero primero necesitamos saber cómo probar la condición de fin de archivo. Simple: el estado de salida de \emph{read} es 1 (es decir, diferente de cero) cuando no hay nada que leer. Esto nos lleva a un bucle \texttt{while} limpio:
\begin{lstlisting}[language=bash]
TERM=vt99 # asumir esto como un valor predeterminado
line=$(tty)
while read dev termtype; do
if [[ $dev == $line ]]; then
TERM=$termtype
export TERM
print "TERM se estableció en $TERM."
break
fi
done
\end{lstlisting}
El bucle \texttt{while} lee cada línea de la entrada en las variables \texttt{dev} y \texttt{termtype}. En cada paso a través del bucle, el \texttt{if} busca una coincidencia entre \texttt{\$dev} y la tty del usuario (\texttt{\$line}, obtenida por la sustitución de comandos desde el comando \emph{tty}). Si se encuentra una coincidencia, se establece \texttt{TERM} y se exporta, se imprime un mensaje y el bucle sale; de lo contrario, \texttt{TERM} permanece en el valor predeterminado de \texttt{vt99}.
Aún no hemos terminado: ¡este código lee desde la entrada estándar, no desde \texttt{/etc/terms}! Necesitamos saber cómo redirigir la entrada a múltiples comandos. Hay varias maneras de hacer esto.
\subsubsection{Redirección de E/S y comandos múltiples}
Una forma de resolver el problema es con un subproceso, como veremos en el próximo capítulo. Esto implica crear un proceso separado para realizar la lectura. Sin embargo, generalmente es más eficiente hacerlo en el mismo proceso; el shell de Korn nos ofrece tres formas de hacer esto.
La primera, que ya hemos visto, es con una función:
\begin{lstlisting}[language=bash]
function findterm {
TERM=vt99 # asumir esto como un valor predeterminado
line=$(tty)
while read dev termtype; do
if [[ $dev == $line ]]; then
TERM=$termtype
export TERM
print "TERM se estableció en $TERM."
break
fi
done
}
findterm < /etc/terms
\end{lstlisting}
Una función actúa como un script en el sentido de que tiene su propio conjunto de descriptores de E/S estándar, que se pueden redirigir en la línea de código que llama a la función. En otras palabras, puedes pensar en este código como si \emph{findterm} fuera un script y escribieras \texttt{findterm < /etc/terms} en la línea de comandos. La declaración \emph{read} toma la entrada de \texttt{/etc/terms} línea por línea, y la función se ejecuta correctamente.
La segunda forma es colocando el redireccionador de E/S al final del bucle, de esta manera:
\begin{lstlisting}[language=bash]
TERM=vt99 # asumir esto como un valor predeterminado
line=$(tty)
while read dev termtype; do
if [[ $dev == $line ]]; then
TERM=$termtype
export TERM
print "TERM se estableció en $TERM."
break
fi
done < /etc/terms
\end{lstlisting}
Puedes usar esta técnica con cualquier construcción de control de flujo, incluyendo \texttt{if...fi, case...esac, for...done, select...done} y \texttt{until...done}. Esto tiene sentido porque todas estas son \emph{declaraciones compuestas} que el shell trata como comandos únicos para estos propósitos. Esta técnica funciona bien, ya que el comando \emph{read} lee una línea a la vez, siempre y cuando toda la entrada se realice dentro de la declaración compuesta.
Colocar el redireccionador de E/S al final es especialmente importante para que los bucles funcionen correctamente. Supongamos que colocas el redireccionador después del comando `read`, así:
\begin{lstlisting}[language=bash]
while read dev termtype < /etc/terms
do
...
done
\end{lstlisting}
En este caso, el shell vuelve a abrir \texttt{/etc/terms} cada vez que entra en el bucle, leyendo la primera línea una y otra vez. Esto crea efectivamente un bucle infinito, algo que probablemente no deseas.
\subsubsection{Bloques de código}
En ocasiones, es posible que desees redirigir la entrada/salida hacia o desde un grupo arbitrario de comandos sin crear un proceso separado. Para hacer eso, necesitas utilizar una construcción que aún no hemos visto. Si rodeas un código con \{ y \},\footnote{Por razones sintácticas oscuras e históricas, las llaves son \emph{palabras clave} del shell. En la práctica, esto significa que el cierre de \} debe ir precedido por un salto de línea o un punto y coma. ¡Caveat emptor!}
el código se comportará como una función sin nombre. Este es otro tipo de declaración compuesta. De acuerdo con el concepto equivalente en el lenguaje C, llamaremos a esto un bloque de código.\footnote{Los programadores de LISP pueden preferir pensar en esto como una \emph{función anónima} o \emph{función lambda}.}
¿Para qué sirve un bloque? En este caso, significa que el código dentro de las llaves (\{ \}) tomará descriptores de entrada/salida estándar tal como lo describimos para las funciones. Esta construcción también es apropiada para el ejemplo actual porque el código solo necesita ser llamado una vez, y el script completo no es lo suficientemente grande como para justificar descomponerlo en funciones. Así es como usamos un bloque en el ejemplo:
\begin{lstlisting}[language=bash]
{
TERM=vt99 # asumir esto como un valor predeterminado
line=$(tty)
while read dev termtype; do
if [[ $dev == $line ]]; then
TERM=$termtype
export TERM
print "TERM se estableció en $TERM."
break
fi
done
} < /etc/terms
\end{lstlisting}
Para ayudarte a entender cómo funciona esto, piensa en las llaves y el código dentro de ellas como si fueran un solo comando, es decir:
\begin{lstlisting}[language=bash]
{ TERM=vt99; line=$(tty); while ... ; } < /etc/terms
\end{lstlisting}
Los archivos de configuración para tareas de administración del sistema como esta son bastante comunes; un ejemplo prominente es \texttt{/etc/hosts}, que enumera las máquinas que son accesibles en una red TCP/IP. Podemos hacer que \texttt{/etc/terms} sea más parecido a estos archivos estándar al permitir líneas de comentarios en el archivo que comiencen con \#, al igual que en los scripts de shell. De esta manera, \texttt{/etc/terms} puede verse así:
\begin{lstlisting}[language=bash]
#
# La consola del sistema es una Shande 531s
console s531
#
# La línea del Prof. Subramaniam tiene un Givalt GL35a
tty01 gl35a
...
\end{lstlisting}
Podemos manejar las líneas de comentarios de dos maneras. Primero, podríamos modificar el bucle \texttt{while} para que ignore las líneas que comienzan con \#. Aprovecharíamos el hecho de que los operadores de igualdad y desigualdad (== y !=) dentro de [[...]] realizan coincidencia de patrones, no solo pruebas de igualdad:
\begin{lstlisting}[language=bash]
if [[ $dev != \#* && $dev == $line ]]; then
...
\end{lstlisting}
El patrón es \#*, que coincide con cualquier cadena que comience con \#. Debemos preceder \# con una barra invertida para que el shell no trate el resto de la línea como un comentario. Además, recuerda del \hyperref[sec:Chapter5]{Capítulo 5} que \&\& combina las dos condiciones de manera que ambas deben ser verdaderas para que toda la condición sea verdadera.
Esto ciertamente funcionaría, pero la forma habitual de filtrar las líneas de comentarios es usar una canalización con \emph{grep}. Le proporcionamos a \emph{grep} la expresión regular \^{}[\^{}\#], que coincide con cualquier cosa excepto líneas que comienzan con \#. Luego, cambiamos la llamada al bloque para que lea desde la salida de la canalización en lugar de leer directamente desde el archivo.\footnote{Desafortunadamente, usar \emph{read} con entrada desde una tubería a menudo es muy ineficiente, debido a problemas en el diseño del shell que no son relevantes aquí.}
\begin{lstlisting}[language=bash]
grep "^[^#]" /etc/terms | {
TERM=vt99
...
}
\end{lstlisting}
También podemos usar \emph{read} para mejorar nuestra solución a la \hyperref[box:6-3]{Tarea 6-3}, en la que emulamos la salida en varias columnas de \emph{ls}. En la solución del capítulo anterior, asumimos, para simplificar, que los nombres de archivo están limitados a 14 caracteres y usamos 14 como ancho de columna fijo. Mejoraremos la solución para que permita \emph{cualquier} longitud de nombre de archivo (como en las versiones modernas de Unix) y utilice la longitud del nombre de archivo más largo (más 2) como el ancho de la columna.
Para mostrar la lista de archivos en formato de varias columnas, necesitamos leer la salida de \emph{ls} dos veces. En el primer paso, encontramos el nombre de archivo más largo y lo usamos para establecer el número de columnas y su ancho; el segundo paso realiza la salida real. Aquí hay un bloque de código para el primer paso:
\begin{lstlisting}[language=bash]
ls "$@" | {
let width=0
while read fname; do
if (( ${#fname} > $width )); then
let width=${#fname}
fi
done
let "width += 2"
let numcols="int(${COLUMNS:-80} / $width)"
}
\end{lstlisting}
Este código parece un ejercicio de una clase de programación del primer semestre. El bucle \texttt{while} recorre la entrada en busca de archivos con nombres más largos que el más largo encontrado hasta ahora; si se encuentra uno más largo, su longitud se guarda como la nueva longitud más larga.
Después de que el bucle termina, agregamos 2 al ancho para permitir espacio entre columnas. Luego dividimos el ancho del terminal por el ancho de la columna para obtener el número de columnas. Como el shell realiza la división en punto flotante, el resultado se pasa a la función \emph{int} para producir un resultado final entero. Recuerda del \hyperref[sec:Chapter3]{Capítulo 3} que la variable integrada \texttt{COLUMNS} a menudo contiene el ancho de visualización; la construcción \texttt{\$\{COLUMNS:-80\}} da un valor predeterminado de 80 si esta variable no está configurada.
Los resultados del bloque son las variables \texttt{width} y \texttt{numcols}. Estas son variables globales, por lo que son accesibles por el resto del código dentro de nuestro script (eventual). En particular, las necesitamos en nuestra segunda pasada por los nombres de archivo. El código para esto se asemeja al código de nuestra solución original; todo lo que necesitamos hacer es reemplazar el ancho de columna fijo y el número de columnas con las variables:
\begin{lstlisting}[language=bash]
set -A filenames $(ls "$@")
typeset -L$width fname
let count=0
while (( $count < ${#filenames[*]} )); do
fname=${filenames[$count]}
print "$fname \c"
let count++
if [[ $((count % numcols)) == 0 ]]; then
print # produce una nueva línea
fi
done
if (( count % numcols != 0 )); then
print
fi
\end{lstlisting}
El script completo consiste en ambas piezas de código. Como otro <<ejercicio para el lector>>, considera cómo podrías reorganizar el código para invocar el comando \emph{ls} solo una vez. (Pista: usa al menos un bucle aritmético \texttt{for}).
\subsubsection{Lectura de la entrada del usuario}
El otro tipo de tarea para la cual \emph{read} es adecuado es solicitar la entrada del usuario. Piénsalo: apenas hemos visto scripts de este tipo hasta ahora en este libro. De hecho, los únicos fueron las soluciones modificadas para la \hyperref[box:5-4]{Tarea 5-4}, que involucraba \texttt{select}.
Como probablemente habrás deducido, \emph{read} se puede utilizar para obtener la entrada del usuario en variables del shell. Podemos usar \emph{print} para solicitar al usuario, así:
\begin{lstlisting}[language=bash]
print -n '¿terminal? '
read TERM
print "TERM es $TERM"
\end{lstlisting}
Así es como se ve cuando se ejecuta:
\begin{lstlisting}[language=bash]
¿terminal? vt99
TERM es vt99
\end{lstlisting}
Sin embargo, para que las solicitudes no se pierdan en una canalización, la convención del shell dicta que las solicitudes deben ir a la salida de error estándar, no a la salida estándar. (Recuerda que \texttt{select} solicita a la salida de error estándar). Podríamos simplemente usar el descriptor de archivo 2 con el redireccionador de salida que vimos anteriormente en este capítulo:
\begin{lstlisting}[language=bash]
print -n '¿terminal? ' >&2
read TERM
print "TERM es $TERM"
\end{lstlisting}
El shell proporciona una mejor manera de hacer lo mismo: si sigues el primer nombre de variable en una declaración \emph{read} con un signo de interrogación (?) y una cadena, el shell utiliza esa cadena como un indicador en la salida de error estándar. En otras palabras:
\begin{lstlisting}[language=bash]
read TERM?'¿terminal? '
print "TERM es $TERM"
\end{lstlisting}
hace lo mismo que lo anterior. La forma del shell es mejor por las siguientes razones. Primero, esto se ve un poco mejor; en segundo lugar, el shell sabe que no debe generar el indicador si la entrada se redirige desde un archivo; y finalmente, este esquema te permite usar el modo \emph{vi} o \emph{emacs} en tu línea de entrada.
Ampliaremos este ejemplo simple mostrando cómo se haría la \hyperref[box:5-4]{Tarea 5-4} si no existiera \texttt{select}. Compara esto con el código en el \hyperref[sec:Chapter6]{Capítulo 6}:
\begin{lstlisting}[language=bash]
set -A termnames gl35a t2000 s531 vt99
print 'Selecciona tu tipo de terminal:'
while true; do
{
print '1) gl35a'
print '2) t2000'
print '3) s531'
print '4) vt99'
} >&2
read REPLY?'¿terminal? '
if (( REPLY >= 1 && REPLY <= 4 )); then
TERM=${termnames[REPLY-1]}
print "TERM es $TERM"
export TERM
break
fi
done
\end{lstlisting}
El bucle \texttt{while} es necesario para que el código se repita si el usuario hace una elección no válida.
Esto es aproximadamente el doble de líneas de código que la primera solución en el \hyperref[sec:Chapter5]{Capítulo 5}, ¡pero exactamente igual que la versión posterior, más amigable para el usuario! Esto muestra que \texttt{select} te ahorra código solo si no te importa usar las mismas cadenas para mostrar las opciones de tu menú que usas dentro de tu script.
Sin embargo, \texttt{select} tiene otras ventajas, como la capacidad de construir menús de varias columnas si hay muchas opciones y un mejor manejo de la entrada de usuario vacía.
\subsubsection{Opciones para \emph{read}}\label{sec:7.2.3.5}
\emph{read} acepta un conjunto de opciones que son similares a las de \emph{print}. La Tabla \ref{Tab:7-7} las enumera.
\begin{table}[h]
\center
\caption{Opciones de \texttt{read}}
\label{Tab:7-7}
\begin{tabular}{|m{4cm}|m{11cm}|} \hline
\textbf{Opción} & \textbf{Función} \\ \hline
-A & Lee palabras en una matriz indexada, empezando por el índice 0. Despliega primero todos los elementos de la matriz. \\\hline
-d \emph{delimitador} & Leer hasta el carácter delimitador, en lugar del carácter por defecto, que es una nueva línea. \\\hline
-n \emph{número} & Leer como máximo el número de bytes\tablefootnote{Esta opción fue añadida en \emph{ksh93e}.} \\\hline
-p & Lectura de la tubería a la coroutine; ver \hyperref[sec:Chapter8]{Capítulo 8}. \\\hline
-r & Crudo; no utilice \textbackslash{} como carácter de continuación de línea. \\\hline
-s & Guardar la entrada en el archivo de historial de comandos; véase el \hyperref[sec:Chapter1]{Capítulo 1}. \\\hline
-t \emph{n segundos} & Espera hasta \emph{n segundos} a que llegue la entrada. Si transcurren \emph{n segundos}, devuelve un estado de salida de fallo. \\\hline
-u\emph{n} & Lectura del descriptor de archivo n. \\\hline
\end{tabular}
\end{table}
Tener que escribir \texttt{read word[0] word[1] word[2] ...} para leer palabras en una matriz es tedioso. También propenso a errores; si el usuario escribe más palabras de las variables de la matriz proporcionadas, las palabras restantes se asignan todas a la última variable de la matriz. La opción \texttt{-A} resuelve esto, leyendo cada palabra una a la vez en las entradas correspondientes de la matriz nombrada.
La opción \texttt{-d} te permite leer hasta algún otro carácter que no sea una nueva línea. En términos prácticos, probablemente nunca necesitarás hacer esto, pero el shell quiere hacerlo posible por si alguna vez lo necesitas.
De manera similar, la opción \texttt{-n} te libera del modo predeterminado de \emph{read} que consume la entrada línea por línea; te permite leer un número fijo de bytes. Esto es muy útil si estás procesando datos heredados de ancho fijo, aunque esto no es muy común en sistemas Unix.
\emph{read} te permite ingresar líneas que son más largas que el ancho de tu dispositivo de visualización mediante el uso de la barra invertida (\textbackslash{}) como un carácter de continuación, al igual que en los scripts de shell. La opción \texttt{-r} anula esto, en caso de que tu script lea desde un archivo que pueda contener líneas que terminan en barras invertidas.
\texttt{read -r} también preserva cualquier otra secuencia de escape que la entrada pueda contener. Por ejemplo, si el archivo \emph{fred} contiene esta línea:
\begin{lstlisting}[language=bash]
A line with a\n escape sequence
\end{lstlisting}
\texttt{read -r fredline} incluirá la barra invertida en la variable \texttt{fredline}, mientras que sin \texttt{-r}, \emph{read} <<se comerá>> la barra invertida. Como resultado:
\begin{lstlisting}[language=bash]
$ read -r fredline < fred
$ print "$fredline"
A line with a
escape sequence
$
\end{lstlisting}
Aquí, \emph{print} interpretó la secuencia de escape \texttt{\textbackslash{}n} y la convirtió en un salto de línea. Sin embargo:
\begin{lstlisting}[language=bash]
$ read fredline < fred
$ print "$fredline"
A line with an
escape sequence
$
\end{lstlisting}
La opción \texttt{-s} te ayuda si estás escribiendo un script altamente interactivo y deseas proporcionar la misma capacidad de historial de comandos que la propia shell tiene. Por ejemplo, supongamos que estás escribiendo una nueva versión de \emph{mail} como un script de shell. Tu bucle básico de comandos podría verse así:
\begin{lstlisting}[language=bash]
while read -s cmd; do
# procesar el comando
done
\end{lstlisting}
El uso de \texttt{read -s} permite al usuario recuperar comandos anteriores para tu programa con el comando CTRL-P en modo \emph{emacs} o el comando ESC k en modo \emph{vi}. El depurador \emph{kshdb} en el \hyperref[sec:Chapter9]{Capítulo 9} utiliza esta función.
La opción \texttt{-t} es bastante útil. Te permite recuperarte en caso de que tu usuario se haya <<ido a almorzar>>, pero tu script tiene mejores cosas que hacer que esperar por la entrada. Le indicas cuántos segundos estás dispuesto a esperar antes de decidir que al usuario simplemente ya no le importa:
\begin{lstlisting}[language=bash]
print -n "OK, Sr. $prisoner, ingrese su nombre, rango y número de serie: "
# esperar dos horas, no más
if read -t $((60 * 60 * 2)) name rank serial
then
# procesar la información
...
else
# el prisionero está siendo silencioso
print '¿El tratamiento silencioso, eh? Solo espera.'
call_evil_colonel -p $prisoner
...
fi
\end{lstlisting}
Si el usuario ingresa datos antes de que expire el tiempo de espera, \texttt{read} devuelve 0 (éxito) y se procesa la parte \texttt{then} del \texttt{if}. Por otro lado, cuando el usuario no ingresa nada, el tiempo de espera expira y \texttt{read} devuelve 1 (fracaso), ejecutando la parte \texttt{else} de la declaración.
Aunque no es una opción para el comando \emph{read}, la variable \texttt{TMOUT} puede afectarlo. Al igual que para \texttt{select}, si \texttt{TMOUT} está configurado con un número que representa cierta cantidad de segundos, el comando \emph{read} se agota si no se ingresa nada dentro de ese tiempo, y devuelve un estado de salida de fracaso. La opción \texttt{-t} anula la configuración de \texttt{TMOUT}.
Finalmente, la opción \texttt{-un} es útil en scripts que leen de más de un archivo al mismo tiempo.
La \hyperref[box:7-4]{Tarea 7-4} es un ejemplo de esto que también utiliza el redireccionador de entrada \texttt{n<} que vimos anteriormente en este capítulo.
\begin{mybox}[Tarea 7-4]\label{box:7-4}
Escribe un script que imprima el contenido de dos archivos uno al lado del otro.
\end{mybox}
Formatearemos la salida para que las dos columnas tengan un ancho fijo de 30 caracteres. Aquí está el código:
\begin{lstlisting}[language=bash]
typeset -L30 f1 f2
while read -u3 f1 && read -u4 f2; do
print "$f1$f2"
done 3<$1 4<$2
\end{lstlisting}
\texttt{read -u3} lee del descriptor de archivo 3, y \texttt{3<\$1} dirige el archivo dado como primer argumento para que sea la entrada en ese descriptor de archivo; lo mismo es válido para el segundo argumento y el descriptor de archivo 4. Recuerda que los descriptores de archivo 0, 1 y 2 ya se utilizan para la entrada/salida estándar. Usamos los descriptores de archivo 3 y 4 para nuestros dos archivos de entrada; es mejor comenzar desde 3 y trabajar hacia arriba hasta el límite del shell, que es 9.
El comando \texttt{typeset} y las comillas alrededor del argumento de \texttt{print} aseguran que las columnas de salida tengan 30 caracteres de ancho y que se preserve el espacio en blanco al final de las líneas del archivo. El bucle \texttt{while} lee una línea de cada archivo hasta que al menos uno de ellos se queda sin entrada.
Supongamos que el archivo \emph{dave} contiene lo siguiente:
\begin{lstlisting}[language=bash]
DAVE
Height: 177.8 cm.
Weight: 79.5 kg.
Hair: brown
Eyes: brown
\end{lstlisting}
Y el archivo \emph{shirley} contiene esto:
\begin{lstlisting}[language=bash]
SHIRLEY
Height: 167.6 cm.
Weight: 65.5 kg.
Hair: blonde
Eyes: blue
\end{lstlisting}
Si el script se llama \emph{twocols}, entonces \texttt{twocols dave shirley} produce esta salida:
\begin{lstlisting}[language=bash]
DAVE SHIRLEY
Height: 177.8 cm. Height: 167.6 cm.
Weight: 79.5 kg. Weight: 65.5 kg.
Hair: brown Hair: blonde
Eyes: brown Eyes: blue
\end{lstlisting}
\section{Procesamiento de la línea de comandos}
Hemos visto cómo el shell procesa líneas de entrada: maneja comillas simples (\texttt{' '}), comillas dobles (\texttt{" "}) y barras invertidas (\textbackslash{}), y separa expansiones de parámetros, comandos y aritmética en palabras, según los delimitadores en la variable IFS. Esto es un subconjunto de las cosas que el shell hace al procesar líneas de comandos.
Esta sección completa la discusión, en ocasiones en detalle. Primero examinamos dos tipos adicionales de sustituciones o expansiones que el shell realiza y que pueden no estar disponibles de manera universal. Luego presentamos la historia completa del orden en que el shell procesa la línea de comandos. A continuación se aborda el uso de \emph{comillas}, que evita que muchas o todas las etapas de sustitución ocurran. Finalmente, cubrimos el comando \emph{eval}, que se puede utilizar para un control programático adicional de las evaluaciones de la línea de comandos.
\subsection{Expansión de llaves y sustitución de procesos}
La \emph{expansión de llaves} es una característica tomada del intérprete de comandos Berkeley \emph{csh} y también disponible en el popular shell \emph{bash}. La expansión de llaves es una forma de ahorrar escritura cuando tienes cadenas que son prefijos o sufijos entre sí. Por ejemplo, supongamos que tienes los siguientes archivos:
\begin{lstlisting}[language=bash]
$ ls
cpp-args.c cpp-lex.c cpp-out.c cpp-parse.c
\end{lstlisting}
Podrías escribir \texttt{vi cpp-\{args,lex,parse\}.c} si deseas editar tres de los cuatro archivos C, y el shell expandiría esto a \texttt{vi cpp-args.c cpp-lex.c cpp-parse.c}. Además, las sustituciones de llaves pueden estar anidadas. Por ejemplo:
\begin{lstlisting}[language=bash]
$ print cpp-{args,l{e,o}x,parse}.c
cpp-args.c cpp-lex.c cpp-lox.c cpp-parse.c
\end{lstlisting}
Esta es una característica útil. No la hemos cubierto hasta ahora porque es posible que tu versión de \emph{ksh} no la tenga. Es una característica opcional que se habilita cuando se compila \emph{ksh}. Sin embargo, se habilita de forma predeterminada cuando \emph{ksh93} se compila desde el código fuente.
La sustitución de procesos te permite abrir múltiples flujos de procesos y alimentarlos en un único programa para su procesamiento. Por ejemplo:
\begin{lstlisting}[language=bash]
awk '...' <(generate_data) <(generate_more_data)
\end{lstlisting}
(Ten en cuenta que los paréntesis son parte de la sintaxis; los escribes literalmente). Aquí, \texttt{generate\_data} y \texttt{generate\_more\_data} representan comandos arbitrarios, incluidos conductos, que producen flujos de datos. El programa \emph{awk} procesa cada flujo sucesivamente, sin darse cuenta de que los datos provienen de múltiples fuentes. Esto se muestra gráficamente en la Figura \ref{fig:7-1}.a.
\newpage
\begin{figure}[h!]
\centering
\includegraphics[scale=0.7]{fig_07}
\caption{\small{Sustitución de procesos para flujos de datos de entrada y salida}}
\label{fig:7-1}
\end{figure}
La sustitución de procesos también se puede utilizar para la salida, especialmente cuando se combina con el programa \emph{tee(1)}, que envía su entrada a varios archivos de salida y a la salida estándar. Por ejemplo:
\begin{lstlisting}[language=bash]
generate_data | tee >(sort | uniq > sorted_data) \
>(mail -s 'raw data' joe) > raw_data
\end{lstlisting}
Este comando utiliza \emph{tee} para (1) enviar los datos a una tubería que ordena y guarda los datos, (2) enviar los datos al programa de correo (\emph{mail}) al usuario \emph{joe}, y (3) redirigir los datos originales a un archivo. Esto se representa gráficamente en la Figura \ref{fig:7-1}.b. La sustitución de procesos, combinada con \emph{tee}, te permite crear gráficos de datos no lineales, liberándote del paradigma lineal tradicional de <<una entrada, una salida>> de las tuberías Unix convencionales.
La sustitución de procesos solo está disponible en sistemas Unix que admiten archivos especiales \texttt{/dev/fd/N} para acceder de manera nombrada a descriptores de archivos ya abiertos. (Esto difiere del uso de \texttt{/dev/fd/N} descrito anteriormente en este capítulo, donde la propia shell interpreta la ruta de acceso. Aquí, debido a que los comandos externos deben poder abrir archivos en \texttt{/dev/fd}, la característica debe ser compatible directamente con el sistema operativo.) La mayoría de los sistemas Unix modernos, incluidos GNU/Linux, admiten esta característica. Al igual que la expansión de llaves, debe habilitarse en tiempo de compilación y es posible que no esté disponible en tu versión de \emph{ksh}. Al igual que la expansión de llaves, se habilita de forma predeterminada cuando \emph{ksh93} se compila a partir del código fuente.
\subsection{Orden de sustitución}
Hemos abordado el procesamiento de líneas de comandos (ver Figura \ref{fig:7-2}) a lo largo de este libro; ahora es un buen momento para explicar todo el proceso. \footnote{Incluso esta explicación está ligeramente simplificada para omitir los detalles más pequeños, por ejemplo, <<medios>> y <<finales>> de comandos compuestos, caracteres especiales dentro de construcciones \texttt{[[...]]} y \texttt{((...))}, etc. La última palabra sobre este tema se encuentra en el libro de referencia \emph{The New KornShell Command and Programming Language} de Morris Bolsky y David Korn, publicado por Prentice-Hall.}
Cada línea que el shell lee desde la entrada estándar o un script se denomina una \emph{tubería}; contiene uno o más comandos separados por cero o más caracteres de tubería (|). Para cada tubería que lee, el shell la descompone en comandos, configura la E/S para la tubería y luego realiza lo siguiente para cada comando:
\begin{figure}[h!]
\centering
\includegraphics[scale=0.5]{fig_08}
\caption{\small{Pasos en el procesamiento de la línea de comandos}}
\label{fig:7-2}
\end{figure}
\begin{enumerate}
\item Divide el comando en tokens separados por el conjunto fijo de metacaracteres: espacio, TAB, nueva línea, ;, (, ), <, >, |, y \&. Los tipos de tokens incluyen palabras, palabras clave, redireccionadores de E/S y puntos y coma.
\item Verifica si el primer token de cada comando es una \emph{palabra clave} sin comillas ni barras invertidas. Si es una palabra clave de apertura (como if y otros iniciadores de estructuras de control, \texttt{function, \{, (, ((,} o \texttt{[[}), el comando es en realidad un \emph{comando compuesto}. El shell configura internamente las cosas para el comando compuesto, lee el próximo comando y comienza el proceso nuevamente. Si la palabra clave no es una abertura de comando compuesto (por ejemplo, es una <<parte>> de una estructura de control como \texttt{then, else} o \texttt{do}, un <<final>> como \texttt{fi} o \texttt{done}, o un operador lógico), el shell señala un error de sintaxis.
\item Verifica la primera palabra de cada comando en la lista de \emph{alias}. Si se encuentra una coincidencia, sustituye la definición del alias y vuelve al Paso 1; de lo contrario, pasa al Paso 4. Este esquema permite alias recursivos; consulta el \hyperref[sec:Chapter3]{Capítulo 3}. También permite definir alias para palabras clave, por ejemplo, \texttt{alias aslongas=while} o \texttt{alias procedure=function}.
\item Sustituye el directorio principal del usuario (\$HOME) por el carácter de tilde (\~{}) si está al principio de una palabra. Sustituye el directorio principal del usuario por \emph{\~{}user}. \footnote{Dos variaciones oscuras de esto: el shell sustituye el directorio actual (\texttt{\$PWD}) por \texttt{\~{}+} y el directorio anterior (\texttt{\$OLDPWD}) por \texttt{\~{}-}}.
La sustitución de tilde ocurre en los siguientes lugares:
\begin{itemize}
\item Como el primer carácter no entre comillas de una palabra en la línea de comandos.
\item Después del signo = en una asignación de variable y después de cualquier : en el valor de una asignación de variable.
\item Para la parte de \emph{palabra} de las sustituciones de variables de la forma \texttt{\$\{variable op palabra\}} (ver \hyperref[sec:Chapter4]{Capítulo 4}).
\end{itemize}
\item Realiza la sustitución de parámetros (variables) para cualquier expresión que comience con un signo de dólar (\$)
\item Realiza la sustitución de comandos para cualquier expresión de la forma \texttt{\$(cadena)} o \texttt{cadena}.
\item Evalúa expresiones aritméticas de la forma \texttt{\$((cadena))}.
\item Realiza la sustitución de procesos, si esa característica está compilada en el shell y tu sistema admite \texttt{/dev/fd}.
\item Realiza la expansión de llaves, si esa característica está compilada en el shell.
\item Toma las partes de la línea que resultaron de la sustitución de parámetros, comandos y aritmética, y las divide nuevamente en palabras. Esta vez utiliza los caracteres en \texttt{\$IFS} como delimitadores en lugar del conjunto de metacaracteres en el Paso 1.
Normalmente, las ocurrencias sucesivas múltiples de caracteres en IFS actúan como un único delimitador, como se esperaría. Esto es cierto solo para caracteres de espacio en blanco, como espacio y TAB. Para caracteres que no son de espacio en blanco, esto no es cierto. Por ejemplo, al leer los campos separados por dos puntos de \texttt{/etc/passwd}, dos dos puntos sucesivos delimitan un campo vacío. Por ejemplo:
\begin{lstlisting}[language=bash]
IFS=:
while read name passwd uid gid fullname homedir shell
do
...
done < /etc/passwd
\end{lstlisting}
Para obtener este comportamiento con campos delimitados por espacios en blanco (por ejemplo, donde los caracteres de TAB delimitan cada campo), coloca dos instancias sucesivas del carácter delimitador en \texttt{IFS}.
\emph{ksh} ignora cualquier valor heredado (del entorno) de \texttt{IFS}. Al iniciar, establece el valor de \texttt{IFS} en el valor predeterminado de espacio, TAB y nueva línea.
\item Realiza la generación de nombres de archivo, también conocida como expansión de comodines, para cualquier aparición de *, ?, y pares [ ]. También procesa los operadores de expresiones regulares que vimos en el \hyperref[sec:Chapter4]{Capítulo 4}.
\item Utiliza la primera palabra como un comando buscando su ubicación según el resto de la lista en el \hyperref[sec:Chapter4]{Capítulo 4}, es decir, como un comando especial incorporado, luego como una función, luego como un comando incorporado regular y, finalmente, como un archivo en cualquiera de los directorios en \texttt{\$PATH}.
\item Ejecuta el comando después de configurar la redirección de E/S y otras cosas similares.
\end{enumerate}
Eso son muchos pasos, ¡y ni siquiera es toda la historia! Pero antes de continuar, un ejemplo debería hacer este proceso más claro. Supongamos que se ha ejecutado el siguiente comando:
\begin{lstlisting}[language=bash]
alias ll="ls -l"
\end{lstlisting}
Supongamos además que existe un archivo llamado \emph{.hist537} en el directorio personal del usuario \emph{fred}, que es \texttt{/home/fred}, y que hay una variable de doble signo de dólar \$\$ cuyo valor es 2537 (veremos qué es esta variable especial en el próximo capítulo).
Ahora veamos cómo el shell procesa el siguiente comando:
\begin{lstlisting}[language=bash]
ll $(whence cc) ~fred/.*$(($$%1000))
\end{lstlisting}
Aquí está lo que sucede con esta línea:
\begin{enumerate}
\item \texttt{ll \$(whence cc) \~{}fred/.*\$((\$\$\%1000))} \\
División de la entrada en palabras.
\item \texttt{ll} no es una palabra clave, por lo que el paso 2 no hace nada.
\item \texttt{ls -l \$(whence cc) \~{}fred/.*\$((\$\$\%1000))} \\
Sustituyendo \texttt{ls -l} por su alias \emph{ll}. Luego, el shell repite los pasos 1 al 3; el paso 2 divide \texttt{ls -l} en dos palabras.
\item \texttt{ls -l \$(whence cc) /home/fred/.*\$((\$\$\%1000))} \\
Expansión de \texttt{\~{}fred} a \texttt{/home/fred}.
\item \texttt{ls -l \$(whence cc) /home/fred/.*\$((2537\%1000))} \\
Sustituyendo 2537 por \$\$.
\item \texttt{ls -l /usr/bin/cc /home/fred/.*\$((2537\%1000))} \\
Haciendo la sustitución de comandos en \texttt{whence cc}.
\item \texttt{ls -l /usr/bin/cc /home/fred/.*537} \\
Evaluando la expresión aritmética 2537\%1000.
\item \texttt{ls -l /usr/bin/cc /home/fred/.*537} \\
Este paso no hace nada. (No hay sustitución de procesos.)
\item \texttt{ls -l /usr/bin/cc /home/fred/.*537} \\
Este paso no hace nada. (No hay llaves para expandir.)
\item \texttt{ls -l /usr/bin/cc /home/fred/.*537} \\
Este paso no hace nada. (No hay texto expandido para dividir.)
\item \texttt{ls -l /usr/bin/cc /home/fred/.hist537} \\
Sustituyendo el nombre de archivo por la expresión comodín .*537.
\item El comando \emph{ls} se encuentra en \texttt{/usr/bin}.
\item Se ejecuta \texttt{/usr/bin/ls} con la opción \texttt{-l} y los dos argumentos.
\end{enumerate}
Aunque esta lista de pasos es bastante directa, no es toda la historia. Todavía hay dos formas de subvertir el proceso: mediante comillas y mediante el comando \emph{eval} avanzado.
\subsection{Citando}
Puedes pensar en las comillas como una forma de hacer que el shell omita algunos de los 13 pasos mencionados anteriormente. En particular:
\begin{description}
\item [Las comillas simples (\texttt{'...'})] evitan \emph{todo} hasta el Paso 11, incluyendo la expansión de alias. Todos los caracteres dentro de un par de comillas simples permanecen sin cambios. No puedes tener comillas simples dentro de comillas simples, incluso si las precedes con barras invertidas.\footnote{Sin embargo, como vimos en el \hyperref[sec:Chapter1]{Capítulo 1}, \texttt['\textbackslash{}''} (es decir, comilla simple, barra invertida, comilla simple, comilla simple) actúa de manera bastante similar a una comilla simple en el medio de una cadena entre comillas simples; por ejemplo, \texttt{'abc'\textbackslash{}''def'} se evalúa como \texttt{abc'def}
\item [Las comillas dobles (\texttt{"..."})] evitan los pasos 1 al 4, más los pasos 8 al 11. Es decir, ignoran los caracteres de tubería, los alias, la sustitución de tilde, la expansión de comodines, la sustitución de procesos, la expansión de llaves y la división en palabras mediante delimitadores (por ejemplo, espacios) dentro de las comillas dobles. Las comillas simples dentro de las comillas dobles no tienen ningún efecto. Pero las comillas dobles permiten la sustitución de parámetros, la sustitución de comandos y la evaluación de expresiones aritméticas. Puedes incluir una comilla doble dentro de una cadena entre comillas dobles precediéndola con una barra invertida (\textbackslash{}). También debes escapar con barra invertida los caracteres \texttt{\$, `} (el delimitador arcaico de sustitución de comandos) y la barra invertida en sí.
\end{description}
La Tabla \ref{Tab:7-8} contiene algunos ejemplos simples que muestran cómo funcionan estas reglas; asumen que se ejecutó la declaración \texttt{dave=bob} y que el directorio personal del usuario \emph{fred} es \texttt{/home/fred}.
Si te preguntas si debes usar comillas simples o dobles en una situación específica de programación de shell, es más seguro usar comillas simples a menos que necesites específicamente la sustitución de parámetros, comandos o aritmética.
\begin{longtable}[h]{|m{3cm}|m{3cm}|}
\caption{Ejemplos de reglas de comillas}
\label{Tab:7-8}\\
\hline
\textbf{Expresión} & \textbf{Valor} \\\hline
\endfirsthead
\hline
\textbf{Expresión} & \textbf{Valor} \\\hline
\endhead
\texttt{\$dave} & \texttt{bob} \\\hline
\texttt{"\$dave"} & \texttt{bob} \\\hline
\texttt{\textbackslash{}\$dave} & \texttt{\$dave} \\\hline
\texttt{'\$dave'} & \texttt{\$dave} \\\hline
\texttt{\textbackslash{}'\$dave\textbackslash{}'} & \texttt{'bob'} \\\hline
% \texttt{"'\$dave'"} & \texttt{'bob'} \\\hline
\texttt{\~{}fred} & \texttt{/home/fred} \\\hline
\texttt{"\~{}fred"} & \texttt{\~{}fred} \\\hline
\texttt{'\~{}fred'} & \texttt{\~{}fred} \\\hline
\end{longtable}
El uso de comillas dobles en los valores de las variables es cada vez más importante al tratar con los resultados de la expansión de comodines. Hoy en día, no es inusual tener archivos y directorios disponibles en sistemas Unix que físicamente existen en sistemas Microsoft Windows y Apple Macintosh. En esos sistemas, los espacios y otros caracteres inusuales, como apóstrofes y comillas invertidas, son comunes en los nombres de archivo. Por lo tanto, para pasar la ruta completa a tu aplicación, asegúrate de citar las cosas adecuadamente.
La \hyperref[box:7-5]{Tarea 7-5} es un ejemplo más avanzado de procesamiento de línea de comandos que debería brindarte una comprensión más profunda del proceso general.
\begin{mybox}[Tarea 7-5]\label{box:7-5}
Personaliza tu cadena de prompt principal para que contenga el directorio actual con notación de tilde (\~{}).
\end{mybox}
Recuerda del \hyperref[sec:Chapter4]{Capítulo 4} que encontramos una forma simple de configurar la cadena de prompt \texttt{PS1} para que siempre contenga el directorio actual: \texttt{PS1='(\$PWD)-> '}.
Un problema con esta configuración es que las cadenas de prompt resultantes pueden volverse muy largas. Una forma de acortarlas es sustituir la notación de tilde por los directorios principales de los usuarios. Esto no se puede hacer con una simple expresión de cadena análoga a la de arriba. La solución es algo complicada y aprovecha las reglas de procesamiento de la línea de comandos.
La idea básica es crear una <<envoltura>> alrededor del comando \emph{cd}, como hicimos en el \hyperref[sec:Chapter5]{Capítulo 5}, que instale el directorio actual con notación de tilde como la cadena de prompt. Veremos cómo crear esta función de envoltura en breve. El código que necesitamos para insertar la notación de tilde es complicado por sí mismo; lo desarrollamos primero.
Comenzamos con una función que, dada una ruta como argumento, imprime su equivalente en notación de tilde si es posible. Para escribir esta función, asumimos que ya tenemos una matriz asociativa llamada \texttt{tilde\_ids}, en la que los subíndices son directorios principales y los valores son nombres de usuario. Por lo tanto, \texttt{print \$\{tilde\_ids[/home/arnold]\}} imprimiría el valor \texttt{arnold}. Aquí está la función, llamada \emph{tildize}:
\begin{lstlisting}[language=bash]
function tildize {
# subdirectorio de nuestro directorio principal
if [[ $1 == $HOME* ]]; then
print "\~${1#$HOME}"
return 0
fi
# bucle sobre los directorios principales intentando hacer coincidir el directorio actual
typeset homedir
for homedir in ${!tilde_ids[*]}; do
if [[ $1 == ${homedir}?(/*) ]]; then
print "\~${tilde_ids[$homedir]}${1#$homedir}"
return 0
fi
done
print "$1"
return 1
}
\end{lstlisting}
La primera cláusula \texttt{if} verifica si la ruta dada está bajo el directorio principal del usuario. Si es así, sustituye la tilde (\~{}) por el directorio principal en la ruta y devuelve.
Si no es así, hacemos un bucle sobre todos los subíndices en \texttt{tilde\_ids}, comparando cada uno con nuestro directorio actual. La prueba hace coincidir los directorios principales por sí mismos o con algún otro directorio añadido (la parte \texttt{?(/*))}. Si se encuentra el directorio principal de un usuario, \texttt{\~{}usuario} se sustituye por el directorio principal completo en la ruta dada, se imprime el resultado y la función sale.
Finalmente, si el bucle \texttt{for} agota todos los usuarios sin encontrar un directorio principal que sea un prefijo de la ruta dada, \texttt{tildize} simplemente devuelve su entrada.
Ahora, ¿cómo creamos la matriz \texttt{tilde\_ids}? Usamos la función \texttt{init\_tilde\_db}. Debe llamarse una vez, desde el archivo \emph{.profile} cuando iniciamos sesión. La matriz \texttt{tilde\_ids} debe declararse explícitamente como una matriz asociativa utilizando typeset \texttt{-A}:
\begin{lstlisting}[language=bash]
# tilde_ids[] es una matriz asociativa global
# que asigna directorios a nombres de usuario
typeset -A tilde_ids
function init_tilde_db {
typeset user homedir # variables locales
awk -F: '{ print $1, $6 }' /etc/passwd |
while read user homedir; do
if [[ $homedir != / ]]; then
tilde_ids[$homedir]=$user
fi
done
}
\end{lstlisting}
Usamos la utilidad \emph{awk} para extraer los primeros y sextos campos del archivo \texttt{/etc/passwd}, que contienen IDs de usuario y directorios principales, respectivamente.\footnote{En entornos grandes con varias máquinas, puede que necesites usar algo como \texttt{ypcat passwd | awk ... o niscat passwd.org\_dir | awk ...} para obtener la misma información. Consulte con el administrador de su sistema.}
En este caso, \emph{awk} actúa como \emph{cut}. El \texttt{-F:} es análogo a \texttt{-d:}, que vimos en el \hyperref[sec:Chapter4]{Capítulo 4}, excepto que \emph{awk} imprime los valores en cada línea separados por espacios, no por dos puntos (:).
El resultado de \emph{awk} se alimenta a un bucle \texttt{while} que verifica la ruta dada como argumento para ver si contiene el directorio principal de algún usuario. (La expresión condicional elimina <<usuarios>> como \texttt{daemon} y \texttt{root}, cuyos directorios principales son root y, por lo tanto, están contenidos en cada ruta completa).
Ahora que tenemos la función \texttt{tildize}, podrías pensar que podríamos usarla en una expresión de sustitución de comandos de la siguiente manera:
\begin{lstlisting}[language=bash]
PS1='$(tildize $PWD)> '
\end{lstlisting}
De hecho, estarías en lo correcto.\footnote{Sin embargo, esto no funciona en \emph{ksh88}.}
Pero hay un costo oculto aquí. La función se ejecuta \emph{cada vez} que el shell imprime el prompt. Incluso si solo presionas ENTER, el shell ejecuta la función \emph{tildize}. Si hay muchos usuarios en tu sistema, el shell recorre todos los directorios principales cada vez. Para evitar esto, escribimos una función \emph{cd} que solo actualiza el prompt cuando realmente cambiamos de directorio. El siguiente código debería ir en tu archivo \emph{.profile} o de entorno, junto con la definición de \texttt{tilde\_ids} y \texttt{tildize}:
\begin{lstlisting}[language=bash]
init_tilde_db # configurar la matriz una vez, al iniciar sesión
function cd {
command cd "$@" # ejecutar el comando cd real
typeset es=$? # guardar el estado de salida en una variable local
PS1="$(tildize $PWD)> "
return $es
}
cd $PWD # establecer el prompt
\end{lstlisting}
Como vimos en el \hyperref[sec:Chapter5]{Capítulo 5}, escribir una función con el mismo nombre que un comando incorporado parece bastante extraño a primera vista. Pero, siguiendo el estándar POSIX, el shell de Korn distingue entre comandos incorporados <<especiales>> y comandos incorporados regulares. Cuando el shell busca comandos para ejecutar, encuentra funciones antes de encontrar comandos incorporados regulares. \emph{cd} es un comando incorporado regular, así que esto funciona. Dentro de la función, usamos el comando ingeniosamente llamado \emph{command} para acceder realmente al comando \emph{cd} real.\footnote{Como se mencionó anteriormente, \texttt{command} no es un comando incorporado especial. ¡Ay del programador del shell que escriba una función llamada \texttt{command!}}
La declaración \texttt{command cd "\$@"} pasa los argumentos de la función al \emph{cd} real para cambiar el directorio. (Como nota al margen, el shell define un alias \texttt{command='command '}, lo que te permite usar \texttt{command} con alias).
Cuando inicias sesión, este código establece \texttt{PS1} en el directorio actual inicial (presumiblemente tu directorio principal). Luego, cada vez que ingresas un comando \emph{cd}, la función se ejecuta para cambiar el directorio y restablecer el prompt.
Por supuesto, la función \texttt{tildize} puede ser cualquier código que formatee la cadena del directorio. Consulta los ejercicios al final de este capítulo para obtener algunas sugerencias.
\subsubsection{Cita ampliada}\label{sec:7.3.3.1}
Las comillas simples y dobles han estado presentes en el shell de Bourne y sus derivados desde el principio (aunque el shell de Bourne original no realiza aritmética ni sustitución de \texttt{\$(...)}). El shell de Korn ofrece versiones variantes de cadenas tanto con comillas simples como dobles, de la siguiente manera.
\begin{description}
\item[\texttt{\$"..."}] Esta versión es la más simple. Es similar a una cadena de comillas dobles regular. Sin embargo, estas cadenas están sujetas a la \emph{traducción de localización} en tiempo de ejecución. Esto se describe más adelante, a continuación.
\item[\texttt{\$'...'}] Esta cadena es similar a una cadena de comillas simples regular en el sentido de que no se realizan sustituciones ni expansiones del shell en el contenido. Sin embargo, el contenido se procesa en busca de secuencias de escape, similares a las utilizadas por el comando \emph{print}. La documentación de \emph{ksh} se refiere a estas como cadenas ANSI C.
\end{description}
Las características de internacionalización del shell del Korn están más allá del alcance de este libro, pero brevemente, funciona así. Cuando se invoca \emph{ksh} en un script con la opción \texttt{-D}, imprime una lista de todas las cadenas \texttt{\$"..."} en la salida estándar. Esta lista puede guardarse y usarse para producir traducciones que se utilizan en tiempo de ejecución cuando el script se ejecuta realmente. Así, en una configuración regional francesa, si hay una traducción disponible para este programa:
\begin{lstlisting}[language=bash]
print $"hello, world" Un saludo bien conocido entre los científicos de la computación
\end{lstlisting}
\emph{ksh} imprimiría bonjour, monde cuando se ejecuta el programa.
El comando \emph{print} permite utilizar secuencias de escape de estilo C para la salida. Y la mayoría de las veces, esto es todo lo que necesitas. Pero ocasionalmente, es útil utilizar la misma notación en los argumentos de otros programas. Este es el propósito de la cadena \texttt{\$'...'}. El contenido no se procesa para la sustitución de variables, comandos o aritmética. Pero sí se procesan las secuencias de escape, como se muestra en la Tabla \ref{Tab:7-9}.
\begin{table}[h]
\center
\caption{Secuencias de escape de cadenas}
\label{Tab:7-9}
\begin{tabular}{|m{2.5cm}|m{5cm}|m{2.5cm}|m{5cm}|} \hline
\textbf{Secuencia} & \textbf{Significado} & \textbf{Secuencia} & \textbf{Significado} \\\hline
\textbackslash{}a & Alerta, Campana ASCII & \textbackslash{}t & TAB \\\hline
\textbackslash{}b & Retroceso & \textbackslash{}v & Tabulación vertical \\\hline
\textbackslash{}x\emph{X} & CTRL-\emph{X} \footnotemark[1] \footnotemark[2] & \textbackslash{}x\emph{HH} & Carácter con valor de dígitos hexadecimales \emph{HH} \\\hline
\textbackslash{}C[.\emph{ce}.] & El elemento de intercambio \emph{ce} \footnotemark[1] \footnotemark[2] (Un elemento de intercalación son dos o más carcteres que se tratan como una unidad a efectos de clasificación). & \textbackslash{}x\emph{\{digs\}} & Valor hexadecimal de los dígitos. Utilice los corchetes cuando los siguientes caracteres sean dígitos hexadecimales que no deban interpretarse. \footnotemark[1] \footnotemark[2] \\\hline
\textbackslash{}e & Carácter de escape ASCII \footnotemark[1] \footnotemark[2] & \textbackslash{}0 & El resto de la cadena se ignora \footnotemark[2] \\\hline
\textbackslash{}E & Carácter de escape ASCII \footnotemark[1] & \textbackslash{}\emph{ddd} & Carácter con valor de dígitos octales \emph{ddd} \\\hline
\textbackslash{}f & Forma de alimentación & \textbackslash{}' & Comillas simples \\\hline
\textbackslash{}n & Nueva línea & \textbackslash{}" & Comillas doble \\\hline
\textbackslash{}r & Retorno de carro & \textbackslash{}\textbackslash{} & Barra invertida literal \\\hline
\end{tabular}
\end{table}
\footnotetext[1]{No en el lenguaje C.}
\footnotetext[2]{Nuevo, empezando por \emph{ksh93l}.}
De valor primordial es el hecho de que puede obtener fácilmente comillas simples y dobles dentro del tipo de cadena \texttt{\$'...'}:
\begin{lstlisting}[language=bash]
$ print $'Una cadena con \'comillas simples\' y \"comillas dobles\" en ella'
Una cadena con 'comillas simples' y "comillas dobles" en ella
\end{lstlisting}
Es interesante el hecho de que las comillas dobles no necesitan ser escapadas, pero que hacerlo tampoco hace daño.
\subsection{eval}
Hemos visto que las comillas te permiten omitir pasos en el procesamiento de la línea de comandos. Luego está el comando \emph{eval}, que te permite repetir el proceso. Realizar el procesamiento de la línea de comandos dos veces puede parecer extraño, pero en realidad es muy potente: te permite escribir scripts que crean cadenas de comandos sobre la marcha y luego las pasan al shell para su ejecución. Esto significa que puedes dar a los scripts <<inteligencia>> para modificar su propio comportamiento mientras se están ejecutando.
La instrucción \emph{eval} le indica al shell que tome los argumentos de \emph{eval} y los vuelva a ejecutar a través de los pasos de procesamiento de la línea de comandos. Para ayudarte a entender las implicaciones de \emph{eval}, comenzaremos con un ejemplo trivial y avanzaremos hacia una situación en la que estamos construyendo y ejecutando comandos sobre la marcha.
\texttt{eval ls} pasa la cadena \texttt{ls} al shell para que la ejecute; el shell imprime una lista de archivos en el directorio actual. Muy simple; no hay nada en la cadena \texttt{ls} que necesite pasar por los pasos de procesamiento de comandos dos veces. Pero considera esto:
\begin{lstlisting}[language=bash]
listpage="ls | more"
$listpage
\end{lstlisting}
En lugar de producir una lista de archivos paginada, el shell trata | y \texttt{more} como argumentos de \emph{ls}, y \emph{ls} se queja de que no existen archivos con esos nombres. ¿Por qué? Porque el carácter de tubería <<aparece>> en el paso 5 cuando el shell evalúa la variable, \emph{después} de que realmente ha buscado los caracteres de tubería (en el paso 2). La expansión de la variable ni siquiera se analiza hasta el paso 10. Como resultado, el shell trata | y \emph{more} como argumentos de \emph{ls}, de modo que \emph{ls} intenta encontrar archivos llamados | y \emph{more} en el directorio actual.
Ahora considera \texttt{eval \$listpage} en lugar de simplemente \texttt{\$listpage}. Cuando el shell llega al último paso, ejecuta el comando \emph{eval} con los argumentos \emph{ls}, | y \emph{more}. Esto hace que el shell vuelva al Paso 1 con una línea que consiste en estos argumentos. Encuentra | en el Paso 2 y divide la línea en dos comandos, \emph{ls} y \emph{more}. Cada comando se procesa de la manera normal (y en ambos casos de manera trivial). El resultado es una lista paginada de los archivos en tu directorio actual.
Ahora es posible que empieces a ver cuán poderoso puede ser \emph{eval}. Es una característica avanzada que requiere una considerable astucia de programación para usarse de manera más efectiva. Incluso tiene un poco del sabor de la inteligencia artificial, ya que te permite escribir programas que pueden <<escribir>> y ejecutar otros programas. \footnote{De hecho, podrías hacer esto sin \emph{eval}, imprimiendo comandos en un archivo temporal y luego <<sourcing>> ese archivo con \texttt{. filename}. Pero eso es mucho menos eficiente.}
Es probable que no uses \emph{eval} para la programación diaria en el shell, pero vale la pena tomarse el tiempo para entender lo que puede hacer.
Como ejemplo más interesante, revisaremos la \hyperref[box:4-1]{Tarea 4-1}, la primera tarea del libro. En ella, construimos una canalización simple que ordena un archivo e imprime las primeras \emph{N} líneas, donde N es 10 por defecto. La canalización resultante fue:
\begin{lstlisting}[language=bash]
sort -nr $1 | head -${2:-10}
\end{lstlisting}
El primer argumento especifica el archivo a ordenar; \texttt{\$2} es el número de líneas a imprimir.
Ahora supongamos que cambiamos un poco la tarea para que, en lugar de 10 líneas, la impresión predeterminada sea de \emph{todo el archivo}. Esto significa que no queremos usar \emph{head} en absoluto en el caso predeterminado. Podríamos hacer esto de la siguiente manera:
\begin{lstlisting}[language=bash]
if [[ -n $2 ]]; then
sort -nr $1 | head -$2
else
sort -nr $1
fi
\end{lstlisting}
En otras palabras, decidimos qué canalización ejecutar según si \texttt{\$2} es nulo o no. Pero aquí hay una solución más compacta:
\begin{lstlisting}[language=bash]
eval sort -nr \$1 ${2:+"| head -\$2"}
\end{lstlisting}
La última expresión en esta línea evalúa a la cadena \texttt{| head -\textbackslash{}\$2} si \texttt{\$2} existe (no es nulo); si \texttt{\$2} es nulo, entonces la expresión también es nula. Escapamos los signos de dólar (\texttt{\textbackslash{}\$}) antes de los nombres de las variables para evitar resultados impredecibles si los valores de las variables contienen caracteres especiales como > o |. El backslash efectivamente pospone la evaluación de las variables hasta que se ejecuta el comando \emph{eval} en sí. Entonces, toda la línea es ya sea:
\begin{lstlisting}[language=bash]
eval sort -nr \$1 | head -\$2
\end{lstlisting}
si \texttt{\$2} está dado o:
\begin{lstlisting}[language=bash]
eval sort -nr \$1
\end{lstlisting}
si \texttt{\$2} es nulo. Una vez más, no podemos simplemente ejecutar este comando sin \emph{eval} porque el tubo está <<descubierto>> después de que el shell intenta dividir la línea en comandos. \emph{eval} hace que el shell ejecute la canalización correcta cuando se proporciona \texttt{\$2}.
A continuación, revisaremos la \hyperref[box:7-3]{Tarea 7-3} de antes en este capítulo, la función \emph{start} que te permite iniciar un comando en segundo plano y guardar su salida estándar y su error estándar en un archivo de registro. Recuerda que la solución de una sola línea para esta tarea tenía la restricción de que el comando no podía contener redireccionadores de salida o tuberías. Aunque lo primero no tiene sentido cuando lo piensas, ciertamente querrías la capacidad de iniciar una tubería de esta manera.
\emph{eval} es la forma obvia de resolver este problema:
\begin{lstlisting}[language=bash]
function start {
eval "$@" > logfile 2>&1 &
}
\end{lstlisting}
La única restricción que esto impone al usuario es que las tuberías y otros caracteres especiales deben estar entre comillas o precedidos por barras invertidas.
La \hyperref[box:7-6]{Tarea 7-6} es una manera de aplicar \emph{eval} junto con varios otros conceptos interesantes de programación de shell.
\begin{mybox}[Tarea 7-6]\label{box:7-6}
Implementa la esencia del programa \emph{make(1)} como un script de shell.
\end{mybox}
\emph{make} se conoce principalmente como una herramienta para programadores, pero parece que alguien encuentra un nuevo uso para ella todos los días. Sin entrar en demasiados detalles innecesarios, \emph{make} lleva un seguimiento de múltiples archivos en un proyecto particular, algunos de los cuales dependen de otros (por ejemplo, un documento depende de su(s) archivo(s) de entrada del procesador de texto). Se asegura de que cuando cambias un archivo, todos los demás archivos que dependen de él se procesen.
Por ejemplo, supongamos que estás escribiendo un libro en DocBook XML. Tienes archivos para los capítulos del libro llamados \emph{ch01.xml, ch02.xml}, y así sucesivamente. La salida generada en PostScript para estos archivos es \emph{ch01.ps, ch02.ps}, etc. La herramienta para convertir DocBook XML en PostScript se llama (por alguna razón extraña) \emph{gmat}. Ejecutas comandos como \texttt{gmat chN.xml} para realizar el procesamiento. (\emph{gmat} sabe crear \emph{ch01.ps} a partir de \emph{ch01.xml}; no necesitas usar la redirección de shell). Mientras trabajas en el libro, tiendes a realizar cambios en varios archivos al mismo tiempo.
En esta situación, puedes usar \emph{make} para llevar un seguimiento de qué archivos deben ser reprocesados, de modo que todo lo que necesitas hacer es escribir \texttt{make}, y determina qué debe hacerse. No necesitas recordar reprocesar los archivos que han cambiado.
¿Cómo lo hace \emph{make}? Simple: compara los tiempos de modificación de los archivos de entrada y salida (llamados \emph{fuentes} y \emph{objetivos} en la terminología de \emph{make}), y si el archivo de entrada es más reciente, \emph{make} lo vuelve a procesar.
Le dices a \emph{make} qué archivos verificar construyendo un archivo llamado \emph{makefile} que tiene construcciones como esta:
\begin{lstlisting}[language=make]
target : source1 source2 ...
commands to make target
\end{lstlisting}
Esto esencialmente dice: <<Para que el objetivo esté actualizado, debe ser más nuevo que todas las \emph{fuentes}. Si no lo es, ejecuta los \emph{comandos} para ponerlo al día>>. Los \emph{comandos} están en una o más líneas que deben comenzar con TABs; por ejemplo, para hacer \emph{ch07.ps}:
\begin{lstlisting}[language=bash]
ch07.ps : ch07.xml
gmat ch07.xml
\end{lstlisting}
Ahora supongamos que escribimos una función de shell llamada \emph{makecmd} que lee y ejecuta una sola construcción de esta forma. Supongamos que el \emph{makefile} se lee desde la entrada estándar. La función se vería así en el siguiente código.
\begin{lstlisting}[language=bash]
function makecmd {
read target colon sources
for src in $sources; do
if [[ $src -nt $target ]]; then
while read cmd && [[ $cmd == \t* ]]; do
print "$cmd"
eval $cmd
done
break
fi
done
}
\end{lstlisting}
Esta función lee la línea con el objetivo y las fuentes; la variable \texttt{colon} es solo un marcador de posición para el \texttt{:}. Luego verifica cada fuente para ver si es más nueva que el objetivo, utilizando el operador de prueba de atributo de archivo \texttt{-nt} que vimos en el \hyperref[sec:Chapter5]{Capítulo 5}. Si la fuente es más nueva, lee, imprime y ejecuta los comandos hasta que encuentra una línea que no comienza con un TAB o llega al final del archivo. (El \emph{make} real hace más que esto; consulta los ejercicios al final de este capítulo). Después de ejecutar los comandos, sale del bucle \texttt{for}, para que no ejecute los comandos más de una vez. (No es necesario quitar el TAB inicial del comando. El shell descarta automáticamente los espacios en blanco iniciales).
\subsubsection{El compilador C como tuberia (pipeline)}
Como ejemplo final de \emph{eval}, revisaremos nuestro viejo amigo \emph{occ}, el front-end del compilador C de los tres capítulos anteriores. Recuerda que el front-end del compilador realiza su trabajo llamando a programas separados para realizar la compilación real de C a código objeto (el programa \emph{ccom}), la optimización del código objeto (\texttt{optimize}), el ensamblaje de archivos de código ensamblador (\emph{as}), y la vinculación final de archivos de código objeto en un programa ejecutable (\emph{ld}). Estos programas separados utilizan archivos temporales para almacenar sus salidas.
Ahora asumiremos que estos componentes (excepto el enlazador) pasan información en una tubería (pipeline) a la salida final de código objeto. En otras palabras, cada componente toma la entrada estándar y produce la salida estándar en lugar de tomar argumentos de nombre de archivo. También cambiaremos una suposición anterior: en lugar de compilar directamente un archivo fuente C a código objeto, \emph{occ} compila C a código ensamblador, que el ensamblador luego ensambla a código objeto.\footnote{Por si sirve de algo, muchos compiladores Unix generan código ensamblador, optimizan el código ensamblador y luego generan código objeto.}
Esto nos permite suponer que \emph{occ} funciona así:
\begin{lstlisting}[language=bash]
ccom < filename.c | as | optimize > filename.o
\end{lstlisting}
O, si prefieres:
\begin{lstlisting}[language=bash]
cat filename.c | ccom | as | optimize > filename.o
\end{lstlisting}
Para ajustar esto al marco adecuado para \emph{eval}, asumamos que las variables \texttt{srcname} y \texttt{objname} contienen los nombres de los archivos fuente y objeto, respectivamente. Entonces, nuestra tubería se convierte en:
\begin{lstlisting}[language=bash]
cat $srcname | ccom | as | optimize > $objname
\end{lstlisting}
Como ya hemos visto, esto es equivalente a:
\begin{lstlisting}[language=bash]
eval cat \$srcname \| ccom \| as \| optimize \> \$objname
\end{lstlisting}
Sabiendo lo que sabemos sobre \emph{eval}, podemos transformar esto en:
\begin{lstlisting}[language=bash]
eval cat \$srcname " | ccom" " | as" " | optimize" \> \$objname
\end{lstlisting}
y a partir de eso en:
\begin{lstlisting}[language=bash]
compile=" | ccom"
assemble=" | as"
optimize=" | optimize"
eval cat \$srcname $compile $assemble $optimize \> \$objname
\end{lstlisting}
Ahora, considera qué sucede si no quieres invocar al optimizador, que es el caso predeterminado de todos modos. (Recuerda que la opción \texttt{-O} invoca al optimizador.) Podemos hacer esto:
\begin{lstlisting}[language=bash]
optimize=""
if -O given then
optimize=" | optimize"
fi
\end{lstlisting}
En el caso predeterminado, \texttt{\$optimize} evalúa a una cadena vacía, haciendo que la tubería final \emph{colapse} a:
\begin{lstlisting}[language=bash]
eval cat $srcname \| ccom \| as \> $objname
\end{lstlisting}
De manera similar, si pasas a \emph{occ} un archivo de código ensamblador (\emph{filename.s}), puedes simplificar el paso de compilación:\footnote{Los lectores astutos se darán cuenta de que, según este razonamiento, manejaríamos los archivos de entrada de código objeto (\emph{filename.o}) con la línea \texttt{eval cat \$srcname > \$objname}, donde los dos nombres son iguales. Esto hará que el shell destruya \emph{filename.o} truncándolo a longitud cero. No nos preocuparemos por esto aquí.}
\begin{lstlisting}[language=bash]
assemble="| as"
if [[ $srcname ends in .s ]]; then
compile=""
fi
\end{lstlisting}
Eso resulta en esta tubería:
\begin{lstlisting}[language=bash]
eval cat \$srcname \| as \> \$objname
\end{lstlisting}
Ahora estamos listos para mostrar la versión completa de <<pipeline>> de \emph{occ}. Es similar a la versión anterior, excepto que para cada archivo de entrada, construye y ejecuta una tubería como se describió anteriormente. Procesa la opción \texttt{-g} (depuración) y el paso de enlace de la misma manera que antes. Aquí está el código:
\begin{lstlisting}[language=bash]
# initialize option-related variables
do_link=true
debug=""
link_libs=""
clib="-lc"
exefile=""
# initialize pipeline components
compile=" | ccom"
assemble=" | as"
optimize=""
# process command-line options
while getopts "cgl:[lib]o:[outfile]O files ..." opt; do
case $opt in
c ) do_link=false ;;
g ) debug="-g" ;;
l ) link_libs+=" -l $OPTARG" ;;
o ) exefile="-o $OPTARG" ;;
O ) optimize=" | optimize" ;;
esac
done
shift $(($OPTIND - 1))
# process the input files
for filename in "$@"; do
case $filename in
*.c )
objname=${filename%.c}.o ;;
*.s )
objname=${filename%.s}.o
compile="" ;;
*.o )
objname=$filename # just link it directly with the rest
compile=""
assemble="" ;;
* )
print "error: $filename is not a source or object file."
exit 1 ;;
esac
# run a pipeline for each input file
eval cat \$filename $compile $assemble $optimize \> \$objname
objfiles+=" $objname"
compile=" | ccom"
assemble=" | as"
done
if [[ $do_link == true ]]; then
ld $exefile $objfiles $link_libs $clib
fi
\end{lstlisting}
Podríamos seguir indefinidamente con ejemplos cada vez más complejos de \emph{eval}, pero nos conformaremos con concluir el capítulo con algunos ejercicios.
\begin{enumerate}
\item Aquí hay algunas formas de mejorar \emph{occ}, nuestro compilador de C:
\begin{itemize}
\item Los compiladores de C del mundo real aceptan la opción \texttt{-S}, que le indica al compilador que suprima el paso de ensamblaje y deje la salida en archivos de código ensamblador cuyos nombres terminan en \texttt{.s}. Modifica \emph{occ} para que reconozca esta opción.
\item El lenguaje C++ es un sucesor evolutivo de C; incluye características avanzadas como la sobrecarga de operadores, la verificación obligatoria del tipo de argumento de función, definiciones de clases, plantillas y muchas más. (No te preocupes si no sabes lo que son). Algunos compiladores de C++ utilizan C como un <<lenguaje ensamblador>>, es decir, compilan archivos fuente de C++ a código C y luego los pasan a un compilador de C para su procesamiento adicional. Supón que los archivos fuente de C++ tienen nombres que terminan en .cc y que \texttt{/lib/cfront} es el <<front-end>> del compilador C++ que produce código C en su salida estándar. Modifica \emph{occ} para que acepte tanto C++ como C, archivos de código ensamblador y de objeto.
\end{itemize}
\item Las posibilidades de personalización de tu cadena de comandos del sistema son prácticamente infinitas. Aquí hay dos mejoras a los esquemas de personalización que hemos visto:
\begin{itemize}
\item Mejora el esquema del directorio actual en la cadena de comandos del sistema limitando la longitud de la cadena de comandos a un número de caracteres que el usuario pueda definir con una variable de entorno.
\item Lee la página del manual de \emph{date(1)} y lee acerca de la variable SECONDS en la página del manual de \emph{ksh(1)}. Organiza las cosas de modo que el shell imprima la hora actual en la cadena de comandos del sistema. (Pista: recuerda que el shell realiza sustitución de variables, comandos y expresiones aritméticas en el valor de PS1 antes de imprimirlo).
\end{itemize}
\item La función \emph{makecmd} en la solución de la \hyperref[box:7-6]{Tarea 7-6} representa una simplificación de la funcionalidad real de \emph{make}. \emph{make} verifica las dependencias de archivos de manera \emph{recursiva}, lo que significa que una fuente en una línea en un archivo \emph{makefile} puede ser un objetivo en otra línea. Por ejemplo, los capítulos del libro en el ejemplo podrían depender de figuras en archivos separados que se hicieron con un paquete de gráficos.
\begin{itemize}
\item Escribe una función llamada \emph{readtargets} que recorra el archivo \emph{makefile} y almacene todos los objetivos en una variable o archivo temporal.
\item En lugar de leer el archivo \emph{makefile} desde la entrada estándar, léelo en una variable de matriz llamada \texttt{lines}. Usa la variable \texttt{curline} como el índice de <<línea actual>>. Modifica \emph{makecmd} para que lea líneas desde la matriz comenzando con la línea actual.
\item \emph{makecmd} simplemente verifica si alguna de las fuentes es más reciente que el objetivo dado. Realmente debería ser una rutina recursiva que se ve así:
\begin{lstlisting}[language=bash]
function makecmd {
target=$1
get sources for $target
for each source src; do
if $src is also a target in this makefile then
makecmd $src
fi
if [[ $src -nt $target ]]; then
run commands to make target
return
fi
done
}
\end{lstlisting}
Implementa esto. Recuerda usar \emph{typeset} para crear variables locales, y piensa en cómo los arrays asociativos podrían ser útiles para hacer un seguimiento de los objetivos, las fuentes y los comandos a ejecutar.
\item Escribe el script \emph{driver} que convierta la función \emph{makecmd} en un programa \emph{make} completo. Esto debería hacer el objetivo dado como argumento, o si no se proporciona ninguno, el primer objetivo listado en el archivo \emph{makefile}.
\end{itemize}
\item Finalmente, aquí hay algunos problemas que realmente ponen a prueba tu conocimiento de \emph{eval} y las reglas de procesamiento de comandos del shell. ¡Resuelve estos y serás un verdadero mago de Korn shell!
\begin{itemize}
\item Los programadores avanzados de shell a veces usan un pequeño truco que incluye \emph{eval}: usar el valor de una variable como el nombre de otra variable. En otras palabras, puedes darle a un script de shell control sobre los nombres de las variables a las que asigna valores. ¿Cómo harías esto? (Pista: si \texttt{\$fred} es igual a \texttt{'dave'}, y \texttt{\$dave} es igual a \texttt{'bob'}, podrías pensar que podrías escribir \texttt{print \$\$fred} y obtener la respuesta \emph{bob}. Esto no funciona realmente, pero va en la dirección correcta. Este ejercicio es fácil de resolver usando \emph{namerefs}, pero vale la pena hacerlo sin ellas para probar tu comprensión de \emph{eval} y las reglas de comillas del shell.)
\item Podrías usar la técnica anterior junto con otros trucos de \emph{eval} para implementar nuevas estructuras de control para el shell. Por ejemplo, intenta escribir un script (o función) que emule el comportamiento del comando \emph{repeat} del C shell:
\begin{lstlisting}[language=bash]
`repeat count command`
\end{lstlisting}
Esto funciona de manera obvia: el \emph{comando} se ejecuta \emph{count} veces.
\end{itemize}
\end{enumerate}