Jakub Žádník 2022-12-22 00:33:26 +02:00 committed by GitHub
parent 440feaf74a
commit 757d7479af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 379 additions and 151 deletions

View File

@ -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 {

View File

@ -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<String> = 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<String> = 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<String> = 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<String> = 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<String> = 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<String> = vec!["--foo".into(), "-b".into(), "-f".into()];
match_suggestions(expected, suggestions);
}

View File

@ -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<String> = call.req(engine_state, stack, 0)?;
let name_span = name.span;
let args: Vec<Spanned<String>> = call.rest(engine_state, stack, 1)?;
let args_expr: Vec<nu_protocol::ast::Expression> =
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

View File

@ -52,70 +52,19 @@ impl Command for External {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let name: Spanned<String> = call.req(engine_state, stack, 0)?;
let args: Vec<Value> = 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<Spanned<String>, 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<Expression> = 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<ExternalCommand, ShellError> {
let name: Spanned<String> = call.req(engine_state, stack, 0)?;
let args: Vec<Value> = 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<Spanned<String>, 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<Expression> = 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<String>,

View File

@ -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");
})
}

View File

@ -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;

View File

@ -96,6 +96,7 @@ impl Command for KnownExternal {
extern_call.add_positional(arg.clone());
}
}
Argument::Unknown(unknown) => extern_call.add_unknown(unknown.clone()),
}
}

View File

@ -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(),

View File

@ -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(|| {

View File

@ -7,6 +7,7 @@ use crate::{DeclId, Span, Spanned};
pub enum Argument {
Positional(Expression),
Named((Spanned<String>, Option<Spanned<String>>, Option<Expression>)),
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<Item = &Expression> {
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),
})
}

View File

@ -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,

View File

@ -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
}

View File

@ -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",
)
}