From 757d7479af15ba5bb47a70bb0635c869c7ee33fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1dn=C3=ADk?= Date: Thu, 22 Dec 2022 00:33:26 +0200 Subject: [PATCH] Add "fall-through" signatures (#7527) Fixes https://github.com/nushell/nushell/issues/4659 Fixes https://github.com/nushell/nushell/issues/5294 Fixes https://github.com/nushell/nushell/issues/6124 fix https://github.com/nushell/nushell/issues/5103 --- crates/nu-cli/src/syntax_highlight.rs | 1 + crates/nu-cli/tests/completions.rs | 62 ++++++++ crates/nu-command/src/system/exec.rs | 52 +++---- crates/nu-command/src/system/run_external.rs | 133 ++++++++++-------- crates/nu-command/tests/commands/exec.rs | 58 ++++++++ crates/nu-command/tests/commands/mod.rs | 2 + crates/nu-parser/src/known_external.rs | 1 + crates/nu-parser/src/parse_keywords.rs | 1 + crates/nu-parser/src/parser.rs | 127 +++++++++++------ crates/nu-protocol/src/ast/call.rs | 9 ++ crates/nu-protocol/src/signature.rs | 8 ++ .../src/sample_config/default_config.nu | 23 ++- src/tests/test_known_external.rs | 53 ++++++- 13 files changed, 379 insertions(+), 151 deletions(-) create mode 100644 crates/nu-command/tests/commands/exec.rs diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index 1f585acdad..9d2258fda3 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -352,6 +352,7 @@ fn find_matching_block_end_in_expr( let opt_expr = match arg { Argument::Named((_, _, opt_expr)) => opt_expr.as_ref(), Argument::Positional(inner_expr) => Some(inner_expr), + Argument::Unknown(inner_expr) => Some(inner_expr), }; if let Some(inner_expr) = opt_expr { diff --git a/crates/nu-cli/tests/completions.rs b/crates/nu-cli/tests/completions.rs index 098434d6f8..b98ece8d27 100644 --- a/crates/nu-cli/tests/completions.rs +++ b/crates/nu-cli/tests/completions.rs @@ -34,6 +34,26 @@ fn completer_strings() -> NuCompleter { NuCompleter::new(std::sync::Arc::new(engine), stack) } +#[fixture] +fn extern_completer() -> NuCompleter { + // Create a new engine + let (dir, _, mut engine, mut stack) = new_engine(); + + // Add record value as example + let record = r#" + def animals [] { [ "cat", "dog", "eel" ] } + extern spam [ + animal: string@animals + --foo (-f): string@animals + -b: string@animals + ] + "#; + assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok()); + + // Instantiate a new completer + NuCompleter::new(std::sync::Arc::new(engine), stack) +} + #[test] fn variables_dollar_sign_with_varialblecompletion() { let (_, _, engine, stack) = new_engine(); @@ -750,3 +770,45 @@ fn filecompletions_triggers_after_cursor() { match_suggestions(expected_paths, suggestions); } + +#[rstest] +fn extern_custom_completion_positional(mut extern_completer: NuCompleter) { + let suggestions = extern_completer.complete("spam ", 5); + let expected: Vec = vec!["cat".into(), "dog".into(), "eel".into()]; + match_suggestions(expected, suggestions); +} + +#[rstest] +fn extern_custom_completion_long_flag_1(mut extern_completer: NuCompleter) { + let suggestions = extern_completer.complete("spam --foo=", 11); + let expected: Vec = vec!["cat".into(), "dog".into(), "eel".into()]; + match_suggestions(expected, suggestions); +} + +#[rstest] +fn extern_custom_completion_long_flag_2(mut extern_completer: NuCompleter) { + let suggestions = extern_completer.complete("spam --foo ", 11); + let expected: Vec = vec!["cat".into(), "dog".into(), "eel".into()]; + match_suggestions(expected, suggestions); +} + +#[rstest] +fn extern_custom_completion_long_flag_short(mut extern_completer: NuCompleter) { + let suggestions = extern_completer.complete("spam -f ", 8); + let expected: Vec = vec!["cat".into(), "dog".into(), "eel".into()]; + match_suggestions(expected, suggestions); +} + +#[rstest] +fn extern_custom_completion_short_flag(mut extern_completer: NuCompleter) { + let suggestions = extern_completer.complete("spam -b ", 8); + let expected: Vec = vec!["cat".into(), "dog".into(), "eel".into()]; + match_suggestions(expected, suggestions); +} + +#[rstest] +fn extern_complete_flags(mut extern_completer: NuCompleter) { + let suggestions = extern_completer.complete("spam -", 6); + let expected: Vec = vec!["--foo".into(), "-b".into(), "-f".into()]; + match_suggestions(expected, suggestions); +} diff --git a/crates/nu-command/src/system/exec.rs b/crates/nu-command/src/system/exec.rs index 34fa016abd..4fc206abcd 100644 --- a/crates/nu-command/src/system/exec.rs +++ b/crates/nu-command/src/system/exec.rs @@ -1,7 +1,7 @@ -use super::run_external::ExternalCommand; -use nu_engine::{current_dir, env_to_strings, CallExt}; +use super::run_external::create_external_command; +use nu_engine::{current_dir, CallExt}; use nu_protocol::{ - ast::{Call, Expr}, + ast::Call, engine::{Command, EngineState, Stack}, Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, }; @@ -19,11 +19,7 @@ impl Command for Exec { Signature::build("exec") .input_output_types(vec![(Type::Nothing, Type::Any)]) .required("command", SyntaxShape::String, "the command to execute") - .rest( - "rest", - SyntaxShape::String, - "any additional arguments for the command", - ) + .allows_unknown_args() .category(Category::System) } @@ -69,36 +65,22 @@ fn exec( let name: Spanned = call.req(engine_state, stack, 0)?; let name_span = name.span; - let args: Vec> = call.rest(engine_state, stack, 1)?; - let args_expr: Vec = - call.positional_iter().skip(1).cloned().collect(); - let mut arg_keep_raw = vec![]; - for one_arg_expr in args_expr { - match one_arg_expr.expr { - // refer to `parse_dollar_expr` function - // the expression type of $variable_name, $"($variable_name)" - // will be Expr::StringInterpolation, Expr::FullCellPath - Expr::StringInterpolation(_) | Expr::FullCellPath(_) => arg_keep_raw.push(true), - _ => arg_keep_raw.push(false), - } - } + let redirect_stdout = call.has_flag("redirect-stdout"); + let redirect_stderr = call.has_flag("redirect-stderr"); + let trim_end_newline = call.has_flag("trim-end-newline"); + + let external_command = create_external_command( + engine_state, + stack, + call, + redirect_stdout, + redirect_stderr, + trim_end_newline, + )?; let cwd = current_dir(engine_state, stack)?; - let env_vars = env_to_strings(engine_state, stack)?; - let current_dir = current_dir(engine_state, stack)?; - - let external_command = ExternalCommand { - name, - args, - arg_keep_raw, - env_vars, - redirect_stdout: true, - redirect_stderr: false, - trim_end_newline: false, - }; - let mut command = external_command.spawn_simple_command(&cwd.to_string_lossy())?; - command.current_dir(current_dir); + command.current_dir(cwd); let err = command.exec(); // this replaces our process, should not return diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index 749e68d2e1..9b29365882 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -52,70 +52,19 @@ impl Command for External { call: &Call, input: PipelineData, ) -> Result { - let name: Spanned = call.req(engine_state, stack, 0)?; - let args: Vec = call.rest(engine_state, stack, 1)?; let redirect_stdout = call.has_flag("redirect-stdout"); let redirect_stderr = call.has_flag("redirect-stderr"); let trim_end_newline = call.has_flag("trim-end-newline"); - // Translate environment variables from Values to Strings - let env_vars_str = env_to_strings(engine_state, stack)?; - - fn value_as_spanned(value: Value) -> Result, ShellError> { - let span = value.span()?; - - value - .as_string() - .map(|item| Spanned { item, span }) - .map_err(|_| { - ShellError::ExternalCommand( - format!("Cannot convert {} to a string", value.get_type()), - "All arguments to an external command need to be string-compatible".into(), - span, - ) - }) - } - - let mut spanned_args = vec![]; - let args_expr: Vec = call.positional_iter().skip(1).cloned().collect(); - let mut arg_keep_raw = vec![]; - for (one_arg, one_arg_expr) in args.into_iter().zip(args_expr) { - match one_arg { - Value::List { vals, .. } => { - // turn all the strings in the array into params. - // Example: one_arg may be something like ["ls" "-a"] - // convert it to "ls" "-a" - for v in vals { - spanned_args.push(value_as_spanned(v)?); - // for arguments in list, it's always treated as a whole arguments - arg_keep_raw.push(true); - } - } - val => { - spanned_args.push(value_as_spanned(val)?); - match one_arg_expr.expr { - // refer to `parse_dollar_expr` function - // the expression type of $variable_name, $"($variable_name)" - // will be Expr::StringInterpolation, Expr::FullCellPath - Expr::StringInterpolation(_) | Expr::FullCellPath(_) => { - arg_keep_raw.push(true) - } - _ => arg_keep_raw.push(false), - } - {} - } - } - } - - let command = ExternalCommand { - name, - args: spanned_args, - arg_keep_raw, + let command = create_external_command( + engine_state, + stack, + call, redirect_stdout, redirect_stderr, - env_vars: env_vars_str, trim_end_newline, - }; + )?; + command.run_with_input(engine_state, stack, input, false) } @@ -135,6 +84,76 @@ impl Command for External { } } +/// Creates ExternalCommand from a call +pub fn create_external_command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + redirect_stdout: bool, + redirect_stderr: bool, + trim_end_newline: bool, +) -> Result { + let name: Spanned = call.req(engine_state, stack, 0)?; + let args: Vec = call.rest(engine_state, stack, 1)?; + + // Translate environment variables from Values to Strings + let env_vars_str = env_to_strings(engine_state, stack)?; + + fn value_as_spanned(value: Value) -> Result, ShellError> { + let span = value.span()?; + + value + .as_string() + .map(|item| Spanned { item, span }) + .map_err(|_| { + ShellError::ExternalCommand( + format!("Cannot convert {} to a string", value.get_type()), + "All arguments to an external command need to be string-compatible".into(), + span, + ) + }) + } + + let mut spanned_args = vec![]; + let args_expr: Vec = call.positional_iter().skip(1).cloned().collect(); + let mut arg_keep_raw = vec![]; + for (one_arg, one_arg_expr) in args.into_iter().zip(args_expr) { + match one_arg { + Value::List { vals, .. } => { + // turn all the strings in the array into params. + // Example: one_arg may be something like ["ls" "-a"] + // convert it to "ls" "-a" + for v in vals { + spanned_args.push(value_as_spanned(v)?); + // for arguments in list, it's always treated as a whole arguments + arg_keep_raw.push(true); + } + } + val => { + spanned_args.push(value_as_spanned(val)?); + match one_arg_expr.expr { + // refer to `parse_dollar_expr` function + // the expression type of $variable_name, $"($variable_name)" + // will be Expr::StringInterpolation, Expr::FullCellPath + Expr::StringInterpolation(_) | Expr::FullCellPath(_) => arg_keep_raw.push(true), + _ => arg_keep_raw.push(false), + } + {} + } + } + } + + Ok(ExternalCommand { + name, + args: spanned_args, + arg_keep_raw, + redirect_stdout, + redirect_stderr, + env_vars: env_vars_str, + trim_end_newline, + }) +} + #[derive(Clone)] pub struct ExternalCommand { pub name: Spanned, diff --git a/crates/nu-command/tests/commands/exec.rs b/crates/nu-command/tests/commands/exec.rs new file mode 100644 index 0000000000..860bc8a61a --- /dev/null +++ b/crates/nu-command/tests/commands/exec.rs @@ -0,0 +1,58 @@ +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn basic_exec() { + Playground::setup("test_exec_1", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + nu -c 'exec nu --testbin cococo a b c' + "# + )); + + assert_eq!(actual.out, "a b c"); + }) +} + +#[test] +fn exec_complex_args() { + Playground::setup("test_exec_2", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + nu -c 'exec nu --testbin cococo b --bar=2 -sab --arwr - -DTEEE=aasd-290 -90 --' + "# + )); + + assert_eq!(actual.out, "b --bar=2 -sab --arwr - -DTEEE=aasd-290 -90 --"); + }) +} + +#[test] +fn exec_fail_batched_short_args() { + Playground::setup("test_exec_3", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + nu -c 'exec nu --testbin cococo -ab 10' + "# + )); + + assert_eq!(actual.out, ""); + }) +} + +#[test] +fn exec_misc_values() { + Playground::setup("test_exec_4", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + nu -c 'let x = "abc"; exec nu --testbin cococo $x [ a b c ]' + "# + )); + + assert_eq!(actual.out, "abc a b c"); + }) +} diff --git a/crates/nu-command/tests/commands/mod.rs b/crates/nu-command/tests/commands/mod.rs index 8640290cf3..8d0e53f929 100644 --- a/crates/nu-command/tests/commands/mod.rs +++ b/crates/nu-command/tests/commands/mod.rs @@ -20,6 +20,8 @@ mod empty; mod enter; mod error_make; mod every; +#[cfg(not(windows))] +mod exec; mod export_def; mod find; mod first; diff --git a/crates/nu-parser/src/known_external.rs b/crates/nu-parser/src/known_external.rs index ea40daf4f5..a23c904572 100644 --- a/crates/nu-parser/src/known_external.rs +++ b/crates/nu-parser/src/known_external.rs @@ -96,6 +96,7 @@ impl Command for KnownExternal { extern_call.add_positional(arg.clone()); } } + Argument::Unknown(unknown) => extern_call.add_unknown(unknown.clone()), } } diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index b3b49d7273..fd91b9c19b 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -499,6 +499,7 @@ pub fn parse_extern( signature.name = name.clone(); signature.usage = usage.clone(); + signature.allows_unknown_args = true; let decl = KnownExternal { name: name.to_string(), diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 711a29560c..77b50f821c 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -827,10 +827,26 @@ pub fn parse_internal_call( &signature, expand_aliases_denylist, ); + if let Some(long_name) = long_name { // We found a long flag, like --bar - error = error.or(err); - call.add_named((long_name, None, arg)); + if matches!(err, Some(ParseError::UnknownFlag(_, _, _, _))) + && signature.allows_unknown_args + { + let (arg, arg_err) = parse_value( + working_set, + arg_span, + &SyntaxShape::Any, + expand_aliases_denylist, + ); + + error = error.or(arg_err); + call.add_unknown(arg); + } else { + error = error.or(err); + call.add_named((long_name, None, arg)); + } + spans_idx += 1; continue; } @@ -846,6 +862,7 @@ pub fn parse_internal_call( if let Some(mut short_flags) = short_flags { if short_flags.is_empty() { + // workaround for completions (PR #6067) short_flags.push(Flag { long: "".to_string(), short: Some('a'), @@ -856,72 +873,88 @@ pub fn parse_internal_call( default_value: None, }) } - error = error.or(err); - for flag in short_flags { - if let Some(arg_shape) = flag.arg { - if let Some(arg) = spans.get(spans_idx + 1) { - let (arg, err) = - parse_value(working_set, *arg, &arg_shape, expand_aliases_denylist); - error = error.or(err); - if flag.long.is_empty() { - if let Some(short) = flag.short { + if matches!(err, Some(ParseError::UnknownFlag(_, _, _, _))) + && signature.allows_unknown_args + { + let (arg, arg_err) = parse_value( + working_set, + arg_span, + &SyntaxShape::Any, + expand_aliases_denylist, + ); + + call.add_unknown(arg); + error = error.or(arg_err); + } else { + error = error.or(err); + for flag in short_flags { + if let Some(arg_shape) = flag.arg { + if let Some(arg) = spans.get(spans_idx + 1) { + let (arg, err) = + parse_value(working_set, *arg, &arg_shape, expand_aliases_denylist); + error = error.or(err); + + if flag.long.is_empty() { + if let Some(short) = flag.short { + call.add_named(( + Spanned { + item: String::new(), + span: spans[spans_idx], + }, + Some(Spanned { + item: short.to_string(), + span: spans[spans_idx], + }), + Some(arg), + )); + } + } else { call.add_named(( Spanned { - item: String::new(), + item: flag.long.clone(), span: spans[spans_idx], }, - Some(Spanned { - item: short.to_string(), - span: spans[spans_idx], - }), + None, Some(arg), )); } + spans_idx += 1; } else { + error = error.or_else(|| { + Some(ParseError::MissingFlagParam( + arg_shape.to_string(), + arg_span, + )) + }) + } + } else if flag.long.is_empty() { + if let Some(short) = flag.short { call.add_named(( Spanned { - item: flag.long.clone(), + item: String::new(), span: spans[spans_idx], }, + Some(Spanned { + item: short.to_string(), + span: spans[spans_idx], + }), None, - Some(arg), )); } - spans_idx += 1; } else { - error = error.or_else(|| { - Some(ParseError::MissingFlagParam( - arg_shape.to_string(), - arg_span, - )) - }) - } - } else if flag.long.is_empty() { - if let Some(short) = flag.short { call.add_named(( Spanned { - item: String::new(), + item: flag.long.clone(), span: spans[spans_idx], }, - Some(Spanned { - item: short.to_string(), - span: spans[spans_idx], - }), + None, None, )); } - } else { - call.add_named(( - Spanned { - item: flag.long.clone(), - span: spans[spans_idx], - }, - None, - None, - )); } } + spans_idx += 1; continue; } @@ -973,6 +1006,16 @@ pub fn parse_internal_call( }; call.add_positional(arg); positional_idx += 1; + } else if signature.allows_unknown_args { + let (arg, arg_err) = parse_value( + working_set, + arg_span, + &SyntaxShape::Any, + expand_aliases_denylist, + ); + + call.add_unknown(arg); + error = error.or(arg_err); } else { call.add_positional(Expression::garbage(arg_span)); error = error.or_else(|| { diff --git a/crates/nu-protocol/src/ast/call.rs b/crates/nu-protocol/src/ast/call.rs index 897f4be892..449305620d 100644 --- a/crates/nu-protocol/src/ast/call.rs +++ b/crates/nu-protocol/src/ast/call.rs @@ -7,6 +7,7 @@ use crate::{DeclId, Span, Spanned}; pub enum Argument { Positional(Expression), Named((Spanned, Option>, Option)), + Unknown(Expression), // unknown argument used in "fall-through" signatures } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -36,6 +37,7 @@ impl Call { self.arguments.iter().filter_map(|arg| match arg { Argument::Named(named) => Some(named), Argument::Positional(_) => None, + Argument::Unknown(_) => None, }) } @@ -46,6 +48,7 @@ impl Call { self.arguments.iter_mut().filter_map(|arg| match arg { Argument::Named(named) => Some(named), Argument::Positional(_) => None, + Argument::Unknown(_) => None, }) } @@ -64,10 +67,15 @@ impl Call { self.arguments.push(Argument::Positional(positional)); } + pub fn add_unknown(&mut self, unknown: Expression) { + self.arguments.push(Argument::Unknown(unknown)); + } + pub fn positional_iter(&self) -> impl Iterator { self.arguments.iter().filter_map(|arg| match arg { Argument::Named(_) => None, Argument::Positional(positional) => Some(positional), + Argument::Unknown(unknown) => Some(unknown), }) } @@ -75,6 +83,7 @@ impl Call { self.arguments.iter_mut().filter_map(|arg| match arg { Argument::Named(_) => None, Argument::Positional(positional) => Some(positional), + Argument::Unknown(unknown) => Some(unknown), }) } diff --git a/crates/nu-protocol/src/signature.rs b/crates/nu-protocol/src/signature.rs index 1a2d5b5440..1a1a09e685 100644 --- a/crates/nu-protocol/src/signature.rs +++ b/crates/nu-protocol/src/signature.rs @@ -118,6 +118,7 @@ pub struct Signature { pub allow_variants_without_examples: bool, pub is_filter: bool, pub creates_scope: bool, + pub allows_unknown_args: bool, // Signature category used to classify commands stored in the list of declarations pub category: Category, } @@ -220,6 +221,7 @@ impl Signature { is_filter: false, creates_scope: false, category: Category::Default, + allows_unknown_args: false, } } @@ -274,6 +276,12 @@ impl Signature { self } + /// Allow unknown signature parameters + pub fn allows_unknown_args(mut self) -> Signature { + self.allows_unknown_args = true; + self + } + /// Add a required positional argument to the signature pub fn required( mut self, diff --git a/crates/nu-utils/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index db72aed2c4..5fdc7a7ca4 100644 --- a/crates/nu-utils/src/sample_config/default_config.nu +++ b/crates/nu-utils/src/sample_config/default_config.nu @@ -101,7 +101,6 @@ module completions { --dry-run(-n) # dry run --exec: string # receive pack program --follow-tags # push missing but relevant tags - --force-with-lease # require old value of ref to be at this value --force(-f) # force updates --ipv4(-4) # use IPv4 addresses only --ipv6(-6) # use IPv6 addresses only @@ -292,7 +291,7 @@ let light_theme = { } # External completer example -# let carapace_completer = {|spans| +# let carapace_completer = {|spans| # carapace $spans.0 nushell $spans | from json # } @@ -325,31 +324,31 @@ let-env config = { command_bar_text: '#C4C9C6' # command_bar: {fg: '#C4C9C6' bg: '#223311' } - + status_bar_background: {fg: '#1D1F21' bg: '#C4C9C6' } # status_bar_text: {fg: '#C4C9C6' bg: '#223311' } highlight: {bg: 'yellow' fg: 'black' } status: { - # warn: {bg: 'yellow', fg: 'blue'} - # error: {bg: 'yellow', fg: 'blue'} + # warn: {bg: 'yellow', fg: 'blue'} + # error: {bg: 'yellow', fg: 'blue'} # info: {bg: 'yellow', fg: 'blue'} } try: { - # border_color: 'red' + # border_color: 'red' # highlighted_color: 'blue' # reactive: false } table: { - split_line: '#404040' + split_line: '#404040' cursor: true - line_index: true + line_index: true line_shift: true line_head_top: true line_head_bottom: true @@ -357,14 +356,14 @@ let-env config = { show_head: true show_index: true - # selected_cell: {fg: 'white', bg: '#777777'} - # selected_row: {fg: 'yellow', bg: '#C1C2A3'} + # selected_cell: {fg: 'white', bg: '#777777'} + # selected_row: {fg: 'yellow', bg: '#C1C2A3'} # selected_column: blue - # padding_column_right: 2 + # padding_column_right: 2 # padding_column_left: 2 - # padding_index_left: 2 + # padding_index_left: 2 # padding_index_right: 1 } diff --git a/src/tests/test_known_external.rs b/src/tests/test_known_external.rs index 30e65ab8ff..2462afc7f9 100644 --- a/src/tests/test_known_external.rs +++ b/src/tests/test_known_external.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test_contains, TestResult}; +use crate::tests::{fail_test, run_test, run_test_contains, TestResult}; // cargo version prints a string of the form: // cargo 1.60.0 (d1fd9fe2c 2022-03-01) @@ -10,10 +10,7 @@ fn known_external_runs() -> TestResult { #[test] fn known_external_unknown_flag() -> TestResult { - fail_test( - r#"extern "cargo version" []; cargo version --no-such-flag"#, - "command doesn't have flag", - ) + run_test_contains(r#"extern "cargo" []; cargo --version"#, "cargo") } /// GitHub issues #5179, #4618 @@ -33,3 +30,49 @@ fn known_external_subcommand_alias() -> TestResult { "cargo", ) } + +#[test] +fn known_external_complex_unknown_args() -> TestResult { + run_test_contains( + "extern echo []; echo foo -b -as -9 --abc -- -Dxmy=AKOO - bar", + "foo -b -as -9 --abc -- -Dxmy=AKOO - bar", + ) +} + +#[test] +fn known_external_batched_short_flag_arg_disallowed() -> TestResult { + fail_test( + "extern echo [-a, -b: int]; echo -ab 10", + "short flag batches", + ) +} + +#[test] +fn known_external_missing_positional() -> TestResult { + fail_test("extern echo [a]; echo", "missing_positional") +} + +#[test] +fn known_external_type_mismatch() -> TestResult { + fail_test("extern echo [a: int]; echo 1.234", "mismatch") +} + +#[test] +fn known_external_missing_flag_param() -> TestResult { + fail_test( + "extern echo [--foo: string]; echo --foo", + "missing_flag_param", + ) +} + +#[test] +fn known_external_misc_values() -> TestResult { + run_test( + r#" + let x = 'abc' + extern echo [] + echo $x [ a b c ] + "#, + "abc a b c", + ) +}