Article directory
need
Recently, I wanted to package one of my Java applications into a command-line tool. After reading several libraries, I finally chose JCommander, combined with the AutoService library, to implement a tool with sub-commands, which is convenient for expanding new sub-commands.
Subcommands are placed under the same package, implement the same interface, and are loaded through java.util.ServiceLocator.
Java Command Line Tool Library
Several commonly used libraries are:
-
JCommander
project address: https://github.com/cbeust/jcommander
Star: 1010 Fork: 227
Document address: http://jcommander.org/ -
picocli
address: https://github.com/remkop/picocli
Star: 336, Fork: 32
Example: https://github.com/kakawait/picocli-spring-boot-starter -
Commons CLI
address: https://commons.apache.org/proper/commons-cli/
address: Open source project from apache common
Update: The last update was 1.5-SNAPSHOT on June 8, 2017 -
Args4j
project address: https://0github.com/kohsuke/args4j
Star: 570 Fork: 151
Document address: http://args4j.kohsuke.org/sample.html
Activity: The last update was 2 years ago
dependent library
<!-- https://mvnrepository.com/artifact/com.google.auto.service/auto-service -->
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.beust/jcommander -->
<dependency>
<groupId>com.beust</groupId>
<artifactId>jcommander</artifactId>
<version>1.82</version>
</dependency>
Define individual subcommands
Create child instruction:
@AutoService(Command.class)
@Parameters(
commandNames = {
CREATE_CMD },
commandDescription = "create a new ebook."
)
@Getter
public class CreateCommand implements Command{
@Parameter(names = {
"--indexUrl", "-u" })
public String indexUrl;
@Override
public void execute() throws CommandException {
System.out.println("create a new book from " + indexUrl);
}
}
Fetch subcommand:
@AutoService(Command.class)
@Parameters(
commandNames = {
FETCH_CMD},
commandDescription = "fetch some articles from website."
)
@Getter
public class FetchCommand implements Command {
@Parameter(names = {
"--indexUrl", "-u"})
public String indexUrl;
@Override
public void execute() throws CommandException {
System.out.println("fetch articles from " + indexUrl);
}
}
Both implement the interface Command:
public interface Command {
default Collection<Command> commands() {
return null;
}
void execute() throws CommandException;
}
Note that subcommands support nested subcommands. Of course, it is generally not used.
main class CLI
public class CLI {
static final String CREATE_CMD = "create";
static final String FETCH_CMD = "fetch";
@Parameter(names = {
"-h", "--help" }, help = true)
private boolean help;
public void exec(String[] args) {
final JCommander.Builder builder = JCommander.newBuilder().addObject(new CLI());
final JCommander jCommander = builder.build();
ServiceLoader.load(Command.class).forEach(command -> CLI.registerCommand(jCommander, command));
JCommander leafCommander = jCommander;
try {
jCommander.parse(args);
final String rootVerb = jCommander.getParsedCommand();
final JCommander rootCommander = jCommander.getCommands().get(rootVerb);
if (rootCommander == null) {
jCommander.usage();
System.exit(1);
}
leafCommander = rootCommander;
do {
final String subVerb = leafCommander.getParsedCommand();
final JCommander subCommander = leafCommander.getCommands().get(subVerb);
if (subCommander != null)
leafCommander = subCommander;
else
break;
} while (true);
final Command command = (Command) leafCommander.getObjects().get(0);
command.execute();
} catch (final CommandException e) {
System.err.printf("%1$s: %2$s. See '%1$s --help'.%n", leafCommander.getProgramName(), e.getMessage());
System.exit(e.getStatus());
} catch (final Exception e) {
System.err.printf("%1$s: %2$s. See '%1$s --help'.%n", leafCommander.getProgramName(), e.getMessage());
System.exit(1);
}
}
private static final void registerCommand(final JCommander jCommander, final Command command) {
jCommander.addCommand(command);
final Parameters commandParameters = command.getClass().getAnnotation(Parameters.class);
if (commandParameters == null || commandParameters.commandNames().length == 0)
return;
final JCommander subCommander = jCommander.getCommands().get(commandParameters.commandNames()[0]);
final Collection<Command> subCommands = command.commands();
if (subCommands != null)
subCommands.forEach(subCommand -> CLI.registerCommand(subCommander, subCommand));
}
public static void main(final String[] args) {
CLI cli = new CLI();
cli.exec(args);
}
}
The principle is simple: ServiceLocator loads all the implementation classes of Command, and then calls the corresponding Command class according to the subcommand. The advantage of AutoService is that you don't have to create related files under META-INF/services yourself.
have a test
@Test
public void testCreateCommand() {
CLI cli = new CLI();
String[] argv = {
"create", "-u", "http://www.sina.com.cn"};
cli.exec(argv);
}
@Test
public void testFetchCommand() {
CLI cli = new CLI();
String[] argv = {
"fetch", "-u", "http://www.csdn.cn"};
cli.exec(argv);
}
reference documents
- https://gist.github.com/mkarg/9d9ca23e6da32b47c7fadaf10ae16ba6
- https://pedrorijo.com/blog/java-service-loader/