Skip to content

fix: support --option=false syntax for boolean flags (#2504) - #2505

Merged
maxandersen merged 2 commits into
jbangdev:mainfrom
stalep:issue_2504
Jun 5, 2026
Merged

fix: support --option=false syntax for boolean flags (#2504)#2505
maxandersen merged 2 commits into
jbangdev:mainfrom
stalep:issue_2504

Conversation

@stalep

@stalep stalep commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Use Boolean wrappers instead of boolean primitives for inherited global flags (verbose, quiet, offline, fresh, preview, stacktrace). This allows afterParse() to distinguish between 'not specified' (null) and 'explicitly set to false' (Boolean.FALSE), so that --offline=false correctly overrides a config-file default of true.

Previously the primitive boolean fields defaulted to false, making it impossible to tell if the user explicitly passed --offline=false or simply didn't specify the flag at all.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • ai-review

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a4032b33-2cb3-453f-9f06-a144da01ba0b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

maxandersen
maxandersen previously approved these changes Jun 3, 2026
@quintesse

quintesse commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

@stalep could you add the following 2 tests to VersionIT?


	@Test
	public void shouldVerboseVersionConfig() {
		assertThat(shell("jbang config set verbose true")).succeeded();
		assertThat(shell("jbang version")).succeeded()
				.outMatches(Pattern.compile(
						"(?s)\\d+\\.\\d+\\.\\d+(\\.\\d+)?(-SNAPSHOT)?" + lineSeparator()))
				.errContains("Repository");
	}

	@Test
	public void shouldVerboseVersionConfigOverride() {
		assertThat(shell("jbang config set verbose true")).succeeded();
		assertThat(shell("jbang --verbose=false version")).succeeded()
				.outMatches(Pattern.compile(
						"(?s)\\d+\\.\\d+\\.\\d+(\\.\\d+)?(-SNAPSHOT)?" + lineSeparator()))
				.errNotContains("Repository");
	}

Btw, the last test currently fails! So we've had a regression at some point and never noticed :-/

Edit: Duh, I'm already running the latest version with AEsh (and therefore this issue) included of course

Edit2: or should we add it to ConfigIT? 🤔

@stalep

stalep commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Added both tests to VersionIT and amended the commit. Thanks for the suggestion!

…ngdev#2504)

Make inherited global flags (verbose, quiet, offline, fresh, preview,
stacktrace) negatable so --no-verbose, --no-offline etc. can be used
to override config-file defaults. Use Boolean wrappers so afterParse()
can distinguish 'not specified' (null) from 'explicitly false'.

For example:
  jbang config set verbose true
  jbang --no-verbose version  # overrides config, runs quietly

Add unit tests for --no-verbose, --no-offline, --no-fresh and
integration tests verifying config override with --no-verbose.
@maxandersen

Copy link
Copy Markdown
Collaborator

tests are about --no-verbose not working as expected....I didn't expect --no-verbose to even be valid to be honest?

@quintesse

Copy link
Copy Markdown
Contributor

I didn't expect --no-verbose to even be valid to be honest?

Indeed, I know it's an options that Picocli supports, but I thought we never used/enabled it

@maxandersen

maxandersen commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Besides --no-verbose might not be really what we want it does seem to reveal an issue with the afterParse() - its called twice...causing overrides that isn't right.

Detailed analysis below:

Build Failure Analysis

Failing test: VersionIT.shouldVerboseVersionConfigOverride() — fails on all platforms (every integration test job + native image jobs).

What the test does

  1. jbang config set verbose true — writes verbose=true to config
  2. jbang --no-verbose version — expects --no-verbose to override the config
  3. Asserts stderr does not contain "Repository" (i.e., verbose output is suppressed)

Root cause

afterParse() is called twice — once for the parent command (JBang) and once for the subcommand (Version), since both extend BaseCommand.

The execution flow for jbang --no-verbose version:

Step Parent (JBang) Subcommand (Version)
1. beforeParse() Util.setVerbose(false)
2. Parse CLI verbose = Boolean.FALSE (from --no-verbose) verbose field is inherited
3. afterParse() Util.setVerbose(false)
4. Default values applied JBangDefaultValueProvider returns "true" from config → verbose = Boolean.TRUE
5. afterParse() on child Util.setVerbose(true)overrides the parent!

The subcommand's inherited verbose field gets its value from JBangDefaultValueProvider (which reads the config file), and since the PR changed the afterParse logic from if (verbose) { setVerbose(true); } (can only enable) to if (verbose != null) { setVerbose(verbose); } (can enable AND disable), the config-based true on the child command now actively re-enables verbose, defeating --no-verbose.

Possible fixes

  1. Only apply inherited flags in the root command's afterParse: Override afterParse in JBang for these flags, and skip them in BaseCommand.afterParse() when the command has a parent.

  2. Track explicit CLI usage: Before applying a flag, check whether it was explicitly provided on the command line vs. defaulted by the provider. Could add a flag or use aesh's ProcessedOption.negatedByUser if accessible.

  3. Change JBangDefaultValueProvider to skip inherited options on subcommands: Don't return config-based defaults for options that are inherited from a parent command, since the parent already handled them.

@maxandersen

Copy link
Copy Markdown
Collaborator

asked if exist on main - something is not right having negatable verbose.

The double afterParse call pattern exists on main too — both JBang (parent) and Version (child) get afterParse() called, and the child's verbose field gets the config value from JBangDefaultValueProvider.

But it's NOT a bug on main because the old logic is one-directional:

  // main — can only ENABLE, never DISABLE
  if (verbose) { Util.setVerbose(true); }

This means:

  • Config verbose=true: Parent gets verbose=true → enables. Child gets verbose=true → enables again. ✅ Works fine.
  • CLI --verbose: Same flow, both enable. ✅
  • No flag, no config: Both are false, neither if fires, verbose stays off. ✅

There's no way to trigger the bug on main because:

  1. There's no --no-verbose flag (no negatable = true)
  2. The old code can only set true, never false — so the child can never undo what the parent set

The PR exposes this latent design issue by making the flags bidirectional (Boolean + negatable). Now the child's afterParse can actively set Util.setVerbose(true) from the config default, overriding the parent's
Util.setVerbose(false) from --no-verbose.

@maxandersen

Copy link
Copy Markdown
Collaborator

Proposed fix

Can this be right? Seems overly complex :)

The fix is to skip config defaults for inherited options on child commands in JBangDefaultValueProvider, so the parent's CLI-parsed value (e.g. --no-verbose) isn't overridden by the child's config default in its afterParse():

diff --git a/src/main/java/dev/jbang/cli/JBangDefaultValueProvider.java b/src/main/java/dev/jbang/cli/JBangDefaultValueProvider.java
index 8530d097..95d7ee9c 100644
--- a/src/main/java/dev/jbang/cli/JBangDefaultValueProvider.java
+++ b/src/main/java/dev/jbang/cli/JBangDefaultValueProvider.java
@@ -33,6 +33,18 @@ public class JBangDefaultValueProvider implements DefaultValueProvider {
 			return null;
 		}
 
+		// Skip config defaults for inherited options on child commands.
+		// The root command (JBang) will get the config default and handle
+		// it in afterParse(). Without this guard, a child command's config
+		// default (e.g. verbose=true) would override a CLI flag like
+		// --no-verbose that was already processed by the parent.
+		if (option.isInherited()
+				&& option.parent() != null
+				&& option.parent().getCommand() != null
+				&& !(option.parent().getCommand() instanceof JBang)) {
+			return null;
+		}
+
 		String optName = option.name().replace("-", "");
 		String fullPath = null;

Tested locally — all VersionIT and TestAeshParsing tests pass.

Prevents child command's config default (e.g. verbose=true) from
overriding a CLI flag like --no-verbose already processed by the parent
in afterParse().
@stalep

stalep commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Applied your fix for JBangDefaultValueProvider — great analysis of the double afterParse() issue. The final commit includes all three parts: Boolean wrappers, negatable = true, and your skip-inherited-on-children guard. All tests pass including the VersionIT config override tests.

@maxandersen
maxandersen merged commit b60479e into jbangdev:main Jun 5, 2026
29 checks passed
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.

3 participants