Skip to content

Commit ecab5fe

Browse files
committed
migration guide and updates
1 parent de6c26f commit ecab5fe

3 files changed

Lines changed: 277 additions & 1 deletion

File tree

content/_index.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ AeshRuntimeRunner.builder()
6767

6868
**Command Definition**
6969
- Annotation-based (`@CommandDefinition`, `@Option`, `@Argument`) or builder API
70+
- Optional [compile-time annotation processor](/docs/aesh/annotation-processor) eliminates runtime reflection
7071
- Automatic parsing and type conversion (String, Integer, Boolean, File, Enum, custom types)
7172
- Support for short (`-v`) and long (`--verbose`) option names
7273
- Required and optional parameters with default values
@@ -77,9 +78,11 @@ AeshRuntimeRunner.builder()
7778
- Dynamic command registration and removal at runtime
7879

7980
**User Experience**
80-
- Automatic help generation (`--help`)
81+
- Automatic help generation (`--help`) with [visibility levels](/docs/aesh/options#visibility-levels)
8182
- Tab completion for commands, options, and values
83+
- Shell completion script generation for bash, zsh, and fish
8284
- Built-in completers for files, booleans, enums
85+
- [Ghost text suggestions](/docs/aesh/ghost-text-suggestions) as you type
8386
- Command history and line editing (via Readline)
8487
- Clear error messages and validation feedback
8588

@@ -95,6 +98,15 @@ AeshRuntimeRunner.builder()
9598
- **Runtime mode** - Single command execution from command-line arguments
9699
- **Programmatic** - Direct command invocation from Java code
97100

101+
**Performance & Native Images**
102+
- [Annotation processor](/docs/aesh/annotation-processor) shifts all reflection to compile time -- **3-4x faster** startup than the reflection path
103+
- **67-655x faster** command registration than picocli in benchmarks
104+
- **8-21x faster** command-line parsing than picocli
105+
- GraalVM native-image friendly -- generated code uses direct `new` calls, no reflection configuration needed
106+
- Zero external dependencies
107+
108+
Coming from picocli? See the [Migration Guide](/docs/aesh/migrating-from-picocli).
109+
98110
### Use Cases
99111

100112
- CLI tools and utilities

content/docs/aesh/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ Ready to build your CLI application?
195195
3. **[Command Definition](command-definition)** - Learn about command structure
196196
4. **[Extensions Library](extensions)** - Use ready-made commands (ls, cd, cat, etc.)
197197
5. **[Examples](examples)** - Explore complete working examples
198+
6. **[Migrating from Picocli](migrating-from-picocli)** - Annotation mapping and migration guide
198199

199200
## Æsh vs Æsh Readline
200201

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
---
2+
date: '2026-04-29T12:00:00+02:00'
3+
draft: false
4+
title: 'Migrating from Picocli'
5+
weight: 19
6+
---
7+
8+
This guide helps you migrate CLI applications from [picocli](https://picocli.info/) to Aesh. The two frameworks use similar annotation-based models, so most migrations are straightforward renaming.
9+
10+
## Why Migrate?
11+
12+
**Startup performance.** Aesh registers commands 67-655x faster than picocli in benchmarks, depending on command complexity. With the optional [annotation processor](../annotation-processor), Aesh eliminates all runtime reflection and is a further 3-4x faster than its own reflection path.
13+
14+
| Benchmark (100 commands) | Aesh (generated) | Picocli (reflection) | Speedup |
15+
|---|---|---|---|
16+
| Flat commands (4 options each) | 40 us | 2,646 us | **67x** |
17+
| Group commands (parent + 2 children) | 26 us | 7,242 us | **282x** |
18+
| Nested groups (3-level hierarchy) | 13 us | 8,776 us | **655x** |
19+
20+
Aesh also parses command lines 8-21x faster than picocli once commands are registered.
21+
22+
**GraalVM native-image.** The annotation processor generates direct `new` calls and cached field accessors, reducing the need for reflection configuration in native images.
23+
24+
**Smaller footprint.** Aesh has no transitive dependencies beyond aesh-readline. Picocli is a single jar but is significantly larger.
25+
26+
## Annotation Mapping
27+
28+
### Commands
29+
30+
| Picocli | Aesh |
31+
|---------|------|
32+
| `@Command(name = "deploy", description = "...")` | `@CommandDefinition(name = "deploy", description = "...")` |
33+
| `@Command(subcommands = {Sub.class})` | `@GroupCommandDefinition(name = "grp", groupCommands = {Sub.class})` |
34+
| `@Command(mixinStandardHelpOptions = true)` | `@CommandDefinition(generateHelp = true)` |
35+
| `@Command(version = "1.0")` | `@CommandDefinition(version = "1.0")` |
36+
| `implements Runnable` or `Callable<Integer>` | `implements Command<CommandInvocation>` |
37+
38+
In picocli, `@Command` handles both simple and group commands. In Aesh, group commands use the separate `@GroupCommandDefinition` annotation with a `groupCommands` attribute listing subcommand classes.
39+
40+
### Options
41+
42+
| Picocli | Aesh |
43+
|---------|------|
44+
| `@Option(names = {"-v", "--verbose"})` | `@Option(shortName = 'v', name = "verbose")` |
45+
| `@Option(names = "-v", arity = "0")` | `@Option(shortName = 'v', hasValue = false)` |
46+
| `@Option(names = "--count", defaultValue = "10")` | `@Option(name = "count", defaultValue = "10")` |
47+
| `@Option(names = "--out", required = true)` | `@Option(name = "out", required = true)` |
48+
| `@Option(names = "--items", split = ",") List<String>` | `@OptionList(name = "items", valueSeparator = ',')` |
49+
| `@Option(names = "-D") Map<String,String>` | `@OptionGroup(shortName = 'D')` |
50+
| `@Option mapFallbackValue = ""` | `@OptionGroup(defaultValue = "")` |
51+
52+
Key differences:
53+
- Aesh `shortName` is a `char`, not a string. The long name defaults to the field name or is set via `name`.
54+
- Boolean flags require `hasValue = false` in Aesh (picocli infers this from the field type or `arity = "0"`).
55+
- List options use the dedicated `@OptionList` annotation instead of putting `split` on `@Option`.
56+
- Map/property options use `@OptionGroup` instead of type inference on `Map` fields.
57+
58+
### Arguments
59+
60+
| Picocli | Aesh |
61+
|---------|------|
62+
| `@Parameters(index = "0")` | `@Argument` |
63+
| `@Parameters List<String>` | `@Arguments` |
64+
| `@Parameters(description = "...")` | `@Argument(description = "...")` |
65+
66+
Aesh uses `@Argument` for a single positional argument and `@Arguments` for multiple. There is no `index` attribute -- field declaration order determines position.
67+
68+
### Other Annotations
69+
70+
| Picocli | Aesh |
71+
|---------|------|
72+
| `@Mixin` | `@Mixin` |
73+
| `@ParentCommand` | `@ParentCommand` |
74+
75+
These work the same way in both frameworks.
76+
77+
### Execution
78+
79+
| Picocli | Aesh |
80+
|---------|------|
81+
| `new CommandLine(cmd).execute(args)` | `AeshRuntimeRunner.builder().command(Cmd.class).args(args).execute()` |
82+
| `new CommandLine(cmd)` + interactive loop | `AeshConsoleRunner.builder().command(Cmd.class).prompt("$ ").start()` |
83+
84+
## Before and After
85+
86+
### Picocli
87+
88+
```java
89+
import picocli.CommandLine;
90+
import picocli.CommandLine.Command;
91+
import picocli.CommandLine.Option;
92+
import picocli.CommandLine.Parameters;
93+
94+
@Command(name = "deploy", description = "Deploy an application",
95+
mixinStandardHelpOptions = true, version = "1.0")
96+
public class DeployCommand implements Callable<Integer> {
97+
98+
@Option(names = {"-e", "--environment"}, defaultValue = "production",
99+
description = "Target environment")
100+
private String environment;
101+
102+
@Option(names = {"-f", "--force"}, description = "Force deployment")
103+
private boolean force;
104+
105+
@Option(names = {"-t", "--tags"}, split = ",",
106+
description = "Deployment tags")
107+
private List<String> tags;
108+
109+
@Option(names = "-D", description = "Properties")
110+
private Map<String, String> properties;
111+
112+
@Parameters(index = "0", description = "Application name")
113+
private String application;
114+
115+
@Override
116+
public Integer call() {
117+
System.out.println("Deploying " + application + " to " + environment);
118+
return 0;
119+
}
120+
121+
public static void main(String[] args) {
122+
int exitCode = new CommandLine(new DeployCommand()).execute(args);
123+
System.exit(exitCode);
124+
}
125+
}
126+
```
127+
128+
### Aesh
129+
130+
```java
131+
import org.aesh.command.*;
132+
import org.aesh.command.invocation.CommandInvocation;
133+
import org.aesh.command.option.*;
134+
import org.aesh.AeshRuntimeRunner;
135+
136+
import java.util.List;
137+
import java.util.Map;
138+
139+
@CommandDefinition(name = "deploy", description = "Deploy an application",
140+
generateHelp = true, version = "1.0")
141+
public class DeployCommand implements Command<CommandInvocation> {
142+
143+
@Option(shortName = 'e', name = "environment", defaultValue = "production",
144+
description = "Target environment")
145+
private String environment;
146+
147+
@Option(shortName = 'f', name = "force", hasValue = false,
148+
description = "Force deployment")
149+
private boolean force;
150+
151+
@OptionList(shortName = 't', name = "tags", valueSeparator = ',',
152+
description = "Deployment tags")
153+
private List<String> tags;
154+
155+
@OptionGroup(shortName = 'D', description = "Properties")
156+
private Map<String, String> properties;
157+
158+
@Argument(description = "Application name", required = true)
159+
private String application;
160+
161+
@Override
162+
public CommandResult execute(CommandInvocation invocation) {
163+
invocation.println("Deploying " + application + " to " + environment);
164+
return CommandResult.SUCCESS;
165+
}
166+
167+
public static void main(String[] args) {
168+
AeshRuntimeRunner.builder()
169+
.command(DeployCommand.class)
170+
.args(args)
171+
.execute();
172+
}
173+
}
174+
```
175+
176+
### What Changed
177+
178+
1. `@Command` became `@CommandDefinition` with `generateHelp = true`
179+
2. `implements Callable<Integer>` became `implements Command<CommandInvocation>`
180+
3. `@Option(names = {"-f", "--force"})` became `@Option(shortName = 'f', name = "force", hasValue = false)` -- boolean flags need `hasValue = false`
181+
4. `@Option(split = ",") List<String>` became `@OptionList(valueSeparator = ',')`
182+
5. `Map<String, String>` with `@Option` became `@OptionGroup`
183+
6. `@Parameters` became `@Argument`
184+
7. `System.out.println` became `invocation.println`
185+
8. Return `CommandResult.SUCCESS` instead of an exit code integer
186+
9. `CommandLine.execute(args)` became `AeshRuntimeRunner.builder()...execute()`
187+
188+
## Key Differences
189+
190+
### Output
191+
192+
Picocli commands use `System.out` directly. Aesh commands use `CommandInvocation.println()`, which routes output through the terminal connection. This enables the same command to work with local terminals, SSH, telnet, and WebSocket connections.
193+
194+
### Exit Codes
195+
196+
Picocli commands return an `int` exit code from `Callable.call()`. Aesh commands return `CommandResult.SUCCESS`, `CommandResult.FAILURE`, or `CommandResult.valueOf(code)`. Parse errors automatically return exit code 2 (POSIX convention).
197+
198+
### Group Commands
199+
200+
Picocli uses `subcommands = {...}` on `@Command`. Aesh uses a separate `@GroupCommandDefinition` annotation:
201+
202+
```java
203+
// Picocli
204+
@Command(name = "remote", subcommands = {AddCommand.class, RemoveCommand.class})
205+
public class RemoteCommand implements Runnable { ... }
206+
207+
// Aesh
208+
@GroupCommandDefinition(name = "remote",
209+
groupCommands = {AddCommand.class, RemoveCommand.class})
210+
public class RemoteCommand implements Command<CommandInvocation> { ... }
211+
```
212+
213+
### Inherited Options
214+
215+
Picocli supports inherited options via `@Option(scope = INHERIT)`. Aesh uses `@Option(inherited = true)` on the parent command. Inherited options can appear before or after the subcommand name on the command line:
216+
217+
```java
218+
@GroupCommandDefinition(name = "app", groupCommands = {RunCommand.class})
219+
public class AppCommand implements Command<CommandInvocation> {
220+
@Option(name = "verbose", hasValue = false, inherited = true)
221+
boolean verbose;
222+
}
223+
```
224+
225+
Both `app --verbose run` and `app run --verbose` set `verbose = true` on the child command.
226+
227+
## Aesh-Only Features
228+
229+
These features have no picocli equivalent:
230+
231+
- **[Negatable booleans](../options#negatable-options)** -- `@Option(negatable = true)` generates `--no-verbose` automatically
232+
- **[Exclusive options](../options#mutually-exclusive-options)** -- `@Option(exclusiveWith = "json")` enforces mutual exclusion at parse time
233+
- **[Option visibility](../options#visibility-levels)** -- `OptionVisibility.BRIEF/FULL/HIDDEN` controls which options appear in `--help` and tab completion
234+
- **[Interactive prompting](../options)** -- `@Option(askIfNotSet = true)` prompts the user for missing required values
235+
- **[Ghost text suggestions](../ghost-text-suggestions)** -- Inline suggestions as the user types
236+
- **[Selectors](../selectors)** -- Interactive list/checkbox selection for option values
237+
- **[Sub-command mode](../sub-command-mode)** -- Enter a context where subcommands run without repeating the parent name
238+
239+
## Shell Completion Scripts
240+
241+
Aesh generates bash, zsh, and fish completion scripts automatically. See [Completers -- Shell Completion Scripts](../completers#shell-completion-scripts) for details.
242+
243+
```java
244+
AeshRuntimeRunner.builder()
245+
.command(DeployCommand.class)
246+
.args(new String[] {"--aesh-completion", "bash"})
247+
.execute();
248+
```
249+
250+
## Annotation Processor
251+
252+
For maximum startup performance, add the [annotation processor](../annotation-processor) to eliminate all runtime reflection:
253+
254+
```xml
255+
<dependency>
256+
<groupId>org.aesh</groupId>
257+
<artifactId>aesh-processor</artifactId>
258+
<version>${aesh.version}</version>
259+
<scope>provided</scope>
260+
</dependency>
261+
```
262+
263+
No code changes needed -- the processor runs at compile time and the runtime automatically uses the generated metadata when available. See [Annotation Processor](../annotation-processor) for setup details.

0 commit comments

Comments
 (0)