denoland/deno · #36835
fix(cli): preserve double dash before the entrypoint
libs/cli_parser/src/parse.rs49 + / 0 −
@@ -119,6 +119,11 @@ fn parse_args( let mut i = 0; let mut positional_index = 0; let mut trailing_mode = false;+ // Set when `--` appears before the first positional: the remaining args+ // fill the positionals literally (a leading hyphen is not a flag).+ let mut positional_only = false;+ // Whether any positional value has been consumed yet.+ let mut positional_started = 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)@@ -161,6 +166,29 @@ fn parse_args( continue; } + // Positional-only mode: a `--` appeared before the first positional of+ // an entrypoint-style command (see the `--` handling below), so consume+ // positional values without interpreting leading hyphens as flags, e.g.+ // `deno run -- -script.ts`. Once the positionals are complete, the+ // command's trailing var args receive the rest. Note that a second `--`+ // is forwarded literally here: only the first `--` is special, and it+ // was already consumed to enter this mode.+ if positional_only {+ if let Some(pos_def) = positional_defs.get(positional_index) {+ apply_value_with_delimiter(result, pos_def, arg)?;+ // Move to next positional unless this one accepts multiple+ match pos_def.num_args {+ NumArgs::ZeroOrMore | NumArgs::OneOrMore => {}+ _ => positional_index += 1,+ }+ } else {+ // This mode is only entered when cmd_def.trailing_var_arg is set.+ result.trailing.push(arg.clone());+ }+ i += 1;+ continue;+ }+ // After `--`, everything is trailing if trailing_mode { result.trailing.push(arg.clone());@@ -178,6 +206,26 @@ fn parse_args( i += 1; continue; }+ // For entrypoint-style commands (a single-value positional followed+ // by trailing var args: run/serve/eval/task/compile), a `--` before+ // the first positional does not start trailing args; it marks the+ // remaining args as positional-only so the entrypoint itself may+ // start with a hyphen (mirrors clap's trailing_var_arg). Commands+ // with a multi-value positional (test/bench/install) instead mirror+ // clap's `.last(true)`: args after `--` bypass the positional and+ // stay trailing, so they fall through here.+ if !positional_started+ && cmd_def.trailing_var_arg+ && let Some(next_pos) = positional_defs.get(positional_index)+ && !matches!(+ next_pos.num_args,+ NumArgs::ZeroOrMore | NumArgs::OneOrMore+ )+ {+ positional_only = true;+ 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.@@ -213,6 +261,7 @@ fn parse_args( } else { // Positional argument if let Some(pos_def) = positional_defs.get(positional_index) {+ positional_started = true; apply_value_with_delimiter(result, pos_def, arg)?; // If this positional has trailing: true, absorb everythinglibs/cli_parser/src/tests.rs73 + / 0 −
@@ -882,6 +882,25 @@ fn run_double_dash_trailing() { assert_eq!(r.trailing, vec!["--", "arg1", "--flag"]); } +#[test]+fn run_double_dash_before_script() {+ let r =+ parse(&TEST_ROOT, &svec!["deno", "run", "--", "-echo.ts", "arg1"]).unwrap();+ assert_eq!(r.get_one("script_arg"), Some("-echo.ts"));+ assert_eq!(r.trailing, vec!["arg1"]);+}++#[test]+fn run_double_dash_before_script_keeps_second_separator() {+ let r = parse(+ &TEST_ROOT,+ &svec!["deno", "run", "--", "-echo.ts", "--", "--flag"],+ )+ .unwrap();+ assert_eq!(r.get_one("script_arg"), Some("-echo.ts"));+ assert_eq!(r.trailing, vec!["--", "--flag"]);+}+ #[test] fn global_flags_before_subcommand() { let r = parse(@@ -1195,6 +1214,48 @@ fn eval_print() { assert_eq!(r.get_one("code_arg"), Some("1+1")); } +#[test]+fn eval_double_dash_before_code() {+ let r = parse(+ &TEST_ROOT,+ &svec!["deno", "eval", "--", "-1; console.log(0)", "arg1"],+ )+ .unwrap();+ assert_eq!(r.get_one("code_arg"), Some("-1; console.log(0)"));+ assert_eq!(r.trailing, vec!["arg1"]);+}++#[test]+fn eval_double_dash_before_code_keeps_second_separator() {+ // Only the first `--` is special (mirrors clap): it was consumed to make+ // the positional literal, so a later `--` is forwarded as-is even though+ // eval strips the separator in `deno eval code -- a`.+ let r =+ parse(&TEST_ROOT, &svec!["deno", "eval", "--", "code", "--", "a"]).unwrap();+ assert_eq!(r.get_one("code_arg"), Some("code"));+ assert_eq!(r.trailing, vec!["--", "a"]);+}++#[test]+fn upgrade_double_dash_stays_trailing() {+ // Commands without trailing var args don't enter positional-only mode:+ // `--` still starts (unused) trailing args, as before.+ let r =+ parse(&TEST_ROOT, &svec!["deno", "upgrade", "--", "v1", "v2"]).unwrap();+ assert_eq!(r.get_one("version-or-hash-or-channel"), None);+ assert_eq!(r.trailing, vec!["v1", "v2"]);+}++#[test]+fn test_double_dash_stays_trailing() {+ // Commands with a multi-value positional (test/bench) mirror clap's+ // `.last(true)`: args after `--` are script args, not files.+ let r =+ parse(&TEST_ROOT, &svec!["deno", "test", "--", "arg1", "--flag"]).unwrap();+ assert_eq!(r.get_many("files"), None);+ assert_eq!(r.trailing, vec!["arg1", "--flag"]);+}+ #[test] fn unknown_flag_error() { let err = parse(@@ -1242,6 +1303,18 @@ fn default_subcommand_with_flags() { assert_eq!(r.get_one("script_arg"), Some("script.ts")); } +#[test]+fn default_subcommand_double_dash_before_script() {+ let r = parse(+ &TEST_ROOT,+ &svec!["deno", "--", "-echo.ts", "--", "--debug"],+ )+ .unwrap();+ assert_eq!(r.subcommand.as_deref(), None);+ assert_eq!(r.get_one("script_arg"), Some("-echo.ts"));+ assert_eq!(r.trailing, vec!["--", "--debug"]);+}+ #[test] fn default_subcommand_with_allow_read_values() { let r = parse(&TEST_ROOT, &svec!["deno", "--allow-read=/tmp", "script.ts"])libs/cli_parser/src/tests_full.rs66 + / 0 −
@@ -9712,6 +9712,72 @@ fn eval_double_dash_stripped() { assert_eq!(flags.argv, svec!["--", "a&b"]); } +#[test]+fn double_dash_before_entrypoint() {+ // A `--` before the first positional makes the entrypoint literal+ // (it may start with a hyphen) instead of starting trailing args.+ let flags = flags_from_vec(svec!["deno", "run", "--", "-x.ts", "a"]).unwrap();+ assert_eq!(+ flags.subcommand,+ DenoSubcommand::Run(RunFlags::new_default("-x.ts".to_string()))+ );+ assert_eq!(flags.argv, svec!["a"]);++ // Bare form without the `run` subcommand.+ let flags = flags_from_vec(svec!["deno", "--", "-x.ts", "a"]).unwrap();+ assert_eq!(+ flags.subcommand,+ DenoSubcommand::Run(RunFlags {+ bare: true,+ ..RunFlags::new_default("-x.ts".to_string())+ })+ );+ assert_eq!(flags.argv, svec!["a"]);++ let flags = flags_from_vec(svec!["deno", "eval", "--", "-1"]).unwrap();+ assert!(+ matches!(flags.subcommand, DenoSubcommand::Eval(e) if e.code == "-1")+ );++ let flags =+ flags_from_vec(svec!["deno", "task", "--", "build", "hello"]).unwrap();+ assert!(+ matches!(flags.subcommand, DenoSubcommand::Task(t) if t.task.as_deref() == Some("build"))+ );+ assert_eq!(flags.argv, svec!["hello"]);++ // test/bench mirror clap's `.last(true)`: args after `--` are script+ // args, never files — even when no files were given before the `--`.+ let flags = flags_from_vec(svec!["deno", "test", "--", "arg1"]).unwrap();+ assert!(+ matches!(&flags.subcommand, DenoSubcommand::Test(t) if t.files.include.is_empty())+ );+ assert_eq!(flags.argv, svec!["arg1"]);++ // create's package is only recognized before the `--` (clap `.last(true)`+ // took everything after it as package args), so this stays an error.+ assert!(flags_from_vec(svec!["deno", "create", "--", "npm:vite"]).is_err());++ // `--` before the first positional of init still routes everything into+ // its trailing positional and strips a second `--` (unchanged behavior).+ let flags = flags_from_vec(svec![+ "deno", "init", "--npm", "--", "vite", "--", "--serve"+ ])+ .unwrap();+ assert_eq!(+ flags.subcommand,+ DenoSubcommand::Init(InitFlags {+ package: Some("npm:vite".to_string()),+ package_args: svec!["--serve"],+ dir: None,+ lib: false,+ serve: false,+ empty: false,+ yes: false,+ })+ );+}+ #[test] fn sync_types_subcommand() { let flags = flags_from_vec(svec!["deno", "sync-types"]).unwrap();