Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `~/<profile home>`) 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`.
Expand Down
1 change: 1 addition & 0 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 23 additions & 2 deletions src/main/java/org/lucee/lucli/LuCLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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
Expand Down Expand Up @@ -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;
Expand Down
38 changes: 38 additions & 0 deletions src/test/java/org/lucee/lucli/LuCLIModuleHelpPreprocessTest.java
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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"}));
}
}
Loading