TL;DR: I built femtocli, a small command-line parsing library with sub-commands, an annotation-based API, and support for parsing Java agent arguments.
Every command-line tool that I, and probably you, write needs a command-line interface (CLI). Writing simply, with one or two options manually, is possible, but becomes tedious fast. Especially when our tool grows, and we add more options and subcommands.
Usually, you would add a library like the fantastic picocli to your project and be fine. It’s a really easy-to-use CLI library with all the features that you would want. So you can write a simple CLI quite quickly and declaratively:
@Command(name = "demo", description = "A demo application")
public class Main implements Runnable {
@CommandLine.Parameters(index = "0", description = "Some parameter")
String parameter;
@Option(names = "--some-flag")
boolean someFlag;
@Override
public void run() { /* print */ }
public static void main(String[] args) {
new CommandLine(new Main()).execute(args);
}
}
The only problem? While the application JAR itself is only around 3KB for these tiny examples, the application JAR with the Picocli 4.7.7 dependency is 407KB.
Is this a problem? It might not be for you, but it certainly can be for small tools like jstall, where the Picocli dependency accounted for over half the JAR size in the minimal version, which is kind of a lot just for the option parsing. Of course, in many situations you don’t care, as there are other libraries, e.g., for JSON parsing, that can easily add multiple megabytes. But for tiny CLI helper tools, it can matter.
I couldn’t find any maintained CLI library with a picocli-like declarative approach and sub-commands under 50KB, so I built one. And the best part: I was able to add all the features I liked (converters, custom footers, …) and ignore the ones I don’t usually need (GraalVM support, ANSI colors, …).
The new library is called femtocli, it requires Java 17 and is MIT licensed:
package me.bechberger.femtocli.examples;
import me.bechberger.femtocli.FemtoCli;
import me.bechberger.femtocli.annotations.Command;
import me.bechberger.femtocli.annotations.Option;
import java.util.concurrent.Callable;
@Command(name = "greet", description = "Greet a person")
class GreetCommand implements Callable<Integer> {
@Option(names = {"-n", "--name"}, description = "Name to greet", required = true)
String name;
@Option(names = {"-c", "--count"}, description = "Count (default: ${DEFAULT-VALUE})", defaultValue = "1")
int count;
@Override
public Integer call() {
for (int i = 0; i < count; i++) System.out.println("Hello, " + name + "!");
return 0;
}
}
@Command(name = "myapp", description = "My CLI application", version = "1.0.0",
subcommands = {GreetCommand.class})
public class QuickStart implements Runnable {
public void run() {
System.out.println("Use 'myapp greet --help'");
}
public static void main(String[] args) {
FemtoCli.run(new QuickStart(), args);
}
}
Try it:
> ./examples/run.sh QuickStart greet --name=World --count=1 Hello, World!Continue reading



