Monday, June 26, 2017

Java Command-Line Interfaces (Part 3): jbock

In the first two posts of this series on command-line parsing in Java, I looked at the Apache Commons CLI and args4j libraries. In this third post in the series, I look at jbock, the self-described "curiously simple CLI parser."

UPDATE (16 November 2017): This post describes using jbock 1.8/1.8.1 and the examples in this post are based on that version. The author of jbock has provided changes to these examples that work with newly-released jbock 2.1 and the 2.1-compatible changes are incorporated in the full source code available on GitHub. Version 2.1 of jbock requires Java 8 and supports Java 9 modules.

My posts on command-line parsing in Java have used examples based on providing a required file name and an optional verbose flag to the Java application. The same approach is used in this post to demonstrate jbock 1.8. The full source code for the example class is available on GitHub, but the code generated by jbock (Main_Parser) is not available as it can be generated.

The approach jbock uses for command-line processing is different than that used by the two previously covered parsing libraries. The previously covered libraries required Java code for parsing command-line arguments to be built against and executed against the libraries' JARs. In other words, the libraries' JARs needed to be on both the compile-time (javac) classpath and on the runtime Java launcher (java) classpath. The jbock approach instead relies on inclusion of the jbock JAR only at compile time. The jbock approach generates Java source code that is completely independent of the jbock library. One could, for example, choose to run jbock to generate these Java source code files once and then version control those generated files and only build and run against the generated files from that point on without needing to build or run against jbock's JAR. The only time the jbock JAR is required is when the generated Java source needs to be regenerated. Because the generated code is generated based on annotations on custom Java classes, it is likely that the jbock code generation would be executed in most cases as part of a normal build rather than version controlling the generated source.

In most situations, I'd use a custom class with a name such as "Arguments" or "CommandLine" when using jbock to parse command-line arguments. However, for this post, I am using a simple Main class to be more similar of an example to the approach used with the other command-line parsing libraries in other posts in this series. Like args4j, jbock uses annotations for the "definition" phase of command-line processing. However, jbock's annotations are on the class's constructor and its arguments rather than args4j's approach of annotating class fields. The jbock constructor-based annotations approach is demonstrated in the next code listing.

jbock "Definition" of Command-Line Options

@CommandLineArguments
public Main(
   @ShortName('v') @LongName("verbose") @Description("Verbosity enabled?")
   final boolean newVerbose,
   @ShortName('f') @LongName("file") @Description("File name and path")
   final Optional<String> newFileName)
{
   verbose = newVerbose;
   file = newFileName.orElse("");
}
// . . .

The "parsing" stage of command-line processing with jbock is demonstrated in the next code listing.

"Parsing" Command-line Options with jbock

final Main_Parser parser = new Main_Parser();
final Main_Parser.Binder binder = parser.parse(arguments);
final Main main = binder.bind();

The Main_Parser class shown in the above code listing is generated by jbock based on the annotations shown in the first code listing. The jbock library processes the annotations of the Main class to determine how to build the Main_Parser class. The generated class's name is based on the name of the class with jbock annotations and concatenated with _Parser. For example, had my class with jbock annotated constructor and constructor arguments been named "Arguments", the generated class would be named "Arguments_Parser".

After the instance of the generated Main_Parser class has had parse invoked on the command-line arguments, that instance's bind() method is invoked to return an instance of the original annotated Main class. The "interrogation" process at this point consists solely of accessing the attributes of that Main instance via its public "get" methods. This is demonstrated in the next code listing.

"Interrogation" Stage of Command-line Processing with jbock

out.println("The file '" + main.getFile() + "' was provided and verbosity is set to '"
   + main.isVerbose() + "'.");

The screen snapshot that follows demonstrates the code in action using jbock to parse the command-line options.

If help or usage information is desired, this can be retrieved from the generated *_Parser (Main_Parser in this case) class as well. Specifically, the generated *_Parser class includes a nested Option enum representing the various options. One can iterate over those option's enum values to retrieve metadata about each option. In the code listing below, the describe(int) method is invoked on each option's enum value (the passed-in integer is the number of spaces to indent).

Obtaining Usage Details with jbock

final Main_Parser parser = new Main_Parser();
if (arguments.length < 1)
{
   for (final Main_Parser.Option option : Main_Parser.Option.values())
   {
      out.println(option.describe(3));
   }
   System.exit(-1);
}

The screen snapshot shown next demonstrates this code in action to print out the options and their descriptions.

The source code discussed in this post is available on GitHub.

Here are some additional characteristics of jbock to consider when selecting a framework or library to help with command-line parsing in Java.

  • jbock is available as open source.
    • UPDATE: jbock creator h908714124 has pointed out in the feedback that jbock is "just a concept project" that is "intentionally minimalistic, so you can actually grok all the parsing rules."
    • UPDATE: h908714124 also invites users of jbock to "open a github issue or make a pull request" if an issue is identified.
  • The current version of jbock (1.8) requires Java SE 8.
  • jbock has no third-party or external dependencies.
  • The jbock 1.8 JAR (jbock-1.8.jar) is approximately 131 KB in size, but this is not as significant as for similar libraries because this JAR is not required at runtime (generated code is independent of the JAR).
  • I did not demonstrate jbock's enforcement of the presence of required command-line parameters because it intentionally does not support that feature. The README states, "Deliberately simple: No converters, default values or required checking. With java 8, it's easy to add this stuff by hand."

The most obvious characteristic of jbock that distinguishes it from most other Java-based command-line parsing libraries is the generation of parsing code entirely at compile time that leaves no runtime dependencies on the jbock library. This would be an obvious advantage in situations where there is concern about the number of classes loaded or the size of the expressed classpath. The README lists multiple items that "set [jbock] apart." These include "no reflection, purely static analysis" and "convenient, flexible property binding via constructor."

Additional References

2 comments:

h908714124 said...

Thanks for this in depth article. I never knew someone was digging through this. Keep in mind that it's just a concept project. It's intentionally minimalistic, so you can actually grok all the parsing rules. If you find something odd, you can open a github issue, or make a pull request. Best regards, h908714124

@DustinMarx said...

h908714124,

I appreciate you providing these additional details. I've quoted most of your comment in updates to my post. I also appreciate some of the ideas embodied in jbock.

Dustin