Completers
Completers provide tab-completion suggestions for options and arguments.
OptionCompleter Interface
public interface OptionCompleter<T extends CompleterInvocation> {
void complete(T completerInvocation);
}Built-in Completers
Æsh includes built-in completers:
BooleanOptionCompleter- Completes boolean valuesDefaultValueOptionCompleter- Uses defined default valuesFileOptionCompleter- Completes file paths
Custom Color Completer
public class ColorCompleter implements OptionCompleter<CompleterInvocation> {
private static final List<String> COLORS = Arrays.asList(
"red", "green", "blue", "yellow", "orange", "purple",
"cyan", "magenta", "white", "black", "gray", "pink"
);
@Override
public void complete(CompleterInvocation invocation) {
String input = invocation.getGivenCompleteValue();
if (input == null || input.isEmpty()) {
// No input yet, show all colors
invocation.addAllCompleterValues(COLORS);
} else {
// Filter colors that start with the input
String lowerInput = input.toLowerCase();
for (String color : COLORS) {
if (color.startsWith(lowerInput)) {
invocation.addCompleterValue(color);
}
}
}
}
}Usage:
@CommandDefinition(name = "theme", description = "Set application theme")
public class ThemeCommand implements Command<CommandInvocation> {
@Option(
shortName = 'b',
name = "background",
completer = ColorCompleter.class,
description = "Background color"
)
private String backgroundColor;
@Option(
shortName = 'f',
name = "foreground",
completer = ColorCompleter.class,
description = "Foreground/text color"
)
private String foregroundColor;
@Override
public CommandResult execute(CommandInvocation invocation) {
invocation.println("Setting theme: " + foregroundColor + " on " + backgroundColor);
return CommandResult.SUCCESS;
}
}When users press Tab:
[myapp]$ theme --background <TAB>
red green blue yellow orange purple
cyan magenta white black gray pink
[myapp]$ theme --background gr<TAB>
gray green
[myapp]$ theme --background green --foreground wh<TAB>
whiteCompleterInvocation
Provides access to:
getGivenCompleteValue()- Current partial inputgetAllGivenValues()- All values for this optiongetCommand()- The command instancegetAeshContext()- Access to runtime contextaddCompleterValue(String)- Add a completion suggestion
Dynamic Completion Based on Other Options
public class DatabaseTableCompleter implements OptionCompleter<CompleterInvocation> {
@Override
public void complete(CompleterInvocation invocation) {
// Access the command to get other option values
MyCommand cmd = (MyCommand) invocation.getCommand();
if (cmd.database != null) {
// Complete tables based on selected database
invocation.addAllCompleterValues(
getTablesForDatabase(cmd.database)
);
}
}
}
@CommandDefinition(name = "query")
public class MyCommand implements Command<CommandInvocation> {
@Option(name = "database", description = "Database name")
private String database;
@Option(
name = "table",
completer = DatabaseTableCompleter.class,
description = "Table name"
)
private String table;
@Override
public CommandResult execute(CommandInvocation invocation) {
return CommandResult.SUCCESS;
}
}Completer for Arguments
Works with @Arguments as well:
@Arguments(completer = CommandNameCompleter.class)
private List<String> commandNames;Shell Completion Script Generation
Aesh can generate completion scripts for bash, zsh, fish, and PowerShell (pwsh) shells. Everything works automatically with the standard AeshRuntimeRunner pattern — no manual flag handling required.
Quick Start
With the standard runner setup, your CLI tool automatically supports shell completion:
public class MyApp {
public static void main(String[] args) {
AeshRuntimeRunner.builder()
.command(MyCommand.class)
.args(args)
.execute();
}
}That’s it. Your users can now generate and install completion scripts:
# Generate completion script (auto-detects shell from $SHELL)
$ myapp --aesh-completion
# Generate for a specific shell
$ myapp --aesh-completion bash
$ myapp --aesh-completion zsh
$ myapp --aesh-completion fish
$ myapp --aesh-completion pwsh
# Auto-detect shell, generate, and install to the standard directory
$ myapp --aesh-completion-installBuilt-in Flags
| Flag | Purpose |
|---|---|
--aesh-completion [bash|zsh|fish|pwsh] | Generate completion script to stdout (dynamic by default) |
--aesh-completion --static [bash|zsh|fish|pwsh] | Generate a static completion script (no JVM at tab-time) |
--aesh-completion-install | Auto-detect shell, generate script, install with confirmation |
--aesh-complete -- <args> | Runtime callback used by the generated shell scripts |
All flags are intercepted by AeshRuntimeRunner.execute() before your command runs, so they never reach your command’s execute() method.
Static vs Dynamic
By default, --aesh-completion generates dynamic scripts. Use --static for static scripts:
| Static | Dynamic (default) | |
|---|---|---|
Custom OptionCompleter support | No | Yes |
| JVM required at tab-time | No | Yes |
| Startup cost per Tab press | None | ~10ms (native) / ~200ms (JVM) |
| Best for | Option names, default values | Runtime-dependent completions |
Supported Shells
| Shell | Generator | Script format |
|---|---|---|
| Bash | BashCompletionGenerator | complete -F functions with compgen -W |
| Zsh | ZshCompletionGenerator | Native compdef/_arguments format |
| Fish | FishCompletionGenerator | complete -c commands with conditions |
| PowerShell | PowerShellCompletionGenerator | Register-ArgumentCompleter -Native with CompletionResult |
Installing Completions
The easiest way is --aesh-completion-install, which auto-detects your shell, generates the script, and installs it with confirmation:
$ myapp --aesh-completion-install
Write completion script to: /home/user/.bash_completion.d/myapp
Proceed? [y/N] y
Completion script installed to /home/user/.bash_completion.d/myapp
Note: 'myapp' must be on your $PATH for completions to work.
Restart your shell or source the file to activate completions.Or install manually by redirecting the output:
# Bash
$ myapp --aesh-completion bash > ~/.local/share/bash-completion/completions/myapp
# Zsh
$ myapp --aesh-completion zsh > ~/.zsh/completions/_myapp
# Fish
$ myapp --aesh-completion fish > ~/.config/fish/completions/myapp.fish
# PowerShell
$ myapp --aesh-completion pwsh > myapp_complete.ps1
# Then add to your $PROFILE:
# . /path/to/myapp_complete.ps1PowerShell Setup
PowerShell completions use Register-ArgumentCompleter -Native, which works with PowerShell 5.1+ (Windows PowerShell) and PowerShell 7+ (cross-platform).
To set up completions permanently, add the generated script to your PowerShell profile:
# Generate the completion script
myapp --aesh-completion pwsh > "$HOME/.config/powershell/completions/myapp_complete.ps1"
# Add to your $PROFILE (run once)
Add-Content -Path $PROFILE -Value '. "$HOME/.config/powershell/completions/myapp_complete.ps1"'
# Reload profile
. $PROFILEPowerShell completions include tooltips — descriptions from your @Option(description = ...) annotations are shown when the user browses completion candidates with Ctrl+Space.
--aesh-completion-install auto-detects the shell and writes the file, PowerShell install writes the script file but requires you to source it from your $PROFILE manually. This is because modifying $PROFILE automatically could surprise users.How Dynamic Completion Works
- You generate a completion script and install it in your shell (via
--aesh-completion-installor manual redirect) - When the user presses Tab, the shell script calls
myapp --aesh-complete -- <partial-args> - Aesh’s completion engine runs (including custom
OptionCompleterimplementations) and prints candidates to stdout - The shell presents the candidates as completion suggestions
What Gets Generated
The generators introspect the command model and produce:
- Option names (long and short forms)
- Option aliases (e.g.,
--eafor--enableassertions) - Negatable options (e.g.,
--no-verbosefor--verbose) - Default value completions (offered when completing option values)
- File path completion for
File,Path, andResource-typed options - Subcommand names for group commands
- Positional argument completion (file completion for
@Argument/@Argumentswith file types)
Commands and options gated by a CommandActivator/OptionActivator (where isActivated() returns false) and options marked OptionVisibility.HIDDEN are excluded from both static and dynamic completions (since 3.18). Before 3.18, the static generators ignored these gates — only dynamic --aesh-complete respected them.
Completion Fallback Control
By default, when no completion candidates are available for an argument, the shell falls back to file/path completion for String, File, and Path types, and no fallback for enum types. You can override this per-argument using completeFallback:
@Argument(description = "Script file")
String script; // DEFAULT: auto-detected as FILES (String type)
@Argument(description = "Catalog name", completeFallback = CompletionFallback.NONE)
String catalog; // No file fallback -- only completer candidates
@Option(name = "workspace", completeFallback = CompletionFallback.DIRECTORIES)
String workspace; // Only directories, no regular files| Value | Behavior |
|---|---|
DEFAULT | Auto-detect: FILES for String/File/Path, NONE for enums |
FILES | Offer file and directory path completion |
DIRECTORIES | Offer only directory completion (no regular files) |
NONE | No fallback – only show candidates from completers |
This is especially useful for commands where some arguments expect file paths and others expect non-file values like identifiers, versions, or names:
@CommandDefinition(name = "jdk", description = "Manage JDKs",
groupCommands = { JdkInstallCommand.class })
public class JdkCommand implements Command<CommandInvocation> { }
@CommandDefinition(name = "install", description = "Install a JDK version")
public class JdkInstallCommand implements Command<CommandInvocation> {
@Argument(description = "JDK version", completeFallback = CompletionFallback.NONE)
String version; // "17", "21" -- not a file path
}Command-Level Default
For commands where most options are not file paths, setting completeFallback = NONE on every String option is tedious. Instead, set it once at the command level on @CommandDefinition:
@CommandDefinition(name = "deploy", description = "Deploy app",
completeFallback = CompletionFallback.NONE)
public class DeployCommand implements Command<CommandInvocation> {
@Option String environment; // NONE (from command-level default)
@Option String region; // NONE (from command-level default)
@Option String version; // NONE (from command-level default)
@Argument(completeFallback = CompletionFallback.FILES)
String artifact; // FILES (explicit option-level override)
}The resolution order is:
- Option-level
completeFallback(most specific) – always wins - Command-level
completeFallbackon@CommandDefinition– used when option isDEFAULT - Type-based heuristic – used when both are
DEFAULT(String/File/Path -> FILES, Enum -> NONE)
The command-level default applies to @Option, @Argument, and @Arguments fields. Options that explicitly set their own completeFallback are never affected by the command-level value.
GraalVM Native Images
Dynamic callback completion is ideal for GraalVM native images. Since native binaries start in ~10ms, tab completion feels instant:
# Build native image
$ mvn package -Pnative
# Install completion — the native binary handles --aesh-complete directly
$ ./target/myapp --aesh-completion-installFor JVM-based tools where users run via java -jar, use a wrapper script and set completionProgramName():
AeshRuntimeRunner.builder()
.command(MyCommand.class)
.completionProgramName("myapp") // matches the wrapper script name
.args(args)
.execute();Programmatic API
For advanced use cases, you can generate scripts programmatically:
import org.aesh.util.completer.ShellCompletionGenerator;
import org.aesh.util.completer.ShellCompletionGenerator.ShellType;
// Static scripts
String bashScript = ShellCompletionGenerator.generate(
ShellType.BASH, MyCommand.class, "myapp");
// Dynamic scripts
String fishScript = ShellCompletionGenerator.generateDynamic(
ShellType.FISH, MyCommand.class, "myapp");
// Using the strategy interface for more control
ShellCompletionGenerator generator = ShellCompletionGenerator.forShell(ShellType.ZSH);
String script = generator.generate(parser, "myapp");Filtering Generated Completions
By default, static generation applies the same gates as dynamic completion: deactivated commands/options and HIDDEN options are excluded. For rules beyond activators — e.g. a platform-specific command group that is unregistered on some builds — pass a CompletionFilter (since 3.18):
import org.aesh.util.completer.CompletionFilter;
CompletionFilter noVm = new CompletionFilter() {
public boolean includeCommand(ProcessedCommand<?, ?> cmd) {
return !"vm".equals(cmd.name());
}
public boolean includeOption(ProcessedCommand<?, ?> cmd, ProcessedOption opt) {
return true;
}
};
// One-shot generation with a filter
String bashScript = ShellCompletionGenerator.generate(
ShellType.BASH, TopCommand.class, "isx",
CompletionFilter.defaults().and(noVm));
// Or via the strategy interface
ShellCompletionGenerator generator = ShellCompletionGenerator.forShell(ShellType.ZSH);
String script = generator.generate(parser, "myapp", CompletionFilter.defaults().and(noVm));CompletionFilter.defaults() is the built-in activator + HIDDEN check, allowAll() includes everything, and and() combines filters. For the common case of platform-conditional commands, prefer a CommandActivator over a custom filter — the command is then gated in help, parsing, and completions with no extra wiring.
Shell Detection
Use AeshRuntimeRunner.detectShell() to detect the current shell from environment variables. This is the same logic used by --aesh-completion and --aesh-completion-install for auto-detection.
import org.aesh.AeshRuntimeRunner;
import org.aesh.util.completer.ShellCompletionGenerator.ShellType;
ShellType shell = AeshRuntimeRunner.detectShell();
if (shell != null) {
System.out.println("Current shell: " + shell);
} else {
System.out.println("Could not detect shell");
}Detection order:
PSModulePath— PowerShell (checked first to avoid misdetection from bash wrapper scripts)FISH_VERSION— FishZSH_VERSION— ZshBASH_VERSION— Bash$SHELLfallback — checks the path for fish/zsh/bash/pwsh