Title
CliError.InvalidValue.message doubles the "Expected: Expected ..." prefix for choice/schema-backed primitives
Body
Effect version
4.0.0-beta.93 (effect/unstable/cli)
Description
Any flag or argument backed by Primitive.choice (i.e. Flag.choice/Flag.choiceWithValue) or by the schema-backed primitives integer, float, boolean, date renders a doubled "Expected: Expected ..." prefix when given an invalid value, and also repeats the offending value twice in the same message.
Reproduction
import { Primitive, CliError } from "effect/unstable/cli"
import { Effect } from "effect"
const sizeChoice = Primitive.choice([
["small", "small"],
["medium", "medium"],
["large", "large"],
])
const program = Effect.gen(function* () {
return yield* Effect.mapError(
sizeChoice.parse("bogus"),
(expected) =>
new CliError.InvalidValue({ option: "size", value: "bogus", expected, kind: "flag" })
)
})
Effect.runPromise(program.pipe(Effect.catch((error) => Effect.sync(() => {
console.log("error.expected:", JSON.stringify(error.expected))
console.log("error.message:", JSON.stringify(error.message))
return error
}))))
This is exactly the path a real CLI hits through Flag.choice("size", [...]) used in a Command — Param.ts's parseFlag/parseArgument map a failed primitiveType.parse(arg) straight into CliError.InvalidValue's expected field.
Actual output
error.expected: "Expected \"small\" | \"medium\" | \"large\", got \"bogus\""
error.message: "Invalid value for flag --size: \"bogus\". Expected: Expected \"small\" | \"medium\" | \"large\", got \"bogus\""
Note both defects in error.message:
"Expected: Expected ..." — doubled prefix.
"bogus" appears twice — once as the flag's own value, once again via the primitive's , got "bogus" suffix.
Expected output
Something like:
Invalid value for flag --size: "bogus". Expected: "small" | "medium" | "large"
(a single "Expected" prefix, and the invalid value shown once)
Root cause
-
Primitive.choice fails with a raw string that already starts with "Expected " and already appends , got ${value}:
return Effect.fail(`Expected ${validChoices}, got ${format(value)}`)
The schema-backed primitives (boolean, float, integer, date, built via makeSchemaPrimitive) fail the same way, surfacing Schema's own decode-error message, which is also of the shape "Expected <X>, got <Y>".
-
Param.ts's parseFlag/parseArgument take that raw failure string verbatim and store it as CliError.InvalidValue's expected field:
const value = yield* Effect.mapError(
primitiveType.parse(arg),
(error) =>
new CliError.InvalidValue({
option: name,
value: arg,
expected: error,
kind: "flag"
})
)
-
CliError.InvalidValue's message getter unconditionally prepends its own "Expected: " label on top of expected, regardless of whether expected already starts with the word "Expected":
override get message() {
if (this.kind === "argument") {
return `Invalid value for argument <${this.option}>: "${this.value}". Expected: ${this.expected}`
}
return `Invalid value for flag --${this.option}: "${this.value}". Expected: ${this.expected}`
}
So any primitive whose failure text is a full sentence beginning with "Expected" (rather than a bare description of the expected shape, e.g. "small" | "medium" | "large") gets double-prefixed, and any primitive whose failure text also repeats the offending value (via a , got <value> suffix) shows that value twice.
Suggested fix
Pick one canonical contract for what a Primitive's failure message / InvalidValue.expected should contain, then make all primitives conform:
- Option A —
expected should be a bare description of the accepted shape (e.g. "small" | "medium" | "large", no "Expected " prefix, no , got <value> suffix), and InvalidValue.message stays as the single source of the "Expected:"/"got" wording. This would mean adjusting Primitive.choice (drop the Expected ${...}, got ${...} wrapper, just fail with validChoices) and stripping/reshaping the Schema decode error before it reaches expected in the schema-backed primitives.
- Option B —
InvalidValue.message detects and strips a leading "Expected " (and/or a trailing , got "<value>") from this.expected before composing the final sentence, so it degrades gracefully regardless of what a given Primitive reports.
Happy to open a PR for whichever direction maintainers prefer — we worked around this downstream in our own CLI (built on effect/unstable/cli) by post-processing the doubled substring in our error-formatting layer, but that's brittle against upstream wording changes and doesn't fix the duplicated-value wart.
Title
CliError.InvalidValue.messagedoubles the "Expected: Expected ..." prefix forchoice/schema-backed primitivesBody
Effect version
4.0.0-beta.93(effect/unstable/cli)Description
Any flag or argument backed by
Primitive.choice(i.e.Flag.choice/Flag.choiceWithValue) or by the schema-backed primitivesinteger,float,boolean,daterenders a doubled "Expected: Expected ..." prefix when given an invalid value, and also repeats the offending value twice in the same message.Reproduction
This is exactly the path a real CLI hits through
Flag.choice("size", [...])used in aCommand—Param.ts'sparseFlag/parseArgumentmap a failedprimitiveType.parse(arg)straight intoCliError.InvalidValue'sexpectedfield.Actual output
Note both defects in
error.message:"Expected: Expected ..."— doubled prefix."bogus"appears twice — once as the flag's own value, once again via the primitive's, got "bogus"suffix.Expected output
Something like:
(a single "Expected" prefix, and the invalid value shown once)
Root cause
Primitive.choicefails with a raw string that already starts with"Expected "and already appends, got ${value}:The schema-backed primitives (
boolean,float,integer,date, built viamakeSchemaPrimitive) fail the same way, surfacingSchema's own decode-error message, which is also of the shape"Expected <X>, got <Y>".Param.ts'sparseFlag/parseArgumenttake that raw failure string verbatim and store it asCliError.InvalidValue'sexpectedfield:CliError.InvalidValue'smessagegetter unconditionally prepends its own"Expected: "label on top ofexpected, regardless of whetherexpectedalready starts with the word "Expected":So any primitive whose failure text is a full sentence beginning with "Expected" (rather than a bare description of the expected shape, e.g.
"small" | "medium" | "large") gets double-prefixed, and any primitive whose failure text also repeats the offending value (via a, got <value>suffix) shows that value twice.Suggested fix
Pick one canonical contract for what a
Primitive's failure message /InvalidValue.expectedshould contain, then make all primitives conform:expectedshould be a bare description of the accepted shape (e.g."small" | "medium" | "large", no"Expected "prefix, no, got <value>suffix), andInvalidValue.messagestays as the single source of the "Expected:"/"got" wording. This would mean adjustingPrimitive.choice(drop theExpected ${...}, got ${...}wrapper, just fail withvalidChoices) and stripping/reshaping theSchemadecode error before it reachesexpectedin the schema-backed primitives.InvalidValue.messagedetects and strips a leading"Expected "(and/or a trailing, got "<value>") fromthis.expectedbefore composing the final sentence, so it degrades gracefully regardless of what a givenPrimitivereports.Happy to open a PR for whichever direction maintainers prefer — we worked around this downstream in our own CLI (built on
effect/unstable/cli) by post-processing the doubled substring in our error-formatting layer, but that's brittle against upstream wording changes and doesn't fix the duplicated-value wart.