Aplicaciones de Línea de Comandos Profesionales con READLINE
LIBRERÍAS
Aplicaciones de Línea de Comandos Profesionales con READLINE
2017-07-30
Por
Richi C. Poweri

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);
    }
}
LISTADO 1. Readline Mínimo

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 usar readline 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);
    }
}
LISTADO 2. Añadiendo historia a nuestra línea de comandos

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 que readline 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;
LISTADO 3. Estructura de datos para comandos

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}
};
LISTADO 4. Definición de comandos

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;
}
LISTADO 5. Función para buscar comandos

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);
    }
LISTADO 6. Nuestro nuevo bucle principal

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 utiliza readline por defecto. Para ello añadimos la siguiente línea justo antes de empezar nuestro bucle:

(...)
  rl_attempted_completion_function = completa_cmd;
  while (!terminamos)
(...)
LISTADO 7. Modificando la función de auto completado

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;     
}
Listado 8. Nuestra función de auto completado

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;
}
LISTADO 9. Generador de comandos para readline

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 comando cargar, 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;
}
Listado 10. Generador de parámetros

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;     
}
LISTADO 11. Nueva función de auto completado con soporte para parámetros

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 a GNU 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.

Header Image Credits: Camille Kimberly

SOBRE Richi C. Poweri
Cuando se trata de programación, Richi es tu chica. Tiene un don especial para los lenguajes de programación, y habla con el sistema operativo de tu a tu. En su tienpo libre Richi ayuda a su familia, en los temas familiares