Command Definition
The @CommandDefinition annotation is used to define a command class.
Required Properties
| Property | Type | Description |
|---|---|---|
name | String | The command name |
Optional Properties
| Property | Type | Default | Description |
|---|---|---|---|
aliases | String[] | {} | Alternative names for the command |
description | String | "" | Command description shown in help |
generateHelp | boolean | false | Auto-generate --help / -h boolean flag. Bypasses required argument validation when set. |
disableParsing | boolean | false | Skip parsing (everything goes to @Arguments) |
version | String | "" | Version string (adds --version, -v option) |
validator | Class<? extends CommandValidator> | NullCommandValidator.class | Validator to run before execution |
resultHandler | Class<? extends ResultHandler> | NullResultHandler.class | Handler to run after execution |
activator | Class<? extends CommandActivator> | NullCommandActivator.class | Activator to check if command is available |
defaultValueProvider | Class<? extends DefaultValueProvider> | NullDefaultValueProvider.class | Dynamic default value resolver |
stopAtFirstPositional | boolean | false | Stop option parsing after the first positional argument |
sortOptions | boolean | false | Sort help options alphabetically by name (after explicit option order) |
helpUrl | String | "" | URL to documentation (shown in --help output) |
helpGroup | String | "" | Group heading when listed as a subcommand in parent’s help |
helpSectionProvider | Class<? extends HelpSectionProvider> | NullHelpSectionProvider.class | Provider for dynamic help sections |
Example
@CommandDefinition(
name = "copy",
aliases = {"cp"},
description = "Copy files",
generateHelp = true
)
public class CopyCommand implements Command<CommandInvocation> {
@Option(shortName = 'r', description = "Recursive copy")
private boolean recursive;
@Override
public CommandResult execute(CommandInvocation invocation) {
// implementation
return CommandResult.SUCCESS;
}
}Command Interface
Your command must implement the Command<T extends CommandInvocation> interface:
public interface Command<T extends CommandInvocation> {
CommandResult execute(T commandInvocation)
throws CommandException, InterruptedException;
}CommandResult
Return one of the following:
CommandResult.SUCCESS- Command completed successfullyCommandResult.FAILURE- Command failedCommandResult.RETURN- Return from current subcommand
Dynamic Default Value Provider
The defaultValueProvider attribute specifies a class that resolves option defaults at runtime. This is useful when defaults come from configuration files, environment variables, or other external sources not known at compile time.
Implementing a Provider
Create a class that implements DefaultValueProvider:
public class ConfigDefaultProvider implements DefaultValueProvider {
@Override
public String defaultValue(ProcessedOption option) {
// Use option.name() and option.parent().name() to build a config key
String key = option.parent().name() + "." + option.name();
return Configuration.get(key); // returns null if not configured
}
}Registering the Provider
@CommandDefinition(
name = "init",
description = "Initialize a project",
defaultValueProvider = ConfigDefaultProvider.class
)
public class InitCommand implements Command<CommandInvocation> {
@Option(defaultValue = "hello")
private String template;
@Option
private String editor;
@Override
public CommandResult execute(CommandInvocation invocation) {
invocation.println("Template: " + template);
invocation.println("Editor: " + editor);
return CommandResult.SUCCESS;
}
}Value Precedence
When determining option values, the precedence is (highest to lowest):
- User-provided value – Explicitly set on the command line
- Dynamic default – Returned by the
DefaultValueProvider(if non-null) - Static default – The
defaultValuefrom the annotation - null – If nothing else is set
If the provider returns null for an option, aesh falls back to the static defaultValue. This lets you use annotation defaults as fallbacks:
// Provider returns "from-config" for template -> uses "from-config"
// Provider returns null for editor -> falls back to static default "vi"
@Option(defaultValue = "vi")
private String editor;See Options - Dynamic Default Values for more details.
Registry-Level Provider
Instead of repeating defaultValueProvider = MyProvider.class on every command, you can set a single provider at the registry or runtime level:
// Via AeshRuntimeRunner
AeshRuntimeRunner.builder()
.command(MyApp.class)
.defaultValueProvider(new ConfigDefaultProvider())
.execute();
// Via AeshCommandRegistryBuilder
CommandRegistry registry = AeshCommandRegistryBuilder.builder()
.defaultValueProvider(new ConfigDefaultProvider())
.command(RunCommand.class)
.command(BuildCommand.class)
.create();
// Via AeshCommandRuntimeBuilder
CommandRuntime runtime = AeshCommandRuntimeBuilder.builder()
.commandRegistry(registry)
.defaultValueProvider(new ConfigDefaultProvider())
.build();The registry-level provider applies to all commands (including group command children) that don’t declare their own per-command provider. Per-command @CommandDefinition(defaultValueProvider = ...) takes precedence when set.
Stop at First Positional
When stopAtFirstPositional = true, option parsing stops as soon as the first positional argument is consumed. All remaining tokens are treated as positional arguments, even if they look like options.
This is useful for commands that pass arguments through to another process:
@CommandDefinition(
name = "run",
description = "Run a script",
stopAtFirstPositional = true,
generateHelp = true
)
public class RunCommand implements Command<CommandInvocation> {
@Option(hasValue = false, description = "Enable verbose output")
private boolean verbose;
@Arguments
private List<String> args;
@Override
public CommandResult execute(CommandInvocation invocation) {
// args contains the script name and all arguments after it
return CommandResult.SUCCESS;
}
}With the command above:
| Input | Result |
|---|---|
run --verbose myscript.java | verbose=true, args=[myscript.java] |
run --verbose myscript.java -Dfoo=bar --help | verbose=true, args=[myscript.java, -Dfoo=bar, --help] |
run myscript.java --verbose | verbose=false, args=[myscript.java, --verbose] |
run --help | Displays help output |
Note that --help (and --version) before the first positional still work normally when generateHelp = true. Only tokens after the first positional are treated as passthrough arguments.
Option Ordering in Help
Use sortOptions to control how options are ordered in help output:
sortOptions = false(default): options are ordered by explicit optionorder, then declaration ordersortOptions = true: options are ordered by explicit optionorder, then alphabetical name
@CommandDefinition(
name = "build",
description = "Build project",
sortOptions = true,
generateHelp = true
)
public class BuildCommand implements Command<CommandInvocation> {
@Option(description = "Always first", order = 10)
private boolean ci;
@Option(description = "Build profile", order = 20)
private String profile;
@Option(description = "Clean output")
private boolean clean;
@Option(description = "Verbose output")
private boolean verbose;
@Override
public CommandResult execute(CommandInvocation invocation) {
return CommandResult.SUCCESS;
}
}The order attribute is defined on @Option, @OptionList, and @OptionGroup.
Help Output Layout
When generateHelp = true, aesh produces styled help output with the following structure:
Description text
Usage: command [-hv] [--config=<config>] [--verbose | --quiet] [COMMAND]
Examples or header text (from HelpSectionProvider)
Options:
-h, --help Display this help and exit
--config=<config> Path to config file
--[no-]verbose Enable verbose output
<scriptOrFile> Script file to run
[<userParams>...] Additional parameters
command commands:
run Run a script
build Build a project
Footer text (from HelpSectionProvider)Key formatting features:
- ANSI colors: option names in yellow, value placeholders in cyan, command name in bold
- Detailed synopsis: grouped boolean flags
[-hv], value options[--config=<config>], mutually exclusive pipes[--verbose | --quiet],[COMMAND]for group commands - Synopsis wrapping: wraps at 80 columns with continuation indentation
- Option column cap: option names wider than 24 characters wrap the description to the next line
- Negatable options: rendered as
--[no-]name(single entry, not two) - Value placeholders: options that accept values show
=<name>(using option name orargumentattribute) @OptionGroupkey=value: rendered as=<key=value>to show the expected syntax- Inline positional arguments: shown after options, not in separate sections
--helpbypasses validation: required arguments are not validated when--helpis used- All ANSI styling is disabled when
ansiMode = false
CommandInvocation
Provides access to:
println(String)- Output text to the consoleprint(String)- Output text without newlinestop()- Stop the consolegetShell()- Access the shellgetHelpInfo(String)- Get help text
Help Group for Subcommands
The helpGroup property on @CommandDefinition (and @GroupCommandDefinition) controls how a subcommand appears in its parent’s help output. Subcommands with the same helpGroup value are displayed together under that heading.
Basic Usage
@GroupCommandDefinition(
name = "cli",
description = "My CLI tool",
generateHelp = true,
groupCommands = {
BuildCommand.class, TestCommand.class,
InstallCommand.class, PublishCommand.class,
InfoCommand.class, VersionCommand.class
}
)
public class CliCommand implements GroupCommand<CommandInvocation> {
@Override
public CommandResult execute(CommandInvocation invocation) {
return CommandResult.SUCCESS;
}
}
@CommandDefinition(name = "build", description = "Build the project", helpGroup = "Build")
public class BuildCommand implements Command<CommandInvocation> { /* ... */ }
@CommandDefinition(name = "test", description = "Run tests", helpGroup = "Build")
public class TestCommand implements Command<CommandInvocation> { /* ... */ }
@CommandDefinition(name = "install", description = "Install dependencies", helpGroup = "Publish")
public class InstallCommand implements Command<CommandInvocation> { /* ... */ }
@CommandDefinition(name = "publish", description = "Publish package", helpGroup = "Publish")
public class PublishCommand implements Command<CommandInvocation> { /* ... */ }
@CommandDefinition(name = "info", description = "Show project info")
public class InfoCommand implements Command<CommandInvocation> { /* ... */ }
@CommandDefinition(name = "version", description = "Show version")
public class VersionCommand implements Command<CommandInvocation> { /* ... */ }Help output:
Usage: cli [<options>]
My CLI tool
Build:
build Build the project
test Run tests
Publish:
install Install dependencies
publish Publish package
Other:
info Show project info
version Show versionHow It Works
- Subcommands with the same
helpGroupvalue are grouped under that heading - Named groups appear first, in the order their first subcommand was defined
- Subcommands without a
helpGroupappear under a default heading - If all subcommands have a
helpGroup, there is no default group
This is separate from the helpGroup property on @Option, which groups options within a single command’s help output. See Options - Help Grouping.
Help Section Provider
The helpSectionProvider property lets you dynamically add sections to a command’s help output at render time. This is useful for showing external plugins, aliases, or dynamically discovered commands without statically defining them.
Implementing a Provider
Create a class that implements HelpSectionProvider:
public class PluginHelpProvider implements HelpSectionProvider {
@Override
public Map<String, List<HelpEntry>> getAdditionalSections() {
Map<String, List<HelpEntry>> sections = new LinkedHashMap<>();
// Discover plugins at runtime
List<HelpEntry> plugins = new ArrayList<>();
plugins.add(new HelpEntry("docker", "Docker integration plugin"));
plugins.add(new HelpEntry("k8s", "Kubernetes deployment plugin"));
sections.put("Plugins", plugins);
return sections;
}
}Registering the Provider
@GroupCommandDefinition(
name = "app",
description = "My application",
generateHelp = true,
groupCommands = {RunCommand.class, BuildCommand.class},
helpSectionProvider = PluginHelpProvider.class
)
public class AppCommand implements GroupCommand<CommandInvocation> {
@Override
public CommandResult execute(CommandInvocation invocation) {
return CommandResult.SUCCESS;
}
}Help output:
Usage: app [<options>]
My application
app commands:
run Run the application
build Build the project
Plugins:
docker Docker integration plugin
k8s Kubernetes deployment pluginMerging with helpGroup
If a provider section name matches an existing helpGroup from statically defined subcommands, the entries are appended to that group rather than creating a duplicate heading.
Header and Footer
HelpSectionProvider can also supply header text (shown before the synopsis) and footer text (shown after everything). Both are default methods returning String, so existing implementations are unaffected:
public class JBangHelpProvider implements HelpSectionProvider {
@Override
public String getHeader() {
return "jbang - Unleash the power of Java\n"
+ " jbang init hello.java (initialize a script)\n"
+ " jbang hello.java [args...] (run a .java file)";
}
@Override
public String getFooter() {
return "See https://jbang.dev for more info";
}
@Override
public Map<String, List<HelpEntry>> getAdditionalSections() {
return Collections.emptyMap();
}
}Help output:
jbang - Unleash the power of Java
jbang init hello.java (initialize a script)
jbang hello.java [args...] (run a .java file)
Usage: jbang [<options>]
JBang tool
Options:
...
See https://jbang.dev for more infoBoth methods return null by default (no header/footer). The text can contain newline characters for multi-line content.
Key Points
- Zero startup cost – The provider class is stored as a reference and only instantiated when help is rendered
- Works with both
@CommandDefinitionand@GroupCommandDefinition - HelpEntry is a simple value class with
name()anddescription()(description is optional) - Return an empty map (not null) if there are no additional sections to show
- Header appears before the synopsis, footer appears after all other content