diff --git a/CHANGELOG.md b/CHANGELOG.md index c75862f..bfa7756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file. ## Unreleased +- **Fix: Root Flags Before a Root Subcommand Under an Aliased Binary:** `wheels --timing cfml 'x'` (any root option such as `--timing`, `--verbose`, `-e prod`, `--timeout=30` before a reserved root subcommand) was handed to the module, because the reserved-token rule treated the subcommand as a module positional when it wasn't at index 1. The leading position now skips root options and their separate values, so the root subcommand runs with the flags as it does without them. Reserved tokens later in a module command are still rewritten as before. Adds `LuCLIModuleHelpPreprocessTest` cases. + - **Fix: `server start --dry-run` Writes Nothing and Shows `--name`:** In a folder without `lucee.json`, the dry run wrote a default `lucee.json` into the project, and its preview showed the folder name even when `--name` was given. A dry run now loads the configuration without persisting the default (`LuceeServerConfig.loadConfig(projectDir, file, persistDefault)`), and the preview applies `--name` as a real start does. Upstream: cybersonic/LuCLI#139. Adds two `ServerCommandHandlerTest` cases. - **Fix: REPL History and Settings Follow `LUCLI_HOME`:** `lucli repl` history, the interactive terminal's history and `Settings` (`settings.json`, used e.g. by `secrets provider use`) were written under `user.home` (`~/.lucli` or `~/`) even when `LUCLI_HOME` / `-Dlucli.home` pointed elsewhere, so an isolated LuCLI home still wrote into the real home, and `settings.json` was not the file `system paths` reports. They now use the active LuCLI home (`LucliPaths`). Settings saved under the old location (`~/.lucli/settings.json`) are read once and copied to the active home when it has none; the old file is left as it is. Upstream: cybersonic/LuCLI#140. Adds `SettingsTest` cases and `ReplCommandHistoryTest`. diff --git a/FORK.md b/FORK.md index 03568f9..7d13fb0 100644 --- a/FORK.md +++ b/FORK.md @@ -86,6 +86,7 @@ No release is tagged for this queue yet; tagging needs the Wheels release owner' | 9 | wheels-dev/LuCLI [#21](https://github.com/wheels-dev/LuCLI/pull/21) (issue: upstream [cybersonic/LuCLI#136](https://github.com/cybersonic/LuCLI/issues/136)) | `--env` / `--envfile` reach a module run through the module shortcut or an aliased binary (re-injected at the root position) | | 10 | wheels-dev/LuCLI [#22](https://github.com/wheels-dev/LuCLI/pull/22) (issue: upstream [cybersonic/LuCLI#140](https://github.com/cybersonic/LuCLI/issues/140)) | REPL / terminal history and `settings.json` follow the active LuCLI home instead of `user.home`; legacy `~/.lucli/settings.json` read once and copied | | 11 | wheels-dev/LuCLI [#23](https://github.com/wheels-dev/LuCLI/pull/23) (issue: upstream [cybersonic/LuCLI#139](https://github.com/cybersonic/LuCLI/issues/139)) | `server start --dry-run` no longer writes a default `lucee.json`; the preview shows `--name` | +| 12 | wheels-dev/LuCLI [#24](https://github.com/wheels-dev/LuCLI/pull/24) (fork-only: follows row 2 / upstream PR [cybersonic/LuCLI#124](https://github.com/cybersonic/LuCLI/pull/124)) | Root options before a reserved root subcommand under an aliased binary run that subcommand instead of the module (`wheels --timing cfml 'x'`) | Upstream (row 8): issue [cybersonic/LuCLI#133](https://github.com/cybersonic/LuCLI/issues/133), PR [cybersonic/LuCLI#134](https://github.com/cybersonic/LuCLI/pull/134) (a `cherry-pick -x` of diff --git a/src/main/java/org/lucee/lucli/LuCLI.java b/src/main/java/org/lucee/lucli/LuCLI.java index 60fe4b4..549e40a 100644 --- a/src/main/java/org/lucee/lucli/LuCLI.java +++ b/src/main/java/org/lucee/lucli/LuCLI.java @@ -789,6 +789,22 @@ private static String[] preprocessModuleHelp(String[] args) { return rewriteModuleHelpOrReserved(args); } + /** Root options that take their value as the next token (when not written --opt=value). */ + static final Set ROOT_OPTIONS_WITH_VALUE = Set.of("--env", "-e", "--envfile", "--timeout"); + + /** + * Index of the first token after the module name ({@code args[0]}) that isn't a root + * option or a root option's separate value. Returns {@code args.length} when there is none. + */ + static int leadingPositionAfterRootOptions(String[] args) { + int i = 1; + while (i < args.length && args[i].startsWith("-") && !"--help".equals(args[i]) && !"-h".equals(args[i])) { + boolean takesNext = ROOT_OPTIONS_WITH_VALUE.contains(args[i]); + i += takesNext ? 2 : 1; + } + return Math.min(i, args.length); + } + /** * Rewrite a module invocation whose args would otherwise be intercepted by * picocli's root subcommands. Assumes {@code args[0]} is already an @@ -829,9 +845,14 @@ static String[] rewriteModuleHelpOrReserved(String[] args) { // user invoked directly, e.g. `wheels server`) — only positionals at // args[2..] belong to the module, so a reserved token there means // picocli would hijack it to a root subcommand. + // The leading position is the first token after the module name that isn't a + // root option: `wheels --timing cfml 'x'` invokes the root `cfml` subcommand + // with --timing, exactly like `wheels cfml 'x'` (it used to be handed to the + // module because `cfml` sat at index 2). + int lead = leadingPositionAfterRootOptions(args); boolean hasReservedPositional = false; - if (!RESERVED_ROOT_SUBCOMMANDS.contains(args[1])) { - for (int i = 2; i < args.length; i++) { + if (lead < args.length && !RESERVED_ROOT_SUBCOMMANDS.contains(args[lead])) { + for (int i = lead + 1; i < args.length; i++) { if (RESERVED_ROOT_SUBCOMMANDS.contains(args[i])) { hasReservedPositional = true; break; diff --git a/src/test/java/org/lucee/lucli/LuCLIModuleHelpPreprocessTest.java b/src/test/java/org/lucee/lucli/LuCLIModuleHelpPreprocessTest.java index 6aff4a3..008bde8 100644 --- a/src/test/java/org/lucee/lucli/LuCLIModuleHelpPreprocessTest.java +++ b/src/test/java/org/lucee/lucli/LuCLIModuleHelpPreprocessTest.java @@ -1,6 +1,7 @@ package org.lucee.lucli; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; @@ -83,4 +84,41 @@ void rootSubcommandAtLeadingPosition_isLeftUnchanged() { LuCLI.rewriteModuleHelpOrReserved(new String[]{"wheels", "server", "start"}) ); } + + // Root options before a reserved root subcommand: the subcommand is still the + // leading token, so it runs as a root command (e.g. `wheels --timing cfml 'x'`). + @Test + void rootFlagBeforeReservedSubcommand_isNotHandedToTheModule() { + String[] args = {"wheels", "--timing", "cfml", "writeOutput(1)"}; + assertArrayEquals(args, LuCLI.rewriteModuleHelpOrReserved(args)); + } + + @Test + void rootOptionWithSeparateValueBeforeReservedSubcommand_isNotHandedToTheModule() { + String[] args = {"wheels", "--env", "prod", "server", "start"}; + assertArrayEquals(args, LuCLI.rewriteModuleHelpOrReserved(args)); + } + + @Test + void rootOptionWithInlineValueBeforeReservedSubcommand_isNotHandedToTheModule() { + String[] args = {"wheels", "--timeout=30", "-v", "run", "x.cfm"}; + assertArrayEquals(args, LuCLI.rewriteModuleHelpOrReserved(args)); + } + + @Test + void rootFlagBeforeModuleSubcommand_reservedPositionalStillRewritten() { + assertArrayEquals( + new String[]{"modules", "run", "wheels", "--verbose", "generate", "controller", "StaticPages", "help"}, + LuCLI.rewriteModuleHelpOrReserved(new String[]{"wheels", "--verbose", "generate", "controller", "StaticPages", "help"}) + ); + } + + @Test + void leadingPositionSkipsRootOptionsAndTheirValues() { + assertEquals(3, LuCLI.leadingPositionAfterRootOptions(new String[]{"wheels", "-t", "--verbose", "cfml"})); + assertEquals(3, LuCLI.leadingPositionAfterRootOptions(new String[]{"wheels", "-e", "prod", "server"})); + assertEquals(2, LuCLI.leadingPositionAfterRootOptions(new String[]{"wheels", "--envfile=.env", "repl"})); + assertEquals(2, LuCLI.leadingPositionAfterRootOptions(new String[]{"wheels", "--env"})); + assertEquals(1, LuCLI.leadingPositionAfterRootOptions(new String[]{"wheels", "--help"})); + } }