diff --git a/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs b/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs index 1263fcaa2a..189fd39daf 100644 --- a/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs +++ b/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs @@ -24,8 +24,8 @@ impl Command for OverlayUse { .allow_variants_without_examples(true) .required( "name", - SyntaxShape::String, - "Module name to use overlay for.", + SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::Nothing]), + "Module name to use overlay for (`null` for no-op).", ) .optional( "as", @@ -61,6 +61,11 @@ impl Command for OverlayUse { call: &Call, input: PipelineData, ) -> Result { + let noop = call.get_parser_info(caller_stack, "noop"); + if noop.is_some() { + return Ok(PipelineData::empty()); + } + let mut name_arg: Spanned = call.req(engine_state, caller_stack, 0)?; name_arg.item = trim_quotes_str(&name_arg.item).to_string(); diff --git a/crates/nu-cmd-lang/src/core_commands/use_.rs b/crates/nu-cmd-lang/src/core_commands/use_.rs index 978638456e..3968e288de 100644 --- a/crates/nu-cmd-lang/src/core_commands/use_.rs +++ b/crates/nu-cmd-lang/src/core_commands/use_.rs @@ -22,7 +22,11 @@ impl Command for Use { Signature::build("use") .input_output_types(vec![(Type::Nothing, Type::Nothing)]) .allow_variants_without_examples(true) - .required("module", SyntaxShape::String, "Module or module file.") + .required( + "module", + SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::Nothing]), + "Module or module file (`null` for no-op).", + ) .rest( "members", SyntaxShape::Any, @@ -54,6 +58,9 @@ This command is a parser keyword. For details, check: call: &Call, input: PipelineData, ) -> Result { + if call.get_parser_info(caller_stack, "noop").is_some() { + return Ok(PipelineData::empty()); + } let Some(Expression { expr: Expr::ImportPattern(import_pattern), .. diff --git a/crates/nu-command/src/env/source_env.rs b/crates/nu-command/src/env/source_env.rs index 348418c247..342c31aed4 100644 --- a/crates/nu-command/src/env/source_env.rs +++ b/crates/nu-command/src/env/source_env.rs @@ -19,8 +19,8 @@ impl Command for SourceEnv { .input_output_types(vec![(Type::Any, Type::Any)]) .required( "filename", - SyntaxShape::String, // type is string to avoid automatically canonicalizing the path - "The filepath to the script file to source the environment from.", + SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::Nothing]), // type is string to avoid automatically canonicalizing the path + "The filepath to the script file to source the environment from (`null` for no-op).", ) .category(Category::Core) } @@ -45,6 +45,10 @@ impl Command for SourceEnv { call: &Call, input: PipelineData, ) -> Result { + if call.get_parser_info(caller_stack, "noop").is_some() { + return Ok(PipelineData::empty()); + } + let source_filename: Spanned = call.req(engine_state, caller_stack, 0)?; // Note: this hidden positional is the block_id that corresponded to the 0th position @@ -99,10 +103,17 @@ impl Command for SourceEnv { } fn examples(&self) -> Vec { - vec![Example { - description: "Sources the environment from foo.nu in the current context", - example: r#"source-env foo.nu"#, - result: None, - }] + vec![ + Example { + description: "Sources the environment from foo.nu in the current context", + example: r#"source-env foo.nu"#, + result: None, + }, + Example { + description: "Sourcing `null` is a no-op.", + example: r#"source-env null"#, + result: None, + }, + ] } } diff --git a/crates/nu-command/src/misc/source.rs b/crates/nu-command/src/misc/source.rs index a12a57ebef..c1be441bb0 100644 --- a/crates/nu-command/src/misc/source.rs +++ b/crates/nu-command/src/misc/source.rs @@ -16,8 +16,8 @@ impl Command for Source { .input_output_types(vec![(Type::Any, Type::Any)]) .required( "filename", - SyntaxShape::Filepath, - "The filepath to the script file to source.", + SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::Nothing]), + "The filepath to the script file to source (`null` for no-op).", ) .category(Category::Core) } @@ -42,6 +42,9 @@ impl Command for Source { call: &Call, input: PipelineData, ) -> Result { + if call.get_parser_info(stack, "noop").is_some() { + return Ok(PipelineData::empty()); + } // Note: two hidden positionals are used here that are injected by the parser: // 1. The block_id that corresponded to the 0th position // 2. The block_id_name that corresponded to the file name at the 0th position @@ -107,6 +110,16 @@ impl Command for Source { example: r#"source ./foo.nu; say-hi"#, result: None, }, + Example { + description: "Sourcing `null` is a no-op.", + example: r#"source null"#, + result: None, + }, + Example { + description: "Source can be used with const variables.", + example: r#"const file = if $nu.is-interactive { "interactive.nu" } else { null }; source $file"#, + result: None, + } ] } } diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index e01743321c..0830545075 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -2367,18 +2367,37 @@ pub fn parse_use( let import_pattern_expr = parse_import_pattern(working_set, args_spans); - let import_pattern = if let Expression { - expr: Expr::ImportPattern(import_pattern), - .. - } = &import_pattern_expr - { - import_pattern.clone() - } else { - working_set.error(ParseError::UnknownState( - "internal error: Import pattern positional is not import pattern".into(), - import_pattern_expr.span, - )); - return (garbage_pipeline(working_set, spans), vec![]); + let import_pattern = match &import_pattern_expr { + Expression { + expr: Expr::Nothing, + .. + } => { + let mut call = call; + call.set_parser_info( + "noop".to_string(), + Expression::new_unknown(Expr::Nothing, Span::unknown(), Type::Nothing), + ); + return ( + Pipeline::from_vec(vec![Expression::new( + working_set, + Expr::Call(call), + Span::concat(spans), + Type::Any, + )]), + vec![], + ); + } + Expression { + expr: Expr::ImportPattern(import_pattern), + .. + } => import_pattern.clone(), + _ => { + working_set.error(ParseError::UnknownState( + "internal error: Import pattern positional is not import pattern".into(), + import_pattern_expr.span, + )); + return (garbage_pipeline(working_set, spans), vec![]); + } }; let (mut import_pattern, module, module_id) = if let Some(module_id) = import_pattern.head.id { @@ -2755,6 +2774,19 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box) -> let (overlay_name, overlay_name_span) = if let Some(expr) = call.positional_nth(0) { match eval_constant(working_set, expr) { + Ok(Value::Nothing { .. }) => { + let mut call = call; + call.set_parser_info( + "noop".to_string(), + Expression::new_unknown(Expr::Bool(true), Span::unknown(), Type::Bool), + ); + return Pipeline::from_vec(vec![Expression::new( + working_set, + Expr::Call(call), + call_span, + Type::Any, + )]); + } Ok(val) => match val.coerce_into_string() { Ok(s) => (s, expr.span), Err(err) => { @@ -3494,6 +3526,20 @@ pub fn parse_source(working_set: &mut StateWorkingSet, lite_command: &LiteComman } }; + if val.is_nothing() { + let mut call = call; + call.set_parser_info( + "noop".to_string(), + Expression::new_unknown(Expr::Nothing, Span::unknown(), Type::Nothing), + ); + return Pipeline::from_vec(vec![Expression::new( + working_set, + Expr::Call(call), + Span::concat(spans), + Type::Any, + )]); + } + let filename = match val.coerce_into_string() { Ok(s) => s, Err(err) => { diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 277ce83e7f..93c8cf4425 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -15,7 +15,7 @@ use nu_engine::DIR_VAR_PARSER_INFO; use nu_protocol::{ ast::*, engine::StateWorkingSet, eval_const::eval_constant, BlockId, DeclId, DidYouMean, FilesizeUnit, Flag, ParseError, PositionalArg, Signature, Span, Spanned, SyntaxShape, Type, - VarId, ENV_VARIABLE_ID, IN_VARIABLE_ID, + Value, VarId, ENV_VARIABLE_ID, IN_VARIABLE_ID, }; use std::{ collections::{HashMap, HashSet}, @@ -3054,6 +3054,14 @@ pub fn parse_import_pattern(working_set: &mut StateWorkingSet, spans: &[Span]) - let head_expr = parse_value(working_set, *head_span, &SyntaxShape::Any); let (maybe_module_id, head_name) = match eval_constant(working_set, &head_expr) { + Ok(Value::Nothing { .. }) => { + return Expression::new( + working_set, + Expr::Nothing, + Span::concat(spans), + Type::Nothing, + ); + } Ok(val) => match val.coerce_into_string() { Ok(s) => (working_set.find_module(s.as_bytes()), s.into_bytes()), Err(err) => { diff --git a/tests/shell/mod.rs b/tests/shell/mod.rs index c2df76005b..6f7d60aa58 100644 --- a/tests/shell/mod.rs +++ b/tests/shell/mod.rs @@ -335,6 +335,51 @@ fn source_empty_file() { }) } +#[test] +fn source_use_null() { + let actual = nu!(r#"source null"#); + assert!(actual.out.is_empty()); + assert!(actual.err.is_empty()); + + let actual = nu!(r#"source-env null"#); + assert!(actual.out.is_empty()); + assert!(actual.err.is_empty()); + + let actual = nu!(r#"use null"#); + assert!(actual.out.is_empty()); + assert!(actual.err.is_empty()); + + let actual = nu!(r#"overlay use null"#); + assert!(actual.out.is_empty()); + assert!(actual.err.is_empty()); +} + +#[test] +fn source_use_file_named_null() { + Playground::setup("source_file_named_null", |dirs, sandbox| { + sandbox.with_files(&[FileWithContent( + "null", + r#"export-env { print "hello world" }"#, + )]); + + let actual = nu!(cwd: dirs.test(), r#"source "null""#); + assert!(actual.out.contains("hello world")); + assert!(actual.err.is_empty()); + + let actual = nu!(cwd: dirs.test(), r#"source-env "null""#); + assert!(actual.out.contains("hello world")); + assert!(actual.err.is_empty()); + + let actual = nu!(cwd: dirs.test(), r#"use "null""#); + assert!(actual.out.contains("hello world")); + assert!(actual.err.is_empty()); + + let actual = nu!(cwd: dirs.test(), r#"overlay use "null""#); + assert!(actual.out.contains("hello world")); + assert!(actual.err.is_empty()); + }) +} + #[test] fn main_script_help_uses_script_name1() { // Note: this test is somewhat fragile and might need to be adapted if the usage help message changes