Si sois usuarios habituales de utilidades como
gdb
o gnuplot
, o simplemente usáis bash
como auténticos pros, entonces sabéis muy bien lo potente que puede ser una línea de comandos. Todos esos programas consiguen ese interfaz tan potente gracias al uso de la librería GNU readline
. En este artículo vamos a ver como sacarle todo el partido para escribir nuestras propias aplicaciones como auténticos pros.
Para ilustrar el uso de
readline
, vamos a utilizar, como no, un programilla de ejemplo al que ir añadiendo funcionalidades para, de forma progresiva, explorar las opciones que nos ofrece readline
. Así que vamos a partir de este pequeño programa.
#include <stdio.h> #include <stdlib.h> #include <readline/readline.h> int main () { char *input = NULL; int terminamos = 0; printf ("Línea de Comandos como un PRO\n"); while (!terminamos) { input = readline ("LCCP $ "); printf ("+ Has introducido el comando: '%s'\n", input); if (!strcmp (input, "exit")) terminamos = 1; free (input); } }El programa anterior lo podéis compilar con el siguiente comando:
$ gcc -o rl01 rl01.c -lreadline
De por que sí
Si lanzáis el programa y lo probáis, veréis que, por el simple hecho de usarreadline
obtenemos unas cuantas ventajas de por que sí.
La primera, es que readline
va a reservar la memoria necesaria para nuestra cadena por nosotros. No tendremos que preocuparnos de llamar a malloc
o realloc
. No es que sea un problema muy grande, pero se agradece y además nuestro programa va a ser más corto, compacto y fácil de leer.
La segunda ventaja... bueno, lanzad el programa y pulsad la tecla TAB
. Sí, funciona como en bash
. Podemos auto-completar los nombres de los ficheros en el directorio actual y navegarlo como hacemos desde la línea de comandos de GNU/Linux.
No está nada mal para un programa tan cortito eh?.
Aquel que no recuerda su historia...
... está condenado a repetirla. Esta es una verdad como un templo, así que lo siguiente que deberíamos añadir a nuestro programa es la capacidad de recordar los comandos que hayamos introducido anteriormente (la historia), de forma que no tengamos que teclear los una y otra vez. Nada más fácil.#include <stdio.h> #include <stdlib.h> #include <readline/readline.h> #include <readline/history.h> int main () { char *input = NULL; int terminamos = 0; printf ("Línea de Comandos como un PRO\n"); while (!terminamos) { input = readline ("LCCP $ "); add_history(input); printf ("+ Has introducido el comando: '%s'\n", input); if (!strcmp (input, "exit")) terminamos = 1; free (input); } }Como podéis ver, solo hemos tenido que añadir un nuevo fichero de cabecera y una llamada a la función
add_history
... más fácil no se puede!. Ahora podéis recompilar el programa con el mismo comando que usamos anteriormente y probarlo de nuevo.
Introducid algunos comandos y, como seguro ya sabéis, utilizad los cursores arriba y abajo para moveros a través de la lista de los comandos introducidos. Cosa como CTRL+R
para hacer búsquedas inversas en la historia de comandos también funciona!
Completado Automático
Si habéis jugado con el programa anterior, habréis que al pulsar TAB, el programa intenta completar el comando con la lista de ficheros, lo cual es bastante útil, pero a nosotros nos interesa que, al menos el primer tab nos muestre los comandos ofrecidos por nuestro programa y no los ficheros en el directorio actual. Así que vamos a añadir un par de comandos a nuestro programa y modificarlo para quereadline
los conozca. readline
no ofrece soporte para esto, la librería se encarga de leer cadenas y ofrecerlas al programa... y ejecutar comandos no es la única razón por la que alguien querría leer una cadena... así que readline
se mantiene tan agnóstico como sea posible de la aplicación que la usa.
Dicho esto, vamos a empezar definiendo una sencilla estructura para almacenar nuestros comandos, añadiendo el siguiente código al principio del programa:
typedef int (*FUNC)(char*); typedef struct cmd_t { char *id; FUNC f; } CMD;Nada especial, nuestros comandos tienen un nombre y una función asociada. Ahora declararemos, justo a continuación nuestros comandos. Para ello necesitamos definir una array que contenga nuestra lista de comandos, y las funciones para cada uno de ellos. Para ahorrarnos escribir los prototipos (y ahorrar algo de espacio), en el pedazo de código que sigue, veréis las funciones asociadas a los comandos, seguidas del array que contiene la lista de comandos.
int func_cargar (char *par) { printf ("Comando cargar con parametros (%s)\n", par); return 0; } int func_procesar (char *par) { printf ("Comando procesar con parametros (%s)\n", par); return 0; } int func_salir (char *par) { printf ("Terminando el programa\n"); return 1; } CMD _cmd[] = { {"cargar", func_cmd1}, {"procesar", func_cmd2}, {"salir", func_exit}, {NULL, NULL} };
Ejecutando los comandos
Ahora solo nos queda modificar el bucle principal para poder interpretar la entrada del usuario y ejecutar la función correcta. Pero antes, necesitamos una pequeña función para localizar la entrada asociada a un determinado comando. Esta es la que hemos escrito nosotros:int busca_cmd (char *c) { int i; for (i = 0; _cmd[i].id; i++) if (!strncmp (_cmd[i].id, c, strlen(_cmd[i].id))) return i; return -1; }Esta implementación no es ni de lejos la mejor, pero nos permite obviar el formato de la entrada del usuario, y buscar en la lista de comandos cualquier comando que aparezca al principio de lo que el usuario haya escrito. Si vais a escribir vuestros propios programas, es mejor que utilicéis algo más robusto. Si queréis usar este código notad que los comandos tienen que estar ordenados por longitud, dentro del array, cuando unos sean prefijos de otros (empiecen por los mismos caracteres)... de lo contrario la función no devolverá el comando que queremos. Bueno, ahora ya podemos modificar el bucle principal, que quedará de la siguiente forma:
while (!terminamos) { input = readline ("LCCP $ "); add_history(input); indx = busca_cmd (input); if (indx >= 0) terminamos = _cmd[indx].f (input); free (input); }Como podéis observar no nos hemos matado. Buscamos el comando, y si lo encontramos, ejecutamos la función asociada con la entrada completa del usuario (lo que también incluye el comando). Ahora que ya tenemos comandos vamos a hacer que nuestro programa los autocomplete!
Auto Completado de Comandos
En nuestra última versión del programa, en la que tenemos 3 comandos, si pulsamos TAB en una línea vacía, obtenemos la lista de ficheros en el directorio actual... pero lo que nos gustaría obtener es la lista de comandos disponibles... verdad?. Pues vamos a ellos. Lo primero que tenemos que hacer es cambiar la función de autocompletado que utilizareadline
por defecto. Para ello añadimos la siguiente línea justo antes de empezar nuestro bucle:
(...) rl_attempted_completion_function = completa_cmd; while (!terminamos) (...)Ahora, cuando pulsemos TAB, la función
completa_cmd
será invocada. Vamos a ver que pinta tiene esta función:
char ** completa_cmd(const char *text, int start, int end) { char **matches; matches = (char **)NULL; if (start == 0) matches = (char **) rl_completion_matches (text, genera_cmd); return matches; }Bien, la función recibe como parámetro el texto que debemos auto completar, seguido de su posición en la cadena introducida por el usuario, donde empieza y donde acaba. Lo que estamos haciendo en nuestra función
completa_comando
es buscar auto completados utilizando una lista de cadenas que le vamos a proporcionar a través de la función genera_cmd
.
Así, si la cadena empieza en 0, eso significa que estamos procesando la primera palabra del texto que introduce el usuario, lo que en nuestro caso tiene que ser obligatoriamente un comando. Sino, dejamos que readline
haga su magia. Esto significa que los posibles parámetros de nuestros comandos se completarán con nombres de ficheros... como sucedía en nuestro programa anterior.
Bueno, veamos que pinta tiene uno de esos generadores:
char * genera_cmd (const char *text, int state) { static int list_index, len; char *name; if (!state) { list_index = 0; len = strlen(text); } while ((name = _cmd[list_index++].id)) { if (strncmp(name, text, len) == 0) { return strdup(name); } } return NULL; }Esta función es llamada por
readline
, tantas veces como sea necesario hasta obtener la lista completa de todas las opciones de autocompletado. El parámetro state
, nos indica cuantas veces hemos sido llamados hasta el momento. Como podéis ver, cuando state
es 0, lo que hacemos es inicializar unas variables para empezar a buscar correspondencias desde el principio de nuestra lista de comandos (list_index=0
). Cada vez que encontramos una correspondencia, se la devolvemos a readline
), hasta que ya no haya más, en cuyo caso devolvemos NULL
, para que readline
sepa que ya hemos terminado.
Para comprobar esto, antes de compilar y ejecutar, vamos añadir otro comando a nuestra lista de comandos, para ver que todo esto está funcionando correctamente. Algo tal que así:
CMD _cmd[] = { {"cargar", func_cargar}, {"procesar", func_procesar}, {"procesar_txt", func_procesar}, {"salir", func_salir}, {NULL, NULL} };Hemos re-utilizando la misma función que usamos para le comando
procesa
. Después detodo, en este ejemplo tan sencillo, las funciones no están haciendo nada. Ahora podéis lanzar el programa, escribir p y pulsar TAB, 2 veces!
AutoCompletando Parámetros
Para terminar nuestro interfaz de línea de comando super profesional, vamos a añadir la capacidad de autocompletar parámetros, y no solo los comandos. Tal y como tenemos todo ahora, una vez que el primer pedazo de la cadena que introduce el usuario (los pedazos se separan por espacios) se haya autocompletado, los siguientes (los parámetros) se van a autocompletar con la función por defecto, es decir, con los nombres de los ficheros en el directorio actual. Esto nos va genial para nuestro comandocargar
, pero para nuestro comando procesa
puede que nos interese utilizar otros parámetros. Bueno, el proceso de auto completado ya sabemos como funciona, lo único que tenemos que hacer es enganchar un generador para nuestros parámetros de alguna forma en nuestro programa.
Primero definamos unos parámetros para nuestro comando procesa:
char *procesa_pars[] ={ "temperatura", "presion", "humedad", NULL};Ahora escribamos un generador para estos parámetros... básicamente un copy y paste de nuestro generador de comandos :).
char * genera_ppar (const char *text, int state) { static int list_index, len; char *name; if (!state) { list_index = 0; len = strlen(text); } while ((name = procesa_pars[list_index++])) { if (strncmp(name, text, len) == 0) { return strdup(name); } } return NULL; }Como podéis ver es exactamente lo mismo, solo que buscamos en una tabla diferente. Ahora solo necesitamos llamar a este generador en el momento adecuado. Para ello vamos a modificar nuestra función
completa_cmd
de la siguiente forma:
char ** completa_cmd(const char *text, int start, int end) { char **matches; char *current = rl_line_buffer; matches = (char **)NULL; if (start == 0) matches = (char **) rl_completion_matches (text, genera_cmd); else if (!strncmp (current, "procesar ", strlen("procesar "))) matches = (char **) rl_completion_matches (text, genera_ppar); return matches; }Como podéis ver, estamos haciendo dos cosas. La primera es obtener la entrada completa del usuario hasta el momento. Este valor se almacena en la variable
rl_line_buffer
. La razón es que, una vez que comencemos a procesar el segundo parámetro de nuestro comando, el parámetro text
que recibimos en la función solo contiene esa parte de la cadena. Necesitamos saber a que comando pertenece el parámetro que podemos comprobar. Puede que haya otras formas de conseguir este resultado, pero esta es bastante sencilla y funciona correctamente.
Resumiendo, lo que hacemos es comprobar si estamos autocompletando el comando procesar
y además no nos encontramos al principio de la línea... es decir, estamos procesando el primer parámetro del comando. En ese caso simplemente llamamos al generador que va a producir los parámetros de procesar
, en lugar de la lista de comandos de primer nivel.
Conclusiones
Esto es todo sobre esta breve y somera introducción aGNU readline
. Como habréis comprobado, es muy fácil conseguir un interfaz bastante potente con unas pocas líneas de código. Para los más curiosos, readline
todavía guarda algunos secretos más... en caso de que queráis investigar.