Home Area Tecnica Programmazione GNU/Linux GNU readline: un tool indispensabile per la linea di comando

Servizi

Elenco dei Professionisti e Consulenti ICT
L'avvocato risponde
Libri Consigliati
Cerco e Offro Lavoro ICT

Sponsor

GALLACCI.COM

I più attivi

Dati e punteggi dell'ultimo anno

Rodolfo Giometti Rodolfo Giometti
28.816 punti
118 articoli
Calogero Bonasia Calogero Bonasia
14.615 punti
96 articoli
Giovanna Casamassima Giovanna Casamassima
0.657 punti
15 articoli
Nanni Bassetti Nanni Bassetti
0.398 punti
14 articoli
Luigi Carbone Luigi Carbone
0.181 punti
7 articoli

I più votati

  1. Rodolfo Giometti
    - 100%
  2. Calogero Bonasia
    - 100%
  3. Luigi Carbone
    - 100%
  4. Nanni Bassetti
    - 100%

Web Partner

CFI - Computer Forensics Italy
Better Software 2010
Area Networking
ILIT match 2010

Ci hanno visitato

Oggi:596
Ieri:949
Totali (14/04/09):153869

I nostri numeri

Articoli pubblicati: 326
Iscritti al portale: 242
Iscritti all'Elenco: 116
Iscritti ML Discussioni: 216
Iscritti ML Articoli: 14
Iscritti ML Lavoro: 93

Donazione

GNU readline: un tool indispensabile per la linea di comando Stampa E-mail
(1 voto, media 5.00 di 5)
Area Tecnica - Programmazione GNU/Linux
Scritto da Rodolfo Giometti   

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! ;

 

Aggiungi commento


Codice di sicurezza
Aggiorna

 

PageRank Checking Icon

Luglio 2010
D L M M G V S
27 28 29 30 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31

Sondaggi

busyCaricamento Sondaggio...

Ultimi Commenti