Argumentz: small & simple command-line arguments parser in Java
Every now and then there is a need to pass arguments to the command-line application. While there are a lot of libraries/frameworks to do that in Java, I still had this feeling “why such an easy thing must be so cumbersome and require so much boilerplate?”.
TL/DR: code.
So I decided: the world needs one more command-line arguments parser for Java. Why? Well, maybe just because I can! First thing to decide on a new project is a name: ‘argumentz’ was available (according to maven central and one popular web search engine). ‘Z’ in ‘Argumentz’ stands for the last letter in the alphabet: I wanted to make the library as simple as possible (but not simpler) and keep user in the total control, so there cannot be anything simpler beyond it, like there are no letters after Z.
Example application
The example application takes following command-line arguments: host (string), port (integer), timeout in seconds (integer), user name (string) and a flag to enable verbose logging. So the execution will look like this:
java -cp . App -u admin -h localhost -p 8080 -s 30 -v
Or with full names of flags:
java -cp . App --user admin --host localhost --port 8080 --seconds 30 --verbose
The default value for user
is “guest”, for port
8080, so if one of those arguments is missing, application will use default values. Each flag has a default value as false, meaning it is disabled until explicitly enabled, pretty standard behavior for a flag.
Setup
To start, checking out prepared empty project from empty-project and slightly cleaning it up:
$ git clone https://github.com/sergey-melnychuk/empty-project.git \
-b maven-java8-junit5 \
args-parser
$ cd args-parser
$ rm -rf .git # remove any association with 'empty-project' repository
Adding dependency to pom.xml
:
<dependency>
<groupId>io.github.sergey-melnychuk</groupId>
<artifactId>argumentz</artifactId>
<version>0.3.9</version>
</dependency>
Describe command-line arguments
To describe desired setup in Arguments, Arguments.builder()
is used:
Argumentz arguments = Argumentz.builder()
.withParam('u', "user", "username to connect to the server", () -> "guest")
.withParam('p', "port", "port for server to listen", Integer::parseInt, () -> 8080)
.withParam('s', "seconds", "timeout in seconds", Integer::parseInt)
.withParam('h', "host", "host for client to connect to")
.withFlag('v', "verbose", "enable extra logging")
.withErrorHandler((RuntimeException e, Argumentz a) -> {
System.err.println(e.getMessage() + "\n");
a.printUsage(System.out);
System.exit(1);
})
.build();
So each argument is either a parameter (String
or actually any type that can be parsed from it) or a flag (just a parameter of type boolean
). Adding parameter with short form -p
and long one --param
can be done in one of the following ways:
- Required, of type
String
:.withParam('p', "param", "description goes here")
- Required, produced from
String
(in this caseint
):.withParam('p', "param", "...", Integer::parseInt)
- Any type
T
is supported, as long as instance ofFunction<String, T>
is provided.
- Any type
- Not required
String
(thus having default value):.withParam('p', "param", "...", () -> "default")
- Not required of any type (e.g.
int
):.withParam('p', "param", "...", Integer::parseInt, () -> 42)
Flag is defined with just .withFlag('f', "flag", "...")
. Distinguish flags between mandatory or optional does not make much sense to me, as well as providing a default value (it is just false
). So flag, is just, well, a flag - either it is there or not.
Error handler provided with .withErrorHandler
is called when the problem arises (missing required argument or parsing arguments value from String
failed). In this example, the error message is printed to stderr
, usage instructions are printed to stdout
and application terminates. One required thing for such error handler to do is to break the execution flow of an application: either throw a runtime exceptions or call System.exit
. Reasons for this requirement are simple: fail fast and let user keep the control.
Match against provided arguments
After there is a definition of Argumentz
, it is possible to match
arguments from String args[]
:
Argumentz.Match match = arguments.match(args);
After that, all matched values are available from the match
instance. No binding to class properties via annotattions, just plain and simple .get{,Int,Flag}(String name)
, inspired by Lightbend Config. Getting all the actual values is pretty straightforward:
String user = match.get("user");
int port = match.getInt("port");
String host = match.get("host");
int seconds = match.getInt("seconds");
boolean verbose = match.getFlag("verbose");
For generic value of type T
, use match.getAs(Class<T> clazz, String name)
. For example if T
is Param
, the call is match.getAs(Param.class, "param")
. The casting from Object
to T
is done by Class<T>::cast
, so if anything goes wrong, ClassCastException
will land into error handler.
Error handling
If an exception is re-thrown from error handler, it will be thrown from arguments.match
. As promised, the user keeps total control under the situation - how to handle the exception is totally up to the user. For example, with try-catch
around arguments.match
it is easy to extract into pure function all code related to the command-line arguments:
static Optional<Params> fetchParams(String args[]) {
Argumentz arguments = Argumentz.builder()
// ...
.withErrorHandler((RuntimeException e, Argumentz a) -> { throw e; })
.build();
try {
Argumentz.Match match = arguments.match(args);
String user = match.get("user");
int port = match.getInt("port");
String host = match.get("host");
int seconds = match.getInt("seconds");
boolean verbose = match.getFlag("verbose");
return Optional.of(new Params(host, port, user, seconds, verbose));
} catch (IllegalArgumentException e) {
log.error("Failed to fetch Params.", e);
return Optional.empty();
}
}
Summary
I was presenting Argumentz, very small, simple and flexible library for parsing command line arguments in Java. It is pure Java, has zero dependencies and has <300 LoC. In the example app, code that handles command-line arguments is straightforward, easy and leaves user in charge.
If you found a bug or a missing feature, please do let me know in comments at the bottom of the page.
P.S. Publishing to Maven Central
The process is pretty much described here.
I was trying to follow it, but still there were some issues along the way. Below is the summary of what I did to achieve successful release and promotion to Maven Central.
Sonatype JIRA
- Create an account
- Create a ticket
GPG
Described here.
$ gpg2 --version
...
$ gpg2 --gen-key
...
$ gpg2 --list-keys
...
pub rsa2048 2020-07-29 [SC] [expires: 2022-07-29]
<key-id-goes-here>
uid [ultimate] Sergey Melnychuk <sergey-melnychuk@users.noreply.github.com>
sub rsa2048 2020-07-29 [E] [expires: 2022-07-29]
...
$ gpg2 --keyserver hkp://pool.sks-keyservers.net --send-keys <key-id>
gpg: sending key <key-id> to hkp://pool.sks-keyservers.net
pom.xml
...
<groupId>io.github.sergey-melnychuk</groupId>
<artifactId>argumentz</artifactId>
<version>0.3.9-SNAPSHOT</version>
<packaging>jar</packaging>
<name>${project.groupId}:${project.artifactId}</name>
<description>Command-line arguments parser in Java. Small, simple and flexible.</description>
<url>https://github.com/sergey-melnychuk/argumentz</url>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<name>Sergey Melnychuk</name>
<url>https://github.com/sergey-melnychuk/</url>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/sergey-melnychuk/argumentz.git</connection>
<developerConnection>scm:git:git@github.com:sergey-melnychuk/argumentz.git</developerConnection>
<url>http://github.com/sergey-melnychuk/argumentz/tree/master</url>
</scm>
...
...
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
...
...
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.7</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>2.5.3</version>
<configuration>
<autoVersionSubmodules>true</autoVersionSubmodules>
<useReleaseProfile>false</useReleaseProfile>
<releaseProfiles>release</releaseProfiles>
<goals>deploy</goals>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
...
~/.m2/settings.xml
<settings>
<servers>
<server>
<id>ossrh</id>
<username>jira-username</username>
<password>jira-password</password>
</server>
</servers>
<profiles>
<profile>
<id>ossrh</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<gpg.executable>gpg2</gpg.executable>
<gpg.passphrase>***</gpg.passphrase>
</properties>
</profile>
</profiles>
</settings>
Release
$ mvn release:clean release:prepare -P release
$ mvn release:perform -P release