Analysieren von Befehlszeilenparametern mit JCommander

1. Übersicht

In diesem Tutorial erfahren Sie, wie Sie mit JCommander Befehlszeilenparameter analysieren. Wir werden einige seiner Funktionen untersuchen, während wir eine einfache Befehlszeilenanwendung erstellen.

2. Warum JCommander?

„Weil das Leben zu kurz ist, um Befehlszeilenparameter zu analysieren“ - Cédric Beust

JCommander, erstellt von Cédric Beust, ist eine auf Anmerkungen basierende Bibliothek zum Parsen von Befehlszeilenparametern . Dies kann den Aufwand für die Erstellung von Befehlszeilenanwendungen verringern und uns dabei helfen, ihnen eine gute Benutzererfahrung zu bieten.

Mit JCommander können wir knifflige Aufgaben wie Parsen, Validieren und Typkonvertierungen auslagern, damit wir uns auf unsere Anwendungslogik konzentrieren können.

3. JCommander einrichten

3.1. Maven-Konfiguration

Beginnen wir mit dem Hinzufügen der jcommander- Abhängigkeit in unserer pom.xml :

 com.beust jcommander 1.78 

3.2. Hallo Welt

Erstellen wir eine einfache HelloWorldApp , die eine einzelne Eingabe namens name verwendet und die Begrüßung „Hello“ druckt .

Da JCommander binden Befehlszeilenargumente auf Felder in einer Java - Klasse , werden wir zunächst eine Definition HelloWorldArgs Klasse mit einem Feldnamen mit kommentierten @Parameter :

class HelloWorldArgs { @Parameter( names = "--name", description = "User name", required = true ) private String name; }

Verwenden wir nun die JCommander- Klasse, um die Befehlszeilenargumente zu analysieren und die Felder in unserem HelloWorldArgs- Objekt zuzuweisen :

HelloWorldArgs jArgs = new HelloWorldArgs(); JCommander helloCmd = JCommander.newBuilder()   .addObject(jArgs)   .build(); helloCmd.parse(args); System.out.println("Hello " + jArgs.getName());

Rufen wir abschließend die Hauptklasse mit denselben Argumenten von der Konsole aus auf:

$ java HelloWorldApp --name JavaWorld Hello JavaWorld

4. Erstellen einer echten Anwendung in JCommander

Betrachten wir nun einen komplexeren Anwendungsfall - einen Befehlszeilen-API-Client, der mit einer Abrechnungsanwendung wie Stripe interagiert, insbesondere mit dem Szenario der gemessenen (oder nutzungsbasierten) Abrechnung. Dieser Abrechnungsservice von Drittanbietern verwaltet unsere Abonnements und Rechnungen.

Stellen wir uns vor, wir betreiben ein SaaS-Geschäft, in dem unsere Kunden Abonnements für unsere Dienste kaufen und die Anzahl der API-Aufrufe für unsere Dienste pro Monat in Rechnung gestellt wird. Wir werden zwei Operationen in unserem Kunden durchführen:

  • Senden : Senden Sie die Menge und den Stückpreis der Nutzung für einen Kunden gegen ein bestimmtes Abonnement
  • Abrufen : Abrufen von Gebühren für einen Kunden basierend auf dem Verbrauch einiger oder aller Abonnements im aktuellen Monat. Wir können diese Gebühren über alle Abonnements aggregieren oder nach Abonnements auflisten

Wir werden den API-Client erstellen, während wir die Funktionen der Bibliothek durchgehen.

Lass uns anfangen!

5. Parameter definieren

Beginnen wir mit der Definition der Parameter, die unsere Anwendung verwenden kann.

5.1. Die @ Parameter- Anmerkung

Durch das Kommentieren eines Felds mit @Parameter wird JCommander angewiesen, ein übereinstimmendes Befehlszeilenargument daran zu binden . @Parameter verfügt über Attribute zur Beschreibung des Hauptparameters, z.

  • Namen - ein oder mehrere Namen der Option, z. B. "–name" oder "-n"
  • Beschreibung - die Bedeutung hinter der Option, um dem Endbenutzer zu helfen
  • erforderlich - ob die Option obligatorisch ist, ist standardmäßig false
  • arity - Anzahl der zusätzlichen Parameter, die die Option verwendet

Konfigurieren wir einen Parameter customerId in unserem Szenario mit gemessener Abrechnung:

@Parameter( names = { "--customer", "-C" }, description = "Id of the Customer who's using the services", arity = 1, required = true ) String customerId; 

Führen Sie nun unseren Befehl mit dem neuen Parameter "–customer" aus:

$ java App --customer cust0000001A Read CustomerId: cust0000001A. 

Ebenso können wir den kürzeren Parameter "-C" verwenden, um den gleichen Effekt zu erzielen:

$ java App -C cust0000001A Read CustomerId: cust0000001A. 

5.2. Erforderliche Parameter

Wenn ein Parameter obligatorisch ist, beendet die Anwendung das Auslösen einer ParameterException, wenn der Benutzer dies nicht angibt:

$ java App Exception in thread "main" com.beust.jcommander.ParameterException: The following option is required: [--customer | -C]

Wir sollten beachten, dass im Allgemeinen jeder Fehler beim Parsen der Parameter zu einer ParameterException in JCommander führt.

6. Eingebaute Typen

6.1. IStringConverter- Schnittstelle

JCommander führt die Typkonvertierung von der Befehlszeilen- String- Eingabe in die Java-Typen in unseren Parameterklassen durch. Die IStringConverter- Schnittstelle übernimmt die Typkonvertierung eines Parameters von String in einen beliebigen Typ. Alle in JCommander integrierten Konverter implementieren diese Schnittstelle.

JCommander bietet standardmäßig Unterstützung für gängige Datentypen wie String , Integer , Boolean , BigDecimal und Enum .

6.2. Single-Arity-Typen

Arity relates to the number of additional parameters an option consumes. JCommander's built-in parameter types have a default arity of one, except for Boolean and List. Therefore, common types such as String, Integer, BigDecimal, Long, and Enum, are single-arity types.

6.3. Boolean Type

Fields of type boolean or Boolean don't need any additional parameter – these options have an arity of zero.

Let's look at an example. Perhaps we want to fetch the charges for a customer, itemized by subscription. We can add a boolean field itemized, which is false by default:

@Parameter( names = { "--itemized" } ) private boolean itemized; 

Our application would return aggregated charges with itemized set to false. When we invoke the command line with the itemized parameter, we set the field to true:

$ java App --itemized Read flag itemized: true. 

This works well unless we have a use case where we always want itemized charges, unless specified otherwise. We could change the parameter to be notItemized, but it might be clearer to be able to provide false as the value of itemized.

Let's introduce this behavior by using a default value true for the field, and setting its arity as one:

@Parameter( names = { "--itemized" }, arity = 1 ) private boolean itemized = true; 

Now, when we specify the option, the value will be set to false:

$ java App --itemized false Read flag itemized: false. 

7. List Types

JCommander provides a few ways of binding arguments to List fields.

7.1. Specifying the Parameter Multiple Times

Let's assume we want to fetch the charges of only a subset of a customer's subscriptions:

@Parameter( names = { "--subscription", "-S" } ) private List subscriptionIds; 

The field is not mandatory, and the application would fetch the charges across all the subscriptions if the parameter is not supplied. However, we can specify multiple subscriptions by using the parameter name multiple times:

$ java App -S subscriptionA001 -S subscriptionA002 -S subscriptionA003 Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003]. 

7.2. Binding Lists Using the Splitter

Instead of specifying the option multiple times, let's try to bind the list by passing a comma-separated String:

$ java App -S subscriptionA001,subscriptionA002,subscriptionA003 Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003]. 

This uses a single parameter value (arity = 1) to represent a list. JCommander will use the class CommaParameterSplitter to bind the comma-separated String to our List.

7.3. Binding Lists Using a Custom Splitter

We can override the default splitter by implementing the IParameterSplitter interface:

class ColonParameterSplitter implements IParameterSplitter { @Override public List split(String value) { return asList(value.split(":")); } }

And then mapping the implementation to the splitter attribute in @Parameter:

@Parameter( names = { "--subscription", "-S" }, splitter = ColonParameterSplitter.class ) private List subscriptionIds; 

Let's try it out:

$ java App -S "subscriptionA001:subscriptionA002:subscriptionA003" Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003]. 

7.4. Variable Arity Lists

Variable arity allows us to declarelists that can take indefinite parameters, up to the next option. We can set the attribute variableArity as true to specify this behavior.

Let's try this to parse subscriptions:

@Parameter( names = { "--subscription", "-S" }, variableArity = true ) private List subscriptionIds; 

And when we run our command:

$ java App -S subscriptionA001 subscriptionA002 subscriptionA003 --itemized Read Subscriptions: [subscriptionA001, subscriptionA002, subscriptionA003]. 

JCommander binds all input arguments following the option “-S” to the list field, until the next option or the end of the command.

7.5. Fixed Arity Lists

So far we've seen unbounded lists, where we can pass as many list items as we wish. Sometimes, we may want to limit the number of items passed to a List field. To do this, we can specify an integer arity value for a List fieldto make it bounded:

@Parameter( names = { "--subscription", "-S" }, arity = 2 ) private List subscriptionIds; 

Fixed arity forces a check on the number of parameters passed to a List option and throws a ParameterException in case of a violation:

$ java App -S subscriptionA001 subscriptionA002 subscriptionA003 Was passed main parameter 'subscriptionA003' but no main parameter was defined in your arg class 

The error message suggests that since JCommander expected only two arguments, it tried to parse the extra input parameter “subscriptionA003” as the next option.

8. Custom Types

We can also bind parameters by writing custom converters. Like built-in converters, custom converters must implement the IStringConverter interface.

Let's write a converter for parsing an ISO8601 timestamp:

class ISO8601TimestampConverter implements IStringConverter { private static final DateTimeFormatter TS_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss"); @Override public Instant convert(String value) { try { return LocalDateTime .parse(value, TS_FORMATTER) .atOffset(ZoneOffset.UTC) .toInstant(); } catch (DateTimeParseException e) { throw new ParameterException("Invalid timestamp"); } } } 

This code will parse the input String and return an Instant, throwing a ParameterException if there's a conversion error. We can use this converter by binding it to a field of type Instant using the converter attribute in @Parameter:

@Parameter( names = { "--timestamp" }, converter = ISO8601TimestampConverter.class ) private Instant timestamp; 

Let's see it in action:

$ java App --timestamp 2019-10-03T10:58:00 Read timestamp: 2019-10-03T10:58:00Z.

9. Validating Parameters

JCommander provides a few default validations:

  • whether required parameters are supplied
  • if the number of parameters specified matches the arity of a field
  • whether each String parameter can be converted into the corresponding field's type

In addition, we may wish to add custom validations. For instance, let's assume that the customer IDs must be UUIDs.

We can write a validator for the customer field that implements the interface IParameterValidator:

class UUIDValidator implements IParameterValidator { private static final String UUID_REGEX = "[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}"; @Override public void validate(String name, String value) throws ParameterException { if (!isValidUUID(value)) { throw new ParameterException( "String parameter " + value + " is not a valid UUID."); } } private boolean isValidUUID(String value) { return Pattern.compile(UUID_REGEX) .matcher(value) .matches(); } } 

Then, we can hook it up with the validateWith attribute of the parameter:

@Parameter( names = { "--customer", "-C" }, validateWith = UUIDValidator.class ) private String customerId; 

If we invoke the command with a non-UUID customer Id, the application exits with a validation failure message:

$ java App --C customer001 String parameter customer001 is not a valid UUID. 

10. Sub-Commands

Now that we've learned about parameter binding, let's pull everything together to build our commands.

In JCommander, we can support multiple commands, called sub-commands, each with a distinct set of options.

10.1. @Parameters Annotation

We can use @Parameters to define sub-commands. @Parameters contains the attribute commandNames to identify a command.

Let's model submit and fetch as sub-commands:

@Parameters( commandNames = { "submit" }, commandDescription = "Submit usage for a given customer and subscription, " + "accepts one usage item" ) class SubmitUsageCommand { //... } @Parameters( commandNames = { "fetch" }, commandDescription = "Fetch charges for a customer in the current month, " + "can be itemized or aggregated" ) class FetchCurrentChargesCommand { //... } 

JCommander uses the attributes in @Parameters to configure the sub-commands, such as:

  • commandNames – name of the sub-command; binds the command-line arguments to the class annotated with @Parameters
  • commandDescription – documents the purpose of the sub-command

10.2. Adding Sub-Commands to JCommander

We add the sub-commands to JCommander with the addCommand method:

SubmitUsageCommand submitUsageCmd = new SubmitUsageCommand(); FetchCurrentChargesCommand fetchChargesCmd = new FetchCurrentChargesCommand(); JCommander jc = JCommander.newBuilder() .addCommand(submitUsageCmd) .addCommand(fetchChargesCmd) .build(); 

The addCommand method registers the sub-commands with their respective names as specified in the commandNames attribute of @Parameters annotation.

10.3. Parsing Sub-Commands

To access the user's choice of command, we must first parse the arguments:

jc.parse(args); 

Next, we can extract the sub-command with getParsedCommand:

String parsedCmdStr = jc.getParsedCommand(); 

In addition to identifying the command, JCommander binds the rest of the command-line parameters to their fields in the sub-command. Now, we just have to call the command we want to use:

switch (parsedCmdStr) { case "submit": submitUsageCmd.submit(); break; case "fetch": fetchChargesCmd.fetch(); break; default: System.err.println("Invalid command: " + parsedCmdStr); } 

11. JCommander Usage Help

We can invoke usage to render a usage guide. This is a summary of all the options that our application consumes. In our application, we can invoke usage on the main command, or alternatively, on each of the two commands “submit” and “fetch” separately.

A usage display can help us in a couple of ways: showing help options and during error handling.

11.1. Showing Help Options

We can bind a help option in our commands using a boolean parameter along with the attribute help set to true:

@Parameter(names = "--help", help = true) private boolean help; 

Then, we can detect if “–help” has been passed in the arguments, and call usage:

if (cmd.help) { jc.usage(); } 

Let's see the help output for our “submit” sub-command:

$ java App submit --help Usage: submit [options] Options: * --customer, -C Id of the Customer who's using the services * --subscription, -S Id of the Subscription that was purchased * --quantity Used quantity; reported quantity is added over the billing period * --pricing-type, -P Pricing type of the usage reported (values: [PRE_RATED, UNRATED]) * --timestamp Timestamp of the usage event, must lie in the current billing period --price If PRE_RATED, unit price to be applied per unit of usage quantity reported 

The usage method uses the @Parameter attributes such as description to display a helpful summary. Parameters marked with an asterisk (*) are mandatory.

11.2. Error Handling

Wir können die ParameterException abfangen und die Verwendung aufrufen , um dem Benutzer zu helfen, zu verstehen, warum ihre Eingabe falsch war. ParameterException enthält die JCommander- Instanz zum Anzeigen der Hilfe:

try { jc.parse(args); } catch (ParameterException e) { System.err.println(e.getLocalizedMessage()); jc.usage(); } 

12. Schlussfolgerung

In diesem Tutorial haben wir JCommander verwendet, um eine Befehlszeilenanwendung zu erstellen. Während wir viele der Hauptfunktionen behandelt haben, enthält die offizielle Dokumentation mehr.

Wie üblich ist der Quellcode für alle Beispiele auf GitHub verfügbar.