|
Chiunque di voi abbia avuto a che fare con la linea di comando avrà senz'altro apprezzato due cose: la possibilità di ridare un comando già digitato semplicemente cercandolo con le freccie «su» e «giù» e la possibilità di completare un comando con la doppia pressione del tasto «TAB».
Quelli più esperti poi apprezzeranno la possibilità di muovere il cursore più velocemente spostandolo direttamente da una parola all'altra, oppure la possibilità di cancellare parte della linea di comando con una combinazione di tasti particolare, ecc.. Bene, vi sieti mai chiesti come e ossibile questo? E perché queste funzionalità alle volte si trovano anche in altri programmi? No? Beh, ve lo dico io! Dipende tutto dalla libreria readline.
Questa libreria, che fa parte del progetto GNU, è alla base di molte interfacce di testo di diversi programmi a cominciare, ovviamente, delle shell. In questo articoletto vedremo molto brevemente come poter utilizzare questa libreria per realizzare un piccolo programma che possa essere utilizzato, ad esempio, per ottenere una serie di informazioni da un ipotetico dispositivo installato sul nostro sistema. Ovviamente, poiché nella realtà il dispositivo non esiste, metteremo del codice ad hoc per simularne il funzionamento.
L'interfaccia di base
Come tutte le librerie che si rispettino anche la readline ha una propria interfaccia verso le applicazioni che intendono utilizzarla. Anche se la readline è molto complessa, ha però una interfaccia di base molto semplice. In questo articolo ci limiteremo alle funzonalità di base che però, come vedremo, sono già abbastanza interessanti da utilizzare.
Ma vediamo subito un semplice corpo di una funzione main() che implementa le funzionalità di base:
printf("TEST shell - version " VERSION "\n");
printf("Copyright (C) 2005 - Rodolfo Giometti \n");
initialize_readline();
/* Loop reading and executing lines until the user quits. */
while (1) {
/* Read the command line */
line = readline(prompt);
if (!line)
break;
/* Remove leading and trailing whitespace from the line.
* Then, if there is anything left, add it to the history list
* and execute it
*/
s = strip_blanks(line);
if (*s) {
add_history(s);
execute_line(s);
}
free(line);
}
return 0;
Come si vede la cosa è abbastanza semplice, la funzione readline() una volta invocata restituisce una stringa che contiene proprio quello che l'utente ha inserito dalla linea di comando, mentre nella variabile prompt è contenuta la stringa che rappresenta (appunto) il prompt da mostrare all'utente. Tutta le gestione dell'editing della linea di comando viene quindi gestita della libreria readline!
Una volta che si è controllato se la stringa è non vuota (altrimenti si esce perché vuol dire che l'utente ha inserito un End-Of-File) la si ripulisce dagli spazi in testa ed in cosa, e se il risultato è non nullo allora lo si passa alla funzione add_history() e quindi alla funzione execute_line().
La funzione add_history() è un'altra funzione propria della libreria readline (anche se in realtà, ad essere pignoli, essa fa parte della libreria history, ma questo è un altro discorso) che permette di gestire la possibilità di rieseguire un comando già dato semplicemente cercandolo con le freccie, mentre la fuzione execute_line() è una funzione ad hoc che si incarica di eseguire il comando che l'utente ci ha appena inserito dalla linea di comando.
E' abbastanza evidente allora che di per se la cosa è abbastanza semplice, ma andiamo avanti nell'esposizione perché occorre chiarire altri punti essenziali della questione.
Scendiamo più a fondo
Vediamo ora come si realizza la funzione forse più interessante ed utile di readline: il completamento dei comandi. Ritorniamo al codice appena presentato e vediamo cosa contiene la funzione initialize_readline():
static void initialize_readline(void)
{
/* Allow conditional parsing of the ~/.inputrc file */
rl_readline_name = NAME;
/* Tell the completer that we want a crack first */
rl_attempted_completion_function = commands_completion;
}
Questa funzione inizializza le variabili rl_readline_name e rl_attempted_completion_function.
La prima funzione serve per definire il nome dell'applicazione per il sistema di parsing del file .inputrc, che e quello che definisce la configurazione di base del funzionamento della libreria stessa. Questo file è lo stesso per tutte le applicazioni che usano la libreria readline e per poter differenziare il funzionamento della libreria a seconda dell'applicazione che la sta utilizzando possiamo appunto utilizzare questo parametro.
Ad esempio, se nel nostro codice definiamo la define NAME come testsh allora è possibile specificare nel file .inputrc una configurazione speciale con:
$if testsh
# Put here special configuration for "testsh"
...
$endif
Non mi addentro di più nella cosa perché è al di fuori dello scopo di questo articolo ma rimando il lettore curioso alle pagine del manuale della libreria.
Con la variabile rl_attempted_completion_function invece si definisce la funzione che permette di impostare le modalità di completamento dei comandi. Questa è la funzione che ci interessa ed è qui che le cose si complicano... :)
Completare un comando
Come appena detto occorre definire la funzione rl_attempted_completion_function la quale viene chiamata da readline ogni qual volta essa ha bisogno di completare un comando e non sa come. In parole povere se l'utente preme il tasto «TAB» per chiedere il completamento del comando allora questa funzione viene attivata.
Vediamo un esempio di come questa potrebbe essere implementata:
static char **commands_completion(const char *text, int start, int end)
{
const char delimiters[] = " \t";
char *token, *cp;
int i;
/* We start from the commands' tree root */
cmds_level = cmds_root;
cp = strndupa(rl_line_buffer, end);
while ((token = strtok(cp, delimiters)) != NULL) {
if (token == NULL)
break;
cp = NULL;
for (i = 0; cmds_level[i].name != NULL; i++)
if (strcmp(token, cmds_level[i].name) == 0) {
cmds_level = cmds_level[i].sub;
if (cmds_level == NULL)
goto out;
}
}
out :
if (cmds_level != NULL)
return rl_completion_matches(text, command_generator);
return NULL;
}
C'è da dire subito che in questo esempio abbiamo utilizzato la struttura seguente per definire la variabile cmds_root:
struct commands_s {
char *name; /* command name */
int (*func)(struct commands_s *cmd,
int argc, char *argv[]); /* function to call to do the job */
char *usage; /* command usage */
char *help; /* command help */
struct commands_s *sub; /* subcommands list */
};
La struttura è semplice, con name si definisce il nome del comando, con func la funzione che esegue il comando stesso, con usage una stringa che ne definisce la sintassi di uso e con help una stringa che riporta un messaggio di aiuto su cosa fa il comando. L'ultima variabile, sub, definisce una lista di sotto comandi disponibili e riferiti al comando stesso.
Un esempio di come questa struttura si riferisca ad un comando tipo help è il seguente:
{
"help", /* the "help" */
func_help,
"help ",
"show a little commands' help",
cmds_root
},
Si noti come la lista dei sottocomandi di help corrisponda in realtà alla lista di tutti i comandi disponibili...
Ma ritorniamo a noi e supponiamo allora di voler realizzare una piccola interfaccia a linea di comando verso un dispositivo che riporta lo stato di alcune grandezze notevoli e supponiamo anche che tale interfaccia possegga un comando del tipo:
show --+--> status --+--> temperatures
| |
| +--> voltages
| |
| +--> IOs
|
+--> version --+--> shell
|
+--> driver
|
+--> hardware
Cioè dando show status voltages si ottengono le letture delle tensioni, mentre dando show version hardware si ottiene la versione dell'hardware controllato. In pratica, come ben si vede, la struttura di tutti i comandi è correlata ad una struttura ad albero, dove, nel nostro caso, la variabile cmds_root è appunto la radice; in particolare abbiamo definito, per quanto riguarda il primo livello di comandi:
cmds_root --+--> exit/quit
|
+--> help
|
+--> reset
|
+--> show
Bene, detto questo se ritorniamo alla nostra funzione commands_completion() possiamo ora capire cosa questa fa: sostanzialmente, partendo dalla radice dei comandi disponibili scorre il contenuto della variabile rl_line_buffer (che contiene il comando inserito dall'utente fino alla sua invocazione) cercando di capire quale particolare percorso di comandi possibili l'utente vuole utilizzare.
Se il percorso si trova, allora si ritorna alla readline il valore di ritorno della funzione rl_completion_matches() che, a sua volta, ha il compito di invocare più volte la funzione command_generator() la quale, ad ogni invocazione, deve ritornare un possibile comando di completamento del comando dell'utente (se questo esiste, ovviamente).
La funzione command_generator() è la seguente:
static char *command_generator(const char *text, int state)
{
static int i, len;
char *name;
if (state == 0) {
i = 0;
len = strlen(text);
}
/* Return the next name which partially matches from the
* command list
*/
while ((name = cmds_level[i++].name) != NULL)
if (strncmp(name, text, len) == 0)
return dupstr(name);
/* If no names matched, then return NULL. */
return NULL;
}
e come di vede è una funzione a stati, nel senso che essa, ogni volta che viene invocata, restituisce un nuovo possibile comando che completa quello dell'utente.
Un esempio: se l'utente scrive show version e poi preme due volte il tasto «TAB» otterrà una lista di comandi possibili, e cioè driver, hardware e shell, questo grazie al fatto che la funzione command_generator() restituisce uno di questi valori ogni volta che viene invocata.
Un esempio pratico
Facciamo ora un esempio pratico per fissare un po' le idee (posso immaginare che la cosa sia un po' «ingarbugliata» ma se analizzate bene il tutto vedrete che non è poi impossibile capire come funziona).
Per coloro che vogliono ben capire il funzionamento del codice che sto utilizzando io possono scaricarsi il tutto dal mio FTP anonimo all'indirizzo: http://ftp.enneenne.com/pub/misc/readline/.
Prima di proseguire vediamo però come sia possibile definire le strutture dati che definisco i vari comandi.
I comandi principali possono essere definiti come segue:
struct commands_s cmds_root[] = {
{
"exit", /* "quit" alias */
func_quit,
"exit",
"quit the program",
NO_SUBCOMMANDS
}, {
"help", /* the "help" */
func_help,
"help ",
"show a little commands' help",
cmds_root
}, {
"reset",
func_reset,
"reset ...",
"reset the device",
cmds_reset
}, {
"show",
func_show,
"show ...",
"show status data",
cmds_show
}, {
"quit",
func_quit,
"quit",
"quit the program",
NO_SUBCOMMANDS
},
END_LIST,
};
Si noti come la funzione che esegue il comando exit è la stessa del comando quit (i due comandi sono quindi sinonimi) e come la lista dei sottocomandi del comando help sia appunto la lista dei comandi principali, questo perché è possibile dare comandi del tipo: help help o help reset ecc..
Per completezza vediamo anche come potrebbe essere il corpo di una funzione che ha il compito di eseguire un comando dell'utente:
int func_show(struct commands_s *cmd, int argc, char *argv[])
{
struct commands_s *command;
/* Check command line */
if (argc < 2) {
cmd_usage(cmd);
return -1;
}
/* Find the subcommand */
command = find_command(cmd->sub, argv[1]);
if (command == NULL) {
printf("%s: no such subcommand\n", argv[1]);
cmd_usage(cmd);
return -1;
}
/* Execute the command */
return command->func(command, --argc, ++argv);
}
Come si nota il corpo della funzione è molto simile ad una funzione main(), ed esso ha semplicemente il compito di cercare, sempre nella stessa struttura ad albero, i sottocomandi del comando show e quindi di eseguire il tutto.
Ma facciamo ora l'esempio compilando il nostro codice:
giometti@zaigor:~/readline$ make
cc -Wall -D_GNU_SOURCE -I../ -O -ggdb -M main.c cmds_tree.c misc.c func_quit.c func_help.c func_reset.c func_show.c > .depend
cc -Wall -D_GNU_SOURCE -I../ -O -ggdb -c -o main.o main.c
cc -Wall -D_GNU_SOURCE -I../ -O -ggdb -c -o cmds_tree.o cmds_tree.c
cc -Wall -D_GNU_SOURCE -I../ -O -ggdb -c -o misc.o misc.c
cc -Wall -D_GNU_SOURCE -I../ -O -ggdb -c -o func_quit.o func_quit.c
cc -Wall -D_GNU_SOURCE -I../ -O -ggdb -c -o func_help.o func_help.c
cc -Wall -D_GNU_SOURCE -I../ -O -ggdb -c -o func_reset.o func_reset.c
cc -Wall -D_GNU_SOURCE -I../ -O -ggdb -c -o func_show.o func_show.c
cc -s -rdynamic -lm -ldl -lreadline main.o cmds_tree.o misc.o func_quit.o func_help.o func_reset.o func_show.o -o testsh
Si noti che per compilare il programma di test c'è bisogno della libreria readline installata sul vostro sistema: necessitate sia dei file binarii sia dei file sorgenti necessari per la compilazione e lo sviluppo.
Se comunque il tutto va bene, date il comando:
giometti@zaigor:~/Projects/enneenne/readline$ ./testsh
TEST shell - version 0.90.0
Copyright (C) 2005 - Rodolfo Giometti
testsh>
A questo punto proviamo subito il completamento dei comandi, scrivete show e quindi date il doppio «TAB»:
testsh> show
status version
testsh> show
Come vedere il programma vi propone i sottocomandi disponibili del comando principale show, ora scrivete il carattere s e quindi date il solito doppio «TAB»:
testsh> show status
Ecco che readline vi completa il comando. Se poi proviamo a dare un comando completo:
testsh> show status temperatures
32.500000*C, 33.800000*C
ecco che questo viene eseguito!
Provate ora a dare un po' di comandi e quindi premete le freccie «su» e «giù» e vedrete che scorreranno i vari comandi che avete già dato in precedenza! Bello no? ;)
Bon direi che possiamo fermarci qua. Vi consiglio di continuare a provare ad utilizzare questo programmino di esempio.
E' interessante secondo me vedere come è stato implementato il comando help, infatti se ad esempio date il solo comando help otterrete:
testsh> help
--- usage ---
help
exit - quit the program
help - show a little commands' help
exit - quit the program
help - show a little commands' help
reset ... - reset the device
show ... - show status data
quit - quit the program
reset ... - reset the device
reset hard - hard reset the board
reset soft - soft reset (init) the board
show ... - show status data
show status ... - show status of ...
show version ... - show version of ...
quit - quit the program
Ma se date help show allora otterrete:
testsh> help show
--- help ---
show status data
--- usage ---
show ...
show status ... - show status of ...
temperatures - show temperatures state
voltages - show voltages state
IOs - show IOs state
show version ... - show version of ...
shell - show shell version
driver - show driver version
hardware - show hardware version
Secondo voi, come mai? Beh, buono studio! ;
|