How to build a Java command-line application with Picocli
Command-line applications remain useful in Australian workplaces, from Brisbane consultancies automating reports to Melbourne developers managing local test environments. A well-designed CLI can be faster to operate than a graphical interface and easier to deploy on a server, container, or scheduled job.
Picocli is a lightweight Java framework for creating command-line interfaces with typed options, subcommands, validation, help output, and shell completion. It reduces repetitive argument-parsing code while keeping the application structure familiar to Java developers.
This tutorial builds a small task manager called tasker. The example covers project setup, command definitions, input validation, testing, and packaging a runnable JAR. The same approach works for database utilities, API clients, deployment tools, and internal business software.
Set up the Java project
Create a Maven project using Java 17 or a later long-term-support release. Java 17 is a practical choice for many Australian organisations because it is widely supported by enterprise tooling and cloud platforms.
Add Picocli to pom.xml:
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.6</version>
</dependency>
A simple package structure keeps responsibilities clear as the CLI grows:
Application.javastarts the commandTaskCommand.javadefines user-facing optionsTaskService.javacontains business logicTaskRepository.javahandles storageTask.javarepresents a task
Keeping parsing separate from business logic makes it easier to replace in-memory storage with JDBC, JPA, or a REST API later.
Define the root command
Create a command class with Picocli annotations. The @Command annotation supplies the application name, description, and version information.
@Command(
name = "tasker",
description = "Manage tasks from the command line",
version = "tasker 1.0",
mixinStandardHelpOptions = true
)
public class TaskCommand implements Runnable {
@Option(
names = {"-t", "--title"},
description = "Task title"
)
private String title;
@Override
public void run() {
if (title == null || title.isBlank()) {
System.out.println("Please provide a task title.");
return;
}
System.out.println("Created task: " + title);
}
}
The standard help mixin automatically adds --help and --version. Users can now run:
java -jar tasker.jar --title "Prepare client report"
For a Sydney team that frequently works from terminals or CI pipelines, predictable options are more useful than hidden application behaviour. Keep option names short, descriptive, and consistent across commands.
Add subcommands and typed options
Real command-line tools usually need several operations. Picocli supports subcommands such as add, list, and remove, allowing each action to have its own options.
@Command(
name = "tasker",
subcommands = {AddCommand.class, ListCommand.class},
mixinStandardHelpOptions = true
)
public class Application implements Runnable {
public static void main(String[] args) {
int exitCode = new CommandLine(new Application()).execute(args);
System.exit(exitCode);
}
@Override
public void run() {
new CommandLine(this).usage(System.out);
}
}
An AddCommand can use typed fields and required options:
@Command(name = "add", description = "Create a task")
public class AddCommand implements Callable<Integer> {
@Option(names = {"-t", "--title"}, required = true)
private String title;
@Option(names = {"-p", "--priority"}, defaultValue = "normal")
private String priority;
@Override
public Integer call() {
System.out.printf("Added '%s' with %s priority%n", title, priority);
return 0;
}
}
Picocli can convert values directly into int, boolean, Path, LocalDate, enums, and custom types. This prevents every command from manually converting strings and reporting parsing errors.
Compare parsing approaches
Picocli is especially valuable when a CLI needs nested commands, validation, or professional help output. For a tiny one-off utility, Java’s built-in argument handling may be sufficient.
| Approach | Best suited to | Strengths | Limitations |
|---|---|---|---|
Manual args parsing |
Very small utilities | No dependency, complete control | Repetitive and error-prone |
| Apache Commons CLI | Basic option-based tools | Familiar option model | Less expressive for command trees |
| Picocli | Production-grade Java CLIs | Subcommands, types, help, completion | Adds a small external dependency |
| Spring Shell | Interactive Spring applications | Rich prompts and Spring integration | Heavier for simple batch commands |
For an application that may later connect to Spring Boot, an authentication service, or a payment gateway, Picocli offers a clean boundary between terminal input and application services. It also suits Australian small businesses that need a lightweight internal tool without operating a full web interface.
Validate input and handle failures
Validation should happen close to the command definition. A custom converter can reject invalid values before the business service runs.
@Option(
names = "--priority",
defaultValue = "normal",
converter = PriorityConverter.class
)
private Priority priority;
Use meaningful exit codes so shell scripts and CI jobs can detect failures. Return 0 for success and a non-zero value for invalid input, missing files, or service errors. Send diagnostic messages to standard error when appropriate.
Security needs the same care as validation. Never print passwords, API tokens, or private customer data in terminal logs. If a tool processes device or account information, review installation security mistakes and require clear authorisation, consent, and secure storage.
Test commands without a terminal
Picocli commands are straightforward to test with CommandLine, an in-memory output stream, and representative arguments.
@Test
void addsTask() {
CommandLine commandLine = new CommandLine(new AddCommand());
int exitCode = commandLine.execute(
"add", "--title", "Book Brisbane venue"
);
assertEquals(0, exitCode);
}
Test both successful and invalid input. Include cases for missing required options, unknown flags, malformed dates, and service exceptions. This is particularly useful when a CLI is deployed across different developer machines, from Windows laptops in Perth to Linux build agents in Canberra.
Package and run the application
Use the Maven Shade Plugin or another packaging plugin to create an executable JAR containing dependencies. A typical command is:
mvn clean package
java -jar target/tasker.jar add --title "Send invoice"
For a smoother user experience, add shell completion, stable help text, and examples to the command description. Respect local conventions such as dd/MM/yyyy when displaying dates, while storing dates in an unambiguous format such as ISO-8601. If the application handles billing, account for Australian dollars and GST in the service layer rather than embedding those rules in argument parsing.
Prepare a maintainable CLI
A Picocli application should remain a thin delivery layer. Commands collect input, invoke services, and translate results into readable output; they should not contain database queries, HTTP details, or complex business decisions.
Before releasing the tool, check the following areas:
- Every command has useful help and examples
- Invalid input produces a clear error and non-zero exit code
- Secrets are read from secure configuration rather than source code
- Output is suitable for both people and shell scripts
- Unit tests cover normal, missing, and malformed arguments
With this structure, tasker can evolve from a small terminal utility into a dependable Java automation tool. Picocli handles the command-line concerns while Core Java, Spring, JDBC, JPA, or external APIs remain available behind a clean application boundary.