Command Mode and CLI Applications
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Not every Quarkus application is a service that stays up waiting for requests. Command mode is Quarkus' support
for the opposite shape: a process that starts, does one piece of work, and exits — a batch job, a migration
script, an admin utility, a one-shot CLI tool. This page covers the @QuarkusMain/QuarkusApplication
lifecycle that command mode runs on, building richer command-line interfaces with Picocli, packaging a CLI as a
fast-starting native executable, running one as a JBang script, and how to recognize when command mode is the
right fit for a given piece of work instead of an always-on HTTP or messaging service.
@QuarkusMain and QuarkusApplication
A command-mode application is a class that implements io.quarkus.runtime.QuarkusApplication and is marked as
the application’s entry point with io.quarkus.runtime.annotations.QuarkusMain. Its single method, run,
receives the process' command-line arguments and returns an int that becomes the process exit code:
package org.acme.cli;
import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.QuarkusApplication;
import io.quarkus.runtime.annotations.QuarkusMain;
@QuarkusMain
public class MyApp implements QuarkusApplication {
@Override
public int run(String... args) {
System.out.println("Hello from command mode, args=" + java.util.Arrays.toString(args));
// do the one-shot piece of work here, then return an exit code
return 0;
}
public static void main(String... args) {
Quarkus.run(MyApp.class, args);
}
}
The standard Java main method does not do the work itself — it hands control to Quarkus.run(MyApp.class,
args), which boots the Quarkus runtime (including the ArC/CDI container, so @Inject works normally inside
run), invokes MyApp#run, and then shuts the runtime down once run returns.
Command-mode lifecycle vs. a long-running service
The default Quarkus application shape — an HTTP service, a messaging consumer — starts the runtime and then
stays running, waiting for inbound requests or messages indefinitely; nothing about a normal request-response
cycle tells the process to exit. Command mode inverts that: once QuarkusApplication#run returns, Quarkus
proceeds straight to shutdown and the process ends. The three moments in a command-mode run are:
-
The runtime starts up — static init and runtime init replay as usual, and the CDI container is fully up before
runis invoked. -
run(String… args)executes — this is the entire useful lifetime of the application; whatever the tool is meant to do happens here (or in objects it invokes). -
The runtime shuts down and the process exits, using `run’s return value as the exit code.
There is no step 4 — nothing keeps the process alive after run returns unless run (or something it started)
explicitly asks Quarkus to wait, which is exactly what Quarkus.waitForExit() is for.
Exit codes
The int returned from run becomes the operating system’s process exit code, which is what lets a command-mode
Quarkus application integrate with anything that inspects exit status — shell scripts, CI pipelines, cron jobs,
container orchestrators treating a non-zero exit as a failed job. Returning 0 signals success; any other value
signals failure, and the caller decides what to do with it (retry, alert, fail the pipeline).
Quarkus.waitForExit()
Some command-mode applications kick off asynchronous work in run — a Vert.x event loop consuming messages, a
scheduled job, a reactive pipeline — and need the process to stay alive after run itself has finished
launching that work, rather than exiting immediately. Quarkus.waitForExit() blocks the calling thread until
something else (a signal, a call to Quarkus.asyncExit(), or an unhandled failure) actually requests shutdown:
@QuarkusMain
public class MyLongRunningCliApp implements QuarkusApplication {
@Override
public int run(String... args) {
// start a scheduler, a Vert.x verticle, a message consumer, etc.
startBackgroundWork();
// keep the process alive until something calls Quarkus.asyncExit()
// or the process receives a shutdown signal
Quarkus.waitForExit();
return 0;
}
}
This is the mechanism that lets a command-mode application behave like a long-running daemon when that is what
the tool actually needs, without abandoning the @QuarkusMain/QuarkusApplication model that gives it a clean
exit code on the way out.
Building CLIs with Picocli
For anything beyond a couple of positional arguments, the quarkus-picocli extension wires the Picocli
command-line parsing library (see the Quarkus Picocli guide) into command
mode, giving CLI applications subcommands, typed options, validation, and generated --help/--version output
for free. Add it with:
./mvnw quarkus:add-extension -Dextensions=picocli
A Picocli command is a plain class annotated with @CommandLine.Command, declaring its options and parameters as
annotated fields, and implementing either Runnable or Callable<Integer> (the latter lets the command return
its own exit code):
package org.acme.cli;
import java.util.concurrent.Callable;
import picocli.CommandLine;
@CommandLine.Command(
name = "greet",
mixinStandardHelpOptions = true,
version = "greet 1.0",
description = "Greets the given name, optionally shouting it.")
public class GreetCommand implements Callable<Integer> {
@CommandLine.Option(names = {"-u", "--uppercase"}, description = "Shout the greeting")
boolean uppercase;
@CommandLine.Parameters(index = "0", description = "The name to greet")
String name;
@Override
public Integer call() {
String greeting = "Hello, " + name + "!";
System.out.println(uppercase ? greeting.toUpperCase() : greeting);
return 0;
}
}
quarkus-picocli supplies its own @QuarkusMain-annotated entry point that drives Picocli’s parser, so the
application only needs to tell it which annotated class is the top-level command:
quarkus.picocli.top-command=org.acme.cli.GreetCommand
With that in place, java -jar target/quarkus-app/quarkus-run.jar World --uppercase parses --uppercase and the
World parameter through Picocli and invokes GreetCommand#call, with the returned integer flowing through as
the process exit code exactly as it would for a plain QuarkusApplication#run. Picocli subcommands are
registered the same way Picocli itself supports — via @CommandLine.Command(subcommands = …) on the top-level
command — and CDI injection into a command works normally since it runs inside a fully-started ArC container.
Interactive shells with Aesh
Picocli covers one-shot commands: parse arguments, run, exit. When a tool instead needs an interactive,
readline-style shell — a prompt that stays open, accepts commands one at a time, supports history and
tab-completion — Quarkus' console support (quarkus-console, built on the Aesh library) is the piece to reach
for instead. Aesh is Quarkus' interactive-console/readline layer, and it also powers the dev-mode terminal (the
one that reacts to keystrokes like r to resume or q to quit while quarkus:dev is running); building a
custom interactive shell on top of it is a more involved setup than a Picocli command and is out of scope for the
minimal example above, but worth knowing about when a one-shot command is not the right interaction model.
Native executables for CLI tools
Startup time matters more for a CLI tool than almost any other Quarkus application shape: an HTTP service pays
its startup cost once and then serves requests for a long time, but a command-mode tool pays that same cost on
every single invocation — a migration script run from CI, a greet command invoked from a shell loop, an
admin task run dozens of times a day all restart the JVM (or the native binary) from scratch each time. This is
exactly the case native-image compilation is built for: a native executable’s near-instant startup turns a CLI
tool that takes a perceptible fraction of a second to boot on the JVM into one that returns control to the shell
essentially immediately. See Native Executables with GraalVM/Mandrel for how to build and package one.
Scripting with JBang
For quick, dependency-free scripting, Quarkus' JBang integration lets a single
.java file declare its own Quarkus dependencies via //DEPS directives and run directly with jbang, with no
pom.xml, no separate build step, and no manually managed classpath:
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS io.quarkus.platform:quarkus-bom:3.39.0@pom
//DEPS io.quarkus:quarkus-picocli
import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.QuarkusApplication;
import io.quarkus.runtime.annotations.QuarkusMain;
@QuarkusMain
public class greet implements QuarkusApplication {
@Override
public int run(String... args) {
System.out.println("Hello, " + (args.length > 0 ? args[0] : "world") + "!");
return 0;
}
public static void main(String... args) {
Quarkus.run(greet.class, args);
}
}
Running jbang greet.java World downloads the declared Quarkus artifacts (cached after the first run), boots
the same command-mode lifecycle described above, and exits — useful for throwaway tooling and quick
reproductions that do not warrant a full Maven/Gradle project.
When command mode is the right shape
Command mode fits work that has a natural beginning and end, rather than work that is naturally always-on:
-
Batch jobs — process a fixed input (a file, a table, a queue drained once), then stop.
-
Database migrations — run a fixed set of schema changes against a target database and exit with a status code CI or a deployment pipeline can check.
-
Admin and maintenance tools — one-off operational tasks (reindexing, cache warm-up, data backfills, ad hoc diagnostics) that an operator or a scheduler invokes on demand.
The contrast is with HTTP endpoints and messaging consumers, which are built to stay up indefinitely, serving an
unbounded stream of requests or messages — there, Quarkus.waitForExit()-style blocking is not an exception,
it is simply what the default application shape already does. Reach for command mode when the unit of work is a
single invocation with a clear success/failure outcome; reach for the default service shape when the unit of
work is "keep responding to whatever arrives next."