MokaByte 81 - Gennaio 2004
Jakarta Commons
IV parte: Jakarta CLI
di
Alessandro "Kazuma" Garbagnati
Qualche volta, anche in Java, può diventare comodo scrivere applicazioni che utilizzano la linea di comando per il passaggio delle informazioni necessarie per il funzionamento. All'interno del progetto Jakarta Commons è presente una piccola libreria, CLI, che permette di semplificare la gestione dei vari parametri che vengono passati proprio attraverso la linea di comando di un programma. Scopriamo come.

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

 
MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it