Summary
CommandOptionDescriptor.ValueType is documented / advertised (help, OpenCLI wire types) but command option values are not type-checked at parse time. Non-bool options are always stored as raw strings. Consumers that declare typeof(int) still receive garbage like "2--printer" and must validate themselves — or silently mis-handle the value.
Framework options already validate (--rows rejects non-integers; --timeout rejects bad durations). Command options should do the same for declared types.
Repro (consumer: winprint wp)
print declares --from-sheet / --to-sheet as typeof(int) via CommandOptionDescriptor.
Typo / missing space (one shell token for the value):
wp print file.md --from-sheet 1 --to-sheet 2--printer "Some Printer" --what-if
Actual
ArgParser accepts --to-sheet value = "2--printer" (no type error).
"Some Printer" becomes a positional argument (second "file").
--printer never appears as an option.
- App-layer binder does
int.TryParse → fails → falls back to 0 (or similar), so the host never surfaces a usage error.
With a clean invalid int alone:
wp print file.md --to-sheet 2--printer --what-if
# host parse succeeds; app may treat range as "all sheets"
Expected
Parse fails at the host with something like:
Invalid value for --to-sheet: '2--printer' (expected int).
exit usage/error, before command dispatch.
Code pointer
ArgParser.AddCommandOptionValue only treats ValueType == typeof(bool) as a flag; otherwise ReadValue stores the next token as a string with no conversion against ValueType:
var isFlag = commandOption!.ValueType == typeof (bool);
// ...
values.Add (name, value); // always string
Compare framework path for --rows, which does int.TryParse and sets error on failure.
ValueType is also used in OpenCLI/help as the advertised type — so the public contract implies typed options, but parse does not enforce it.
Suggested fix
After reading a non-flag command option value, validate (and optionally normalize) against commandOption.ValueType:
| ValueType |
Behavior |
typeof(bool) |
flag (current) |
typeof(int) |
int.TryParse (invariant); fail with clear message on miss |
typeof(string) / default |
keep raw string |
| other (future) |
same pattern as needed, or document "string only + bool + int" |
Store the canonical string form of the parsed value (e.g. "2") so existing string-keyed CommandOptions stays stable, or document that consumers still receive strings but host guarantees they parse.
Reject missing value (already done via ReadValue).
Optional UX (not required for this issue): if a non-string option's value token starts with - and looks like an option, error "option requires a value" rather than consuming --foo as the value — separate from glued 2--printer which does not start with -.
Tests
- Command option
typeof(int), value 42 → success, options dict has "42" (or equivalent).
- Value
2--printer / abc / empty → ParseResult.Success == false, error mentions option name and expected type.
typeof(bool) flag still works without a following value.
typeof(string) still accepts any token including non-numeric / 2--printer.
- Regression: required int option missing still fails as today.
Why
Without host-side validation, every consumer reimplements binders that either:
- silently coerce bad values (winprint
GetInt → 0), or
- fail late with domain errors that look unrelated (e.g. treating the next token as a file path).
Typed descriptors without typed parse is a footgun for every app on this stack.
Context
Discovered while debugging winprint CLI misuse against a Brother printer:
Summary
CommandOptionDescriptor.ValueTypeis documented / advertised (help, OpenCLI wire types) but command option values are not type-checked at parse time. Non-bool options are always stored as raw strings. Consumers that declaretypeof(int)still receive garbage like"2--printer"and must validate themselves — or silently mis-handle the value.Framework options already validate (
--rowsrejects non-integers;--timeoutrejects bad durations). Command options should do the same for declared types.Repro (consumer: winprint
wp)printdeclares--from-sheet/--to-sheetastypeof(int)viaCommandOptionDescriptor.Typo / missing space (one shell token for the value):
Actual
ArgParseraccepts--to-sheetvalue ="2--printer"(no type error)."Some Printer"becomes a positional argument (second "file").--printernever appears as an option.int.TryParse→ fails → falls back to0(or similar), so the host never surfaces a usage error.With a clean invalid int alone:
Expected
Parse fails at the host with something like:
exit usage/error, before command dispatch.
Code pointer
ArgParser.AddCommandOptionValueonly treatsValueType == typeof(bool)as a flag; otherwiseReadValuestores the next token as a string with no conversion againstValueType:Compare framework path for
--rows, which doesint.TryParseand setserroron failure.ValueTypeis also used in OpenCLI/help as the advertised type — so the public contract implies typed options, but parse does not enforce it.Suggested fix
After reading a non-flag command option value, validate (and optionally normalize) against
commandOption.ValueType:typeof(bool)typeof(int)int.TryParse(invariant); fail with clear message on misstypeof(string)/ defaultStore the canonical string form of the parsed value (e.g.
"2") so existing string-keyedCommandOptionsstays stable, or document that consumers still receive strings but host guarantees they parse.Reject missing value (already done via
ReadValue).Optional UX (not required for this issue): if a non-string option's value token starts with
-and looks like an option, error "option requires a value" rather than consuming--fooas the value — separate from glued2--printerwhich does not start with-.Tests
typeof(int), value42→ success, options dict has"42"(or equivalent).2--printer/abc/ empty →ParseResult.Success == false, error mentions option name and expected type.typeof(bool)flag still works without a following value.typeof(string)still accepts any token including non-numeric /2--printer.Why
Without host-side validation, every consumer reimplements binders that either:
GetInt→0), orTyped descriptors without typed parse is a footgun for every app on this stack.
Context
Discovered while debugging winprint CLI misuse against a Brother printer:
--printer+ app-level validation; defers type parse to this issue)