Skip to content

feat: replace picocli with aesh for CLI parsing#2453

Open
stalep wants to merge 2 commits into
jbangdev:mainfrom
stalep:feature/aesh-migration
Open

feat: replace picocli with aesh for CLI parsing#2453
stalep wants to merge 2 commits into
jbangdev:mainfrom
stalep:feature/aesh-migration

Conversation

@stalep
Copy link
Copy Markdown

@stalep stalep commented Apr 28, 2026

Migrate jbang's CLI framework from picocli to aesh, leveraging aesh's compile-time annotation processor to eliminate runtime reflection and improve startup performance.

Key changes:

  • Replace all picocli annotations (@command, @option, @parameters) with aesh equivalents (@CommandDefinition, @GroupCommandDefinition, @option, @argument, @arguments, @OptionList, @OptionGroup, @mixin)
  • Convert Callable commands to Command with CommandResult return type
  • Port custom type converters, parameter consumers, and preprocessors to aesh Converter and OptionParser interfaces
  • Implement external plugin command discovery in help output
  • Add default value provider using aesh's post-parse processing
  • Wire mutual exclusion validation via aesh's exclusiveWith
  • Restore version checking, dynamic completion, option aliases, negatable options, and grouped help sections
  • Regenerate native-image configuration for aesh
  • Update tests for aesh CLI parsing

Performance (vs picocli, median):

  • Native image: 6ms vs 33ms (5.5x faster) for single commands
  • JDK 25: 109ms vs 228ms (2.1x faster)
  • JDK 11: 149ms vs 269ms (1.8x faster)

Summary by CodeRabbit

  • New Features

    • Added CLI benchmark tests/task, improved dynamic shell completion, async version check, and an “External” help section for discovered plugins.
  • Bug Fixes

    • Standardized invalid-input exit codes, improved help/option output and unknown/alias handling, and clearer deprecation messaging.
  • Build & Configuration

    • Updated build/test task behavior (benchmark separation) and adjusted native-image reachability/resources; .gitignore expanded.
  • Documentation

    • Regenerated CLI manpages to reflect the new command behavior and output.

Review Change Stack

Comment thread src/test/java/dev/jbang/BaseTest.java
@stalep stalep force-pushed the feature/aesh-migration branch 2 times, most recently from ad9ecbe to ff9018e Compare May 2, 2026 07:20
@maxandersen maxandersen requested a review from Copilot May 2, 2026 07:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates JBang’s CLI layer from picocli to aesh, replacing command annotations, parsing helpers, default-value handling, and related test infrastructure so the CLI can run without picocli’s runtime reflection model.

Changes:

  • Reworks core CLI plumbing to use aesh commands, mixins, parsers, and default-value resolution.
  • Replaces picocli-specific helpers/converters with aesh-specific implementations and updates command classes accordingly.
  • Updates test coverage and build wiring for the new parser, including new parsing tests and a benchmark task.

Reviewed changes

Copilot reviewed 67 out of 69 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/test/java/dev/jbang/cli/TestTemplate.java Swaps tests to JBang.execute.
src/test/java/dev/jbang/cli/TestStartupBenchmark.java Adds tagged CLI startup benchmark tests.
src/test/java/dev/jbang/cli/TestPlugins.java Adapts help-output assertion for aesh output.
src/test/java/dev/jbang/cli/TestJdk.java Rewrites JDK command tests around parsed CLI invocations.
src/test/java/dev/jbang/cli/TestInit.java Updates init tests to new execute helper.
src/test/java/dev/jbang/cli/TestInfo.java Switches info tests to typed parseCommand.
src/test/java/dev/jbang/cli/TestExternalDeps.java Updates run parsing tests to aesh path.
src/test/java/dev/jbang/cli/TestExport.java Reworks export tests for new command API.
src/test/java/dev/jbang/cli/TestEdit.java Updates edit tests to aesh parsing/execution.
src/test/java/dev/jbang/cli/TestDeps.java Moves deps tests to JBang.execute.
src/test/java/dev/jbang/cli/TestConfig.java Adjusts config tests for aesh syntax/behavior.
src/test/java/dev/jbang/cli/TestCatalog.java Updates catalog tests to new execute helper.
src/test/java/dev/jbang/cli/TestArguments.java Rewrites parser-behavior tests around aesh.
src/test/java/dev/jbang/cli/TestApp.java Updates app command tests and typed parsing.
src/test/java/dev/jbang/cli/TestAlias.java Reworks alias tests for new parser and option syntax.
src/test/java/dev/jbang/cli/TestAeshParsing.java Adds focused aesh parsing regression tests.
src/test/java/dev/jbang/cli/TemplatePropertyConverterTest.java Replaces removed converter tests with property parser tests.
src/test/java/dev/jbang/TestConfiguration.java Updates config-default tests for new parser flow.
src/test/java/dev/jbang/BaseTest.java Replaces picocli-based test runner helper.
src/native-image/config/reflect-config.json Removes old picocli/native-image reflection config.
src/main/java/dev/jbang/util/ConsoleOutput.java Replaces picocli ANSI rendering with local ANSI helper.
src/main/java/dev/jbang/cli/Wrapper.java Converts wrapper command to aesh group/subcommand structure.
src/main/java/dev/jbang/cli/VersionProvider.java Deletes obsolete picocli version provider.
src/main/java/dev/jbang/cli/Version.java Converts version command to aesh options/output.
src/main/java/dev/jbang/cli/Trust.java Converts trust commands to aesh group/subcommands.
src/main/java/dev/jbang/cli/TemplatePropertyConverter.java Removes picocli-specific template property converter.
src/main/java/dev/jbang/cli/StrictParameterPreprocessor.java Removes picocli preprocessor in favor of aesh parser.
src/main/java/dev/jbang/cli/StrictOptionParser.java Adds aesh parser for --opt=value-only behavior.
src/main/java/dev/jbang/cli/ScriptMixin.java Ports script-related options to aesh annotations.
src/main/java/dev/jbang/cli/RunMixin.java Reimplements runtime-option parsing/default handling.
src/main/java/dev/jbang/cli/Run.java Converts run command to aesh argument model.
src/main/java/dev/jbang/cli/NativeMixin.java Ports native-image flags to aesh.
src/main/java/dev/jbang/cli/KeyValueConsumer.java Removes obsolete picocli parameter consumer.
src/main/java/dev/jbang/cli/JdkProvidersMixin.java Ports hidden JDK-provider option to aesh.
src/main/java/dev/jbang/cli/JBangDefaultValueProvider.java Adds aesh-backed default value provider.
src/main/java/dev/jbang/cli/Init.java Converts init command and rewires property/template handling.
src/main/java/dev/jbang/cli/HelpMixin.java Removes obsolete picocli help mixin.
src/main/java/dev/jbang/cli/FormatMixin.java Removes obsolete picocli format mixin.
src/main/java/dev/jbang/cli/ExternalCommandsProvider.java Adds external plugin help-section provider for aesh help.
src/main/java/dev/jbang/cli/ExportMixin.java Removes old export picocli mixin.
src/main/java/dev/jbang/cli/Edit.java Converts edit command to aesh and new defaults flow.
src/main/java/dev/jbang/cli/Deps.java Converts deps commands to aesh group/subcommands.
src/main/java/dev/jbang/cli/DeprecatedMessageHandler.java Removes picocli-specific deprecated-flag handler.
src/main/java/dev/jbang/cli/DependencyInfoMixin.java Ports dependency/repository/property options to aesh.
src/main/java/dev/jbang/cli/DebugOptionParser.java Adds aesh parser for debug option fallback semantics.
src/main/java/dev/jbang/cli/Config.java Converts config commands to aesh and new argument parsing.
src/main/java/dev/jbang/cli/Completion.java Replaces picocli completion generation with aesh generator.
src/main/java/dev/jbang/cli/CommaSeparatedConverter.java Removes obsolete picocli list converter.
src/main/java/dev/jbang/cli/CatalogFileOptionsMixin.java Adds shared aesh catalog-file option mixin.
src/main/java/dev/jbang/cli/Cache.java Converts cache commands to aesh group/subcommands.
src/main/java/dev/jbang/cli/BuildMixin.java Ports build-related options to aesh.
src/main/java/dev/jbang/cli/Build.java Converts build command to aesh argument model.
src/main/java/dev/jbang/cli/BaseCommand.java Replaces picocli base command lifecycle with aesh lifecycle.
src/main/java/dev/jbang/cli/BaseBuildCommand.java Rewires shared build command setup for aesh mixins.
src/main/java/dev/jbang/cli/AIOptions.java Removes old picocli AI option holder.
src/main/java/dev/jbang/Main.java Replaces picocli entrypoint execution with aesh runtime.
src/it/java/dev/jbang/it/AbstractHelpBaseIT.java Updates integration help assertion for new help output.
build.gradle Swaps picocli deps for aesh and adds benchmark task.
.gitignore Ignores new local planning/cache files.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/main/java/dev/jbang/cli/ScriptMixin.java Outdated
Comment thread src/main/java/dev/jbang/cli/BaseCommand.java Outdated
Comment thread src/main/java/dev/jbang/cli/JBangDefaultValueProvider.java Outdated
Comment thread src/main/java/dev/jbang/cli/Config.java
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 69 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/main/java/dev/jbang/cli/Config.java Outdated
Comment thread src/main/java/dev/jbang/cli/BuildMixin.java
Comment thread src/main/java/dev/jbang/cli/JBangDefaultValueProvider.java Outdated
@stalep stalep force-pushed the feature/aesh-migration branch 2 times, most recently from 09449a7 to 674b80e Compare May 3, 2026 07:20
Comment thread src/main/java/dev/jbang/Main.java Outdated
private static volatile Set<String> subcommandNames;

static Set<String> getSubcommandNames() {
if (subcommandNames == null) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no methods in aesh to do this outside looking up annotations manually?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, aesh doesn't expose the registered command names outside of a running CommandRegistry. The annotation lookup is the most lightweight approach — avoids building the full runtime just to get names.

Comment thread src/main/java/dev/jbang/cli/DependencyInfoMixin.java Outdated
@maxandersen
Copy link
Copy Markdown
Collaborator

difference in behaviour on help info.

before:

jbang alias
Missing required subcommand
Usage: jbang alias -o [-hx] --fresh --quiet --verbose [COMMAND]
Manage aliases for scripts.
      --fresh        Make sure we use fresh (i.e. non-cached) resources.
  -h, --help         Display help/info. Use 'jbang <command> -h' for detailed
                       usage.
  -o, --offline      Work offline. Fail-fast if dependencies are missing. No
                       connections will be attempted
      --quiet        jbang will be quiet, only print when error occurs.
      --verbose      jbang will be verbose on what it does.
  -x, --stacktrace   Print exceptions stacktraces to stderr (even when quiet).
Commands:
  add     Add alias for script reference.
  list    Lists locally defined aliases or from the given catalog.
  remove  Remove existing alias.

now:

jbang alias
Missing required subcommand for 'alias'

@maxandersen
Copy link
Copy Markdown
Collaborator

@stalep i made a pr #2456 that pass on main but fail on this PR.

Comment thread src/main/java/dev/jbang/Main.java Outdated
Comment thread src/main/java/dev/jbang/cli/BaseCommand.java Outdated
Comment thread src/main/java/dev/jbang/cli/DependencyInfoMixin.java
@stalep stalep force-pushed the feature/aesh-migration branch from 674b80e to febaf10 Compare May 4, 2026 14:05
Comment thread src/main/java/dev/jbang/cli/ScriptMixin.java Outdated
@Option(shortName = 'i', name = "interactive", hasValue = false, description = "Activate interactive mode")
public Boolean interactive;

public void resolveAfterParse() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this + its 3(?) calls needed ? some difference in how aesh and picocli handles mixins or ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

picocli handled --debug and --jfr config fallbacks via IParameterConsumer/IParameterPreprocessor during parsing. aesh doesn't have those hooks, so resolveAfterParse() applies the Configuration-based defaults after parsing. Called from the 3 commands that use RunMixin (Run, AliasAdd, AppInstall).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it not make sense to have such api in aesh or is there some downside ?

Comment thread src/main/java/dev/jbang/Main.java Outdated
@jabrena
Copy link
Copy Markdown

jabrena commented May 4, 2026

Hi, maybe it is a bit late, I see the conversation and an effort in this PR, but why is the motivation to change the dependency in jbang?

Maybe it is a strong change in the solution.

Juan Antonio

@maxandersen
Copy link
Copy Markdown
Collaborator

As you can notice it's been around as long as picocli itself - so it's not a random project :)

With 1.5-2x on jvm and 5 time faster startup in native is very noticeable. It's the main thing that makes Jbang feel "sluggish" so definitely high in the list.

You'll notice that thus far not a single integration test changed so it gives fairly high confidence it will work.

I'll keeep testing on it and would love others to give a try with this build to see if they find issues.

@jabrena
Copy link
Copy Markdown

jabrena commented May 4, 2026

Oki, so performance is the main argument.

I can help on testing if required, happy to help.

@stalep stalep force-pushed the feature/aesh-migration branch from ef9bc5c to 7eacd2d Compare May 4, 2026 17:52
Comment thread src/main/java/dev/jbang/cli/Cache.java
Comment thread src/main/java/dev/jbang/cli/Cache.java Outdated
abstract class BaseExportCommand extends BaseCommand {

@CommandLine.Mixin
ExportMixin exportMixin;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason why not keep using mixin ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aesh does not support mixin inheritance atm.

@maxandersen
Copy link
Copy Markdown
Collaborator

about jbang config system - it seem to work..but its alot of extra code.

...and do we have to remember on all commands to set JBangDefaultValueProvider ?

stalep added a commit to stalep/jbang that referenced this pull request May 20, 2026
- Rename Cache.CacheClear fields to match option names (e.g. urls->url,
  jars->jar, groovys->groovyc) for consistency as suggested by Max
- Add comment explaining FALLBACK_OPTIONS in JBangDefaultValueProvider
  to clarify why debug and jfr are excluded from default value lookup
- Move null-element filtering inside the args!=null guard in
  Main.handleDefaultRun() to avoid unnecessary work on null input
@stalep
Copy link
Copy Markdown
Author

stalep commented May 20, 2026

about jbang config system - it seem to work..but its alot of extra code.

...and do we have to remember on all commands to set JBangDefaultValueProvider ?

This has been fixed by an improvement in aesh.

@maxandersen
Copy link
Copy Markdown
Collaborator

maxandersen commented May 27, 2026

@jabrena @wfouche @quintesse would be great to hear if you spot any issues with this variant of jbang.

would like to merge this sooner rather than later :)

Migrate jbang's CLI framework from picocli to aesh, leveraging aesh's
compile-time annotation processor to eliminate runtime reflection and
improve startup performance.

Key changes:
- Replace all picocli annotations with aesh equivalents
- Convert Callable<Integer> commands to Command<CommandInvocation>
- Use aesh's fallbackValue for three-state options (--debug, --jfr,
  --module, --code, --open) replacing custom StrictOptionParser
- Set DefaultValueProvider at registry level instead of per-command
- Implement external plugin discovery via ExternalCommandsProvider
- Port DebugOptionParser for --debug peek-ahead pattern matching
- Add subcommand name consistency test
- Use terminal-api ANSI constants in ConsoleOutput
- Rewrite docs generator (genadoc.java) for aesh command model with
  alias, negatable, paramLabel, and boolean flag support
- Regenerate all CLI reference documentation

Error handling:
- Unwrap aesh RuntimeException to find root IllegalArgumentException
  for clean error messages and EXIT_INVALID_INPUT (2) exit codes
- Add catch-all in BaseCommand.execute() for unexpected exceptions
- Replace System.exit() in handleDefaultRun() with ExitException
- Add 'Missing required subcommand' message for group commands

Performance (vs picocli, median):
- Native image: 6ms vs 33ms (5.5x faster) for single commands
- JDK 25: 109ms vs 228ms (2.1x faster)
- JDK 11: 149ms vs 269ms (1.8x faster)

Dependencies:
- org.aesh:aesh:3.8, org.aesh:readline-api:3.8
- Removed: info.picocli:picocli
@maxandersen maxandersen force-pushed the feature/aesh-migration branch from ad5bf78 to 885cde1 Compare May 27, 2026 21:45
The native build (PR jbangdev#2473) now produces platform-suffixed binaries
like jbang-linux-x64.bin / jbang-windows-x64.bin.exe. Match by
suffix (.bin / .bin.exe) instead of exact file name so 'jbang
wrapper install' works again when run from the native image.
@maxandersen maxandersen force-pushed the feature/aesh-migration branch from 885cde1 to 4d740b3 Compare May 27, 2026 21:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants