Enable conditional source and use patterns by allowing null as a no-op module (#14773)

Related:
- #14329
- #13872
- #8214

# Description & User-Facing Changes

This PR allows enables the following uses, which are all no-op.
```nushell
source null
source-env null
use null
overlay use null
```

The motivation for this change is conditional sourcing of files. For
example, with this change `login.nu` may be deprecated and replaced with
the following code in `config.nu`
```nushell
const login_module = if $nu.is-login { "login.nu" } else { null }
source $login_module
```

# Tests + Formatting
I'm hoping for CI to pass 😄

# After Submitting
Add a part about the conditional sourcing pattern to the website.
This commit is contained in:
Bahex 2025-01-09 15:37:27 +03:00 committed by GitHub
parent 5cf6dea997
commit 79f19f2fc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 160 additions and 25 deletions

View file

@ -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<PipelineData, ShellError> {
let noop = call.get_parser_info(caller_stack, "noop");
if noop.is_some() {
return Ok(PipelineData::empty());
}
let mut name_arg: Spanned<String> = call.req(engine_state, caller_stack, 0)?;
name_arg.item = trim_quotes_str(&name_arg.item).to_string();

View file

@ -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<PipelineData, ShellError> {
if call.get_parser_info(caller_stack, "noop").is_some() {
return Ok(PipelineData::empty());
}
let Some(Expression {
expr: Expr::ImportPattern(import_pattern),
..

View file

@ -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<PipelineData, ShellError> {
if call.get_parser_info(caller_stack, "noop").is_some() {
return Ok(PipelineData::empty());
}
let source_filename: Spanned<String> = 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<Example> {
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,
},
]
}
}

View file

@ -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<PipelineData, ShellError> {
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,
}
]
}
}

View file

@ -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<Call>) ->
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) => {

View file

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

View file

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