Paolo Guccini

"Impossibile" non è mai la risposta giusta

GESTIRE LA COMMAND LINE

I sorgenti sono disponibili per il download.

Una riflessione sull’importanza e caratteristiche della command line per introdurre uno strumento utile soprattutto per chi sviluppa in Dos: una classe per gestire la Command Line.

Introduzione

Tracciato a grandi linee, lo sviluppo del software attraversa varie fasi: l’analisi, l’implementazione del nucleo fondamentale o kernel, il debugging, la costruzione dei moduli secondari e dell’interfaccia utente, il debugging finale di tutti i moduli.
Il successo o fallimento di un programma è il risultato concomitante di molti fattori: utilità e funzionalità, affidabilità, flessibilità, semplicità d’uso, costo, supporto, manualistica, lingua, corretto metodo di pubblicizzazione e diffusione o commercializzazione, concorrenza dei prodotti analoghi, hardware e sistema operativo richiesto e/ o supportato. E molti altri ancora.
Di conseguenza, affinché un programma possa riscuotere il favore degli utenti deve essere concepito, realizzato e diffuso considerando tutti gli elementi sopra citati.
Con questo preambolo ho voluto focalizzare la vostra attenzione su un aspetto generalmente assai trascurato della programmazione: raramente un programma, per quanto valido, viene utilizzato quanto meriterebbe se gli utenti non lo trovano di semplice impiego.
E il tipico caso di parte del software che fino a qualche anno fa era presente sulle BBS: utility sprovviste di documentazione e con un’interfaccia utente poco esplicativa che talvolta scoraggiava anche i più facinorosi ottimisti.
L’interfaccia utente intesa come modalità di comunicazione e interazione utente/programma (e non alle interfacce grafiche quali Windows, OS/2 o System 7 del Mac) ha quasi sempre rivestito un ruolo marginale durante l’attività di analisi e sviluppo, considerata come un qualcosa di secondario, di scarsa rilevanza. Pensiamo ai comandi delle vecchie versioni del Dos, decisamente poco friendly.
In sintesi, indipendentemente dall’ambiente in cui si sviluppa software, è necessario ricordarsi che l’utente non desidera dover sempre sfogliare il manuale per vedere l’esatta sintassi di uno switch o cosa mettere in un campo raramente utilizzato. Di conseguenza è necessario cercare di rendergli la vita il più facile possibile se si desidera che sia soddisfatto del programma.
Ma se bisogna sempre pensare alle esigenze del l’utente, chi pensa a quelle degli sviluppatori?
Da queste ed altre premesse è nata la classe che verrà discussa qui di seguito.

La linea di comando

Abbiamo accennato ai motivi per cui si consiglia di curare maggiormente la linea di comando, soprattutto per quei programmi completamente sprovvisti di interattività che prendono le informazioni di cui necessitano dalla linea di comando e poi iniziano il loro lavoro.
Certamente la sempre maggiore diffusione degli ambienti grafici potrebbe far sembrare questo argomento oramai sorpassato, superfluo. Ma non è così: esistono varie situazioni in cui la linea di comando si rivela molto comoda o addirittura irrinunciabile.
Ad esempio, può essere comodo poter fornire sulla linea di comando tutti i dati di cui il programma abbisogna senza doverli digitare ogni volta nelle appropriate maschere: questo permetterebbe di utilizzare il programma all’interno di un file batch evitando all’utente di essere presente durante l’elaborazione in quanto il programma, estraendo i dati dalla command line, non avrebbe bisogno di ulteriori input. Pensate alle Norton Utility. Può invece essere indispensabile il dover comunica re al programma un qualcosa a priori come il tipo di scheda video, cosa che accadeva fino a qualche anno fa con alcuni programmi.
Chi ha iniziato a programmare in C spesso ha trovato allegata al compilatore una funzione di nome getopt() che assolveva degnamente il ruolo di facilitare la gestione della linea di comando. Ma alcuni suoi limiti saltavano subito all’occhio, soprattutto per coloro che non conoscevano lo Unix e il suo modo di gestirla. Nonostante tutto, rappresentava una soluzione già pronta per l’uso in ossequio al motto fondamentale per i programmatori C: non dover ogni volta reinventare la ruota oppure riscoprire l’acqua calda.
Per inciso, le differenze rispetto al sistema del Dos consistevano in pochi punti: più switch potevano essere raggruppati assieme senza dover ripetere per ognuno la barra (‘/’) e, se ben ricordo, i relativi parametri dovevano essere separati dallo switch dal carattere due punti. Inoltre getopt() era case sensitive, ovvero riconosceva come diversi gli switch in maiuscolo da quelli in minuscolo.
Comunque, se il C disponeva della getopt(), il C++ meritava qualcosa di più, oserei dire qualcosa di classe, se mi consentite la battuta.
Vediamo gli elementi che possono apparire sulla linea di comando; si possono individuare quattro entità: la prima è il nome del programma da eseguire, le altre tre costituiscono i tre tipi di informazioni che la classe CmdLine riconosce e gestisce:

  • argomenti
  • switch
  • parametri

Il Dos (come del resto anche molti altri sistemi operativi) mette a disposizione due operatori detti pipe e redirector. Essi non verranno qui discussi in quanto esulano dagli scopi di quest’articolo, ma se vi interessassero informazioni in merito potete trovarne in [ENCI] pagg. 53 e 67.
Torniamo alle entità precedentemente menzionate ed approfondiamole singolarmente.

Argomenti, switch e parametri

La nomenclatura riveste un ruolo fondamentale per la corretta trasmissione dei concetti, quindi ci soffermeremo sui seguenti termini: argomento, switch, parametro. Per semplicità possiamo, a titolo di esempio, rifarci alla sintassi del comando XCOPY del DOS:

C:\> XCOPY *.CPP *. BAK /s /d:01-01-95 /V

Su questa ipotetica linea di comando possiamo individuare due argomenti, i quali appaiono sempre immediatamente dopo il nome del programma da eseguire; nell’esempio il primo argomento è *.cpp ed il secondo è *.bak.
Lo switch è generalmente identificato mediante il carattere barra ‘/’ che lo precede (potrebbe anche essere utilizzato il carattere trattino (‘-‘) in base alla configurazione adottata per il DOS); esso appare dopo gli eventuali argomenti.
Il parametro invece è situato dopo lo switch a cui si riferisce. Nell’esempio /S /D /V sono switch, mentre 01-01-95 è il parametro (primo ed unico) dello switch; i due punti che nell’esempio separano lo switch dal parametro sono un metodo Unix che il Dos ha ereditato in alcuni comandi: infatti il DOS utilizza generalmente soltanto lo spazio per separare lo switch dal relativo parametro.
Vediamo una forma generale d’esempio di come appare una linea di comando:

nomeprogr arg1 argn /sw1 /swn par1 par2

In esso nomeprogr identifica il nome del programma eseguibile, arg1 e argn identificano gli argomenti, /sw1 uno switch privo di parametri mentre /swn è un altro switch con i relativi parametri.
Ora che abbiamo preso confidenza con i tre concetti sopra esposti possiamo andare avanti.

main(), argc e argv

Come ben sappiamo, un programma C o C++ inizia elaborando la funzione main(), la quale può ricevere dal Dos i seguenti parametri: un array chiamato argv di tipo char ** puntante alle varie stringhe digitate sulla linea di comando ed un int di nome argc indicante quante stringhe sono puntate da argv; un dettaglio curioso è che la prima stringa puntata da argv (ovvero argv[0]) è il nome del programma eseguibile a cui main() appartiene.
In pratica la dichiarazione della funzione main() sarà:

int main( int argc , char ** argv)

Per inciso, la funzione main() può contenere un terzo parametro: char ** envp, il quale è un array di puntatori ad una copia delle stringhe d’ambiente (environment), che non sono altro che i SET eseguiti nel Dos come ad esempio il Prompt, il Set Temp, il Path, eccetera; non viene utilizzata una variabile come argc per contenere il numero di stringhe puntate da envp in quanto la fine dei dati viene individuata da un NULL pointer.
E' importante notare che il programma riceve una copia delle variabili d’ambiente e di conseguenza le eventuali modifiche che il program ma esegue su di essa non si riflettono su quelle reali e conseguentemente rimarranno valide solo per il programma che le modifica e solo finché non termina la sua elaborazione (vedi [KJ], [ENCI]).
I nomi dei parametri argc, argv, envp non sono obbligatori, fissi o invariabili, ma costituiscono uno standard fra i programmatori C/C++, quindi è consigliabile conformarsi ad essi.

La classe CmdLine

Veniamo ora alla presentazione di questa classe. Essa si avvale di varie funzioni che espletano i seguenti compiti:

  • acquisizione degli argomenti
  • acquisizione degli switch
  • acquisizione dei parametri di uno switch
  • test di presenza di un determinato switch
  • abilitazione del case-sensitive per i nomi degli switch

La tecnica di acquisizione dei dati dalla command line da parte della classe è immediata: le è sufficiente eseguire la copia di quanto puntato da argv in una stringa dichiarata e gestita dal programma. La classe si occupa di evitare eventuali overflow causati da valori di argv più grandi di quanto attesi: il constructor accetta i parametri seguenti:

  • argc
  • argv
  • lunghezza massima per lo switch
  • lunghezza massima per il parametro e l’argomento
  • flag indicante se gli switch sono case sensitive

Esclusi i primi due parametri necessari, gli altri sono opzionali: nella definizione della classe il constructor contiene il valore di default, quindi è sufficiente modificare tali valori in relazione alle proprie necessità e armonizzare i programmi di conseguenza. Ma ritengo che la soluzione migliore sia non affidarsi ai valori di default, ma comunicarli esplicitamente ricorrendo alla keyword sizeof avendo cura di decrementarne il valore di i per lasciare lo spazio necessario per il NULL finale delle stringhe.
Per meglio comprendere quanto descritto, il listato I contiene un esempio di come utilizzare correttamente il constructor.
Concettualmente la classe è estremamente semplice: essa si basa su un indice (di tipo private chiamato CmdLine::argc che non è l’argc della funzione main()) che, in base al tipo di funzione chiamata, viene incrementato finché argv :[CmdLine::argc] non contiene quanto richiesto. Ad esempio, se la prima richiesta del programma è di avere il primo switch, la relativa funzione cercherà il primo elemento di argv che inizi con la barra (/), scavalcando di fatto gli argomenti. Non sono stati inseriti dei controlli sulla corretta sequenzialità delle chiamate alla classe da parte del programma perché teoricamente esso dovrebbe procedere nel seguente modo: eseguire prima il loop relativo agli argomenti e poi quello degli switch con nidificato al suo interno il loop per i parametri; ma se lo ritenete necessario, la relativa gestione è implementabile senza difficoltà.
Passiamo ora alla descrizione delle funzioni membro.

La gestione degli argomenti

Per acquisire gli argomenti della linea comando, la classe fornisce la funzione int getnextarg(char * argstr) che, chiamata ciclicamente, avvalora la variabile argstr (che viene terminata con un NULL) con i vari argomenti presenti sulla linea comando nell’ordine in cui erano sulla stessa, restituendo contemporaneamente 1.
Quando gli argomenti sono terminati la funzione restituisce zero.
Un esempio di corretta gestione potrebbe essere quello riportato nel listato 2, ove getnextarg() viene richiamata due volte per avere i due argomenti necessari al programma (infile e outfile) ed una terza, in cui il programma si attende che la funzione restituisca false, affinché venga controllata l’assenza di argomenti inattesi.

Gli switch e i relativi parametri

Per leggere sequenzialmente gli switch è fornita la funzione int getnextswitch(char * swstr), la quale avvalora swstr con il nome dei vari switch che incontra ad ogni chiamata, restituendo 1.
Allorquando non ci sono più switch. la funzione restituisce zero.
Il carattere identificativo dello switch (la sbarra ‘/’ o il trattino ‘-’ in relazione al tipo di set di nazione) non viene copiato nella stringa swstr.
Per a ere i parametri dello switch appena letto mediante la funzione getnextsw( char * swstr ), si utilizza la funzione int getnextpar( char * parstr) che avvalora la stringa parstr con i vari parametri che trova, restituendo 1 se è stato trovato un parametro o zero se non ne sono stati trovati.
Siccome getnextpar( char * parstr ) restituisce il parametro relativo allo switch restituito da getnextsw( char * swstr), è indispensabile accoppiarne l’uso come appare nel frammento di codice del listato 3.
Ovviamente si deve considerare che:

  • gli switch possono richiedere anche più argomenti e conseguentemente la if di getnextpar() andrebbe sostituita con un ciclo while;
  • l’utente potrebbe immettere troppi parametri per un determinato switch e bisognerebbe quindi verificare anche questa possibile situazione anomala;
  • gli argomenti spesso sono i nomi di file sui quali il programma opera, perciò il dimensionamento di argstr e parstr (che sono dei char definiti dal programma) dovrebbe teneme di conto;

inoltre gli argomenti e i parametri vengono trattati dalla classe come aventi la stessa massima lunghezza.
La funzione getnextsw(), operando mediante un contatore interno alla classe CmdLine che si occupa di memorizzare la posizione dell’ultimo switch estratto (CmdLine::argc, come precedentemente descritto), non può riposizionarsi su uno switch precedente. Nel caso sia necessario eseguire nuovamente lo scan della linea di comando, la classe mette a disposizione la funzione void nextreset(void) che fa sì che la successive chiamate alle varie funzioni rincomincino a considerare i dati dall’inizio dell’array argv.

Test di presenza

Si può rendere necessario conoscere a priori se sulla linea di comando è stato specificato o meno un determinato switch.
Allo scopo esiste la funzione int testswitch(char * swstr) che restituisce i se lo switch puntato da swstr è presente, altrimenti zero.
Anche se forse a prima vista può apparire come non molto utile, vi sono situazioni in cui essa risulta fondamentale.

Nomi case sensitive

A seconda che lo stesso nome dello switch possa essere immesso sia in maiuscolo che in minuscolo senza essere interpretato come due switch diversi, la funzione void setcasesensitive( int yn) consente di selezionare questo tipo di gestione: se yn viene avvalorato a zero (false), le funzioni che trattano gli switch riconosceranno come uguali gli switch /INPUT, /input / inpUT.
Se invece yn viene avvalorato a 1 (true), le varie funzioni saranno case-sensitive, conseguentemente gli switch del precedente esempio saranno considerati completamente diversi fra loro.

Altre funzioni

E' stata descritta la necessità di porre attenzione al corretto dimensionamento delle stringhe di appoggio in cui CmdLine copia gli argomenti, gli switch, o i parametri al fine di evitare overflow.
È stato descritto come il constructor della classe richieda questi valori o accetti il default previsto nella dichiarazione di funzione (interna alla classe).
Ma esiste anche un’altra possibilità: dichiarare tali valori dopo la allocazione della classe mediante funzioni specifiche.
La dimensione dello switch può essere dichiarata mediante void setswlen(int) mentre, per fissare la lunghezza massima del parametro e dell’argomento esiste la funzione void setparlen(int) (sempre avendo cura di non dimenticarsi di lasciare lo spazio al NULL).
Questa possibilità è stata introdotta principalmente per motivi di maggior flessibilità: non diventa necessario dichiarare tali dimensioni all’atto di creare la classe e diviene possibile gestire il tutto anche tramite allocazioni dinamiche con le new/delete.
Esiste un’ultima funzione: char * getswparadr(void). Essa restituisce l’indirizzo dell’argomento, switch o parametro corrente, ovvero ciò a cui CmdLine::argc punta.
È stata inserita fra le funzioni di tipo public perché vi sono rare situazioni in cui torna comodo disporre direttamente dell’indirizzo, ma è consigliabile utilizzarla a ragion veduta preferendo, ove possibile, le varie funzioni dedicate.

Conclusioni

Mi auguro che troviate questa classe funzionale e vi piaccia. Personalmente la trovo soddisfacente anche se qualche piccolo ritocco, soprattutto togliendo i parametri di default del constructor, potrebbero migliorarla.
Ma questa classe non è solo una soluzione preconfezionata da prendere così com’è o da lasciar perdere: essendo i listati qui allegati potete apportare le modifiche che ritenete più opportune. Come al solito mi congedo ricordandovi che tutti i suggerimenti e le critiche costruttive sono graditissimi e possono essere lo spunto per migliorie che potrete eventualmente vedere su queste pagine.
Non mi resta che augurarvi buon lavoro!

Bibliografia

[KJ] Kris Jamsa, "Programmare in C", Mondadori, 1991, p. 314 e segg.
[ENCI] AA.VV., "Enciclopedia dell’Ms-Dos", Gruppo Editoriale Jackson, 1990, p. 65.

Il testo e' stato acquisito tramite OCR dalla rivista su cui e' stato pubblicato e velocemente ricontrollato.
Le segnalazioni di errori saranno molto gradite e si possono fare alla pagina Contatti.

Tratto da:
Paolo Guccini
Rivista DEV Computer Programming
Edizioni Infomedia
Gennaio 1996