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)
|
.allow_variants_without_examples(true)
|
||||||
.required(
|
.required(
|
||||||
"name",
|
"name",
|
||||||
SyntaxShape::String,
|
SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::Nothing]),
|
||||||
"Module name to use overlay for.",
|
"Module name to use overlay for (`null` for no-op).",
|
||||||
)
|
)
|
||||||
.optional(
|
.optional(
|
||||||
"as",
|
"as",
|
||||||
|
@ -61,6 +61,11 @@ impl Command for OverlayUse {
|
||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> 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)?;
|
let mut name_arg: Spanned<String> = call.req(engine_state, caller_stack, 0)?;
|
||||||
name_arg.item = trim_quotes_str(&name_arg.item).to_string();
|
name_arg.item = trim_quotes_str(&name_arg.item).to_string();
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,11 @@ impl Command for Use {
|
||||||
Signature::build("use")
|
Signature::build("use")
|
||||||
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
||||||
.allow_variants_without_examples(true)
|
.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(
|
.rest(
|
||||||
"members",
|
"members",
|
||||||
SyntaxShape::Any,
|
SyntaxShape::Any,
|
||||||
|
@ -54,6 +58,9 @@ This command is a parser keyword. For details, check:
|
||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
if call.get_parser_info(caller_stack, "noop").is_some() {
|
||||||
|
return Ok(PipelineData::empty());
|
||||||
|
}
|
||||||
let Some(Expression {
|
let Some(Expression {
|
||||||
expr: Expr::ImportPattern(import_pattern),
|
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)])
|
.input_output_types(vec![(Type::Any, Type::Any)])
|
||||||
.required(
|
.required(
|
||||||
"filename",
|
"filename",
|
||||||
SyntaxShape::String, // type is string to avoid automatically canonicalizing the path
|
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.",
|
"The filepath to the script file to source the environment from (`null` for no-op).",
|
||||||
)
|
)
|
||||||
.category(Category::Core)
|
.category(Category::Core)
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,10 @@ impl Command for SourceEnv {
|
||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> 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)?;
|
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
|
// 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> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![Example {
|
vec![
|
||||||
|
Example {
|
||||||
description: "Sources the environment from foo.nu in the current context",
|
description: "Sources the environment from foo.nu in the current context",
|
||||||
example: r#"source-env foo.nu"#,
|
example: r#"source-env foo.nu"#,
|
||||||
result: None,
|
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)])
|
.input_output_types(vec![(Type::Any, Type::Any)])
|
||||||
.required(
|
.required(
|
||||||
"filename",
|
"filename",
|
||||||
SyntaxShape::Filepath,
|
SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::Nothing]),
|
||||||
"The filepath to the script file to source.",
|
"The filepath to the script file to source (`null` for no-op).",
|
||||||
)
|
)
|
||||||
.category(Category::Core)
|
.category(Category::Core)
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,9 @@ impl Command for Source {
|
||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> 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:
|
// Note: two hidden positionals are used here that are injected by the parser:
|
||||||
// 1. The block_id that corresponded to the 0th position
|
// 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
|
// 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"#,
|
example: r#"source ./foo.nu; say-hi"#,
|
||||||
result: None,
|
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_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),
|
expr: Expr::ImportPattern(import_pattern),
|
||||||
..
|
..
|
||||||
} = &import_pattern_expr
|
} => import_pattern.clone(),
|
||||||
{
|
_ => {
|
||||||
import_pattern.clone()
|
|
||||||
} else {
|
|
||||||
working_set.error(ParseError::UnknownState(
|
working_set.error(ParseError::UnknownState(
|
||||||
"internal error: Import pattern positional is not import pattern".into(),
|
"internal error: Import pattern positional is not import pattern".into(),
|
||||||
import_pattern_expr.span,
|
import_pattern_expr.span,
|
||||||
));
|
));
|
||||||
return (garbage_pipeline(working_set, spans), vec![]);
|
return (garbage_pipeline(working_set, spans), vec![]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mut import_pattern, module, module_id) = if let Some(module_id) = import_pattern.head.id {
|
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) {
|
let (overlay_name, overlay_name_span) = if let Some(expr) = call.positional_nth(0) {
|
||||||
match eval_constant(working_set, expr) {
|
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(val) => match val.coerce_into_string() {
|
||||||
Ok(s) => (s, expr.span),
|
Ok(s) => (s, expr.span),
|
||||||
Err(err) => {
|
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() {
|
let filename = match val.coerce_into_string() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
|
@ -15,7 +15,7 @@ use nu_engine::DIR_VAR_PARSER_INFO;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::*, engine::StateWorkingSet, eval_const::eval_constant, BlockId, DeclId, DidYouMean,
|
ast::*, engine::StateWorkingSet, eval_const::eval_constant, BlockId, DeclId, DidYouMean,
|
||||||
FilesizeUnit, Flag, ParseError, PositionalArg, Signature, Span, Spanned, SyntaxShape, Type,
|
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::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
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 head_expr = parse_value(working_set, *head_span, &SyntaxShape::Any);
|
||||||
|
|
||||||
let (maybe_module_id, head_name) = match eval_constant(working_set, &head_expr) {
|
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(val) => match val.coerce_into_string() {
|
||||||
Ok(s) => (working_set.find_module(s.as_bytes()), s.into_bytes()),
|
Ok(s) => (working_set.find_module(s.as_bytes()), s.into_bytes()),
|
||||||
Err(err) => {
|
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]
|
#[test]
|
||||||
fn main_script_help_uses_script_name1() {
|
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
|
// Note: this test is somewhat fragile and might need to be adapted if the usage help message changes
|
||||||
|
|
Loading…
Reference in a new issue