mirror of
https://github.com/nushell/nushell
synced 2025-01-13 05:38:57 +00:00
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:
parent
5cf6dea997
commit
79f19f2fc7
7 changed files with 160 additions and 25 deletions
|
@ -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();
|
||||
|
||||
|
|
|
@ -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),
|
||||
..
|
||||
|
|
19
crates/nu-command/src/env/source_env.rs
vendored
19
crates/nu-command/src/env/source_env.rs
vendored
|
@ -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 {
|
||||
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,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
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_expr
|
||||
{
|
||||
import_pattern.clone()
|
||||
} else {
|
||||
} => 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) => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue