{"record":{"id":"5daeee991c64f508","repo":"googleworkspace/cli","slug":"unexpected-error-reading-remove-argument-e","errorCode":null,"errorMessage":"Unexpected error reading --remove argument: {e}","messagePattern":"Unexpected error reading --remove argument: (.+?)","errorType":"exception","errorClass":"GwsError","httpStatus":null,"severity":"warning","filePath":"crates/google-workspace-cli/src/helpers/gmail/reply.rs","lineNumber":430,"sourceCode":"         </div>\",\n        attribution, quoted_body,\n    )\n}\n\n// --- Argument parsing ---\n\nfn parse_reply_args(matches: &ArgMatches) -> Result<ReplyConfig, GwsError> {\n    // try_get_one because +reply doesn't define --remove (only +reply-all does).\n    // Explicit match distinguishes \"arg not defined\" from unexpected errors.\n    let remove = match matches.try_get_one::<String>(\"remove\") {\n        Ok(val) => val\n            .map(|s| s.trim().to_string())\n            .filter(|s| !s.is_empty())\n            .map(|s| Mailbox::parse_list(&s))\n            .filter(|v| !v.is_empty()),\n        Err(clap::parser::MatchesError::UnknownArgument { .. }) => None,\n        Err(e) => {\n            return Err(GwsError::Other(anyhow::anyhow!(\n                \"Unexpected error reading --remove argument: {e}\"\n            )))\n        }\n    };\n\n    Ok(ReplyConfig {\n        message_id: matches.get_one::<String>(\"message-id\").unwrap().to_string(),\n        body: matches.get_one::<String>(\"body\").unwrap().to_string(),\n        from: parse_optional_mailboxes(matches, \"from\"),\n        extra_to: parse_optional_mailboxes(matches, \"to\"),\n        cc: parse_optional_mailboxes(matches, \"cc\"),\n        bcc: parse_optional_mailboxes(matches, \"bcc\"),\n        remove,\n        html: matches.get_flag(\"html\"),\n        attachments: parse_attachments(matches)?,\n    })\n}\n","sourceCodeStart":412,"sourceCodeEnd":448,"githubUrl":"https://github.com/googleworkspace/cli/blob/a3768d0e82ad83cca2da97724e46bea4ff0e6dbd/crates/google-workspace-cli/src/helpers/gmail/reply.rs#L412-L448","documentation":"`matches.try_get_one::<String>(\"remove\")` returned a `clap::parser::MatchesError` other than `UnknownArgument`. The code intentionally tolerates `UnknownArgument` because plain `+reply` does not define `--remove` (only `+reply-all` does); any other variant — typically `Downcast`/`WrongType` (the arg exists but was declared with a non-String value parser) or an invalid-UTF8 value — surfaces here. It is effectively an internal contract violation between the two subcommands' argument definitions.","triggerScenarios":"A code change declares `--remove` in `+reply` with `value_parser` producing a non-String type (e.g. `Vec<String>` via `num_args` or a custom parser); passing non-UTF8 bytes in `--remove` on Unix; a clap version bump altering MatchesError semantics.","commonSituations":"Almost exclusively a maintainer/developer error when editing reply.rs argument definitions — end users supplying normal string values never trigger it; CI builds after a clap upgrade.","solutions":["If you modified the arg definitions, ensure `--remove` is declared as `Arg::new(\"remove\").value_parser(clap::builder::StringValueParser::default())` (or plain String) in every subcommand that defines it.","After a clap major upgrade, re-check `MatchesError` variants and keep the explicit `UnknownArgument` arm.","As a user, re-install a released CLI build instead of a locally patched one.","Run `cargo test` in crates/google-workspace-cli — reply arg parsing has unit coverage that catches this."],"exampleFix":"// before: only UnknownArgument is tolerated\nErr(clap::parser::MatchesError::UnknownArgument { .. }) => None,\nErr(e) => return Err(GwsError::Other(anyhow::anyhow!(\"Unexpected error reading --remove argument: {e}\"))),\n\n// after: also tolerate Downcast by reading as the concrete declared type, or assert the invariant in debug builds\nErr(clap::parser::MatchesError::UnknownArgument { .. }) => None,\nErr(e) => {\n    debug_assert!(false, \"--remove declared with non-String value parser: {e}\");\n    None\n}","handlingStrategy":"try-catch","validationCode":"// Ensure every subcommand that reads --remove declares it as a plain String arg\nfn assert_remove_is_string(cmd: &clap::Command) {\n    for sub in cmd.get_subcommands() {\n        if let Some(arg) = sub.get_arguments().find(|a| a.get_id() == \"remove\") {\n            debug_assert!(arg.get_value_parser().type_id() == std::any::TypeId::of::<String>(),\n                \"--remove must be String-valued in {}\", sub.get_name());\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":"// Exhaustively match clap's MatchesError so future variants surface as explicit, typed failures:\nmatch matches.try_get_one::<String>(\"remove\") {\n    Ok(val) => val,\n    Err(clap::parser::MatchesError::UnknownArgument { .. }) => None,\n    Err(clap::parser::MatchesError::Downcast { .. }) => None, // tolerate type drift deliberately\n    Err(e) => return Err(GwsError::Other(anyhow::anyhow!(\"Unexpected error reading --remove argument: {e}\"))),\n}","preventionTips":["When two subcommands share a flag name, declare identical value parsers (String) in both.","Cover +reply and +reply-all arg parsing in unit tests so a MatchesError regression fails CI, not users.","After bumping clap, re-read the MatchesError enum — new variants fall into the fatal arm."],"tags":["clap","argument-parsing","reply","developer-error","type-mismatch"],"backgroundTag":"clap-argument-mismatch","analyzedSha":"a3768d0e82ad83cca2da97724e46bea4ff0e6dbd","analyzedAt":"2026-08-16T19:51:46.516Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}