Aprendiendo_Korn_Shell/Secciones/Capitulo9.tex

1068 lines
74 KiB
TeX

Esperamos haber logrado convencerte de que el shell Korn puede utilizarse como un entorno de programación Unix serio. Ciertamente, tiene muchas características, estructuras de control, etc. Pero otra parte esencial de un entorno de programación son las herramientas de \emph{soporte integradas y potentes}. Por ejemplo, hay una amplia variedad de editores de pantalla, compiladores, depuradores, perfiles, referenciadores cruzados, etc., para lenguajes como C, C++ y Java. Si programas en alguno de estos lenguajes, probablemente das por sentadas tales herramientas, y sin duda te horrorizarías ante la idea de tener que desarrollar código con, por ejemplo, el editor \emph{ed} y el depurador de lenguaje máquina \emph{adb}.
Pero, ¿qué hay de las herramientas de soporte para el Korn shell? Por supuesto, puedes usar cualquier editor que desees, incluyendo \emph{vi} y \emph{Emacs}. Y debido a que el shell es un lenguaje interpretado, no necesitas un compilador.\footnote{De hecho, si realmente te preocupa la eficiencia, hay compiladores de código de shell en el mercado; algunos convierten scripts de shell a código C que a menudo se ejecuta bastante más rápido; sin embargo, estas herramientas suelen ser para scripts de Bourne shell. Otros <<compiladores>> simplemente convierten el script a una forma binaria para que los clientes no puedan leer el programa.}
Pero no hay otras herramientas disponibles. El problema más serio es la falta de un depurador.
Este capítulo aborda esa carencia. El shell tiene algunas características que ayudan en la depuración de scripts de shell; veremos estas en la primera parte del capítulo. El shell Korn también tiene un par de características nuevas, no presentes en la mayoría de los shells de Bourne, que hacen posible implementar una herramienta de depuración completa. Mostramos estas características; más importante aún, presentamos \emph{kshdb}, un depurador de Korn shell que las utiliza. \emph{kshdb} es básico pero bastante utilizable, y su implementación sirve como un ejemplo extendido de varias técnicas de programación de shell de todo este libro.
\section{Ayudas básicas para depuración}
¿Qué tipo de funcionalidad necesitas para depurar un programa? En el nivel más empírico, necesitas una forma de determinar \emph{qué} está causando que tu programa se comporte mal y dónde está el problema en el código. Por lo general, comienzas con un \emph{qué} obvio (como un mensaje de error, una salida inapropiada, un bucle infinito, etc.), intentas retroceder hasta encontrar un "qué" que esté más cerca del problema real (por ejemplo, una variable con un valor incorrecto, una opción incorrecta para un comando) y eventualmente llegas al "dónde" exacto en tu programa. Luego puedes preocuparte por \emph{cómo} solucionarlo.
Observa que estos pasos representan un proceso de empezar con información obvia y terminar con hechos a menudo oscuros deducidos e intuidos. Las ayudas para la depuración facilitan la deducción e intuición al proporcionar información relevante fácilmente o incluso automáticamente, preferiblemente sin modificar tu código.
La ayuda más simple para la depuración (para cualquier lenguaje) es la instrucción de salida, como \emph{print} en el caso del shell. De hecho, los programadores de la vieja escuela depuraban su código Fortran insertando tarjetas WRITE en sus mazos. Puedes depurar colocando muchas declaraciones de salida en tu código (y eliminándolas más tarde), pero tendrás que dedicar mucho tiempo a reducir no solo la información exacta que deseas sino también dónde necesitas verla. También probablemente tendrás que sumergirte en mucha salida para encontrar la información que realmente deseas.
\subsection{Establecer opciones}
Afortunadamente, el shell tiene algunas características básicas que te brindan funcionalidad de depuración más allá de la de \emph{print}. Las más básicas son las opciones del comando \texttt{set -o} (como se cubrió en el \hyperref[sec:Chapter3]{Capítulo 3}). Estas opciones también se pueden usar en la línea de comandos al ejecutar un script, como muestra la Tabla \ref{Tab:9-1}.
La opción \emph{verbose} simplemente imprime (en el error estándar) cualquier entrada que reciba el shell. Es útil para encontrar el punto exacto en el que un script está fallando. Por ejemplo, supongamos que tu script se ve así:
\begin{lstlisting}[language=bash]
fred
bob
dave
pete
ed
ralph
\end{lstlisting}
\begin{table}[h]
\center
\caption{Opciones de depuración}
\label{Tab:9-1}
\begin{tabular}{|m{2cm}|m{3cm}|m{10cm}|} \hline
\textbf{Opción \texttt{set -o}} & \small{\textbf{Opción de línea de comandos}} & \textbf{Acción} \\ \hline
noexec & -n & No ejecute comandos; compruebe sólo errores de sintaxis \\\hline
verbose & -v & Eco de comandos antes de ejecutarlos \\\hline
xtrace & -x & Comandos de eco tras el procesamiento de la línea de comandos \\\hline
\end{tabular}
\end{table}
Ninguno de estos comandos son programas Unix estándar, y todos realizan su trabajo en silencio. Digamos que el script falla con un mensaje críptico como <<violación de segmento>>. Esto no te dice nada sobre qué comando causó el error. Si escribes \texttt{ksh -v nombredelscript}, podrías ver esto:
\begin{lstlisting}[language=bash]
fred
bob
dave
segmentation violation
pete
ed
ralph
\end{lstlisting}
Ahora sabes que \emph{dave} es el probable culpable, aunque también es posible que \emph{dave} haya fallado debido a algo que esperaba que \emph{fred} o \emph{bob} hicieran (por ejemplo, crear un archivo de entrada) que hicieron incorrectamente.
La opción \emph{xtrace} es más potente: imprime cada comando y sus argumentos después de que el comando ha pasado por la sustitución de parámetros, la sustitución de comandos y los demás pasos del procesamiento de la línea de comandos (como se describe en el \hyperref[sec:Chapter7]{Capítulo 7}). Si es necesario, la salida se cita de tal manera que se pueda reutilizar más tarde como entrada para el shell.
Aquí tienes un ejemplo:
\begin{lstlisting}[language=bash]
$ set -o xtrace
$ fred=bob
+ fred=bob
$ print "$fred"
+ print bob
bob
$ ls -l $(whence emacs)
+ whence emacs
+ ls -l /usr/bin/emacs
-rwxr-xr-x 2 root root 3471896 Mar 16 20:17 /usr/bin/emacs
$
\end{lstlisting}
Como puedes ver, \emph{xtrace} inicia cada línea que imprime con +. Esto es realmente personalizable: es el valor de la variable de shell integrada PS4.\footnote{Como con PS1 y PS3, esta variable también se somete a la sustitución de parámetros, comandos y aritmética antes de que se imprima su valor.}
Si estableces PS4 en \texttt{xtrace-> } (por ejemplo, en tu \emph{.profile} o archivo de entorno), obtendrás listados de \emph{xtrace} que se ven así:
\begin{lstlisting}[language=bash]
$ ls -l $(whence emacs)
xtrace-> whence emacs
xtrace-> ls -l /usr/bin/emacs
-rwxr-xr-x 2 root root 3471896 Mar 16 20:17 /usr/bin/emacs
$
\end{lstlisting}
Una forma aún mejor de personalizar PS4 es usar una variable integrada que aún no hayamos visto: \texttt{LINENO}, que contiene el número de la línea que se está ejecutando actualmente en un script de shell. Coloca esta línea en tu \emph{.profile} o archivo de entorno:
\begin{lstlisting}[language=bash]
PS4='line $LINENO: '
\end{lstlisting}
Usamos la misma técnica que con PS1 en el \hyperref[sec:Chapter3]{Capítulo 3}: usando comillas simples para posponer la evaluación de la cadena hasta cada vez que el shell imprime el indicador. Esto imprime mensajes en la forma \texttt{line N:} en tu salida de \emph{trace}. Incluso podrías incluir el nombre del script de shell que estás depurando en este indicador usando el parámetro posicional \$0:
\begin{lstlisting}[language=bash]
PS4='$0 line $LINENO: '
\end{lstlisting}
Como otro ejemplo, digamos que estás tratando de rastrear un error en un script llamado \emph{fred} que contiene este código:
\begin{lstlisting}[language=bash]
dbfmq=$1.fmq
...
fndrs=$(cut -f3 -d' ' $dfbmq)
\end{lstlisting}
Escribes \emph{fred bob} para ejecutarlo de la manera normal, y se cuelga. Luego escribes `
texttt{ksh -x fred bob}, y ves esto:
\begin{lstlisting}[language=bash]
+ dbfmq=bob.fmq
...
+ + cut -f3 -d
\end{lstlisting}
Se cuelga nuevamente en este punto. Notas que \emph{cut} no tiene un argumento de nombre de archivo, lo que significa que debe haber algo mal con la variable \emph{dbfmq}. Pero ha ejecutado correctamente la instrucción de asignación \texttt{dbfmq=bob.fmq}... ¡ah, ja! Cometiste un error tipográfico en el nombre de la variable dentro de la construcción de sustitución de comandos.\footnote{Deberíamos admitir que si hubieras activado la opción \emph{nounset} en la parte superior de este script, el shell habría señalado este error.}
Lo corriges y el script funciona correctamente.
Cuando se establece a nivel global, la opción \emph{xtrace} se aplica al script principal y a cualquier función de estilo POSIX (aquellas creadas con la sintaxis de \texttt{nombre ()}). Si el código que estás tratando de depurar llama a funciones de estilo \texttt{function} que están definidas en otro lugar (por ejemplo, en tu \emph{.profile} o archivo de entorno), puedes rastrear estas de la misma manera con una opción al comando \emph{typeset}. Simplemente escribe el comando \texttt{typeset -ft nombrefuncion} y la función con el nombre dado se rastreará cada vez que se ejecute. Escribe \texttt{typeset +ft nombrefuncion} para apagar el rastreo. También puedes poner \texttt{set -o xtrace} en el cuerpo de la función, lo cual es bueno cuando la función está dentro del script que se está depurando.
La última opción es \emph{noexec}, que lee el script de shell y verifica errores de sintaxis pero no ejecuta nada. Vale la pena usarla si tu script es sintácticamente complejo (muchos bucles, bloques de código, operadores de cadenas, etc.) y el error tiene efectos secundarios (como crear un archivo grande o colgar el sistema).
Puedes activar estas opciones con \texttt{set -o} en tus scripts de shell, y, como se explica en el \hyperref[sec:Chapter3]{Capítulo 3}, desactivarlas con \texttt{set +o opción}. Por ejemplo, si estás depurando un script con un efecto secundario desagradable, y lo has localizado en un cierto fragmento de código, puedes preceder ese fragmento con \texttt{set -o xtrace} (y, tal vez, cerrarlo con \texttt{set +o xtrace}) para observarlo con más detalle.
\textbf{NOTA:} La opción \emph{noexec} es una opción <<unidireccional>>. ¡Una vez activada, no puedes desactivarla! Esto se debe a que el shell solo imprime comandos y no los ejecuta. Esto incluye el comando \texttt{set +o noexec} que querrías usar para desactivar la opción. Afortunadamente, esto solo se aplica a scripts de shell; el shell ignora esta opción cuando es interactivo.
\subsection{Señales falsas}
Un conjunto más sofisticado de ayudas para depuración son las <<señales de depuración simuladas en el shel>> que pueden usarse en declaraciones \emph{trap} para que el shell actúe bajo ciertas condiciones. Recuerda del capítulo anterior que \emph{trap} te permite instalar algún código que se ejecuta cuando se envía una señal específica a tu script.
Las señales simuladas actúan como señales reales, pero son generadas por el shell (a diferencia de las señales reales, que el sistema operativo subyacente genera). Representan eventos en tiempo de ejecución que probablemente sean interesantes para depuradores, ya sean humanos o herramientas de software, y pueden tratarse de la misma manera que las señales reales dentro de scripts de shell. Se enumeran en la Tabla \ref{Tab:9-2}.
\begin{table}[h]
\center
\caption{Señales simuladas}
\label{Tab:9-2}
\begin{tabular}{|m{3cm}|m{12cm}|} \hline
\small{\textbf{Señal simulada}} & \textbf{Cuando se envía} \\ \hline
EXIT & El shell sale de una función o script \\\hline
ERR & Un comando devuelve un estado de salida distinto de cero \\\hline
DEBUG & Antes de cada declaración (después en \emph{ksh88}) \\\hline
KEYBD & Al leer caracteres en los modos de edición (no para depuración) \\\hline
\end{tabular}
\end{table}
La señal KEYBD no se usa para depuración. Es una función avanzada, para la cual posponemos la discusión hasta el \hyperref[sec:Chapter10]{Capítulo 10}.
\subsubsection{EXIT}
La trampa EXIT, cuando se establece, ejecuta su código cuando la función o script en el que se estableció sale. Aquí hay un ejemplo simple:
\begin{lstlisting}[language=bash]
function func {
trap 'print "saliendo de la función"' EXIT
print 'inicio de la función'
}
trap 'print "saliendo del script"' EXIT
print 'inicio del script'
func
\end{lstlisting}
Si ejecutas este script, verás esta salida:
\begin{lstlisting}[language=bash]
inicio del script
inicio de la función
saliendo de la función
saliendo del script
\end{lstlisting}
En otras palabras, el script comienza estableciendo la trampa para su propia salida. Luego imprime un mensaje y finalmente llama a la función. La función hace lo mismo, establece una trampa para su salida e imprime un mensaje. (Recuerda que las funciones de estilo de \texttt{function} pueden tener sus propias trampas locales que anulan cualquier trampa establecida por el script circundante, mientras que las funciones POSIX comparten trampas con el script principal).
La función luego sale, lo que hace que el shell le envíe la señal simulada EXIT, que a su vez ejecuta el código \texttt{print "saliendo de la función"}. Luego, el script sale, y se ejecuta su propio código de trampa EXIT. Observa también que las trampas <<se apilan>>; la señal simulada EXIT se envía a cada función en ejecución a medida que cada función llamada más recientemente sale.
Una trampa EXIT ocurre sin importar cómo salga el script o la función, ya sea de manera normal (al finalizar la última instrucción), mediante una declaración explícita de \emph{salida} o \emph{retorno}, o al recibir una señal <<real>> como INT o TERM. Considera el siguiente programa absurdo de adivinanzas de números:
\begin{lstlisting}[language=bash]
trap 'print "¡Gracias por jugar!"' EXIT
magicnum=$(($RANDOM%10+1))
print 'Adivina un número entre 1 y 10:'
while read guess'?número> '; do
sleep 10
if (( $guess == $magicnum )); then
print '¡Correcto!'
exit
fi
print 'Incorrecto.'
done
\end{lstlisting}
Este programa elige un número entre 1 y 10 obteniendo un número aleatorio (a través de la variable integrada \texttt{RANDOM}, consulta el \hyperref[sec:ApendiceB]{Apéndice B}), extrayendo el último dígito (el resto al dividir por 10) y sumando 1. Luego te pide una adivinanza y, después de 10 segundos, te dice si adivinaste correctamente.
Si lo haces, el programa sale con el mensaje <<¡Gracias por jugar!>>, es decir, ejecuta el código de la trampa EXIT. Si te equivocas, te pide de nuevo y repite el proceso hasta que aciertes. Si te aburres de este pequeño juego y presionas CTRL-C mientras esperas a que te diga si acertaste, también verás el mensaje.
\subsubsection{ERR}
La señal simulada \texttt{ERR} te permite ejecutar código cada vez que un comando en el script o función circundante sale con un estado distinto de cero. El código de la trampa ERR puede aprovechar la variable integrada ?, que contiene el estado de salida del comando anterior. Sobrevive a la trampa y es accesible al principio del código de manejo de la trampa.
Un uso simple pero efectivo de esto es colocar el siguiente código en un script que desees depurar:
\begin{lstlisting}[language=bash]
function errtrap {
typeset es=$?
print "ERROR: El comando salió con estado $es."
}
trap errtrap ERR
\end{lstlisting}
La primera línea guarda el estado de salida distinto de cero en la variable local \texttt{es}.
Por ejemplo, si el shell no puede encontrar un comando, devuelve un estado 1. Si colocas el código en un script con una línea de sin sentido (como <<lskdjfafd>>), el shell responde con:
\begin{lstlisting}[language=bash]
scriptname: line N: lskdjfafd: not found
ERROR: command exited with status 1.
\end{lstlisting}
\emph{N} es el número de la línea en el script que contiene el comando incorrecto. En este caso, el shell imprime el número de línea como parte de su propio mecanismo de informe de errores, ya que el error fue un comando que el shell no pudo encontrar. Pero si el estado de salida distinto de cero proviene de otro programa, el shell no informa el número de línea. Por ejemplo:
\begin{lstlisting}[language=bash]
function errtrap {
typeset es=$?
print "ERROR: El comando salió con estado $es."
}
trap errtrap ERR
function bad {
return 17
}
bad
\end{lstlisting}
Esto solo imprime \texttt{ERROR: El comando salió con estado 17.}
Sería obviamente una mejora incluir el número de línea en este mensaje de error. La variable integrada \texttt{LINENO} existe, pero si la usas dentro de una función, se evalúa como el número de línea en la función, no en el archivo en general. En otras palabras, si usas \texttt{\$LINENO} en la declaración \emph{print} en el procedimiento \emph{errtrap}, siempre se evaluará como 2.
Para resolver este problema, simplemente pasamos \texttt{\$LINENO} como argumento al controlador de trampas, rodeándolo con comillas simples para que no se evalúe hasta que la señal simulada realmente llegue:
\begin{lstlisting}[language=bash]
function errtrap {
typeset es=$?
print "ERROR línea $1: El comando salió con estado $es."
}
trap 'errtrap $LINENO' ERR
\end{lstlisting}
Si usas esto con el ejemplo anterior, el resultado es el mensaje \texttt{ERROR línea 12: El comando salió con estado 17}. Esto es mucho más útil. Veremos una variación de esta técnica en breve.
Este código simple no es realmente un mal mecanismo de depuración universal. Toma en cuenta que un estado de salida distinto de cero no necesariamente indica una condición o evento indeseable: recuerda que cada construcción de control con una condición (\texttt{if, while}, etc.) usa un estado de salida distinto de cero para significar <<falso>>. En consecuencia, el shell no genera trampas \emph{ERR} cuando las declaraciones o expresiones en las partes <<condición>> de las estructuras de control producen estados de salida distintos de cero.
Pero una desventaja es que los estados de salida no son tan uniformes (o incluso tan significativos) como deberían ser, como explicamos en el \hyperref[sec:Chapter5]{Capítulo 5}. Un estado de salida específico no tiene por qué decir nada sobre la naturaleza del error o incluso que hubo un error.
\subsubsection{DEBUG}
La última señal simulada relacionada con la depuración, DEBUG, provoca que el código de la trampa se ejecute antes de cada declaración en la función o script circundante.\footnote{Esto es un cambio notable desde \emph{ksh88}, donde la trampa se ejecutaba \emph{después} de cada declaración.}
Esto tiene dos posibles usos. El primero es para los humanos, como una especie de método <<brutal>> para rastrear un cierto elemento del estado de un programa que notas que está yendo mal.
Por ejemplo, notas que el valor de una variable en particular está fuera de control. El enfoque ingenuo sería insertar muchas declaraciones de \emph{print} para verificar el valor de la variable en varios puntos. La trampa DEBUG facilita esto:
\begin{lstlisting}[language=bash]
function dbgtrap {
print "badvar es $badvar"
}
trap dbgtrap DEBUG
... Sección de código donde ocurre el problema ...
trap - DEBUG # desactivar la trampa DEBUG
\end{lstlisting}
Este código imprime el valor de la variable problemática antes de cada declaración entre las dos \emph{trampas}.
El segundo y mucho más importante uso de la trampa DEBUG es como un primitivo para implementar depuradores de Korn shell. De hecho, sería justo decir que la trampa DEBUG reduce la tarea de implementar un depurador de shell útil de un proyecto de desarrollo de software a una tarea manejable. Hablaremos de esto en breve.
\subsubsection{Orden de entrega de señales}
Es posible que múltiples señales lleguen simultáneamente (o cerca). En ese caso, el shell ejecuta los comandos de trampa en el siguiente orden:
\begin{enumerate}
\item DEBUG
\item ERR
\item Señales Unix reales, en orden de número de señal
\item EXIT
\end{enumerate}
\subsection{Funciones de disciplina}
En el \hyperref[sec:Chapter4]{Capítulo 4}, presentamos la notación de variables compuestas del shell Korn, como \texttt{\$\{person.nanme\}}. Utilizando esta notación, \emph{ksh93} proporciona funciones especiales, llamadas \emph{funciones de disciplina}, que te brindan control sobre las variables cuando se hacen referencias, se asignan y se desestablecen. Versiones simples de tales funciones podrían verse así:
\begin{lstlisting}[language=bash]
dave=dave # Crear la variable
function dave.set { # Llamado cuando se asigna a dave
print "dave acaba de ser asignado como '${.sh.value}'"
}
function dave.get { # Llamado cuando se recupera $dave
print "valor de dave referenciado, es '$dave'" # esto es seguro
.sh.value="dave estuvo aquí" # Cambiar lo que $dave devuelve, dave no cambió
}
function dave.unset { # Llamado cuando se desestablece dave
print "adiós dave!"
unset dave # de hecho, haz que dave desaparezca
}
\end{lstlisting}
\textbf{NOTA:} La función de disciplina \emph{unset} debe usar realmente el comando \emph{unset} para desestablecer la variable; esto no provoca un bucle infinito. De lo contrario, la variable no se desestablecerá, lo que a su vez conduce a un comportamiento muy sorprendente.
Esto es lo que sucede una vez que todas estas funciones están en su lugar:
\begin{lstlisting}[language=bash]
$ print $dave
valor de dave referenciado, es 'dave' # Desde dave.get
dave estuvo aquí # Desde print
$ dave='¿quién es este tipo dave, de todos modos?'
dave acaba de ser asignado como '¿quién es este tipo dave, de todos modos?' # Desde dave.set
$ unset dave
adiós dave! # Desde dave.unset
$ print $dave
$
\end{lstlisting}
Las funciones de disciplina solo se pueden aplicar a variables globales. No se pueden usar con variables locales, aquellas que creas con \emph{typeset} dentro de una función de estilo de función.
La Tabla \ref{Tab:9-3} resume las funciones de disciplina integradas.
\begin{table}[h]
\center
\caption{Funciones de disciplina predefinidas}
\label{Tab:9-3}
\begin{tabular}{|m{3cm}|m{12cm}|} \hline
\textbf{Nombre} & \textbf{Propósito} \\ \hline
variable.get & Llamada cuando se recupera el valor de una variable. La asignación a \texttt{.sh.value} cambia el valor devuelto pero no la variable en sí. \\\hline
variable.set & Llamada cuando se asigna una variable. \texttt{\$\{.sh.value\}} es el nuevo valor que se asigna. Asignar a \texttt{.sh.value} cambia el valor que se está asignando. \\\hline
variable.unset & Llamada cuando una variable se desestablece. Esta función debe usar unset en la variable para que realmente se desactive. \\\hline
\end{tabular}
\end{table}
Como acabamos de ver, dentro de las funciones de disciplina, hay dos variables especiales que el shell establece y que te brindan información, así como una variable que puedes establecer para cambiar el comportamiento del shell. La Tabla \ref{Tab:9-4} describe estas variables y lo que hacen.
\begin{table}[h]
\center
\caption{Variables especiales para usar en funciones de disciplina}
\label{Tab:9-4}
\begin{tabular}{|m{3cm}|m{12cm}|} \hline
\textbf{Variable} & \textbf{Propósito} \\ \hline
.sh.name & Nombre de la variable para la que se ejecuta la función de disciplina. \\\hline
.sh.subscript & El subíndice actual de una variable de matriz. (Las funciones de disciplina se aplican a toda la matriz, no a cada elemento con subíndice). \\\hline
.sh.value & El nuevo valor que se asigna en una función de disciplina set. Si se asigna en una función de disciplina get, cambia el valor devuelto. \\\hline
\end{tabular}
\end{table}
A primera vista, no está claro cuál es el valor de las funciones de disciplina. Pero son perfectas para implementar una característica de depuración muy útil, llamada puntos de observación \emph{watchpoints}. Ahora estamos listos para empezar a escribir nuestro depurador de scripts de shell.
\section{Un depurador para Korn Shell}
Los depuradores disponibles comercialmente ofrecen mucha más funcionalidad que las opciones \emph{establecidas} y las señales simuladas del shell. Los más avanzados tienen interfaces gráficas fabulosas, compiladores incrementales, evaluadores simbólicos y otras comodidades por el estilo. Pero prácticamente todos los depuradores modernos, incluso los más modestos, tienen funciones que te permiten <<asomarte>> a un programa mientras se ejecuta, para examinarlo en detalle y en términos de su lenguaje fuente. Específicamente, la mayoría de los depuradores te permiten hacer estas cosas:
\begin{itemize}
\item Especificar puntos en los que el programa detiene la ejecución y entra en el depurador. Estos se llaman puntos de interrupción \emph{breakpoints}.
\item Ejecutar solo una parte del programa a la vez, generalmente medida en declaraciones de código fuente. Esta capacidad se llama a menudo paso a paso \emph{stepping}.
\item Examinar y posiblemente cambiar el estado del programa (por ejemplo, valores de variables) en medio de una ejecución, es decir, cuando se detiene en un punto de interrupción o después de un paso a paso.
\item Especificar variables cuyos valores deben imprimirse cuando se cambian o acceden. Estas se llaman a menudo puntos de observación (\emph{watchpoints}).
\item Hacer todo lo anterior sin tener que cambiar el código fuente.
\end{itemize}
Nuestro depurador, llamado \emph{kshdb}, tiene estas características y algunas más. Aunque es una herramienta básica, sin demasiadas florituras, no es un juguete. El sitio web del libro, \url{http://www.oreilly.com/catalog/korn2/}, tiene un enlace para descargar una copia de todos los programas de ejemplo del libro, incluido \emph{kshdb}. Si no tienes acceso a Internet, puedes escribir o escanear el código. De cualquier manera, puedes usar \emph{kshdb} para depurar tus propios scripts de shell, y debes sentirte libre de mejorarlo. Esta es la versión 2.0 del depurador. Incluye algunos cambios sugeridos por Steve Alston, y la función de puntos de observación es completamente nueva. Sugeriremos algunas mejoras al final de este capítulo.
\subsection{Estructura del depurador}
El código para \emph{kshdb} tiene varias características que vale la pena explicar con algún detalle. La más importante es el principio básico en el que funciona: convierte un script de shell en un depurador para sí mismo, agregando funcionalidad de depuración al principio y luego ejecuta el nuevo script.
\subsubsection{El script controlador}
Por lo tanto, el código tiene dos partes: la que implementa la funcionalidad del depurador y la que instala esa funcionalidad en el script que se está depurando. La segunda parte, que veremos primero, es el script llamado \emph{kshdb}. Es muy simple:
\begin{lstlisting}[language=bash]
# kshdb -- Depurador de Korn Shell
# Controlador principal: construye el script completo (con encabezado) y lo ejecuta
print "Depurador de Korn Shell versión 2.0 para ksh '${.sh.version}'" >&2
_guineapig=$1
if [[ ! -r $1 ]]; then
# archivo no encontrado o no legible
print "No se puede leer $_guineapig." >&2
exit 1
fi
shift
_tmpdir=/tmp
_libdir=. # establecer a un directorio real al instalar
_dbgfile=$_tmpdir/kshdb$$ # archivo temporal para el script que se está depurando (copia)
cat $_libdir/kshdb.pre $_guineapig > $_dbgfile
exec ksh $_dbgfile $_guineapig $_tmpdir $_libdir "$@"
\end{lstlisting}
\emph{kshdb} toma como argumento el nombre del script que se está depurando, al que, por brevedad, llamaremos <,programa de pruebas>>. Cualquier argumento adicional se pasa al programa de pruebas como sus parámetros posicionales. Observa que \texttt{\$\{.sh.version\}} indica la versión del shell Korn para el mensaje de inicio.
Si el argumento es inválido (el archivo no es legible), \emph{kshdb} sale con un estado de error. De lo contrario, después de un mensaje introductorio, construye un nombre de archivo temporal como vimos en el \hyperref[sec:Chapter8]{Capítulo 8}. Si no tienes (o no tienes acceso a) \texttt{/tmp} en tu sistema, puedes sustituir un directorio diferente por \texttt{\_tmpdir}. \footnote{Todos los nombres de funciones y variables (excepto los locales de las funciones) en \emph{kshdb} comienzan con un guion bajo (\_), para minimizar la posibilidad de conflictos con los nombres en programa de pruebas. Una solución más orientada a \emph{ksh93} sería usar una variable compuesta, por ejemplo, \texttt{\_db.tmpdir}, \texttt{\_db.libdir}, y así sucesivamente.}
Además, asegúrate de que \texttt{\_libdir} esté configurado con el directorio donde residen los archivos \emph{kshdb.pre} y \emph{kshdb.fns} (que veremos pronto). \texttt{/usr/share/lib} es una buena opción si tienes acceso a él.
La instrucción \emph{cat} construye el archivo temporal: consiste en un archivo que veremos pronto llamado \emph{kshdb.pre}, que contiene el código real del depurador, seguido inmediatamente por una copia del programa en cuestión. Por lo tanto, el archivo temporal contiene un script de shell que se ha convertido en un depurador para sí mismo.
\subsubsection{exec}
La última línea ejecuta este script con \emph{exec}, una instrucción que aún no hemos visto. Hemos elegido esperar hasta ahora para presentarla porque, como creemos que estarás de acuerdo, puede ser peligrosa. \emph{exec} toma sus argumentos como una línea de comandos y ejecuta el comando en lugar del programa actual, en el mismo proceso. En otras palabras, el shell que ejecuta el script anterior \emph{terminará inmediatamente} y será reemplazado por los argumentos de \emph{exec}. Las situaciones en las que querrías usar \emph{exec} son pocas, raras y bastante arcanas, aunque esta es una de ellas.
En este caso, \emph{exec} simplemente ejecuta el script de shell recién construido, es decir, el programa de pruebas con su depurador, en otro shell Korn. Pasa al nuevo script tres argumentos: los nombres del programa de pruebas original (\texttt{\$\_guineapig}), el directorio temporal (\texttt{\$\_tmpdir}), y el directorio donde se mantienen \emph{kshdb.pre} y \emph{kshdb.fns}, seguidos de los parámetros posicionales del usuario, si los hay.
\emph{exec} también se puede usar solo con un redireccionador de E/S; esto hace que el redireccionador tenga efecto durante el resto del script o sesión de inicio. Por ejemplo, la línea \texttt{exec 2>errlog} en la parte superior de un script dirige la propia salida de error estándar del shell al archivo \emph{errlog} durante todo el script. Esto también se puede usar para mover la entrada o salida de un coproceso a un descriptor de archivo numerado normal. Por ejemplo, \texttt{exec 5<\&p} mueve la salida del coproceso (que es entrada para el shell) al descriptor de archivo 5. De manera similar, \texttt{exec 6>\&p} mueve la entrada del coproceso (que es salida del shell) al descriptor de archivo 6. El alias predefinido \texttt{redirect='command exec'} es más mnemotécnico.
\subsection{El preámbulo}
Ahora veremos el código que se agrega al script que se está depurando; lo llamamos \emph{preámbulo}. Se mantiene en el siguiente archivo, \emph{kshdb.pre}, que también es bastante simple:
\begin{lstlisting}[language=bash]
# preámbulo de kshdb para la versión 2.0 de kshdb
# se antepone al script de shell que se está depurando
# argumentos:
# $1 = nombre del script original de pruebas
# $2 = directorio donde se almacenan los archivos temporales
# $3 = directorio donde se almacenan kshdb.pre y kshdb.fns
_dbgfile=$0
_guineapig=$1
_tmpdir=$2
_libdir=$3
shift 3 # mueve los argumentos del usuario al lugar correspondiente
. $_libdir/kshdb.fns # lee las funciones de depuración
_linebp=
_stringbp=
let _trace=0 # inicializa el rastreo de ejecución como apagado
typeset -A _lines
let _i=1 # lee el archivo programa de pruebas en el array de líneas
while read -r _lines[$_i]; do
let _i=$_i+1
done < $_guineapig
trap _cleanup EXIT # borra archivos antes de salir
let _steps=1 # no. de declaraciones para ejecutar después de que se establece la trampa
LINENO=0
trap '_steptrap $LINENO' DEBUG
\end{lstlisting}
Las primeras líneas guardan los tres argumentos fijos en variables y los desplazan para que los parámetros posicionales (si los hay) sean los que el usuario proporcionó en la línea de comandos como argumentos para programa de pruebas. Luego, el preámbulo lee otro archivo, \emph{kshdb.fns}, que contiene la esencia del depurador como definiciones de funciones. Ponemos este código en un archivo separado para minimizar el tamaño del archivo temporal. Examinaremos \emph{kshdb.fns} en breve.
A continuación, \emph{kshdb.pre} inicializa las dos listas de puntos de interrupción como vacías y el rastreo de ejecución como apagado (ver más abajo), luego lee el programa de pruebas en un array de líneas. Hacemos esto último para que el depurador pueda acceder a las líneas en el script al realizar ciertas verificaciones y para que la función de rastreo de ejecución pueda imprimir líneas de código a medida que se ejecutan. Utilizamos un array asociativo para mantener el código fuente del script de shell, para evitar el límite incorporado (aunque grande) de 4096 elementos para arrays indexados. (Admitimos que nuestro uso es un poco inusual; usamos números de línea como índices, pero en lo que respecta al shell, estos son solo cadenas que contienen solo dígitos).
La verdadera diversión comienza en el último grupo de líneas de código, donde configuramos el depurador para que comience a funcionar. Utilizamos dos comandos \emph{trap} con señales falsas. El primero establece una rutina de limpieza (que simplemente borra el archivo temporal) que se llamará en EXIT, es decir, cuando el script termine por cualquier motivo. El segundo, y más importante, configura la función \emph{\_steptrap} para que se llame antes de cada declaración.
\emph{\_steptrap} recibe un argumento que se evalúa como el número de la línea en programa de pruebas que se acaba de ejecutar. Usamos la misma técnica con la variable integrada \texttt{LINENO} que vimos anteriormente en el capítulo, pero con un giro añadido: si asignas un valor a \texttt{LINENO}, lo utiliza como el próximo número de línea e incrementa a partir de ahí. La instrucción \texttt{LINENO=0} reinicia la numeración de líneas para que la primera línea en programa de pruebas sea la línea 1.
Después de que se establece la trampa DEBUG, termina el preámbulo. La trampa DEBUG se ejecuta \emph{antes} de la siguiente declaración, que es la primera declaración del programa de pruebas. El shell entra así en \emph{\_steptrap} por primera vez. La variable \emph{\_steps} se configura para que \emph{\_steptrap} ejecute su última cláusula \texttt{elif}, como verás en breve, y entre en el depurador. Como resultado, la ejecución se detiene justo antes de que se ejecute la primera declaración del programa de pruebas, y el usuario ve un indicador \texttt{kshdb>}; el depurador está ahora en pleno funcionamiento.
\subsection{Funciones del depurador}
La función \emph{\_steptrap} es el punto de entrada al depurador; está definida en el archivo \emph{kshdb.fns}, que se enumera por completo al final de este capítulo. Aquí está \emph{\_steptrap}:
\begin{lstlisting}[language=bash]
# Aquí antes de cada declaración en el script que se está depurando.
# Maneja paso a paso y puntos de interrupción.
function _steptrap {
_curline=$1 # el argumento es el número de la línea que se ejecutó
(( $_trace )) && _msg "$PS4 línea $_curline: ${_lines[$_curline]}"
if (( $_steps >= 0 )); then # si está en modo paso a paso
let _steps="$_steps - 1" # decrementa el contador
fi
# primera verificación: si se alcanza el punto de interrupción de número de línea
if _at_linenumbp; then
_msg "Se alcanzó el punto de interrupción de línea en la línea $_curline"
_cmdloop # punto de interrupción, entra en el depurador
# segunda verificación: si se alcanza el punto de interrupción de cadena
elif _at_stringbp; then
_msg "Se alcanzó el punto de interrupción de cadena en la línea $_curline"
_cmdloop # punto de interrupción, entra en el depurador
# si no, verifica si existe y es verdadera la condición de interrupción
elif [[ -n $_brcond ]] && eval $_brcond; then
_msg "La condición de interrupción '$_brcond' es verdadera en la línea $_curline"
_cmdloop # condición de interrupción, entra en el depurador
# finalmente, verifica si está en modo paso a paso y el número de pasos ha terminado
elif (( _steps == 0 )); then # si está en modo paso a paso y es el momento de detenerse
_msg "Detenido en la línea $_curline"
_cmdloop # entra en el depurador
fi
}
\end{lstlisting}
\emph{\_steptrap} comienza estableciendo \emph{\_curline} en el número de la línea del programa de pruebas que se acaba de ejecutar. Si el rastreo de ejecución está activado, imprime el indicador de rastreo de ejecución PS4 (a la manera del modo \emph{xtrace}), el número de línea y la línea de código en sí.
Luego hace una de dos cosas: entra en el depurador, cuyo corazón es la función \emph{\_cmdloop}, o simplemente devuelve el control para que el shell pueda ejecutar la siguiente declaración. Elige lo primero si se ha alcanzado un punto de interrupción o una condición de interrupción (ver más abajo), o si el usuario entró en esta declaración.
\subsubsection{Comandos}
Explicaremos en breve cómo \emph{\_steptrap} determina estas cosas; ahora veremos \emph{\_cmdloop}. Es un bucle de comandos típico, que se asemeja a una combinación de las declaraciones de caso que vimos en el \hyperref[sec:Chapter5]{Capítulo 5} y el bucle del calculador que vimos en el \hyperref[sec:Chapter8]{Capítulo 8}.
\begin{lstlisting}[language=bash]
# Bucle de comandos del depurador.
# Aquí al inicio de la sesión del depurador, cuando se alcanza un punto de interrupción,
# después de paso a paso. Opcionalmente aquí dentro de un punto de observación.
function _cmdloop {
typeset cmd args
while read -s cmd"?kshdb> " args; do
case $cmd in
\#bp )
_setbp $args ;; # establecer punto de interrupción en número de línea o cadena.
\#bc )
_setbc $args ;; # establecer condición de interrupción.
\#cb )
_clearbp ;; # borrar todos los puntos de interrupción.
\#g )
return ;; # iniciar/reanudar ejecución
\#s )
let _steps=${args:-1} # paso simple N veces (por defecto 1)
return ;;
\#wp )
_setwp $args ;; # establecer un punto de observación
\#cw )
_clearwp $args ;; # borrar uno o más puntos de observación
\#x )
_xtrace ;; # alternar rastreo de ejecución
\#\? | \#h )
_menu ;; # imprimir menú de comandos
\#q )
exit ;; # salir
\#* )
_msg "Comando no válido: $cmd" ;;
* )
eval $cmd $args ;; # de lo contrario, ejecutar comando de shell
esac
done
}
\end{lstlisting}
En cada iteración, \emph{\_cmdloop} imprime un indicador, lee un comando y lo procesa. Usamos \texttt{read -s} para que el usuario pueda aprovechar la edición de línea de comandos dentro de \emph{kshdb}. Todos los comandos de \emph{kshdb} comienzan con \# para evitar confusiones con los comandos de shell. Cualquier cosa que no sea un comando de \emph{kshdb} (y no comience con \#) se pasa al shell para su ejecución. El uso de \# como carácter de comando evita que un comando mal escrito tenga algún efecto perjudicial cuando la última declaración lo captura y lo ejecuta a través de \emph{eval}. La Tabla \ref{Tab:9-5} resume los comandos del depurador.
\begin{table}[h]
\center
\caption{Comandos de \emph{kshdb}}
\label{Tab:9-5}
\begin{tabular}{|m{3cm}|m{12cm}|} \hline
\textbf{Comando} & \textbf{Acción} \\ \hline
\#bp \emph{N} & Establecer punto de interrupción en la línea N. \\\hline
\#bp \emph{str} & Establecer punto de interrupción en la siguiente línea que contenga \emph{str}. \\\hline
\#bp & Lista de puntos de interrupción y condición de interrupción. \\\hline
\#bc \emph{str} & Establece la condición de rotura en \emph{str}. \\\hline
\#bc & Condición de rotura clara. \\\hline
\#cb & Borra todos los puntos de interrupción. \\\hline
\#g & Iniciar o reanudar la ejecución (go). \\\hline
\#s \emph{[N]} & Paso a través de \emph{N} sentencias (por defecto 1). \\\hline
\#wp [-c] \emph{var} get & Establece un watchpoint en la variable \emph{var} cuando se recupera el valor. Con \texttt{-c}, entra en el bucle de comandos desde dentro del punto de control. \\\hline
\#wp [-c] \emph{var} set & Establece un watchpoint en la variable \emph{var} cuando se asigna el valor. Con \texttt{-c}, entra en el bucle de comandos desde dentro del punto de control. \\\hline
\#wp [-c] \emph{var} unset & Establece un punto de control en la variable \emph{var} cuando la variable no está establecida. Con \texttt{-c}, entra en el bucle de comandos desde dentro del punto de control. \\\hline
\#cw \emph{var discipline} & Borra el punto de control dado. \\\hline
\#wc & Borra todos los puntos de vigilancia. \\\hline
\#x & Activar el seguimiento de la ejecución. \\\hline
\#h, \#? & Imprime un menú de ayuda. \\\hline
\#q & Salir \\\hline
\end{tabular}
\end{table}
Antes de examinar los comandos individuales, es importante que entiendas cómo pasa el control a través de \emph{\_steptrap}, el bucle de comandos y el programa de pruebas.
\emph{\_steptrap} se ejecuta antes de cada declaración en el programa de pruebas como resultado de la declaración de \texttt{trap ... DEBUG} en el preámbulo. Si se ha alcanzado un punto de interrupción o el usuario escribió previamente un comando de paso (\#s), \emph{\_steptrap} llama al bucle de comandos. Al hacerlo, interrumpe efectivamente el shell que está ejecutando el programa de pruebas para entregar el control al usuario. \footnote{De hecho, los programadores de sistemas de bajo nivel pueden pensar en todo el mecanismo de \emph{trampa} como bastante similar a un esquema de manejo de interrupciones.}
El usuario puede invocar comandos del depurador, así como comandos de shell que se ejecutan en el mismo shell que el programa de pruebas. Esto significa que puedes usar comandos de shell para verificar los valores de las variables, las trampas de señales y cualquier otra información local al script que se está depurando.
El bucle de comandos se ejecuta, y el usuario mantiene el control, hasta que el usuario escribe \texttt{\#g, \#s} o \texttt{\#q}. Veamos en detalle qué sucede en cada uno de estos casos.
\texttt{\#g} tiene el efecto de ejecutar el programa de pruebas sin interrupciones hasta que termine o alcance un punto de interrupción. Pero en realidad, simplemente sale del bucle de comandos y vuelve a \emph{\_steptrap}, que también sale. El shell retoma el control; ejecuta la siguiente declaración en el script del programa de pruebas y llama a \emph{\_steptrap} nuevamente. Suponiendo que no hay un punto de interrupción, esta vez \emph{\_steptrap} simplemente sale nuevamente, y el proceso se repite hasta que hay un punto de interrupción o el programa de pruebas ha terminado.
\subsubsection{Paso a paso}
Cuando el usuario escribe \texttt{\#s}, el código del bucle de comandos establece la variable \texttt{\_steps} en el número de pasos que el usuario desea ejecutar, es decir, en el argumento proporcionado. Supongamos en un principio que el usuario omite el argumento, lo que significa que \texttt{\_steps} se establece en 1. Luego, el bucle de comandos sale y devuelve el control a \emph{\_steptrap}, que (como se mencionó anteriormente) sale y devuelve el control al shell. El shell ejecuta la siguiente declaración y vuelve a \emph{\_steptrap}, que ve que \texttt{\_steps} es 1 y lo decrementa a 0. Luego, la tercera cláusula \texttt{elif} ve que \texttt{\_steps} es 0, por lo que imprime un mensaje de <<detenido>> y llama al bucle de comandos.
Ahora supongamos que el usuario proporciona un argumento a \#s, digamos 3. \texttt{\_steps} se establece en 3. Entonces sucede lo siguiente:
\begin{enumerate}
\item Después de que se ejecuta la siguiente declaración, \emph{\_steptrap} se llama nuevamente. Entra en la primera cláusula \texttt{if}, ya que \texttt{\_steps} es mayor que 0. \emph{\_steptrap} decrementa \texttt{\_steps} a 2 y sale, devolviendo el control al shell.
\item Este proceso se repite, se ejecuta otro paso en el programa de pruebas, y \texttt{\_steps} se convierte en 1.
\item Se ejecuta una tercera declaración y volvemos a \emph{\_steptrap}. \texttt{\_steps} se decrementa a 0, se ejecuta la tercera cláusula \texttt{elif}, y \emph{\_steptrap} rompe de nuevo al bucle de comandos.
\end{enumerate}
El efecto general es que se ejecutan tres pasos y luego el depurador toma el control nuevamente.
Finalmente, el comando \#q sale. La trampa EXIT luego llama a la función \emph{\_cleanup}, que simplemente borra el archivo temporal y sale del programa completo.
Todos los demás comandos del depurador (\#bp, \#bc, \#cb, \#wp, \#cw, \#x, y comandos de shell) hacen que el shell permanezca en el bucle de comandos, lo que significa que el usuario prolonga la interrupción del shell.
\subsubsection{Puntos de ruptura}
Ahora examinaremos los comandos relacionados con los puntos de interrupción y el mecanismo de puntos de interrupción en general. El comando \#bp llama a la función \emph{\_setbp}, que puede establecer dos tipos de puntos de interrupción, según el tipo de argumento proporcionado. Si es un número, se trata como un número de línea; de lo contrario, se interpreta como una cadena que debería contener la línea de punto de interrupción.
Por ejemplo, el comando \texttt{\#bp 15} establece un punto de interrupción en la línea 15, y \texttt{\#bp grep} establece un punto de interrupción en la siguiente línea que contiene la cadena \emph{grep --}, sea cual sea el número que resulte. Aunque siempre puedes consultar una lista numerada de un archivo, \footnote{\texttt{pr -n filename} imprime una lista numerada en la salida estándar en versiones derivadas de Unix basadas en System V. Algunos sistemas antiguos derivados de BSD no lo admiten. Si esto no funciona en tu sistema, prueba con \texttt{cat -n filename}, o si eso no funciona, crea un script de shell con la única línea \texttt{awk '\{ printf("\%d\textbackslash{}t\%s\textbackslash{}n", NR, \$0 \}' \$1}}.
los argumentos de cadena para \#bp pueden hacer que esto no sea necesario.
Aquí está el código para \emph{\_setbp}:
\begin{lstlisting}[language=bash]
# Establece punto(s) de interrupción en los números de línea o cadenas dados
# agregando patrones a las variables de punto de interrupción
function _setbp {
if [[ -z $1 ]]; then
_listbp
elif [[ $1 == +([0-9]) ]]; then # número, establece punto de interrupción en esa línea
_linebp="${_linebp}$1|"
_msg "Punto de interrupción en la línea " $1
else # cadena, establece punto de interrupción en la siguiente línea con la cadena
_stringbp="${_stringbp}$@|"
_msg "Punto de interrupción en la próxima línea que contiene '$@'."
fi
}
\end{lstlisting}
\emph{\_setbp} establece los puntos de interrupción almacenándolos en las variables \texttt{\_linebp} (puntos de interrupción de número de línea) y \texttt{\_stringbp} (puntos de interrupción de cadena). Ambos tienen puntos de interrupción separados por caracteres de tubería, por razones que se harán claras en breve. Esto implica que los puntos de interrupción son acumulativos; establecer nuevos puntos de interrupción no borra los antiguos.
La única forma de eliminar los puntos de interrupción es con el comando \emph{\#cb}, que (en la función \emph{\_clearbp}) los borra todos de una vez simplemente restableciendo las dos variables a nulo. Si no recuerdas qué puntos de interrupción has establecido, el comando \#bp sin argumentos los enumera.
Las funciones \emph{\_at\_linenumbp} y \emph{\_at\_stringbp} son llamadas por \emph{\_steptrap} después de cada declaración; verifican si el shell ha llegado a un punto de interrupción de número de línea o de cadena, respectivamente.
Aquí está \emph{\_at\_linenumbp}:
\begin{lstlisting}[language=bash]
# Verifica si el próximo número de línea es un punto de interrupción.
function _at_linenumbp {
[[ $_curline == @(${_linebp%\|}) ]]
}
\end{lstlisting}
\emph{\_at\_linenumbp} aprovecha el carácter de tubería como el separador entre los números de línea: construye una expresión regular de la forma \texttt{\@(N1|N2|...)} tomando la lista de números de línea \emph{\_linebp}, eliminando el | final y rodeándolo con \texttt{\@(} y \texttt{)}. Por ejemplo, si \texttt{\$\_linebp} es \texttt{3|15|19|}, la expresión resultante es \texttt{\@(3|15|19)}.
Si la línea actual es cualquiera de estos números, la condición se vuelve verdadera, y \emph{\_at\_linenumbp} también devuelve un estado de salida <<verdadero>> (0).
La verificación de un punto de interrupción de cadena funciona sobre el mismo principio, pero es ligeramente más complicada; aquí está \emph{\_at\_stringbp}:
\begin{lstlisting}[language=bash]
# Busca puntos de interrupción de cadena para ver si la próxima línea en el script coincide.
function _at_stringbp {
[[ -n $_stringbp && ${_lines[$_curline]} == *@(${_stringbp%\|})* ]]
}
\end{lstlisting}
La condición primero verifica si \texttt{\$\_stringbp} no es nulo (lo que significa que se han definido puntos de interrupción de cadena). Si no es así, la condición se evalúa como falsa, pero si lo es, su valor depende de la coincidencia de patrones después del \&\&, que prueba la línea actual para ver si contiene alguna de las cadenas de puntos de interrupción.
La expresión en el lado derecho del signo igual doble es similar a la de \emph{\_at\_linenumbp} anterior, excepto que tiene * antes y después. Esto da expresiones de la forma \texttt{*\@(S1|S2|...)*}, donde las S son los puntos de interrupción de cadena. Esta expresión coincide con cualquier línea que contenga alguna de las posibilidades entre paréntesis.
El lado izquierdo del signo igual doble es el texto de la línea actual en el programa de pruebas. Entonces, si este texto coincide con la expresión regular, hemos llegado a un punto de interrupción de cadena; en consecuencia, la expresión condicional y \texttt{\_at\_stringbp} devuelven un estado de salida 0.
\emph{\_steptrap} prueba cada condición por separado, para que pueda decirte qué tipo de punto de interrupción detuvo la ejecución. En ambos casos, llama al bucle principal de comandos.
\subsection{Condiciones de interrupción}
\emph{kshdb} tiene otra característica relacionada con los puntos de interrupción: la \emph{condición de interrupción}. Esta es una cadena que el usuario puede especificar y que se evalúa como un comando; si es verdadera (es decir, devuelve un estado de salida 0), el depurador entra en el bucle de comandos. Dado que la condición de interrupción puede ser cualquier línea de código de shell, hay mucha flexibilidad en lo que se puede probar. Por ejemplo, puedes interrumpir cuando una variable alcanza un cierto valor (por ejemplo, \texttt{(( \$x < 0 ))}) o cuando un texto en particular se ha escrito a un archivo (\texttt{grep string archivo}). Probablemente pensarás en todo tipo de usos para esta característica. \footnote{Ten en cuenta que si tu condición de interrupción produce alguna salida estándar (o error estándar), la verás antes de cada declaración. Además, asegúrate de que tu condición de interrupción no tarde mucho tiempo en ejecutarse; de lo contrario, tu script se ejecutará muy, muy lentamente.}
Para establecer una condición de interrupción, escribe \texttt{\#bc string}. Para eliminarla, escribe \texttt{\#bc} sin argumentos; esto instala la cadena nula, que se ignora. \emph{\_steptrap} evalúa la condición de interrupción \texttt{\$\_brcond} solo si no es nula. Si la condición de interrupción se evalúa como 0, la cláusula \texttt{if} es verdadera y, una vez más, \emph{\_steptrap} llama al bucle de comandos.
\subsubsection{Rastreo de ejecución}
La siguiente característica es el rastreo de ejecución, disponible a través del comando \texttt{\#x}. Esta función está diseñada para superar el hecho de que un usuario de \emph{kshdb} no puede usar \texttt{set -o xtrace} mientras depura (ingresándolo como un comando de shell), porque su alcance se limita a la función \emph{\_cmdloop}. \footnote{De hecho, ingresando \texttt{typeset -ft funcname}, el usuario puede habilitar el rastreo de manera específica por función, pero probablemente sea mejor tenerlo todo bajo el control del depurador.}
La función \emph{\_xtrace} alterna el rastreo de ejecución simplemente asignando a la variable \emph{\_trace} la negación lógica de su valor actual, de modo que alterna entre 0 (apagado) y 1 (encendido). La preámbulo lo inicializa en 0.
\subsubsection{Puntos de observación}
\emph{kshdb} aprovecha las funciones de disciplina del shell para proporcionar puntos de observación. Puedes establecer un punto de observación en cualquier variable cuando se recupera o cambia el valor de la variable, o cuando la variable se anula. Opcionalmente, el punto de observación se puede configurar para ingresar también al bucle de comandos. Haces esto con el comando \#wp, que a su vez llama a \emph{\_setwp}:
\begin{lstlisting}[language=bash]
# Establece un punto de observación en una variable
# uso: _setwp [-c] var disciplina
# $1 = variable
# $2 = get|set|unset
typeset -A _watchpoints
function _setwp {
typeset funcdef do_cmdloop=0
if [[ $1 == -c ]]; then
do_cmdloop=1
shift
fi
funcdef="function $1.$2 { "
case $2 in
get) funcdef+="_msg $1 \(\$$1\) recuperado, línea \$_curline"
;;
set) funcdef+="_msg $1 configurado a "'${.sh.value}'", línea \$_curline"
;;
unset) funcdef+="_msg $1 anulado en línea \$_curline"
funcdef+=$'\nunset '"$1"
;;
*) _msg función de punto de observación $2 no válida
return 1
;;
esac
if ((do_cmdloop)); then
funcdef+=$'\n_cmdloop'
fi
funcdef+=$'\n}'
eval "$funcdef"
_watchpoints[$1.$2]=1
}
\end{lstlisting}
Esta función ilustra varias técnicas interesantes. Lo primero que hace es declarar algunas variables locales y verificar si se invocó con la opción \texttt{-c}. Esto indica que el punto de observación debería ingresar al bucle de comandos.
La idea general es construir el texto de la función de disciplina apropiada en la variable \texttt{funcdef}. El valor inicial es la palabra clave \texttt{function}, el nombre de la función de disciplina y la llave izquierda de apertura. El espacio después de la llave es importante, para que el shell la reconozca correctamente como una palabra clave.
Luego, para cada tipo de función de disciplina, el bloque \texttt{case} agrega el cuerpo de función apropiado a la cadena \texttt{funcdef}. El código utiliza barras invertidas colocadas de manera prudente para obtener la mezcla correcta de evaluación inmediata y diferida de variables del shell. Considera el caso \texttt{get}: para \texttt{\textbackslash{}(}, la barra invertida se mantiene intacta para usarla como un carácter de cotización dentro del cuerpo de la función de disciplina. Para \texttt{\textbackslash{}\$\$1}, la cotización sucede de la siguiente manera: el \texttt{\textbackslash{}\$} se convierte en un \$ dentro de la función, mientras que el \$1 se evalúa inmediatamente dentro de la cadena entre comillas dobles.
En el caso de que se haya suministrado la opción \texttt{-c}, utiliza la notación \texttt{\$'...'} para agregar una nueva línea y llamar a \emph{\_cmdloop} al cuerpo de la función, y luego al final agrega otra nueva línea y una llave derecha de cierre. Finalmente, mediante \emph{eval}, instala la función recién creada.
Por ejemplo, si se usó \texttt{-c}, el texto de la función \emph{get} generada para la variable \texttt{count} termina viéndose así:
\begin{lstlisting}[language=bash]
function count.get {
_msg count \($count\) recuperado, línea $_curline
_cmdloop
}
\end{lstlisting}
Al final de \emph{\_setwp}, \texttt{\_watchpoints[\$1.\$2]} se establece en 1. Esto crea una entrada en el array asociativo \texttt{\_watchpoints} indexada por el nombre de la función de disciplina. Esto almacena convenientemente los nombres de todos los puntos de observación para cuando queramos borrarlos.
Los puntos de observación se borran con el comando \texttt{\#cw}, que a su vez ejecuta la función \emph{\_clearwp}. Aquí está:
\begin{lstlisting}[language=bash]
# Borrar puntos de observación:
# sin argumentos: borrar todos
# dos argumentos: igual que para establecer: var get|set|unset
function _clearwp {
if [ $# = 0 ]; then
typeset _i
for _i in ${!_watchpoints[*]}; do
unset -f $_i
unset _watchpoints[$_i]
done
elif [ $# = 2 ]; then
case $2 in get | set | unset)
unset -f $1.$2
unset _watchpoints[$1.$2] ;;
*) _msg $2: punto de observación no válido ;;
esac
fi
}
\end{lstlisting}
Cuando se invoca sin argumentos, \emph{\_clearwp} borra todos los puntos de observación, recorriendo todos los subíndices en el array asociativo \texttt{\_watchpoints}. De lo contrario, si se invoca con dos argumentos, el nombre de la variable y la función de disciplina, anula la función utilizando \texttt{unset -f}. En ambos casos, también se anula la entrada en \texttt{\_watchpoints}.
\subsubsection{Limitaciones}
\emph{kshdb} no fue diseñado para llevar el estado del arte del depurador hacia adelante o tener un exceso de funciones. Tiene las características básicas más útiles; su implementación es compacta y (esperamos) comprensible. Pero tiene algunas limitaciones importantes. Las que conocemos se describen en la lista que sigue:
\begin{itemize}
\item Los puntos de interrupción de cadena no pueden comenzar con dígitos ni contener caracteres de tubería (|) a menos que estén correctamente escapados.
\item Solo puedes establecer puntos de interrupción, ya sea de número de línea o de cadena, en líneas del programa de pruebas que contienen lo que la documentación del shell llama \emph{comandos simples}, es decir, comandos Unix reales, integrados en el shell, llamadas a funciones o alias. Si estableces un punto de interrupción en una línea que contiene solo espacios en blanco o un comentario, el shell siempre omitirá ese punto de interrupción. Más importante aún, las palabras clave de control como \texttt{while, if, for, do, done} e incluso condicionales (\texttt{[[...]]} y \texttt{((...))}) tampoco funcionarán, a menos que haya un comando simple en la misma línea.
\item \emph{kshdb} no <<desciende> a los scripts de shell que se llaman desde el programa de pruebas. Para hacer esto, debes editarlo y cambiar una llamada a \emph{scriptname} a \texttt{kshdb scriptname}.
\item De manera similar, las subcáscaras se tratan como una sola declaración gigantesca; no puedes descender en ellas en absoluto.
\item El programa de pruebas no debe atrapar las señales ficticias DEBUG o EXIT; de lo contrario, el depurador no funcionará.
\item Las variables que están \emph{tipadas} (ver \hyperref[sec:Chapter4]{Capítulo 4}) no son accesibles en condiciones de interrupción. Sin embargo, puedes usar el comando del shell \emph{print} para verificar sus valores.
\item El manejo de errores de comando es débil. Por ejemplo, un argumento no numérico para \texttt{\#s} hará que falle.
\item Los puntos de observación que invocan al bucle de comandos son frágiles. Para \emph{ksh93m} en GNU/Linux, intentar anular un punto de observación cuando está en el bucle de comandos invocado desde el punto de observación hace que el shell genere un volcado de núcleo. Pero esto no sucede en todas las plataformas, y esto se solucionará eventualmente.
\end{itemize}
Muchos de estos problemas no son insuperables; consulta los ejercicios.
\subsection{Ejemplo de sesión kshdb}
A continuación, mostraremos una transcripción de una sesión real con \emph{kshdb}, en el que el programa de pruebas es (una versión ligeramente modificada) de la solución para la \hyperref[box:6-3]{Tarea 6-3}. Para mayor comodidad, aquí tienes una lista numerada del script, que llamaremos \emph{lscol}.
\begin{lstlisting}[language=bash]
set -A filenames $(ls $1)
typeset -L14 fname
let numfiles=${#filenames[*]}
let numcols=5
for ((count = 0; $count < $numfiles ; )); do
fname=${filenames[count]}
print -n "$fname "
let count++
if (( count % numcols == 0 )); then
print # nueva línea
fi
done
if (( count % numcols != 0 )); then
print
fi
\end{lstlisting}
Aquí está la transcripción de la sesión de \emph{kshdb}:
\begin{lstlisting}[language=bash]
$ kshdb lscol book
Korn Shell Debugger version 2.0 for ksh Version M 1993-12-28 m
Stopped at line 1
kshdb> #bp 4
Breakpoint at line 4
kshdb> #g
Reached line breakpoint at line 4
kshdb> #s
Stopped at line 6
kshdb> print $numcols
5
kshdb> #bc (( count == 10 ))
Break when true: (( count == 10 ))
kshdb> #g
appa.xml appb.xml appc.xml appd.xml appf.xml
book.xml ch00.xml ch01.xml ch02.xml ch03.xml
Break condition '(( count == 10 ))' true at line 10
kshdb> #bc
Break condition cleared.
kshdb> #bp newline
Breakpoint at next line containing 'newline'.
kshdb> #g
Reached string breakpoint at line 11
kshdb> print $count
10
kshdb> let count=9
kshdb> #g
ch03.xml Reached string breakpoint at line 11
kshdb> #bp
Breakpoints at lines:
4
Breakpoints at strings:
newline
Break on condition:
kshdb> #g
ch04.xml ch05.xml ch06.xml ch07.xml ch08.xml
Reached string breakpoint at line 11
kshdb> #g
ch09.xml ch10.xml colo1.xml copy.xml
$
\end{lstlisting}
Primero, observa que le dimos al script de pruebas el argumento \texttt{book}, lo que significa que queremos listar los archivos en ese directorio. Comenzamos estableciendo un punto de interrupción simple en la línea 4 y ejecutamos el script. Se detiene antes de ejecutar la línea 4 (\texttt{let numcols=5}). Emitimos el comando \texttt{\#s} para ejecutar paso a paso el comando (es decir, para ejecutarlo realmente). Luego emitimos un comando \emph{print} del shell para mostrar que la variable \texttt{numcols} está configurada correctamente.
A continuación, establecemos una condición de interrupción, indicándole al depurador que intervenga cuando \texttt{count} sea 10, y reanudamos la ejecución. Efectivamente, el programa de pruebas imprime 10 nombres de archivo y se detiene en la línea 10, justo después de incrementar \texttt{count}. Borramos la condición de interrupción escribiendo \texttt{\#bc} sin un argumento, ya que de lo contrario, el shell se detendría después de cada declaración hasta que la condición se volviera falsa.
El siguiente comando muestra cómo funciona el mecanismo de punto de interrupción de cadena. Le decimos al depurador que interrumpa cuando llegue a una línea que contenga la cadena \texttt{newline}. Esta cadena está en un comentario en la línea 11. Observa que no importa que la cadena esté en un comentario, solo importa que la línea en la que se encuentra contenga un comando real. Continuamos la ejecución y el depurador alcanza el punto de interrupción en la línea 11.
Después de eso, mostramos cómo podemos usar el depurador para cambiar el estado del programa de pruebas mientras se ejecuta. Vemos que \texttt{\$count} sigue siendo mayor que 10; lo cambiamos a 9. En la siguiente iteración del bucle \texttt{while}, el script accede al mismo nombre de archivo que acaba de hacerlo (\emph{ch03.xml}), incrementa \texttt{count} de nuevo a 10 y alcanza nuevamente el punto de interrupción de cadena. Finalmente, listamos los puntos de interrupción y avanzamos hasta el final, momento en el que sale.
\subsection{Ejercicios}
Concluimos este capítulo con algunos ejercicios, que son mejoras sugeridas para \emph{kshdb}.
\begin{enumerate}
\item Mejora el manejo de errores de comandos de las siguientes maneras:
\begin{enumerate}
\item Para argumentos numéricos de \texttt{\#bp}, verifica que sean números de línea válidos para el programa de pruebas en particular.
\item Verifica que los argumentos de \texttt{\#s} sean números válidos.
\item Cualquier otro manejo de errores que se te ocurra.
\end{enumerate}
\item Mejora el comando \texttt{\#cb} para que el usuario pueda eliminar puntos de interrupción específicos (por cadena o número de línea).
\item Elimina la principal limitación en el mecanismo de puntos de interrupción:
\begin{enumerate}
\item Mejóralo de manera que si el número de línea seleccionado no contiene un comando Unix real, se utilice en su lugar la línea más cercana por encima de ella como punto de interrupción.
\item Haz lo mismo para los puntos de interrupción de cadena. (Pista: primero traduce cada comando de punto de interrupción de cadena en uno o más comandos de punto de interrupción por número de línea).
\end{enumerate}
\item Implementa una opción que provoque una interrupción en el depurador cada vez que un comando sale con un estado distinto de cero:
\begin{enumerate}
\item Impleméntalo como la opción de línea de comandos \texttt{-e}.
\item Impleméntalo como los comandos del depurador \texttt{\#be} (para activar la opción) y \texttt{\#ne} (para desactivarla). (Pista: no podrás usar el \emph{trap ERR}, pero ten en cuenta que cuando entras en \emph{\_steptrap}, \texttt{\$?} sigue siendo el estado de salida del último comando que se ejecutó).
\end{enumerate}
\item Añade la capacidad de <<descender>> a los scripts que en los que el programa de pruebas llama (es decir, subprocesos de shell) como la opción de línea de comandos \texttt{-s}. Una forma de implementar esto es cambiar el script \emph{kshdb} para que inserte llamadas recursivas a \emph{kshdb} en el programa de pruebas. Puedes hacer esto filtrando el programa de pruebas a través de un bucle que lee cada línea y determina, con los comandos \texttt{whence -v} y \emph{file(1)} (consulta la página de manual), si la línea es una llamada a otro script de shell. \footnote{Ten en cuenta que este método debería capturar la mayoría de los scripts de shell separados, pero no todos. Por ejemplo, no capturará scripts de shell que sigan a punto y coma (por ejemplo, \texttt{cmd1; cmd2}).}
Si es así, antepón \texttt{kshdb -s} a la línea y escríbela en el nuevo archivo; si no, simplemente pásala tal como está.
\item Añade soporte para múltiples condiciones de interrupción, de modo que \emph{kshdb} detenga la ejecución cuando cualquiera de ellas sea verdadera e imprima un mensaje que indique cuál es verdadera. Haz esto almacenando las condiciones de interrupción en una lista separada por dos puntos o en un array. Intenta que esto sea lo más eficiente posible, ya que la verificación debe realizarse antes de cada declaración.
\item Añade cualquier otra característica que se te ocurra.
\end{enumerate}
Finalmente, aquí tienes el código fuente completo para el archivo de funciones del depurador \emph{kshdb.fns}:
\begin{lstlisting}[language=bash]
# Aquí antes de cada declaración en el script en depuración.
# Maneja el paso individual y los puntos de interrupción.
function _steptrap {
_curline=$1 # el argumento es el número de línea que acaba de ejecutarse
(( $_trace )) && _msg "$PS4 línea $_curline: ${_lines[$_curline]}"
if (( $_steps >= 0 )); then # si está en modo paso a paso
let _steps="$_steps - 1" # decrementa el contador
fi
# primera comprobación: si se alcanza un punto de interrupción por número de línea
if _at_linenumbp; then
_msg "Se alcanzó un punto de interrupción en la línea $_curline"
_cmdloop # punto de interrupción, entra en el depurador
# segunda comprobación: si se alcanza un punto de interrupción por cadena
elif _at_stringbp; then
_msg "Se alcanzó un punto de interrupción de cadena en la línea $_curline"
_cmdloop # punto de interrupción, entra en el depurador
# si no, comprueba si existe una condición de interrupción y es verdadera
elif [[ -n $_brcond ]] && eval $_brcond; then
_msg "La condición de interrupción '$_brcond' es verdadera en la línea $_curline"
_cmdloop # condición de interrupción, entra en el depurador
# finalmente, comprueba si está en modo paso a paso y el número de pasos ha terminado
elif (( _steps == 0 )); then # si está en modo paso a paso y es hora de detenerse
_msg "Detenido en la línea $_curline"
_cmdloop # entra en el depurador
fi
}
# Bucle de comandos del depurador.
# Aquí al inicio de la sesión del depurador, cuando se alcanza un punto de interrupción,
# después de un paso individual. Opcionalmente aquí dentro de un punto de control.
function _cmdloop {
typeset cmd args
while read -s cmd"?kshdb> " args; do
case $cmd in
\#bp ) _setbp $args ;; # establece un punto de interrupción en el número de línea o cadena.
\#bc ) _setbc $args ;; # establece una condición de interrupción.
\#cb ) _clearbp ;; # elimina todos los puntos de interrupción.
\#g ) return ;; # iniciar/reanudar la ejecución.
\#s ) let _steps=${args:-1} # ejecutar N declaraciones una vez (predeterminado 1).
return ;;
\#wp ) _setwp $args ;; # establece un punto de control en una variable.
\#cw ) _clearwp $args ;; # elimina uno o más puntos de control.
\#x ) _xtrace ;; # alternar seguimiento de ejecución on/off.
\#\? | \#h ) _menu ;; # imprimir menú de comandos.
\#q ) exit ;; # salir.
\#* ) _msg "Comando no válido: $cmd" ;;
* ) eval $cmd $args ;; # de lo contrario, ejecuta el comando de shell.
esac
done
}
# Verifica si el próximo número de línea es un punto de interrupción.
function _at_linenumbp {
[[ $_curline == @(${_linebp%\|}) ]]
}
# Busca puntos de interrupción de cadena para ver si la siguiente línea del script coincide.
function _at_stringbp {
[[ -n $_stringbp && ${_lines[$_curline]} == *@(${_stringbp%\|})* ]]
}
# Imprime el mensaje dado en la salida de error estándar.
function _msg {
print -r -- "$@" >&2
}
# Establece punto(s) de interrupción en números de línea o cadenas
# mediante la adición de patrones a las variables de puntos de interrupción.
function _setbp {
if [[ -z $1 ]]; then
_listbp
elif [[ $1 == +([0-9]) ]]; then # número, establece un punto de interrupción en esa línea
_linebp="${_linebp}$1|"
_msg "Punto de interrupción en la línea " $1
else # string, establece un punto de interrupción en la siguiente línea con el string
_stringbp="${_stringbp}$@|"
_msg "Punto de interrupción en la siguiente línea que contiene '$@'."
fi
}
# Lista puntos de interrupción y condición de interrupción
function _listbp {
_msg "Puntos de interrupción en líneas:"
_msg "$(print $_linebp | tr '|' ' ')"
_msg "Puntos de interrupción en cadenas:"
_msg "$(print $_stringbp | tr '|' ' ')"
_msg "Interrupción en condición:"
_msg "$_brcond"
}
# Establece o elimina la condición de interrupción.
function _setbc {
if [[ $# = 0 ]]; then
_brcond=
_msg "Condición de interrupción eliminada."
else
_brcond="$*"
_msg "Interrumpir cuando sea verdadero: $_brcond"
fi
}
# Elimina todos los puntos de interrupción.
function _clearbp {
_linebp=
_stringbp=
_msg "Todos los puntos de interrupción eliminados."
}
# Alterna la función de seguimiento de ejecución on/off.
function _xtrace {
let _trace="! $_trace"
if (( $_trace )); then
_msg "Seguimiento de ejecución activado."
else
_msg "Seguimiento de ejecución desactivado."
fi
}
# Imprime menú de comandos.
function _menu {
_msg 'Comandos kshdb:
#bp N establece un punto de interrupción en la línea N
#bp str establece un punto de interrupción en la siguiente línea que contiene str
#bp lista puntos de interrupción y condición de interrupción
#bc str establece la condición de interrupción a str
#bc elimina la condición de interrupción
#cb elimina todos los puntos de interrupción
#wp [-c] var discipline establece un punto de control en una variable
#cw elimina todos los puntos de control
#g inicia/reanuda la ejecución
#s [N] ejecuta N declaraciones (predeterminado 1)
#x alterna el seguimiento de ejecución on/off
#h, #? imprime este menú
#q salir'
}
# Borra archivos temporales antes de salir.
function _cleanup {
rm $_dbgfile 2>/dev/null
}
# Establece un punto de control en una variable
# uso: _setwp [-c] var disciplina
# $1 = variable
# $2 = get|set|unset
typeset -A _watchpoints
function _setwp {
typeset funcdef do_cmdloop=0
if [[ $1 == -c ]]; then
do_cmdloop=1
shift
fi
funcdef="function $1.$2 { "
case $2 in
get) funcdef+="_msg $1 \(\$$1\) obtenido, línea \$_curline"
;;
set) funcdef+="_msg $1 establecido a "'${.sh.value}'", línea \$_curline"
;;
unset) funcdef+="_msg $1 eliminado en la línea \$_curline"
funcdef+=$'\nunset '"$1"
;;
*) _msg función de punto de control no válida $2
return 1
;;
esac
if ((do_cmdloop)); then
funcdef+=$'\n_cmdloop'
fi
funcdef+=$'\n}'
eval "$funcdef"
_watchpoints[$1.$2]=1
}
# Elimina puntos de control:
# sin argumentos, elimina todos
# con dos argumentos: lo mismo que para el establecimiento: var get|set|unset
function _clearwp {
if [ $# = 0 ]; then
typeset _i
for _i in ${!_watchpoints[*]}; do
unset -f $_i
unset _watchpoints[$_i]
done
elif [ $# = 2 ]; then
case $2 in
get | set | unset)
unset -f $1.$2
unset _watchpoints[$1.$2]
;;
*) _msg $2: punto de control no válido
;;
esac
fi
}
\end{lstlisting}