/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
libs/cli_parser/src/parse.rs
674 строки
21 KB
Bartek Iwańczuk
refactor: cut over CLI flag parsing to deno_cli_parser (#35982)
07 авг 2026, 12:18
Не верифицирован
07 авг 2026, 12:18
7ee9311
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use crate::error::CliError; use crate::error::CliErrorKind; use crate::types::*; /// Parse command-line arguments against a command definition. /// /// This is the main entry point. It walks argv left-to-right, matches /// subcommands and flags against the static `CommandDef`, and returns /// a `ParseResult` with all parsed values. pub fn parse( def: &CommandDef, args: &[String], ) -> Result<ParseResult, CliError> { let mut result = ParseResult::new(); // Skip argv[0] (binary name) let args = if args.is_empty() { args } else { &args[1..] }; // Determine if the first non-flag arg is a subcommand let sub_def = resolve_subcommand(def, args); if let Some(sub) = sub_def { result.subcommand = Some(sub.name.to_string()); // Parse full args (parse_args will skip the subcommand token) parse_args(sub, def, args, Some(sub.name), &mut result)?; } else if let Some(default_name) = def.default_subcommand { // No subcommand matched - use the default subcommand's args // but leave result.subcommand as None so the converter knows // this is a bare/default invocation (not an explicit subcommand). if let Some(default_def) = def.find_subcommand(default_name) { parse_args(default_def, def, args, None, &mut result)?; // result.subcommand stays None - convert layer handles default } else { parse_args(def, def, args, None, &mut result)?; } } else { // No subcommands - parse against root command parse_args(def, def, args, None, &mut result)?; } Ok(result) } /// Determine which subcommand (if any) matches the args. /// Returns the subcommand def and the name of the subcommand token /// (so parse_args can skip it when it encounters it). fn resolve_subcommand<'a>( def: &'a CommandDef, args: &[String], ) -> Option<&'a CommandDef> { // Walk past any global flags to find the subcommand position. let mut i = 0; while i < args.len() { let arg = &args[i]; if arg == "--" { break; } if arg.starts_with("--") { // Long flag - check if it's a global flag let flag_name = arg.trim_start_matches('-'); let flag_name = flag_name.split('=').next().unwrap_or(flag_name); if let Some(arg_def) = def .all_args() .find(|a| a.global && a.long == Some(flag_name)) { i += 1; // Skip the value if this flag takes one if arg_def.num_args != NumArgs::Exact(0) && arg_def.action != ArgAction::SetTrue && !arg.contains('=') { i += 1; } continue; } break; } else if arg.starts_with('-') && arg.len() > 1 { // Short flag - check if it's a global flag let short = arg.chars().nth(1).unwrap(); if let Some(arg_def) = def.all_args().find(|a| a.global && a.short == Some(short)) { i += 1; if arg_def.num_args != NumArgs::Exact(0) && arg_def.action != ArgAction::SetTrue && arg.len() == 2 { i += 1; } continue; } break; } else { // Positional - check if it's a subcommand if let Some(sub) = def.find_subcommand(arg) { return Some(sub); } break; } } None } /// Parse arguments against a command definition. /// `cmd_def` is the matched subcommand (or root), `root_def` is the root /// for resolving global args. `skip_subcommand` is the subcommand name /// token to skip when encountered as a positional arg. fn parse_args( cmd_def: &CommandDef, root_def: &CommandDef, args: &[String], skip_subcommand: Option<&str>, result: &mut ParseResult, ) -> Result<(), CliError> { let mut i = 0; let mut positional_index = 0; let mut trailing_mode = false; let mut found_subcommand = skip_subcommand.is_none(); let mut passthrough_from: Option<usize> = None; // Per-positional trailing: when set, ALL remaining args (including flags) // go into this positional's values instead of being parsed as flags. let mut positional_trailing_def: Option<&ArgDef> = None; // Collect all positional arg defs in order let positional_defs: Vec<&ArgDef> = cmd_def.all_args().filter(|a| a.positional).collect(); while i < args.len() { let arg = &args[i]; // Per-positional trailing mode: absorb everything into this positional. // // INVARIANT: this path always strips `--` and does NOT honor // `cmd_def.keep_double_dash` (unlike the two command-level `--` paths // below). That's correct only because every subcommand with a `.trailing()` // positional (init/create/x) has `keep_double_dash: false`. If a subcommand // ever needs both a `.trailing()` positional and `--` retention, this path // must be taught to honor the flag. if let Some(trail_def) = positional_trailing_def { debug_assert!( !cmd_def.keep_double_dash, "keep_double_dash is ignored for `.trailing()` positionals; see INVARIANT above" ); if arg == "--" { // `--` still transitions to command-level trailing // so that `deno init --npm vite -- --serve` puts --serve // into the positional (through normal trailing -> handled below) i += 1; while i < args.len() { set_arg_value(result, trail_def, args[i].clone())?; i += 1; } continue; } set_arg_value(result, trail_def, arg.clone())?; i += 1; continue; } // After `--`, everything is trailing if trailing_mode { result.trailing.push(arg.clone()); i += 1; continue; } if arg == "--" { // Check if the next positional has `trailing: true` - if so, // args after `--` go into that positional, not result.trailing. if let Some(next_pos) = positional_defs.get(positional_index) && next_pos.trailing { positional_trailing_def = Some(next_pos); i += 1; continue; } trailing_mode = true; // Keep the `--` in the forwarded args for subcommands that mirror clap's // `.last(true)` / external-subcommand behavior; strip it otherwise. if cmd_def.keep_double_dash { result.trailing.push(arg.clone()); } i += 1; continue; } // Skip the subcommand token when we encounter it if !found_subcommand && let Some(sub_name) = skip_subcommand && !arg.starts_with('-') && (arg == sub_name || cmd_def.aliases.contains(&arg.as_str())) { found_subcommand = true; // If passthrough, collect everything after if cmd_def.passthrough { passthrough_from = Some(i + 1); break; } i += 1; continue; } if arg.starts_with("--") { // Long flag i = parse_long_flag(cmd_def, root_def, args, i, result)?; } else if arg.starts_with('-') && arg.len() > 1 { // Short flag(s) i = parse_short_flag(cmd_def, root_def, args, i, result)?; } else { // Positional argument if let Some(pos_def) = positional_defs.get(positional_index) { apply_value_with_delimiter(result, pos_def, arg)?; // If this positional has trailing: true, absorb everything // remaining into it (including flags) if pos_def.trailing { positional_trailing_def = Some(pos_def); i += 1; continue; } // Move to next positional unless this one accepts multiple match pos_def.num_args { NumArgs::ZeroOrMore | NumArgs::OneOrMore => { // Stay on this positional, it absorbs more } _ => { positional_index += 1; // Check if the next positional has trailing: true. // If so, enter per-positional trailing mode now. if let Some(next_pos) = positional_defs.get(positional_index) && next_pos.trailing { positional_trailing_def = Some(next_pos); i += 1; continue; } // If we just consumed the last positional and trailing_var_arg, // everything remaining goes to trailing if cmd_def.trailing_var_arg && positional_index >= positional_defs.len() { i += 1; // Strip a leading `--` separator unless this subcommand keeps it // in the forwarded argv (clap `.last(true)` / external behavior, // e.g. `deno run x.ts -- -a` forwards `["--", "-a"]`, but // `deno eval code -- a` forwards `["a"]`). if !cmd_def.keep_double_dash && i < args.len() && args[i] == "--" { i += 1; } while i < args.len() { result.trailing.push(args[i].clone()); i += 1; } continue; } } } } else { // No more positional defs - treat as trailing or error if cmd_def.trailing_var_arg { result.trailing.push(arg.clone()); } else { return Err(CliError::new( CliErrorKind::UnexpectedPositional, format!("unexpected argument '{arg}'"), )); } } i += 1; } } // Handle passthrough: collect everything after the subcommand token if let Some(start) = passthrough_from { for arg in args.iter().skip(start) { result.trailing.push(arg.clone()); } } // `--help`/`--version` preempt validation (mirrors clap): the command is // never executed, so missing/conflicting args don't matter. A value-taking // `--version` (e.g. `upgrade --version 1.2.3`) is a normal arg, not the // root version flag. if result.contains("help") || (result.contains("version") && result.get_one("version").is_none()) { return Ok(()); } // Enforce required args: every arg marked `required` must have been provided. for arg_def in cmd_def.all_args() { if arg_def.required && !result.contains(arg_def.name) { return Err(CliError::new( CliErrorKind::MissingRequired, format!( "the following required arguments were not provided: {}", arg_def.name ), )); } } // Enforce mutually exclusive args (clap's `conflicts_with`). for arg_def in cmd_def.all_args() { if result.contains(arg_def.name) { for other in arg_def.conflicts { if result.contains(other) { return Err(CliError::new( CliErrorKind::InvalidValue, format!( "the argument '{}' cannot be used with '{}'", display_arg(arg_def), cmd_def .all_args() .find(|a| a.name == *other) .map(display_arg) .unwrap_or_else(|| (*other).to_string()) ), )); } } } } // Enforce arg dependencies (clap's `requires`): when an arg is present, // every arg it requires must be present too. A missing requirement is // waived when the required arg conflicts with another present arg // (mirrors clap, e.g. `--no-clear-screen` requires `--watch` but is // fine with `--watch-hmr`, which conflicts with `--watch`). for arg_def in cmd_def.all_args() { if result.contains(arg_def.name) { for required in arg_def.requires { if !result.contains(required) && !requirement_waived_by_conflict(cmd_def, result, required) { return Err(CliError::new( CliErrorKind::MissingRequired, format!( "the following required arguments were not provided: --{required}" ), )); } } } } Ok(()) } /// Whether a missing required arg conflicts with any arg that is present, /// which waives the requirement (clap behavior). fn requirement_waived_by_conflict( cmd_def: &CommandDef, result: &ParseResult, required: &str, ) -> bool { let required_conflicts = cmd_def .all_args() .find(|a| a.name == required) .map(|a| a.conflicts) .unwrap_or(&[]); cmd_def.all_args().any(|a| { result.contains(a.name) && (a.conflicts.contains(&required) || required_conflicts.contains(&a.name)) }) } /// Parse a `--long-flag` or `--long-flag=value`. fn parse_long_flag( cmd_def: &CommandDef, root_def: &CommandDef, args: &[String], pos: usize, result: &mut ParseResult, ) -> Result<usize, CliError> { let arg = &args[pos]; let after_dashes = &arg[2..]; // strip leading -- // Handle --flag=value let (flag_name, inline_value) = match after_dashes.find('=') { Some(eq_pos) => { (&after_dashes[..eq_pos], Some(&after_dashes[eq_pos + 1..])) } None => (after_dashes, None), }; // Look up the arg definition in the subcommand first, then root globals let arg_def = cmd_def .find_arg_long(flag_name) .or_else(|| { root_def.all_args().find(|a| { a.global && (a.long == Some(flag_name) || a.long_aliases.contains(&flag_name)) }) }) .ok_or_else(|| CliError::unknown_flag(arg, cmd_def))?; match arg_def.action { ArgAction::SetTrue => { set_arg_bool(result, arg_def); // If there's an inline value for a bool flag with num_args Optional, // consume it (e.g., --help=full) if let Some(val) = inline_value && (arg_def.num_args == NumArgs::Optional || arg_def.num_args == NumArgs::ZeroOrMore) { set_arg_value(result, arg_def, val.to_string())?; } Ok(pos + 1) } ArgAction::Count => { increment_arg_count(result, arg_def); Ok(pos + 1) } ArgAction::Set | ArgAction::Append => { match arg_def.num_args { NumArgs::Exact(0) => { set_arg_bool(result, arg_def); Ok(pos + 1) } NumArgs::Optional => { if let Some(val) = inline_value { // Track occurrence for this flag appearance set_arg_bool(result, arg_def); apply_value_with_delimiter(result, arg_def, val)?; } else if arg_def.require_equals { // Optional with require_equals and no `=`: present but no value set_arg_bool(result, arg_def); } else if pos + 1 < args.len() && !args[pos + 1].starts_with('-') { set_arg_value(result, arg_def, args[pos + 1].clone())?; return Ok(pos + 2); } else { set_arg_bool(result, arg_def); } Ok(pos + 1) } NumArgs::ZeroOrMore => { set_arg_bool(result, arg_def); if let Some(val) = inline_value { apply_value_with_delimiter(result, arg_def, val)?; } else if !arg_def.require_equals { // Consume subsequent non-flag args let mut next = pos + 1; while next < args.len() && !args[next].starts_with('-') { set_arg_value(result, arg_def, args[next].clone())?; next += 1; } return Ok(next); } Ok(pos + 1) } NumArgs::OneOrMore | NumArgs::Exact(_) => { if let Some(val) = inline_value { apply_value_with_delimiter(result, arg_def, val)?; Ok(pos + 1) } else if pos + 1 < args.len() { apply_value_with_delimiter(result, arg_def, &args[pos + 1])?; Ok(pos + 2) } else { Err(CliError::missing_value(arg)) } } } } } } /// Parse short flags like `-A`, `-f value`, `-fvalue`, or combined `-ABC`. fn parse_short_flag( cmd_def: &CommandDef, root_def: &CommandDef, args: &[String], pos: usize, result: &mut ParseResult, ) -> Result<usize, CliError> { let arg = &args[pos]; let chars: Vec<char> = arg[1..].chars().collect(); let mut ci = 0; while ci < chars.len() { let short = chars[ci]; let arg_def = cmd_def .find_arg_short(short) .or_else(|| { root_def.all_args().find(|a| { a.global && (a.short == Some(short) || a.short_aliases.contains(&short)) }) }) .ok_or_else(|| CliError::unknown_flag(&format!("-{short}"), cmd_def))?; match arg_def.action { ArgAction::SetTrue => { set_arg_bool(result, arg_def); ci += 1; } ArgAction::Count => { increment_arg_count(result, arg_def); ci += 1; } ArgAction::Set | ArgAction::Append => { match arg_def.num_args { NumArgs::Exact(0) => { set_arg_bool(result, arg_def); ci += 1; } NumArgs::Optional | NumArgs::ZeroOrMore => { set_arg_bool(result, arg_def); // If there are remaining chars, check if the next // char is itself a known short flag. If so, keep // iterating (combined shorts). Otherwise treat // remaining as value. if ci + 1 < chars.len() { let next_char = chars[ci + 1]; let next_is_flag = cmd_def .find_arg_short(next_char) .or_else(|| { root_def .all_args() .find(|a| a.global && a.short == Some(next_char)) }) .is_some(); if !next_is_flag { let remaining: String = chars[ci + 1..].iter().collect(); let value = remaining.strip_prefix('=').unwrap_or(&remaining); apply_value_with_delimiter(result, arg_def, value)?; return Ok(pos + 1); } } ci += 1; } _ => { // This flag takes a value if ci + 1 < chars.len() { // Rest of the short flags string is the value: -fvalue or -f=value let remaining: String = chars[ci + 1..].iter().collect(); let value = remaining.strip_prefix('=').unwrap_or(&remaining); apply_value_with_delimiter(result, arg_def, value)?; return Ok(pos + 1); } else if pos + 1 < args.len() { // Next arg is the value apply_value_with_delimiter(result, arg_def, &args[pos + 1])?; return Ok(pos + 2); } else { return Err(CliError::missing_value(&format!("-{short}"))); } } } } } } Ok(pos + 1) } /// Apply a value, splitting by delimiter if configured. fn apply_value_with_delimiter( result: &mut ParseResult, arg_def: &ArgDef, value: &str, ) -> Result<(), CliError> { if let Some(delim) = arg_def.value_delimiter { if value.is_empty() { // Empty value (e.g., --flag=) - preserve it as an empty string set_arg_value(result, arg_def, String::new())?; } else { for part in value.split(delim) { set_arg_value(result, arg_def, part.to_string())?; } } } else { set_arg_value(result, arg_def, value.to_string())?; } Ok(()) } /// Set a boolean flag in the parse result. fn set_arg_bool(result: &mut ParseResult, arg_def: &ArgDef) { if let Some(existing) = result.args.iter_mut().find(|a| a.name == arg_def.name) { existing.is_present = true; existing.count += 1; } else { result.args.push(ParsedArg { name: arg_def.name, values: Vec::new(), is_present: true, count: 1, }); } } /// Add a value to an arg in the parse result, validating it against the arg's /// `value_parser` first (mirrors clap rejecting invalid values at parse time). fn set_arg_value( result: &mut ParseResult, arg_def: &ArgDef, value: String, ) -> Result<(), CliError> { if let Some(parser) = arg_def.value_parser { let flag = arg_def .long .map(|l| format!("--{l}")) .unwrap_or_else(|| arg_def.name.to_string()); parser.validate(&value, &flag)?; } if let Some(existing) = result.args.iter_mut().find(|a| a.name == arg_def.name) { existing.is_present = true; match arg_def.action { ArgAction::Set => { // Last value wins existing.values = vec![value]; } _ => { existing.values.push(value); } } } else { result.args.push(ParsedArg { name: arg_def.name, values: vec![value], is_present: true, count: 1, }); } Ok(()) } /// Increment the count for a Count-action arg. fn increment_arg_count(result: &mut ParseResult, arg_def: &ArgDef) { if let Some(existing) = result.args.iter_mut().find(|a| a.name == arg_def.name) { existing.count += 1; existing.is_present = true; } else { result.args.push(ParsedArg { name: arg_def.name, values: Vec::new(), is_present: true, count: 1, }); } } /// Render an arg the way clap did in conflict messages: `--long <value>` for /// value-taking flags, `--long` for booleans, `<NAME>` for positionals. fn display_arg(arg: &ArgDef) -> String { if arg.positional { return format!("<{}>", arg.value_name.unwrap_or(arg.name)); } let flag = match (arg.long, arg.short) { (Some(long), _) => format!("--{long}"), (None, Some(short)) => format!("-{short}"), (None, None) => arg.name.to_string(), }; match arg.num_args { NumArgs::Exact(0) => flag, _ => format!("{flag} <{}>", arg.value_name.unwrap_or(arg.name)), } }