diff --git a/crates/nu-cmd-lang/src/core_commands/export_extern_wrapped.rs b/crates/nu-cmd-lang/src/core_commands/export_extern_wrapped.rs new file mode 100644 index 0000000000..6c5f60fcd1 --- /dev/null +++ b/crates/nu-cmd-lang/src/core_commands/export_extern_wrapped.rs @@ -0,0 +1,56 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type}; + +#[derive(Clone)] +pub struct ExportExternWrapped; + +impl Command for ExportExternWrapped { + fn name(&self) -> &str { + "export extern-wrapped" + } + + fn usage(&self) -> &str { + "Define an extern with a custom code block and export it from a module." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export extern-wrapped") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .required("def_name", SyntaxShape::String, "definition name") + .required("params", SyntaxShape::Signature, "parameters") + .required("body", SyntaxShape::Block, "wrapper code block") + .category(Category::Core) + } + + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn is_parser_keyword(&self) -> bool { + true + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Export the signature for an external command", + example: r#"export extern-wrapped my-echo [...rest] { echo $rest }"#, + result: None, + }] + } + + fn search_terms(&self) -> Vec<&str> { + vec!["signature", "module", "declare"] + } +} diff --git a/crates/nu-cmd-lang/src/core_commands/mod.rs b/crates/nu-cmd-lang/src/core_commands/mod.rs index 8b0f2d33ec..ad75a84875 100644 --- a/crates/nu-cmd-lang/src/core_commands/mod.rs +++ b/crates/nu-cmd-lang/src/core_commands/mod.rs @@ -15,6 +15,7 @@ mod export_const; mod export_def; mod export_def_env; mod export_extern; +mod export_extern_wrapped; mod export_module; mod export_use; mod extern_; @@ -55,6 +56,7 @@ pub use export_const::ExportConst; pub use export_def::ExportDef; pub use export_def_env::ExportDefEnv; pub use export_extern::ExportExtern; +pub use export_extern_wrapped::ExportExternWrapped; pub use export_module::ExportModule; pub use export_use::ExportUse; pub use extern_::Extern; diff --git a/crates/nu-cmd-lang/src/default_context.rs b/crates/nu-cmd-lang/src/default_context.rs index daf6a5ecbe..b4178d4cef 100644 --- a/crates/nu-cmd-lang/src/default_context.rs +++ b/crates/nu-cmd-lang/src/default_context.rs @@ -33,6 +33,7 @@ pub fn create_default_context() -> EngineState { ExportDef, ExportDefEnv, ExportExtern, + ExportExternWrapped, ExportUse, ExportModule, Extern, diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index d3709c82ae..186422f6a6 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -49,6 +49,7 @@ pub const UNALIASABLE_PARSER_KEYWORDS: &[&[u8]] = &[ b"extern", b"extern-wrapped", b"export extern", + b"export extern-wrapped", b"alias", b"export alias", b"export-env", @@ -211,7 +212,7 @@ pub fn parse_def_predecl(working_set: &mut StateWorkingSet, spans: &[Span]) { } signature.name = name.clone(); - //let decl = signature.predeclare(); + let decl = KnownExternal { name, usage: "run external command".into(), @@ -536,19 +537,17 @@ pub fn parse_extern( // Checking that the function is used with the correct name // Maybe this is not necessary but it is a sanity check - let (name_span, split_id) = if spans.len() > 1 - && (working_set.get_span_contents(spans[0]) == b"export" - || working_set.get_span_contents(spans[0]) == b"export-wrapped") - { - (spans[1], 2) - } else { - (spans[0], 1) - }; + let (name_span, split_id) = + if spans.len() > 1 && (working_set.get_span_contents(spans[0]) == b"export") { + (spans[1], 2) + } else { + (spans[0], 1) + }; let extern_call = working_set.get_span_contents(name_span).to_vec(); if extern_call != b"extern" && extern_call != b"extern-wrapped" { working_set.error(ParseError::UnknownState( - "internal error: Wrong call name for extern function".into(), + "internal error: Wrong call name for extern or extern-wrapped command".into(), span(spans), )); return garbage_pipeline(spans); @@ -1011,6 +1010,7 @@ pub fn parse_export_in_block( } b"export module" => parse_module(working_set, lite_command, None).0, b"export extern" => parse_extern(working_set, lite_command, None), + b"export extern-wrapped" => parse_extern(working_set, lite_command, None), _ => { working_set.error(ParseError::UnexpectedKeyword( String::from_utf8_lossy(&full_name).to_string(), @@ -1193,13 +1193,15 @@ pub fn parse_export_in_module( comments: lite_command.comments.clone(), parts: spans[1..].to_vec(), }; + let extern_name = [b"export ", kw_name].concat(); + let pipeline = parse_extern(working_set, &lite_command, Some(module_name)); - let export_def_decl_id = if let Some(id) = working_set.find_decl(b"export extern") { + let export_def_decl_id = if let Some(id) = working_set.find_decl(&extern_name) { id } else { working_set.error(ParseError::InternalError( - "missing 'export extern' command".into(), + "missing 'export extern' or 'export extern-wrapped' command".into(), export_span, )); return (garbage_pipeline(spans), vec![]); @@ -1460,7 +1462,7 @@ pub fn parse_export_in_module( } _ => { working_set.error(ParseError::Expected( - "def, def-env, alias, use, module, or extern keyword", + "def, def-env, alias, use, module, const, extern or extern-wrapped keyword", spans[1], )); @@ -1469,9 +1471,9 @@ pub fn parse_export_in_module( } } else { working_set.error(ParseError::MissingPositional( - "def, def-env, extern, alias, use, or module keyword".into(), + "def, def-env, alias, use, module, const, extern or extern-wrapped keyword".to_string(), Span::new(export_span.end, export_span.end), - "def, def-env, extern, alias, use, or module keyword.".to_string(), + "def, def-env, alias, use, module, const, extern or extern-wrapped keyword".to_string(), )); vec![] @@ -1794,7 +1796,7 @@ pub fn parse_module_block( } _ => { working_set.error(ParseError::ExpectedKeyword( - "def, const, def-env, extern, alias, use, module, export or export-env keyword".into(), + "def, const, def-env, extern, extern-wrapped, alias, use, module, export or export-env keyword".into(), command.parts[0], )); diff --git a/src/tests/test_known_external.rs b/src/tests/test_known_external.rs index a0e3b662f7..5d7ee01f59 100644 --- a/src/tests/test_known_external.rs +++ b/src/tests/test_known_external.rs @@ -39,6 +39,34 @@ fn known_external_complex_unknown_args() -> TestResult { ) } +#[test] +fn known_external_from_module() -> TestResult { + run_test_contains( + r#"module spam { + export extern echo [] + } + + use spam echo + echo foo -b -as -9 --abc -- -Dxmy=AKOO - bar + "#, + "foo -b -as -9 --abc -- -Dxmy=AKOO - bar", + ) +} + +#[test] +fn known_external_wrapped_from_module() -> TestResult { + run_test_contains( + r#"module spam { + export extern-wrapped my-echo [...rest] { ^echo $rest } + } + + use spam + spam my-echo foo -b -as -9 --abc -- -Dxmy=AKOO - bar + "#, + "foo -b -as -9 --abc -- -Dxmy=AKOO - bar", + ) +} + #[test] fn known_external_short_flag_batch_arg_allowed() -> TestResult { run_test_contains("extern echo [-a, -b: int]; echo -ab 10", "-b 10") diff --git a/tests/shell/pipeline/commands/external.rs b/tests/shell/pipeline/commands/external.rs index 12f35fca2e..d78a5a515c 100644 --- a/tests/shell/pipeline/commands/external.rs +++ b/tests/shell/pipeline/commands/external.rs @@ -121,7 +121,7 @@ fn command_not_found_error_suggests_typo_fix() { } #[test] -fn command_not_found_error_shows_not_found() { +fn command_not_found_error_shows_not_found_1() { let actual = nu!(r#" export extern "foo" []; foo @@ -129,6 +129,15 @@ fn command_not_found_error_shows_not_found() { assert!(actual.err.contains("'foo' was not found")); } +#[test] +fn command_not_found_error_shows_not_found_2() { + let actual = nu!(r#" + export extern-wrapped my-foo [...rest] { foo }; + my-foo + "#); + assert!(actual.err.contains("did you mean")); +} + #[test] fn command_substitution_wont_output_extra_newline() { let actual = nu!(r#"