Si intende, per command line, la riga di testo digitata al prompt di sistema:
essa contiene un comando DOS (che può essere il nome di un programma),
completo degli eventuali parametri da avviamento. Ad esempio, la command
line
pippo /a /b file0 *.dat
lancia il programma PIPPO passandogli quattro parametri, separati
tra loro da spazi.
E' noto che il linguaggio C mette a disposizione un metodo semplice e standardizzato
per accedere ai parametri della command line: allo scopo è sufficiente
dichiarare la funzione main()
con due parametri, nell'ordine un intero e un array di stringhe (un array
di puntatori a carattere), convenzionalmente chiamati argc e argv
(arguments counter e arguments vector):
void main(int argc, char **argv)
....
La variabile argc contiene il numero di parole, separate da spazi,
presenti sulla command line (incluso, dunque, il nome del programma); gli
elementi di argv referenziano le parole componenti la command
line (argv[0] punta al nome del programma, completo
di pathname[1]). Ogni sequenza di caratteri compresa tra due o più
spazi (o tabulazioni) è considerata un parametro (se si intende
passare al programma una stringa contenente spazi come un unico parametro
è sufficiente racchiuderla tra doppi apici, "come questa")
e le wildcard non vengono espanse (con riferimento all'esempio sopra riportato,
argv[4] in PIPPO contiene "*.dat").
Come argv, anche envp (environment pointer) è
un array di puntatori a stringa, o meglio un array di puntatori a carattere.
Il lavoro di parsing (cioè di scansione) della command line e di
valorizzazione di inzializzazione delle stringhe e dei rispettivi puntatori
è svolto da due funzioni di libreria, non documentate in quanto
implementate per uso interno: nelle librerie del C Borland i loro nomi
sono _setargv__() e _setenvp__()[3].
_setargv__() e _setenvp__()
Appare evidente che _setargv__() e _setenvp__() svolgono
un compito utile solo se effettivamente il programma ha necessità
di conoscere le stringhe che compongono l'environment e, rispettivamente,
la command line. Esse vengono tuttavia invocate dal modulo
di startup in ogni caso: è possibile accrescere l'efficienza
dei programma che non necessitano di tali informazioni con un semplice
stratagemma.
#pragma option -k- /* evita stack frame nelle funzioni senza parametri */
void _setargv__(void) /* sostituisce la _setargv__() di libreria */
{
}
void _setenvp__(void) /* sostituisce la _setenvp__() di libreria */
{
}
void main(void) /* non usa command line ed environment */
{
....
}
Naturalmente si può dichiarare una sola funzione fittizia, qualora
l'altra sia necessaria:
#pragma option -k- /* evita stack frame nelle funzioni senza parametri */
void _setenvp__(void) /* sostituisce la _setenvp__() di libreria */
{
}
void main(int argc, char **argv) /* non usa l'environment */
{
....
}
La dichiarazione di entrambe le funzioni _set...() riduce di alcune
centinaia di byte la dimensione dell'eseguibile[4]
(.COM o .EXE) e ne rende leggermente più rapido
il caricamento.
Entrambe _setargv__() e _setenvp__() sono eseguite prima
di main(), siano esse
le funzioni di libreria o quelle dichiarate nel sorgente: in questo caso
nulla vieta al programmatore di crearne versioni personalizzate, non necessariamente
"nullafacenti".
WILDARGS.OBJ
Una versione più sofisticata di _setargv__()
è quella implementata nel modulo WILDARGS.OBJ (fornito
con il compilatore): essa è in grado di espandere le wildcards e
restituire in argv l'elenco completo dei file corrispondenti ai
nomi "collettivi" specificati sulla command line. Se il file
PIPPO.C è compilato con:
bcc pippo.c wildargs.obj
il vettore argv ricavato dalla command line dell'esempio presentato
poco sopra può avere più di quattro
elementi, in quanto *.dat è espanso in tante stringhe quanti
sono i file .dat effettivamente presenti nella directory. Solo
se non vi è alcun file .datargv ha cinque elementi
ed argv[4] è ancora "*.dat".
Le variabili argc e argv e il file WILDARGS.OBJ
costituiscono gli strumenti standard per la gestione della command line:
essi rendono disponibili gli elemeti (stringhe) che la compongono. Chi
desideri andare "al di là" del C, ed accedere direttamente
alla riga di comando digitata al prompt[5],
deve vedersela con il Program Segment Prefix.
Infatti, nel PSP, il byte ad offset 80h memorizza la lunghezza
della command line (escluso il nome del programma ed incluso il CR
terminale); ad offset 81h si trova la riga di comando (a partire
dal primo spazio o tabulazione seguente il nome del programma), che include
il carattere di CR.
Il miniprogramma dell'esempio seguente, compilato sotto Borland C++
2.0, stampa la command line, o meglio la sequenza di argomenti passati
al programma tramite questa.
#include <stdio.h>
#include <dos.h> // per MK_FP()
#include <string.h> // per _fstrncpy()
void main(void)
{
int cmdlen;
char cmdline[127];
extern unsigned _psp; // indirizzo di segmento del PSP
cmdlen = (int)(*(char far *)MK_FP(_psp,0x80));
_fstrncpy((char far *)cmdline,(char far *)MK_FP(_psp,0x81),cmdlen);
cmdline[cmdlen] = NULL;
puts(cmdline);
}
Si noti che main() non
ha parametri: la command line viene infatti letta direttamente nel PSP.
La variabile cmdlen viene valorizzata con la lunghezza della command
line: il doppio cast è necessario in quanto detto dato è
costituito, nel PSP, da un solo byte; il cast
di MK_FP() a (int
far *) avrebbe l'effetto di restituire una word in cui il byte più
significativo è, in realtà, il primo carattere della riga
di comando (di solito un blank). Il buffer cmdline è dimensionato
a 127 byte: infatti tale è la massima lunghezza della command line
(incluso il CR terminale, che deve essere sostituito con un NULL
per ottenere una stringa stampabile dalle funzioni C). Per copiare in cmdline
la porzione di di nostro interesse è utilizzata _fnstrcpy(),
che è equivalente a strncpy() ma accetta puntatori far[6]. Se le librerie del compilatore utilizzate
non includono una versione far della strncpy() è
necessario copiare la stringa byte per byte, ad esempio con un ciclo while:
....
register i = 0;
....
while((cmdline[i] = *(char far *)MK_FP(_psp,i+0x81)) != (char)0x0D)
i++;
cmdline[i] = NULL;
....
Dal momento che la seconda metà (gli ultimi 128 byte) del è
utilizzata dal DOS come DTA (Disk Transfer Address)
di default per il programma, è indispensabile che le operazioni
descritte siano le prime eseguite dal programma stesso, onde evitare che
la command line sia sovrascritta.
Una gestione Unix-like
Nei sistemi Unix la command line è spesso utilizzata per fornire
al programma invocato direttive, dette opzioni, di modifica degli eventuali
default di funzionamento. Nel tempo, la modalità di elencazione
delle opzioni ha raggiunto, sia pure in modo non esplicitamente formale,
un livello di standardizzazione tale da poter parlare di una vera e propria
sintassi delle opzioni, in parte ripresa e spesso seguita anche in ambiente
DOS.
I parametri di main()argc e argv costituiscono solo una base di partenza per
implementare una gestione Unixlike delle opzioni di command line:
il resto è lasciato alla buona volontà del programmatore.
Proviamo a scendere nel dettaglio: lo scopo è definire una sintassi
per le command line option analoga a quella comunemente seguita nei sistemi
Unix, e realizzare un insieme di funzioni libreria in grado di implementare
in qualunque programma la gestione delle opzioni secondo quella sintassi.
La sintassi
Va innazitutto precisato che per opzione si intende un carattere alfanumerico,
detto optLetter, preceduto sulla command line da un particolare
carattere, detto switch character, che ha proprio lo scopo di identificare
la optLetter quale opzione. In ambiente DOS lo switch character è,
solitamente, la barra '/'; nei sistemi Unix è usato il
trattino ''. Il programma può prevedere che l'opzione
supporti un parametro, detto argomento, rappresentato da una sequenza di
caratteri qualsiasi, nel qual caso il carattere alfanumerico che rappresenta
l'opzione è detto argLetter (e non optLetter).
Il programma riconosce le optLetter e le argLetter in quanto esse sono
elencate in una stringa, convenzionalmente denominata optionS,
in base alle regole elencate di seguito:
Le optLetter e le argLetter non possono essere segni di interpunzione.
3)
Se il programma deve considerare equivalenti maiuscole e
minuscole è necessario inserire in optionS sia la maiuscola
che la minuscola per ogni optLetter o argLetter.
4)
Ogni argLetter è seguita dal carattere ':'.
5)
Per le argLetter non è data alcuna indicazione sugli
argomenti validi: spetta al programma valutarli ed accettarli o segnalare
eventuali errori.
Lo switch character e la optLetter (o argLetter seguita dal proprio argomento)
costituiscono un option cluster (gruppo di opzioni), nell'ambito
del quale valgono le regole seguenti:
1)
Lo switch deve essere preceduto da uno spazio e seguito immediatamente
da optLetter o argLetter.
2)
Uno switch isolato tra due spazi è un errore di sintassi.
3)
Uno switch seguito da un carattere non compreso tra le optLetter
e le argLetter riconosciute dal programma è un errore di sintassi.
4)
Una optLetter può essere seguita da uno spazio o dalla
successiva optLetter o argLetter.
5)
Una argLetter deve essere seguita dal proprio argomento,
che non può essere uno spazio.
6)
Tra una argLetter e l'argomento può esservi uno spazio
oppure un carattere ':'.
7)
Se un argomento inizia con il carattere ':', questo
deve essere ripetuto.
8)
Un argomento può contenere (anche quale carattere
iniziale) lo switch character; se contiene spazi deve essere compreso tra
virgolette.
9)
Un argomento deve essere seguito da uno spazio.
10)
Maiuscole e minuscole non sono equivalenti, tanto in optLetter
e argLetter quanto negli argomenti.
11)
Gli option cluster devono essere elencati tutti all'inizio
della command line; la prima stringa presente in essa non introdotta dallo
switch character è considerata il primo dei parametri nonoption.
12)
Se il primo parametro nonoption inizia con lo switch
character, questo deve essere ripetuto due volte.
13)
La ripetizione, nella command line, della medesima opzione
è sintatticamente lecita: spetta al programma valutare se ciò
costituisca un errore.
Spaventati? Non è il caso: tale insieme di regole è complesso
solo apparentemente. In effetti esso formalizza una realtà probabilmente
già nota. Vediamo un paio di esempi.
Se optionS è "A:F:PuU:wXZ:", nella command
line:
PROG /uPFPi /X /AL /f UnFile AltraStringa
PROG è il nome del programma e sono individuate le optLetter
'u', 'P' e 'X', nonché le argLetter 'F'
(il cui argomento è "Pi") e 'A' (con
l'argomento "L"), mentre la lettera 'f' non
è un'opzione valida; le stringhe "UnFile" e "AltraStringa"
costituiscono validi parametri nonoption.
Se optionS è "AT:J:", nella command
line
PROG -A -T:4 -AJ::k -T2 --123- -w- - Parola "Due parole"
PROG è il nome del programma ed è riconosciuta la
optLetter 'A', presente due volte; sono inoltre individuate le
argLetter 'J' (con l'argomento ":k") e 'T',
anch'essa presente due volte (con gli argomenti, rispettivamente, "4"
e "2"). Il primo parametro nonoption è la
stringa "123"; i successivi sono le stringhe
"w", "-", "Parola"
e "Due parole". Il parametro ""
è valido, in quanto non è il primo nonoption parameter
(nel qual caso dovrebbe essere digitato come due trattini).
Se optionS è, ancora, "AT:J:", nella
command line
PROG -J -J "UnaSolaParola" -A
PROG è il nome del programma ed è riconosciuta la
argLetter 'J', il cui argomento è "J".
Il primo nonoption argument è la stringa "UnaSolaParola".
La stringa "A" rappresenta il secondo parametro
nonoption e non viene riconosciuta quale optLetter in quanto tutto
ciò che segue il primo nonoption argument è comunque
considerato tale a sua volta.
Le funzioni per la libreria
Sulla scorta delle regole descritte è possibile realizzare le funzioni
necessarie per una gestione Unix-like[8]
della command line. Di seguito è presentato il listato di PARSEOPT.H,
header file incluso nel sorgente delle funzioni e necessario anche per
la realizzazione di programmi che le richiamano. Esso contiene i prototipi
delle funzioni, i templates delle strutture appositamente definiti ed alcune
costanti manifeste.
L'interfaccia utente di alto livello è costituito dalla parseoptions(),
che gestisce al proprio interno, secondo la sintassi descritta poco sopra,
tutte le operazioni necessarie per processare le opzioni specificate sulla
riga di comando del programma.
I suoi primi due parametri sono gli ormai noti argc e argv,
che devono quindi essere dichiarati parametri di main();
il terzo parametro è la stringa optionS, che, come descritto,
contiene l'elenco delle optLetter e delle argLetter. Il quarto parametro
è l'indirizzo di un array di strutture di tipo VOPT, il
cui template è definito, come si vede, in PARSEOPT.H.
Ogni struttura è formata da due campi:
Il campo opt è un carattere e rappresenta una optLetter
o una argLetter, mentre il campo fun, puntatore a funzione, contiene
l'indirizzo della funzione di validazione dell'optLetter o argLetter:
parseoptions(), quando
viene individuata sulla command line la optLetter o argLetter del campo
opt, lancia la funzione il cui indirizzo è contenuto in
fun, passandole un puntatore a struttura OPT (di cui
diremo tra breve) e un intero esprimente la posizione dell'opzione sulla
command line. La funzione a cui punta fun è definita dall'utente.
Vediamo un esempio: se la optionS contiene la optLetter 'a'
e abbiamo definito la funzione valid_a(), che deve essere invocata
quando a è specificata sulla riga di comando, si ha:
Tralasciamo, per il momento, il contenuto di valid_a() e delle
altre funzioni di validazione opzioni per analizzare alcune altre importanti
caratteristiche dell'array valfuncs: il campo opt dell'ultimo
elemento dell'array deve essere il carattere ASCII 0 (NULL),
in quanto è proprio questo il "segnale" che consente a
parseoptions() di capire
che nell'array non vi sono altri elementi. Il campo fun corrispondente
può, a sua volta, contenere NULL; se invece è inizializzato
con un puntatore a funzione, parseoptions()
lo utilizza per invocare quella funzione per ogni oggetto nonoption
incontrato sulla command line dopo l'ultima opzione. E' proprio questo
il caso dell'esempio: se ipotizziamo che sulla riga di comando l'ultima
opzione sia seguita da tre nonoption items, valid_glob()
è chiamata tre volte.
Inoltre, se il campo opt di uno degli elementi di valfuncs
contiene il valore definito dalla costante manifestaERRCHAR, la funzione
indirizzata dal campo fun corrispondente, nell'esempio valid_err(),
è chiamata ogni volta che sulla command line è incontrata
una optLetter o argLetter non valida (non inclusa nella optionS).
L'array valfuncs ha pertanto numero di elementi pari alle optLetter
e argLetter in optionS, più, opzionalmente, un elemento
per la gestione delle opzioni errate, più un elemento finale con
campo opt inizializzato a NULL (e campo fun
nullo o puntatore alla funzione di validazione dei nonoptions items).
La restituisce NULL in caso di errore
di sistema[9], nel qual caso può essere utilizzata perror()
per visualizzare la descrizione dell'errore. Se, al contrario, l'elaborazione
termina con successo viene restituito un puntatore ad array di strutture
OPT, il cui template è definito in PARSEOPT.H:
struct OPT {
char opt;
char *arg;
int val;
};
Vediamo il significato di ogni elemento dell'array (nell'esempio optArray)
e, per ogni elemento, il significato dei singoli campi. La prima struttura
OPT presente in optArray (optArray[0]) contiene
informazioni sugli elementi successivi. In particolare, il campo opt
contiene l'indice[10], nell'array stesso,
dell'elemento relativo al primo nonoption item presente sulla command
line; in altre parole optArray[optArray[0].opt] è la
struttura OPT che, nell'array, si riferisce al primo nonoption
item. Il campo arg contiene sempre argv[0], cioè
il puntatore alla stringa che rappresenta il path completo del programma.
Il campo val contiene il numero dei nonoption items presenti
sulla riga di comando; se non ve ne sono, entrambi i campi opt
e val sono inizializzati a 0.
Nell'array optArray troviamo poi un elemento per ogni optLetter
e argLetter incontrata sulla command line. Nel caso di una optLetter, il
campo opt contiene l'optLetter stessa, il campo arg è
NULL e il campo val contiene l'intero restituito dalla funzione
di validazione (campo fun della struct VOPT). Nel caso
di una argLetter, invece, il campo arg punta ad una stringa contenente
l'argomento fornito alla argLetter stessa sulla command
line. Si noti che arg è sempre puntatore a stringa,
anche nel caso in cui l'argomento della optLetter sia un numero: in tal
caso occorre utilizzare l'appropriata funzione di conversione (ad esempio
atoi()) per ottenere il valore numerico.
Se la optLetter o argLetter incontrata non si trova in errorS,
il campo arg contiene il puntatore alla stringa ERROR_S
definita in PARSEOPT.H.
Nella riga di comando, i due punti (":") isolati sono
interpretati come opzione illecita ed il campo arg contiene la
stringa ILLEGAL_S (anch'essa definita in PARSEOPT.H).
In entrambi i casi appena descritti, il campo val assume valore
1.
Si noti ancora che assume che lo switch character
utilizzato sia quello di default per il sistema operativo: in DOS esso
è la barra ("/"); se si preferisce utilizzare
un altro carattere per introdurre le opzioni, ad esempio il trattino ("")
secondo la convenzione Unix, occorre modificare il default mediante una
chiamata a setswitch().
Ipotizziamo ora che il listato d'esempio faccia parte di un programma chiamato
PROG ed invocato con la seguente riga
di comando:
PROG /a /c14 /b pippo "Qui, Quo, Qua" pluto
Al ritorno da parseoptions()optArray contiene 6 elementi,
valorizzati come segue:
optArray[0].opt
4
4 è l'indice dell'elemento di optArray
relativo al primo nonoption item, se ve ne sono; altrimenti esso esprime
il numero di elementi di cui si compone optArray.
optArray[0].arg
pathname di PROG
E' argv[0].
optArray[0].val
3
Indica che vi sono 3 nonoption item. Essi sono referenziati
da optArray[4], optArray[5] e optArray[6]; si
osservi inoltre che 6, ricavabile con (optArray[0].opt + optArray[0].val
1), è l'indice dell'ultimo elemento dell'array. Infine,
argv[argc-optArray[0].val] è il primo nonoption item
in argv.
optArray[1].opt
'a'
Prima opzione sulla command line.
optArray[1].arg
NULL
'a' è una optLetter: non ha argomento.
optArray[1].val
valore restituito da valid_a()
Se 'a' non fosse un'opzione valida, o fosse specificata
in modo errato, il campo conterrebbe il valore della costante manifesta
ERRCHAR (1).
optArray[2].opt
'c'
Seconda opzione sulla command line.
optArray[2].arg
"14"
'c' è una argLetter: "14"
è il suo argomento, sempre sotto forma di stringa.
optArray[2].val
valore restituito da valid_c()
Se 'c' non fosse un'opzione valida, o fosse specificata
in modo errato, il campo conterrebbe il valore della costante manifesta
ERRCHAR (1).
optArray[3].opt
'b'
Terza opzione sulla command line.
optArray[3].arg
NULL
'b' è una optLetter: non ha argomento.
optArray[3].val
valore restituito da valid_b()
Se 'b' non fosse un'opzione valida, o fosse specificata
in modo errato, il campo conterrebbe il valore della costante manifesta
ERRCHAR (1).
optArray[4].opt
0
Primo nonoption item sulla command line.
optArray[4].arg
"pippo"
Il nonoption item stesso, come stringa.
optArray[4].val
valore restituito da valid_glob()
Sempre 0.
optArray[5].opt
1
Secondo nonoption item sulla command line.
optArray[5].arg
"Qui, Quo, Qua"
Il nonoption item stesso, come stringa.
optArray[5].val
valore restituito da valid_glob()
optArray[4].opt
2
Primo nonoption item sulla command line.
optArray[4].arg
"pluto"
Il nonoption item stesso, come stringa.
optArray[4].val
valore restituito da valid_glob()
E' il momento di descrivere le funzioni di validazione, quelle, cioè,
i cui puntatori sono contenuti nei campi fun degli elementi dell'array
valfuncs. Va precisato subito che il codice di dette funzioni
dipende dalle esigenze del programma. Generalmente, ad ogni optLetter e
argLetter corrisponde una appropriata funzione di validazione, ma nulla
vieta di utilizzare una medesima funzione per controllare più opzioni:
è sufficiente che i campi fun a queste corrispondenti siano
tutti inizializzati con lo stesso puntatore. Analoghe considerazioni valgono
a proposito della funzione richiamata in caso di opzione errata, e di quella
richiamata una volta per ogni nonoption item.
Ad esempio, la funzione valid_err() potrebbe visualizzare un messaggio
di errore e interrompere il programma, tramite la funzione di libreria
exit().
Tutte queste funzioni, comunque, devono restituire un intero al fine di
valorizzare secondo le esigenze del programmatore i campi val
degli elementi dell'array di strutture OPT;
inoltre tutte ricevono in ingresso, come parametri, il puntatore ad una
struct OPT (la quale altro non è che l'elemento dell'array
optArray corrispondente a quell'opzione) in cui i campi opt
e arg sono già valorizzati come mostrato nella tabella
sopra esposta, mentre il campo val contiene 0. Può
risultare utile, nelle funzioni di validazione delle argLetter, utilizzare
il campo arg per effettuare i necessari controlli di validità
dell'argomento associato all'argLetter stessa. L'intero che le funzioni
di validazione ricevono come secondo parametro esprime la posizione dell'argLetter
(o optLetter) sulla command line, e può essere utilizzato per le
opportune verifiche qualora la posizione dell'opzione sia importante.
Nei programmi si ha spesso la necessità di modificare il comportamento
dell'algoritmo a seconda che un'opzione sia stata o meno specificata: in
questi casi può essere utile un flag, inizializzato dalla funzione
di validazione e controllato laddove occorra nel corso dell'elaborazione.
Un metodo efficiente di implementare tale tecnica di gestione delle opzioni
è rappresentato da una variabile globale in cui ogni bit è
associato ad una opzione. La funzione di validazione pone a 1
il bit; questo è poi verificato da altre funzioni del programma,
che possono accedere a quella variabile, proprio in quanto globale.
Tornando al nostro esempio, le funzioni di validazione potrebbero agire
su un intero senza segno:
unsigned int optBits;
int valid_a(struct OPT *tmp,int no)
{
....
return(optBits |= 1);
}
int valid_b(struct OPT *tmp,int no)
{
....
return(optBits |= 2);
}
int valid_c(struct OPT *tmp,int no)
{
register int test;
test = atoi(tmp->arg);
if(test < 0 || test >= 10)
valid_err(tmp,no);
return(optBits |= 4);
}
int valid_glob(struct OPT *tmp,int no)
{
static int progressiveLen;
return(progressiveLen += strlen(tmp->arg);
}
int valid_err(struct OPT *tmp,int no)
{
fprintf(stderr,"Errore: opzione '%c' non valida.\n",tmp->opt);
exit(-1);
}
I bit della variabile globale optBits sono associati alle singole
opzioni: in particolare, il bit 0 corrisponde all'optLetter 'a',
il bit 1 all'optLetter 'b' e il bit 2 all'argLetter 'c';
si noti che le costanti utilizzate per valorizzarli sono potenze di 2
(in particolare, 2 elevato ad esponente pari al numero del bit).
In tal modo è possibile con un'operazione di OR
su bit (composta con l'assegnamento) modificare
il singolo bit desiderato. In qualunque altro punto del programma può
utilizzare un test analogo al seguente:
if(optBits & 2)....
per verificare se l'opzione 'b' è stata specificata. L'uso
di costanti manifeste come
Si noti che in luogo di una variabile integral si può utilizzare
un campo di bit, con il vantaggio, tra l'altro,
di non essere costretti ad utilizzare le operazioni di AND e ORsu bit, in quanto ogni campo rappresenta di
per sé un'opzione
Si noti che valid_c(), in caso di errore, chiama valid_err()
per interrompere il programma. Inoltre, la scelta dei nomi delle funzioni
di validazione è, ovviamente, libera: quelli utilizzati nell'esempio
non sono vincolanti, né rappresentano un default.
Va ancora osservato che parseoptions()
è perfettamente compatibile con WILDARGS.OBJ:
se come nonoption item sono specificati uno o
più nomi di file, essi verranno trattati nel modo consueto: l'argv
che parseoptions()
riceve come secondo parametro contiene già
i puntatori ai nomi generati dall'espansione delle wildcard "*"
e "?".
Ed ecco, finalmente (?), il sorgente completo delle funzioni, listate in
ordine alfabetico, che realizzano il meccanismo sin qui descritto. Per
un esempio pratico di utilizzo vedere la utility DRVSET.
La funzione storeopt()
è il cuore del meccanismo. Essa è
progettata[11] per scandire una stringa alla ricerca di optLetter e
argLetter e, per queste ultime, isolare l'argomento fornito. La storeopt()
analizza una sola stringa ad ogni chiamata, perciò deve essere utilizzata
all'interno di un loop che provveda a gestire opportunamente i puntatori
contenuti in argv. A ciò provvede parseopt(),
che, al ritorno da storeopt()
si occupa di lanciare la funzione di validazione dell'opzione mediante
l'indirezione del puntatore contenuto nel campo fun della struttura
di template VOPT:
(*(valfuncs+i)->fun)(tmp,carg)
I parametri tmp e carg, coerentemente con il prototipo
delle funzioni di validazione, sono il puntatore alla struct
OPT e l'intero rappresentante la posizione dell'opzione sulla
command line.
Se restituisce una condizione di errore (tramite gopError()),
parseopt() ricerca
nell'array valfuncs un elemento il cui campo opt sia
inizializzato con il valore della costante manifesta ERRCHAR e,
se questo esiste, lancia la funzione di validazione corrispondente (valid_err()
nell'esempio di poco fa).
Quando segnala, tramite la restituzione di un valore negativo, che non
vi sono più optLetter e argLetter, parseopt()
considera i restanti elementi di argv come nonoption items:
se il campo fun dell'ultimo elemento dell'array valfuncs
non è NULL viene lanciata la funzione da esso indirizzata
una volta per ogni nonoption item (la funzione è sempre la
stessa, ma cambiano i valori dei campi della struct
OPT di cui essa riceve il puntatore.
Ad ogni iterazione parseopt()
alloca la memoria necessaria per aggiungere all'array di strutture OPT
quella corrispondente all'item della command line attualmente processato;
al termine dell'elaborazione essa restituisce l'indirizzo dell'array oppure
NULL in caso di errore (fallita allocazione della memoria).
Oltre ai parametri richiesti dalla parseoptions(),
la parseopt() necessita
dello switch character (il carattere che introduce
le optLetter e argLetter) e delle due stringhe da utilizzare come campi
arg per le opzioni errate e illecite. Risulta evidente, a questo
punto, che parseoptions()
è semplicemente un "guscio" di alto livello per parseopt(),
alla quale passa, oltre ai parametri ricevuti, anche quelli appena elencati,
fornendone valori di default. In particolare, per le due stringhe sono
utilizzate le costanti manifeste ERROR_S e ILLEGAL_S
definite in PARSEOPT.H,
mentre per lo switch character è utilizzato il valore restituito
dalla getswitch(),
che richiede al DOS il carattere di default.
Al riguardo, sono necessarie alcune precisazioni. Come si è detto,
lo switch character di default è la barra in DOS e il trattino in
Unix: ciò implica che se si desidera realizzare in ambiente DOS
un'interfaccia il più possibile Unixlike occorre dimenticarsi
di parseoptions() e chiamare direttamente parseopt(),
avendo cura di fornirle come parametro lo switch character desiderato.
In alternativa è possibile modificare l'impostazione di default
del DOS mediante la setswitch(),
prima di chiamare parseoptions().
La setswitch() richiede
come parametro il carattere che si desidera impostare come nuovo default
e restuisce il precedente default (1 in caso di errore).
La getswitch() non
richiede parametri e restituisce il default attuale. Le due funzioni si
basano sull'int 21h, servizio 37h, subfunzioni 00h (GetSwitchChar)
e 01h (SetSwitchChar); va sottolineato che detto servizio non
è ufficialmente documentato e, pertanto, potrebbe non essere disponibile
in tutte le versioni di DOS: in particolare, a partire dal DOS 5.0, la
subfunzione 01h è ignorata (non determina la restituzione di un
errore, ma non ha comunque alcun effetto) ed è perciò necessario
utilizzare direttamente parseopt() se si desidera utilizzare il
trattino come switch character.
INT 21H, SERV. 37H, SUBF. 00H: GET SWITCH CHARACTER
00h se OK, FFh in caso di errore (funzione
non supportata).
Note
Questa chiamata è ignorata dal DOS a partire dalla
versione 5.0.
Va infine osservato che il listato potrebbe essere suddiviso in più
sorgenti, uno per ogni funzione (con la sola eccezione di gopError()
e storeopt(), da riunire
in un unico file[12]); dalla compilazione
si otterrebbero così più file .OBJ, da inserire
in una libreria. Se ne avvantaggerebbero gli eseguibili incorporanti le
funzionalità descritte, dal momento che in essi bverrebbero inclusi
dal linker solo i moduli necessari.
OK, andiamo avanti a leggere il libro...