Introduzione
La scrittura di applicazioni Java, nella stragrande
maggioranza dei casi, comporta una interazione con l'utente
che, normalmente, avviene attraverso un'interfaccia
grafica. Vi sono differenti tipologie di GUI (Graphic
User Interface), come l'AWT, l'Abstract Window Toolkit
presente sin dalla prima versioni di Java, o la sua
"evoluzione" Swing.
A queste dobbiamo aggiungere anche diverse librerie
esterne, alcune di esse commerciali ed altre open-source
e, soprattutto, il Web, che, considerando il numero
sempre crescente di applicazioni che lo utilizzano,
deve essere considerato una GUI a tutti gli effetti.
Vi sono, però, casi in cui l'utilizzo di una
interfaccia grafica non è necessaria perchè
l'interazione con l'utente non è costante ma
avviene solo in fase iniziale. In altri casi, inoltre,
l'applicazione interagisce con altre applicazioni e
questo non avviene attraverso l'utilizzo di una GUI,
ma attraverso la linea di comando.
La
linea di comando
In Java, quando si vuole dare ad una classe l'opportunità
di essere eseguita, è necessario creare un metodo
main, la cui signature è questa:
public
static void main(String[] args)
Senza
questo metodo, infatti, la Java Virtual Machine non
potrà eseguire la classe, lanciando l'eccezione:
Exception
in thread "main" java.lang.NoClassDefFoundError:
...
L'argomento
args viene passato dalla Virtual Machine al nostro codice,
e per costruirlo vengono utilizzati tutti i parametri,
ossia le stringhe che seguono il nome della nostra classe.
Un semplicissimo esempio può dimostrare questo
comportamento:
public
class MainTest {
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("Nessun
parametro");
}
else {
System.out.println("Presente/i
" + args.length + " parametro/i.");
for (int i=0; i<args.length;
i++) {
System.out.println("Parametro
" + (i+1) + ": " + args[i]);
}
}
}
}
L'esecuzione
di questo programmino permette di capire come la Java
Virtual Machine utilizzi lo spazio come separatore tra
i vari parametri e come questi siano inseriti nell'array
di stringhe che viene passato al metodo main nell'esatto
ordine in cui appaiono sulla linea di comando.
Se volessimo passare un parametro che contiene al suo
interno uno o più spazi, dovremmo includerlo
tra doppi apici. Provate, infatti, a vedere il risultato
di queste due esecuzioni:
java
MainTest Alessandro A. Garbagnati
java MainTest "Alessandro A. Garbagnati"
Una
volta acqusiti, i parametri del nostro programma potranno
essere utilizzati all'interno della nostra applicazione,
scrivendone il codice per la gestione che, ovviamente,
sia in grado di verificarne la presenza, l'integrità
e l'ordine, nella speranza che l'utente li fornisca
nel modo corretto, altrimenti...
La
proposta di Jakarta
Chiunque abbia mai avuto la necessità di sviluppare
applicazioni che richiedevano una gestione appena più
complessa di quella classica, vista sopra, ha dovuto
sicuramente scrivere un bel po' di righe di codice,
magari costruendo una propria libreria da riutilizzare
anche in altri progetti, come feci tanto tempo fa quando
realizzai la mia "Parameters".
Ma per chi non avesse ancora affrontato questo problema
o per chi ne volesse utilizzare una esistente, ecco
CLI (Command Line Interface) la proposta del progetto
Jakarta Commons, la cui homepage si trova all'indirizzo
http://jakarta.apache.org/commons/cli/.
Figura 1 - La homepage del componente CLI
Questo
progetto nasce dall'esperienza maturata all'interno
di altri progetti simili, alcuni dei quali realizzati
in altri linguaggi e segue la classica logica di gestione
dei parametri dei sistemi Unix, dove i parametri sono
caratterizzati da una combinazione chiave (precduta
dal segno "-") e valore, separati da uno spazio.
Nei casi in cui il parametro sia di tipo booleano (vero
o falso), la chiave non viene seguita da alcun valore
e, spesso, viene identificata col nome di "flag".
Se è presente, il parametro assume il valore
di vero, altrimenti è falso. In più, quando
un parametro è segnalato come obbligatorio, se
questo venisse omesso, l'applicazione ritornerebbe immediatamente
un errore.
Il più grosso vantaggio di questo sistema è
che l'ordine di inserimento dei parametri non è
fondamentale. L'utente non deve stare attento a questo
particolare, ma può focalizzarsi solo ed esclusivamente
sulle informationi da passare.
Il componente Commons CLI è stato progettato
e sviluppato per cercare di semplificare le operazioni
di definizione dei parametri, il parsing dalla linea
di comando e, soprattutto, la loro gestione. Per il
download di CLI è necessario utilizzare la pagina
di download comune di tutti i progetti del gruppo Jakarta,
all'indirizzo http://jakarta.apache.org/site/binindex.cgi,
per la versione binaria, oppure http://jakarta.apache.org/site/sourceindex.cgi
per i sorgenti. Questo componente non richiede alcuna
libreria addizionale, quindi una volta scaricato, è
pronto all'uso.
Un
semplice esempio
La prima fase è quella della definizione e, forse,
anche quella dove la libreria può ancora migliorare.
Lo sviluppatore deve costruire un oggetto di tipo Options,
nel quale inserisce tutte le possibili opzioni (i parametri)
che sono accettati dal programma. Ogni singola opzione
corrisponde ad un oggetto di tipo Option.
Prendendo spunto dall'esempio disponibile sulle pagine
del progetto (http://jakarta.apache.org/commons/cli/usage.html),
cerchiamo di sviluppare un semplice programmino che
ci fornisca la data odierna e l'ora se richiesto attraverso
un flag:
//
inizializza l'oggetto che contiene tutte le opzioni
Options opts = new Options();
// aggiungi il parametro booleano "-o"
opts.addOption("o", false, "mostra anche
l'ora");
Il
metodo addOption() è il modo più semplice
ed immediato per impostare una opzione, fornendo la
chiave, l'indicazioe se la chiave richiede un valore,
ed il nome che apparirà nell'help.
Un altro modo per ottenere lo stesso risultato è
questo:
//
inizializza l'oggetto che contiene tutte le opzioni
Options opts = new Options();
// crea un oggetto opzione
Option oraOpt = new Option("o", false, "mostra
anche l'ora");
opts.addOption(oraOpt);
La
creazione esplicita di un oggetto temporaneo, come in
questo caso, diventa necessaria quando, ad esempio,
si renda necessario definire una opzione obbligatoria.
A
questo punto la fase di definizione per il nostro esempio
può dichiararsi conclusa e si deve quindi passare
alla fase di parsing:
//
Definizione dell'oggetto CommandLine
CommandLine cmdLine;
try {
Parser parser = new PosixParser();
cmdLine
= parser.parse(opts, args);
}
catch
(ParseException pE) {
//
gestione errore...
}
Per
effettuare il parsing è necessario creare un
oggetto PosixParser() che implementa l'interfaccia CommandLineParser
(ed estende Parser), offerta da questa libreria. Il
metodo parse() di questa interfaccia necessita sia dell'oggetto
che definisce le opzioni, sia dell'array di stringhe
che contiene i parametri e ci ritornerà un oggetto
di tipo CommandLine che ci servirà per ottenere
tutte le informazioni necessarie sui parametri.
Per sapere se è stato richiesto di stampare anche
l'ora, basta interrogare questo oggetto e comportarsi
di conseguenza:
//
Definisci la stringa per la visualizzazione della data
String formatStr = "d MMMM yyyy";
// se e' stata richiesta l'ora, aggiungila nella stringa
if (cmdLine.hasOption("o")) {
formatStr
+= " - HH:mm:ss";
}
// Stampa la data e l'eventuale ora
DateFormat formatter = new SimpleDateFormat(formatStr);
System.out.println(formatter.format(new Date()));
Ovviamente
CLI non gestisce solo parametri di tipo booleano, ma
anche quelli classici che forniscono un valore. In questo
caso dovremmo aggiungere al nostro codice una linea
per definire la nuova opzione, simile alla precedente,
con la sola differenza che questa necessita di un argomento:
opts.addOption("f",
true, "formato data");
con
la conseguente differente gestione dell'output dell'applicazione,
che prevede l'uso del parametro "f", qualora
sia presente, altrimenti verrà utilizzato un
formato standard:
String
formatStr;
if (cmdLine.hasOption("f")) {
formatStr = cmdLine.getOptionValue("f");
} else {
formatStr = "d MMMM, yyyy";
}
if (cmdLine.hasOption("o")) {
formatStr += " - HH:mm:ss";
}
Opzioni obbligatorie e gestione degli
errori
Vi sono, però, dei casi in cui una applicazione
ha la necessità di ricevere alcune informazioni
fondamentali per il suo funzionamento e la loro assenza
può generare complicazioni se non impedirne il
corretto funzionamento. In questi casi è oltremodo
necessario che il sistema che si occupa della gestione
dei parametri sia in grado di definire quali siano le
opzioni obbligatorie e, soprattutto, fornire un modo
semplice per gestire gli errori che derivano dall'assenza
degli stessi o, eventualmente, anche dalla loro incompletezza.
Come
già accennato in precedenza, la definizione dell'obbligatorietà
di una opzione è un po' più complessa
rispetto a quello che abbiamo visto nell'esempio precedente.
E' necessario creare esplicitamente un oggetto di tipo
Option e, quindi, usare l'opportuno setter per attivarne
l'attributo "required".
Option
fOpt = new Option("f", true, "formato
data");
fOpt.setRequired(true);
opts.addOption(fOpt);
A
questo punto l'utilizzatore del programma è obbligato
ad inserire il parametro per poter procedere con il
funzionamento del programma. In caso contrario, il metodo
parse() genererà una eccezione di tipo ParseException
durante la sua esecuzione. Come sempre, anche in questo
caso, attraverso una corretta analisi dell'eccezione
consente di fornire precise indicazioni all'utente per
correggere il proprio errore e di conseguenza poter
utilizzare l'applicazione.
Il metodo parse() genera, a seconda del tipo di errore,
una precisa eccezione, sottoclasse di ParseException.
Le più comuni sono MissingOptionException e MissingArgumentException.
La prima viene generata quando l'utilizzatore dell'applicazione
non fornisce una opzione che è stata definita
obbligatoria, mentre la seconda viene lanciata quando
manca l'argomento (il valore) ad una opzione che lo
prevede.
Un possibile modo per poter effettuare il parsing e
gestirne gli eventuali errori potrebbe essere questo:
try
{
Parser
parse = new PosixParser();
cmdLine
= parser.parse(opts, args);
}
catch (MissingArgumentException maE) {
System.out.println(maE.getMessage());
System.exit(1);
}
catch (MissingOptionException moE) {
System.out.println("Option"
+ moE.getMessage() + " is required" );
System.exit(1);
}
catch (Exception e) {
System.out.println("Errore:
" + e.toString());
System.exit(1);
}
che
fornisce, all'utilizzatore, un messaggio specifico per
aiutarlo a correggere l'errore stesso e, quindi, usare
l'applicazione correttamente.
Figura
2 - All'interno della pagina dei possibili scenari
di utilizzo, all'indirizzo http://jakarta.apache.org/commons/cli/usage.html,
viene mostrato l'esempio di Ant
L'aiuto
Ma come fa il nostro utente a sapere quali siano le
opzioni che può e dovrebbe usare, quale sia la
sintassi corretta e quali siano le opzioni obbligatorie
e quali no? La risposta più ovvia sarebbe quella
di leggere il manuale o la documentazione, ma questa
non è sempre disponibile. Anche in questo caso
Commons CLI fornisce, attraverso la classe HelpFormatter,
gli strumenti per facilitare la gestione di una pagina
di aiuto, così come avviene per i classici programmi
da linea di comando.
L'utilizzo di questa classe però, è molto
meno intuitiva di quanto dovrebbe. I vari metodi sono
piuttosto disordinati e non molto intuitivi e la documentazione
per questa versione non fornisce sufficienti informazioni
per poterli utilizzare al meglio. Con le opportune prove
sul campo è possibile comunque usufruire della
classe per fornire all'utente le informazioni su come
utilizzare l'applicazione ad esempio in questo modo:
try
{
Parser
parse = new PosixParser();
cmdLine
= parser.parse(opts, args);
}
catch (MissingArgumentException maE) {
System.out.println(maE.getMessage());
help.printHelp("NomeProgramma",
opts, true);
System.exit(1);
}
catch (MissingOptionException moE) {
System.out.println("Option"
+ moE.getMessage() + " is required" );
help.printHelp("NomeProgramma",
opts, true);
System.exit(1);
}
catch (Exception e) {
System.out.println("Errore:
" + e.toString());
help.printHelp("NomeProgramma",
opts, true);
System.exit(1);
}
Questa è senza dubbio la sintassi più
semplice e permette di ottenere un buon risultato con
il minimo sforzo. Sebbene questo non sia fondamentale,
l'aspetto estetico, ovviamente, non può essere
modificato.
E' comunque possibile dare una maggior personalizzazione
al risultato, aggiungendo un proprio testo prima e dopo
la lista delle opzioni e modificando le distanze dal
bordo e tra i vari testi.
Ovviamente per fare questo è necessario scrivere
un maggior numero di linee di codice:
//
crea un oggetto PrintWriter dallo standard error
PrintWriter writer = new PrintWriter(System.err);
// crea l'oggetto HelpFormatter
HelpFormatter help = new HelpFormatter();
// definisci la testata e la parte finale
String header = "Questa e' la testata";
String footer = "Questo e' il pie' di pagina";
// scrivi l'help completo all'interno del PrintWriter
// utilizzando i valori di default
help.printHelp(writer, help.defaultWidth, "NomeProgramma",
header,
opts, help.defaultLeftPad, help.defaultDescPad, footer);
// scrivi e chiudi il writer
writer.flush();
writer.close();
che
genererebbe questo risultato:
usage:
NomeProgramma
Questa e' la testata
-f formato data
-o mostra anche l'ora
Questo e' il pie' di pagina
Eventualmente
sarebbe anche possibile costruire un metodo esterno
che si occupa di effettuare la visualizzazione della
pagina di help, e che, eventualmente, gestisce anche
eventuali errori:
public
static void showHelp(Options opts, Exception ex) {
//
crea l'oggetto PrintWriter e helpFormatter
PrintWriter
writer = new PrintWriter(System.err);
HelpFormatter
help = new HelpFormatter();
//
metti il messaggio di errore qualora presente
if
(ex != null) {
writer.print("Error:
");
if
(ex instanceof MissingOptionException) {
writer.println("option
" + ex.getMessage() + " is required.");
}
else {
writer.println(ex.getMessage());
}
writer.println();
}
//
la testata e' una riga vuota, il pie' di pagina e' il
copyright
String
header = "";
String
footer = "--- © 2004 - blah blah blah ---";
//
scrivi l'help completo con i valori di default
help.printHelp(writer,
help.defaultWidth, "NomeProgramma", header,
opts,
help.defaultLeftPad, help.defaultDescPad, footer);
//
scrivi e chiudi il writer
writer.flush();
writer.close();
}
Questo
metodo potrebbe poi essere chiamato in caso di errore
o su semplice richiesta dell'utilizzatore, magari inserendo
un parametro "-h" per visualizzare subito
la pagina di aiuto.
Contro
e pro...
Come per una buona parte dei componenti che fanno parte
del progetto Jakarta Commons, anche per questo è
evidente la necessità di crescere. Nell'utilizzare
CLI, infatti, anche solo facendo alcune prove, è
molto facile trovare punti nel quale il prodotto può
essere migliorato oppure funzionalità che potrebbero
essere implementate per rendere il componente più
completo.
Un primo esempio di aspetti che possono essere migliorati
è, certo, quello che riguarda i messaggi generati
dalle varie eccezioni, che non sono stati progettati
in modo coerente e, ovviamente, sono in inglese.
Se provate a dimenticare una opzione obbligatoria, il
metodo getMessage() dell'eccezione MissingOptionException
ritornerà la chiave dell'opzione mancante permettendo
di inserirla all'interno di una stringa, magari localizzata
in italiano.
Purtroppo questo non succede con l'eccezione MissingArgumentException
che, come detto in precedenza, viene generata quando
manca l'argomento ad una opzione che lo richiede. In
questo caso, infatti, il messaggio di errore fornito
dal metodo getMessage() è "no argument for:"
seguito dall'opzione errata, e il metodo getLocalizedMessage()
non è stato implementato. Questo costringe lo
sviluppatore a fare qualche operazione per estrarne
la sola parte utile ed utilizzarla all'interno di un
messaggio completamente in italiano, ottenendo un risultato
certamente poco pulito.
Un altro aspetto della libreria che richiede un po'
di lavoro è la parte relativa alla classe HelpFormatter
per la gestione dell'aiuto, che ad un primo approccio
sembra assolutamente disordinata e poco logica. Ad essere
onesti, una buona parte dei problemi scaturiscono dalla
documentazione della classe che manca e, quindi, non
permette di comprenderne le potenzialità. E'
anche possibile che, con i metodi attualmente presenti,
sia possibile migliorare e, magari rendere più
elegante il codice di visualizzazione dell'aiuto visto
in precedenza.
Vi sono poi altre piccole migliorie, probabilmente meno
importanti, forse dei capricci per semplificare ancora
di più il lavoro dello sviluppatore. Un esempio
potrebbe essere la creazione di un metodo che permetta
di aggiungere una opzione obbligatoria alle opzioni,
senza doverla creare esplicitamente.
Un altro piccolo capriccio consisterebbe nell'aggiunta
alla classe CommandLine, di metodi che ritornino i valori
delle varie opzioni in differenti tipi, o anche primitive,
oltre che alla classica stringa.
E'
ovvio che così come ci sono alcuni aspetti negativi,
il componente ha anche alcuni lati positivi che lo rendono
sicuramente interessante.
Uno di questi è, senza dubbio, il parsing. Nell'esempio
di questo articolo, questo è stato effettuato
attraverso la classe PosixParser, che rispetta la logica
più diffusa per la gestione dei parametri su
linea di comando. Esistono però altre due implementazioni
dell'interfaccia di parsing, la BasicParser, estremamente
elementare e la GnuParser, che utilizza una differente
logica, anch'essa molto diffusa all'interno del mondo
Unix. Ovviamente, qualora si voglia inventare il proprio
sistema di interpretazione personale, basta semplicemente
creare la propria classe, estendendo la classe astratta
Parser.
Interessante è anche le potenzialità che
vengono offerte con le varie opzioni. Oltre alle proprietà
di base ed al flag che la rende obbligatoria, una opzione
può essere anche dichiarata "multipla",
ossia che accetta più di un argomento, lasciando
anche aperta la definizione del carattere di separazione.
Figura 3 - La tabella che mostra le differenti
proprietà delle opzioni, all'indirizzo http://jakarta.apache.org/commons/cli/properties.html.
Conclusioni
Dare un giudizio obiettivo a questo componente, avendone
sviluppato uno simile e senza cadere in banali confronti,
è piuttosto difficile. Certo è che se
dovessi partire da zero, probabilmente non svilupperei
qualcosa di completamente mio, ma utilizzaerei Commons
CLI come punto di partenza, magari "completandolo"
nelle sue parti mancanti (o partecipando al progetto
direttamente). Il mio consiglio, comunque, è
quello di provarlo seriamente, prima di partire con
lo sviluppo di qualcosa di simile, soprattutto se si
ha intenzione (o la necessità) di sviluppare
programmi e applicazioni che interagiscono seriamente
con la linea di comando.
Risorse
Gli esempi descritti
in questo articolo
|