Skip to content

Run boolean flag fallbacks when the flag is absent - #7229

Open
WikiRik wants to merge 1 commit into
Effect-TS:mainfrom
WikiRik:cli-boolean-flag-fallbacks
Open

Run boolean flag fallbacks when the flag is absent#7229
WikiRik wants to merge 1 commit into
Effect-TS:mainfrom
WikiRik:cli-boolean-flag-fallbacks

Conversation

@WikiRik

@WikiRik WikiRik commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Quick second PR of the day, this one has more tests and does contradict with an earlier commit although I think this is for the better. Feel free to close/adjust if you don't agree.

The following is from the clanker again.


Flag.withFallbackConfig and Flag.withFallbackPrompt are silent no-ops when applied to a boolean flag. Both fallbacks are driven by the MissingOption / MissingArgument errors, and an absent boolean flag never produces one — the parser resolves it to false, which is correct CLI behaviour on its own. The consequence is that the fallback simply never runs.

The JSDoc on Flag.withFallbackConfig uses a boolean flag as its only example, so this is the shape people copy:

const verbose = Flag.boolean("verbose").pipe(
  Flag.withFallbackConfig(Config.boolean("VERBOSE"))
)

Nothing fails — a config value is just ignored, which is easy to ship and hard to notice.

Reproduction

Command.make("probe", {
  bool: Flag.boolean("bool").pipe(Flag.withFallbackConfig(Config.boolean("PROBE_VALUE"))),
  str: Flag.string("str").pipe(Flag.withFallbackConfig(Config.string("PROBE_VALUE")))
}, Effect.fn(function*({ bool, str }) {
  yield* Console.log(`bool=${bool} str=${str}`)
}))
$ PROBE_VALUE=true probe
bool=false str=true

One environment variable, one combinator, two outcomes depending only on the flag's primitive type.

The fix

Both combinators now detect the boolean flag a param reads and consult the fallback when that flag is absent from the parsed flags, before parsing resolves it to false.

No parser change was needed. The parser already canonicalizes aliases and records --no-flag as "false" under the canonical flag name, so "no entry in ParsedArgs.flags" is exactly "absent from the command line" — the same check parseFlag already makes before defaulting to false.

Precedence is unchanged everywhere else:

  • --flag and --no-flag both win over the fallback.
  • optional and withDefault supply their own value first, so the fallback is not consulted — matching how they already shadow the fallback for non-boolean flags.
  • A param with orElse alternatives is left alone, since the alternative may supply the value.
  • Missing config still yields false; a cancelled prompt still fails with MissingOption.

Open question for maintainers: should withFallbackPrompt change too?

The withFallbackConfig half is an unambiguous bug fix. The withFallbackPrompt half is not, and I'd like your call on it.

It reverses a deliberate decision from 2e3e4b246 ("Add Flag.withFallbackPrompt"), whose spec said:

Boolean flags that are absent still default to false and do not trigger the fallback prompt (consistent with existing parsing rules).

There was a passing test asserting exactly that, which this PR inverts.

The case for changing it: it is the identical no-op, for the identical reason. If withFallbackConfig on a boolean flag should do something, it is hard to argue withFallbackPrompt should not.

The case against: a boolean flag is inherently optional, so "absent" is not really a missing value the way it is for a required string flag. Prompting on every invocation where --flag was omitted is a much more visible behaviour change than reading an environment variable.

Happy to drop that half and keep this to withFallbackConfig (the commit is easy to split), or to keep both. If they stay split, the alternative for the prompt side is to document the limitation instead — the JSDoc examples currently promise behaviour that does not happen.

Testing

packages/effect/test/unstable/cli/Param.test.ts — 9 new cases across both combinators: the fallback fires when the flag is absent, --no-flag beats the fallback, missing config yields false, withDefault beats the fallback, an unparseable config value becomes InvalidValue, and a cancelled prompt fails with MissingOption.

With the source change reverted, 4 of them fail and 26 pass. The other 5 pass either way by design — they pin the precedence rules the fix must not disturb.

pnpm vitest run --project effect packages/effect/test/unstable/cli/Param.test.ts

pnpm lint and pnpm check are clean.

🤖 Generated with Claude Code

`Param.withFallbackConfig` and `Param.withFallbackPrompt` both trigger on
the `MissingOption` / `MissingArgument` errors, which a boolean flag never
produces: an absent boolean flag parses as `false`. Applying either
combinator to a `Flag.boolean` was therefore a silent no-op, including in
the example documented on `Flag.withFallbackConfig`.

Both combinators now detect the boolean flag they read and consult the
fallback when that flag is absent from the parsed flags, before parsing
resolves it to `false`. The parser records aliases and `--no-` negations
under the canonical flag name, so an explicit `--flag` or `--no-flag`
still wins. Params wrapped by `optional` or `withDefault`, and those with
`orElse` alternatives, keep supplying their own value.

A missing config still falls back to `false`; a cancelled prompt fails
with `MissingOption`, as it does for other flags.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ea3d2f1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
effect Patch
@effect/ai-anthropic Patch
@effect/ai-openai Patch
@effect/ai-openai-compat Patch
@effect/ai-openrouter Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch
@effect/opentelemetry Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node Patch
@effect/platform-node-shared Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/vitest Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@WikiRik
WikiRik marked this pull request as ready for review August 13, 2026 10:37
@github-actions

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
basic.ts 6.91 KB 6.91 KB 0.00 KB (0.00%)
batching.ts 9.72 KB 9.72 KB 0.00 KB (0.00%)
brand.ts 6.50 KB 6.50 KB 0.00 KB (0.00%)
cache.ts 10.63 KB 10.63 KB 0.00 KB (0.00%)
config.ts 21.05 KB 21.05 KB 0.00 KB (0.00%)
differ.ts 19.99 KB 19.99 KB 0.00 KB (0.00%)
http-client.ts 21.56 KB 21.56 KB 0.00 KB (0.00%)
logger.ts 10.88 KB 10.88 KB 0.00 KB (0.00%)
metric.ts 8.86 KB 8.86 KB 0.00 KB (0.00%)
optic.ts 6.67 KB 6.67 KB 0.00 KB (0.00%)
pubsub.ts 14.90 KB 14.90 KB 0.00 KB (0.00%)
queue.ts 11.57 KB 11.57 KB 0.00 KB (0.00%)
schedule.ts 10.74 KB 10.74 KB 0.00 KB (0.00%)
schema-class.ts 19.61 KB 19.61 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 29.57 KB 29.57 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 25.81 KB 25.81 KB 0.00 KB (0.00%)
schema-string-transformation.ts 13.49 KB 13.49 KB 0.00 KB (0.00%)
schema-string.ts 10.99 KB 10.99 KB 0.00 KB (0.00%)
schema-template-literal.ts 15.30 KB 15.30 KB 0.00 KB (0.00%)
schema-toArbitrary.ts 21.73 KB 21.73 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 24.20 KB 24.20 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 18.96 KB 18.96 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 18.78 KB 18.78 KB 0.00 KB (0.00%)
schema-toFormatter.ts 18.65 KB 18.65 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 22.81 KB 22.81 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 19.30 KB 19.30 KB 0.00 KB (0.00%)
schema.ts 18.87 KB 18.87 KB 0.00 KB (0.00%)
stm.ts 12.66 KB 12.66 KB 0.00 KB (0.00%)
stream.ts 9.66 KB 9.66 KB 0.00 KB (0.00%)

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.

1 participant