Mengurai Parameter Baris Perintah dengan JCommander

1. Ikhtisar

Dalam tutorial ini, kita akan belajar bagaimana menggunakan JCommander untuk mengurai parameter baris perintah. Kami akan menjelajahi beberapa fiturnya saat kami membangun aplikasi baris perintah sederhana.

2. Mengapa JCommander?

“Karena masa pakai terlalu singkat untuk mengurai parameter baris perintah” - Cédric Beust

JCommander, dibuat oleh Cédric Beust, adalah pustaka berbasis anotasi untuk parsing parameter baris perintah . Ini dapat mengurangi upaya membangun aplikasi baris perintah dan membantu kami memberikan pengalaman pengguna yang baik untuk mereka.

Dengan JCommander, kita dapat melepaskan tugas rumit seperti penguraian, validasi, dan konversi tipe, untuk memungkinkan kita fokus pada logika aplikasi kita.

3. Menyiapkan JCommander

3.1. Konfigurasi Maven

Mari kita mulai dengan menambahkan ketergantungan jcommander di pom.xml kita :

 com.beust jcommander 1.78 

3.2. Halo Dunia

Mari buat HelloWorldApp sederhana yang mengambil satu input bernama name dan mencetak salam, “Halo” .

Karena JCommander mengikat argumen baris perintah ke bidang dalam kelas Java , pertama-tama kita akan mendefinisikan kelas HelloWorldArgs dengan nama bidang yang dianotasi dengan @Parameter :

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

Sekarang, mari gunakan kelas JCommander untuk mengurai argumen baris perintah dan menetapkan bidang di objek HelloWorldArgs kita :

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

Terakhir, mari panggil kelas utama dengan argumen yang sama dari konsol:

$ java HelloWorldApp --name JavaWorld Hello JavaWorld

4. Membangun Aplikasi Nyata di JCommander

Sekarang kita sudah siap dan berjalan, mari kita pertimbangkan kasus penggunaan yang lebih kompleks - klien API baris perintah yang berinteraksi dengan aplikasi penagihan seperti Stripe, terutama skenario Penagihan Terukur (atau berbasis penggunaan). Layanan penagihan pihak ketiga ini mengelola langganan dan faktur kami.

Bayangkan kita menjalankan bisnis SaaS, di mana pelanggan membeli langganan ke layanan kami dan ditagih untuk jumlah panggilan API ke layanan kami per bulan. Kami akan melakukan dua operasi di klien kami:

  • submit : Kirimkan kuantitas dan harga satuan penggunaan untuk pelanggan terhadap langganan tertentu
  • ambil : Ambil biaya untuk pelanggan berdasarkan konsumsi pada beberapa atau semua langganan mereka di bulan ini - kami bisa mendapatkan biaya ini dikumpulkan dari semua langganan atau diperinci berdasarkan setiap langganan

Kami akan membangun klien API saat kami menelusuri fitur perpustakaan.

Mari kita mulai!

5. Mendefinisikan Parameter

Mari kita mulai dengan mendefinisikan parameter yang dapat digunakan aplikasi kita.

5.1. The @Parameter Anotasi

Menganotasi bidang dengan @Parameter memberi tahu JCommander untuk mengikat argumen baris perintah yang cocok dengannya . @Parameter memiliki atribut untuk mendeskripsikan parameter utama, seperti:

  • nama - satu atau beberapa nama opsi, misalnya “–name” atau “-n”
  • deskripsi - arti di balik opsi, untuk membantu pengguna akhir
  • diperlukan - apakah opsinya wajib, defaultnya salah
  • arity - jumlah parameter tambahan yang digunakan opsi

Mari kita konfigurasikan parameter customerId dalam skenario penagihan terukur kita:

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

Sekarang, mari jalankan perintah kita dengan parameter “–customer” yang baru:

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

Demikian pula, kita dapat menggunakan parameter "-C" yang lebih pendek untuk mencapai efek yang sama:

$ java App -C cust0000001A Read CustomerId: cust0000001A. 

5.2. Parameter yang Diperlukan

Jika sebuah parameter wajib, aplikasi keluar dan melemparkan ParameterException jika pengguna tidak menentukannya:

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

Kita harus mencatat bahwa, secara umum, setiap kesalahan dalam mengurai hasil parameter dalam ParameterException di JCommander.

6. Jenis Built-In

6.1. Antarmuka IStringConverter

JCommander melakukan konversi tipe dari input String baris perintah ke dalam tipe Java di kelas parameter kami. The IStringConverter antarmuka menangani konversi jenis parameter dari String untuk semua jenis sewenang-wenang. Jadi, semua konverter bawaan JCommander mengimplementasikan antarmuka ini.

Di luar kotak, JCommander hadir dengan dukungan untuk tipe data umum seperti String , Integer , Boolean , BigDecimal , dan Enum .

6.2. Jenis Aritas Tunggal

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

Kita bisa menangkap ParameterException dan penggunaan panggilan untuk membantu pengguna memahami mengapa masukan mereka salah. ParameterException berisi instance JCommander untuk menampilkan bantuan:

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

12. Kesimpulan

Dalam tutorial ini, kami menggunakan JCommander untuk membangun aplikasi baris perintah. Meskipun kami membahas banyak fitur utama, masih ada lagi di dokumentasi resmi.

Seperti biasa, kode sumber untuk semua contoh tersedia di GitHub.