Allow parse-time evaluation of calls, pipelines and subexpressions (#9499)

Co-authored-by: Antoine Stevan <44101798+amtoine@users.noreply.github.com>
This commit is contained in:
Jakub Žádník 2023-08-26 16:41:29 +03:00 committed by GitHub
parent 3d73287ea4
commit 5ac5b90aed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 849 additions and 161 deletions

View File

@ -1,5 +1,5 @@
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value, Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value,
}; };
@ -27,6 +27,10 @@ impl Command for Describe {
.category(Category::Core) .category(Category::Core)
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
_engine_state: &EngineState, _engine_state: &EngineState,
@ -34,6 +38,46 @@ impl Command for Describe {
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
run(call, input)
}
fn run_const(
&self,
_working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
run(call, input)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Describe the type of a string",
example: "'hello' | describe",
result: Some(Value::test_string("string")),
},
/*
Example {
description: "Describe a stream of data, collecting it first",
example: "[1 2 3] | each {|i| $i} | describe",
result: Some(Value::test_string("list<int> (stream)")),
},
Example {
description: "Describe the input but do not collect streams",
example: "[1 2 3] | each {|i| $i} | describe --no-collect",
result: Some(Value::test_string("stream")),
},
*/
]
}
fn search_terms(&self) -> Vec<&str> {
vec!["type", "typeof", "info", "structure"]
}
}
fn run(call: &Call, input: PipelineData) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
let no_collect: bool = call.has_flag("no-collect"); let no_collect: bool = call.has_flag("no-collect");
@ -67,33 +111,6 @@ impl Command for Describe {
span: head, span: head,
} }
.into_pipeline_data()) .into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Describe the type of a string",
example: "'hello' | describe",
result: Some(Value::test_string("string")),
},
/*
Example {
description: "Describe a stream of data, collecting it first",
example: "[1 2 3] | each {|i| $i} | describe",
result: Some(Value::test_string("list<int> (stream)")),
},
Example {
description: "Describe the input but do not collect streams",
example: "[1 2 3] | each {|i| $i} | describe --no-collect",
result: Some(Value::test_string("stream")),
},
*/
]
}
fn search_terms(&self) -> Vec<&str> {
vec!["type", "typeof", "info", "structure"]
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,6 +1,6 @@
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
Category, Example, ListStream, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Category, Example, ListStream, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
Value, Value,
@ -31,6 +31,10 @@ it returns it. Otherwise, it returns a list of the arguments. There is usually
little reason to use this over just writing the values as-is."# little reason to use this over just writing the values as-is."#
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -38,22 +42,18 @@ little reason to use this over just writing the values as-is."#
call: &Call, call: &Call,
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
call.rest(engine_state, stack, 0).map(|to_be_echoed| { let args = call.rest(engine_state, stack, 0);
let n = to_be_echoed.len(); run(engine_state, args, call)
match n.cmp(&1usize) {
// More than one value is converted in a stream of values
std::cmp::Ordering::Greater => PipelineData::ListStream(
ListStream::from_stream(to_be_echoed.into_iter(), engine_state.ctrlc.clone()),
None,
),
// But a single value can be forwarded as it is
std::cmp::Ordering::Equal => PipelineData::Value(to_be_echoed[0].clone(), None),
// When there are no elements, we echo the empty string
std::cmp::Ordering::Less => PipelineData::Value(Value::string("", call.head), None),
} }
})
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let args = call.rest_const(working_set, 0);
run(working_set.permanent(), args, call)
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
@ -76,6 +76,29 @@ little reason to use this over just writing the values as-is."#
} }
} }
fn run(
engine_state: &EngineState,
args: Result<Vec<Value>, ShellError>,
call: &Call,
) -> Result<PipelineData, ShellError> {
args.map(|to_be_echoed| {
let n = to_be_echoed.len();
match n.cmp(&1usize) {
// More than one value is converted in a stream of values
std::cmp::Ordering::Greater => PipelineData::ListStream(
ListStream::from_stream(to_be_echoed.into_iter(), engine_state.ctrlc.clone()),
None,
),
// But a single value can be forwarded as it is
std::cmp::Ordering::Equal => PipelineData::Value(to_be_echoed[0].clone(), None),
// When there are no elements, we echo the empty string
std::cmp::Ordering::Less => PipelineData::Value(Value::string("", call.head), None),
}
})
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
#[test] #[test]

View File

@ -1,5 +1,5 @@
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet};
use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Type, Value}; use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Type, Value};
#[derive(Clone)] #[derive(Clone)]
@ -24,6 +24,10 @@ impl Command for Ignore {
vec!["silent", "quiet", "out-null"] vec!["silent", "quiet", "out-null"]
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
_engine_state: &EngineState, _engine_state: &EngineState,
@ -35,6 +39,16 @@ impl Command for Ignore {
Ok(PipelineData::empty()) Ok(PipelineData::empty())
} }
fn run_const(
&self,
_working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
input.into_value(call.head);
Ok(PipelineData::empty())
}
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![Example { vec![Example {
description: "Ignore the output of an echo command", description: "Ignore the output of an echo command",

View File

@ -1,5 +1,5 @@
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, Record, ShellError, Signature, Type, Value, Category, Example, IntoPipelineData, PipelineData, Record, ShellError, Signature, Type, Value,
}; };
@ -26,14 +26,27 @@ impl Command for Version {
"Display Nu version, and its build configuration." "Display Nu version, and its build configuration."
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, _stack: &mut Stack,
call: &Call, call: &Call,
input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
version(engine_state, stack, call, input) version(engine_state, call)
}
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
version(working_set.permanent(), call)
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
@ -45,12 +58,7 @@ impl Command for Version {
} }
} }
pub fn version( pub fn version(engine_state: &EngineState, call: &Call) -> Result<PipelineData, ShellError> {
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
// Pre-allocate the arrays in the worst case (12 items): // Pre-allocate the arrays in the worst case (12 items):
// - version // - version
// - branch // - branch

View File

@ -3,7 +3,7 @@ use std::path::Path;
use super::PathSubcommandArguments; use super::PathSubcommandArguments;
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack}; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Spanned, engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value, SyntaxShape, Type, Value,
@ -45,6 +45,10 @@ impl Command for SubCommand {
"Get the final component of a path." "Get the final component of a path."
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -67,6 +71,27 @@ impl Command for SubCommand {
) )
} }
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let args = Arguments {
replace: call.get_flag_const(working_set, "replace")?,
};
// This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
input.map(
move |value| super::operate(&get_basename, &args, value, head),
working_set.permanent().ctrlc.clone(),
)
}
#[cfg(windows)] #[cfg(windows)]
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![

View File

@ -2,7 +2,7 @@ use std::path::Path;
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack}; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Spanned, engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value, SyntaxShape, Type, Value,
@ -53,6 +53,10 @@ impl Command for SubCommand {
"Get the parent directory of a path." "Get the parent directory of a path."
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -76,6 +80,28 @@ impl Command for SubCommand {
) )
} }
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let args = Arguments {
replace: call.get_flag_const(working_set, "replace")?,
num_levels: call.get_flag_const(working_set, "num-levels")?,
};
// This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
input.map(
move |value| super::operate(&get_dirname, &args, value, head),
working_set.permanent().ctrlc.clone(),
)
}
#[cfg(windows)] #[cfg(windows)]
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![

View File

@ -1,9 +1,9 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use nu_engine::current_dir; use nu_engine::{current_dir, current_dir_const};
use nu_path::expand_path_with; use nu_path::expand_path_with;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack}; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Type, Value, engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Type, Value,
}; };
@ -45,6 +45,10 @@ impl Command for SubCommand {
If you need to distinguish dirs and files, please use `path type`."# If you need to distinguish dirs and files, please use `path type`."#
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -66,6 +70,26 @@ If you need to distinguish dirs and files, please use `path type`."#
) )
} }
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let args = Arguments {
pwd: current_dir_const(working_set)?,
};
// This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
input.map(
move |value| super::operate(&exists, &args, value, head),
working_set.permanent().ctrlc.clone(),
)
}
#[cfg(windows)] #[cfg(windows)]
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![

View File

@ -1,9 +1,9 @@
use std::path::Path; use std::path::Path;
use nu_engine::env::current_dir_str; use nu_engine::env::{current_dir_str, current_dir_str_const};
use nu_path::{canonicalize_with, expand_path_with}; use nu_path::{canonicalize_with, expand_path_with};
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack}; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Type, Value, engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Type, Value,
}; };
@ -48,6 +48,10 @@ impl Command for SubCommand {
"Try to expand a path to its absolute form." "Try to expand a path to its absolute form."
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -71,6 +75,28 @@ impl Command for SubCommand {
) )
} }
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let args = Arguments {
strict: call.has_flag("strict"),
cwd: current_dir_str_const(working_set)?,
not_follow_symlink: call.has_flag("no-symlink"),
};
// This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
input.map(
move |value| super::operate(&expand, &args, value, head),
working_set.permanent().ctrlc.clone(),
)
}
#[cfg(windows)] #[cfg(windows)]
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![

View File

@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack}; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
engine::Command, Category, Example, PipelineData, Record, ShellError, Signature, Span, Spanned, engine::Command, Category, Example, PipelineData, Record, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value, SyntaxShape, Type, Value,
@ -46,6 +46,10 @@ impl Command for SubCommand {
the output of 'path parse' and 'path split' subcommands."# the output of 'path parse' and 'path split' subcommands."#
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -53,29 +57,24 @@ the output of 'path parse' and 'path split' subcommands."#
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head;
let args = Arguments { let args = Arguments {
append: call.rest(engine_state, stack, 0)?, append: call.rest(engine_state, stack, 0)?,
}; };
let metadata = input.metadata(); run(call, &args, input)
}
match input { fn run_const(
PipelineData::Value(val, md) => { &self,
Ok(PipelineData::Value(handle_value(val, &args, head), md)) working_set: &StateWorkingSet,
} call: &Call,
PipelineData::ListStream(..) => Ok(PipelineData::Value( input: PipelineData,
handle_value(input.into_value(head), &args, head), ) -> Result<PipelineData, ShellError> {
metadata, let args = Arguments {
)), append: call.rest_const(working_set, 0)?,
PipelineData::Empty { .. } => Err(ShellError::PipelineEmpty { dst_span: head }), };
_ => Err(ShellError::UnsupportedInput(
"Input value cannot be joined".to_string(), run(call, &args, input)
"value originates from here".into(),
head,
input.span().unwrap_or(call.head),
)),
}
} }
#[cfg(windows)] #[cfg(windows)]
@ -147,6 +146,27 @@ the output of 'path parse' and 'path split' subcommands."#
} }
} }
fn run(call: &Call, args: &Arguments, input: PipelineData) -> Result<PipelineData, ShellError> {
let head = call.head;
let metadata = input.metadata();
match input {
PipelineData::Value(val, md) => Ok(PipelineData::Value(handle_value(val, args, head), md)),
PipelineData::ListStream(..) => Ok(PipelineData::Value(
handle_value(input.into_value(head), args, head),
metadata,
)),
PipelineData::Empty { .. } => Err(ShellError::PipelineEmpty { dst_span: head }),
_ => Err(ShellError::UnsupportedInput(
"Input value cannot be joined".to_string(),
"value originates from here".into(),
head,
input.span().unwrap_or(call.head),
)),
}
}
fn handle_value(v: Value, args: &Arguments, head: Span) -> Value { fn handle_value(v: Value, args: &Arguments, head: Span) -> Value {
match v { match v {
Value::String { ref val, .. } => join_single(Path::new(val), head, args), Value::String { ref val, .. } => join_single(Path::new(val), head, args),

View File

@ -2,7 +2,7 @@ use std::path::Path;
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack}; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
engine::Command, Category, Example, PipelineData, Record, ShellError, Signature, Span, Spanned, engine::Command, Category, Example, PipelineData, Record, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value, SyntaxShape, Type, Value,
@ -48,6 +48,10 @@ impl Command for SubCommand {
On Windows, an extra 'prefix' column is added."# On Windows, an extra 'prefix' column is added."#
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -70,6 +74,27 @@ On Windows, an extra 'prefix' column is added."#
) )
} }
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let args = Arguments {
extension: call.get_flag_const(working_set, "extension")?,
};
// This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
input.map(
move |value| super::operate(&parse, &args, value, head),
working_set.permanent().ctrlc.clone(),
)
}
#[cfg(windows)] #[cfg(windows)]
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![

View File

@ -3,7 +3,7 @@ use std::path::Path;
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_path::expand_to_real_path; use nu_path::expand_to_real_path;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack}; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Spanned, engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value, SyntaxShape, Type, Value,
@ -52,6 +52,10 @@ absolute or both relative. The argument path needs to be a parent of the input
path."# path."#
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -74,6 +78,27 @@ path."#
) )
} }
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let args = Arguments {
path: call.req_const(working_set, 0)?,
};
// This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
input.map(
move |value| super::operate(&relative_to, &args, value, head),
working_set.permanent().ctrlc.clone(),
)
}
#[cfg(windows)] #[cfg(windows)]
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![

View File

@ -1,7 +1,7 @@
use std::path::{Component, Path}; use std::path::{Component, Path};
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack}; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Type, Value, engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Type, Value,
}; };
@ -36,6 +36,10 @@ impl Command for SubCommand {
"Split a path into a list based on the system's path separator." "Split a path into a list based on the system's path separator."
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -56,6 +60,25 @@ impl Command for SubCommand {
) )
} }
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let args = Arguments;
// This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
input.map(
move |value| super::operate(&split, &args, value, head),
working_set.permanent().ctrlc.clone(),
)
}
#[cfg(windows)] #[cfg(windows)]
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![

View File

@ -2,7 +2,7 @@ use std::path::Path;
use nu_path::expand_tilde; use nu_path::expand_tilde;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack}; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Type, Value, engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Type, Value,
}; };
@ -43,6 +43,10 @@ impl Command for SubCommand {
If nothing is found, an empty string will be returned."# If nothing is found, an empty string will be returned."#
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -63,6 +67,25 @@ If nothing is found, an empty string will be returned."#
) )
} }
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let args = Arguments;
// This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
input.map(
move |value| super::operate(&r#type, &args, value, head),
working_set.permanent().ctrlc.clone(),
)
}
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example { Example {

View File

@ -3,7 +3,7 @@ use nu_cmd_base::input_handler::{operate, CmdArgument};
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::ast::CellPath; use nu_protocol::ast::CellPath;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet};
use nu_protocol::Category; use nu_protocol::Category;
use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value}; use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
@ -62,6 +62,10 @@ impl Command for SubCommand {
vec!["size", "count"] vec!["size", "count"]
} }
fn is_const(&self) -> bool {
true
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -70,11 +74,17 @@ impl Command for SubCommand {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?; let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
let args = Arguments { run(cell_paths, engine_state, call, input)
cell_paths: (!cell_paths.is_empty()).then_some(cell_paths), }
graphemes: grapheme_flags(call)?,
}; fn run_const(
operate(action, args, input, call.head, engine_state.ctrlc.clone()) &self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let cell_paths: Vec<CellPath> = call.rest_const(working_set, 0)?;
run(cell_paths, working_set.permanent(), call, input)
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
@ -101,6 +111,19 @@ impl Command for SubCommand {
} }
} }
fn run(
cell_paths: Vec<CellPath>,
engine_state: &EngineState,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let args = Arguments {
cell_paths: (!cell_paths.is_empty()).then_some(cell_paths),
graphemes: grapheme_flags(call)?,
};
operate(action, args, input, call.head, engine_state.ctrlc.clone())
}
fn action(input: &Value, arg: &Arguments, head: Span) -> Value { fn action(input: &Value, arg: &Arguments, head: Span) -> Value {
match input { match input {
Value::String { val, .. } => Value::int( Value::String { val, .. } => Value::int(

View File

@ -34,3 +34,9 @@ fn echo_range_handles_exclusive_down() {
assert_eq!(actual.out, "[3,2]"); assert_eq!(actual.out, "[3,2]");
} }
#[test]
fn echo_const() {
let actual = nu!("const x = (echo spam); $x");
assert_eq!(actual.out, "spam");
}

View File

@ -81,3 +81,9 @@ fn replaces_basename_of_path_ending_with_double_dot() {
let expected = join_path_sep(&["some/file.txt/..", "eggs"]); let expected = join_path_sep(&["some/file.txt/..", "eggs"]);
assert_eq!(actual.out, expected); assert_eq!(actual.out, expected);
} }
#[test]
fn const_path_basename() {
let actual = nu!("const name = ('spam/eggs.txt' | path basename); $name");
assert_eq!(actual.out, "eggs.txt");
}

View File

@ -135,3 +135,9 @@ fn replaces_dirname_of_way_too_many_levels() {
let expected = join_path_sep(&["eggs", "some/dir/with/spam.txt"]); let expected = join_path_sep(&["eggs", "some/dir/with/spam.txt"]);
assert_eq!(actual.out, expected); assert_eq!(actual.out, expected);
} }
#[test]
fn const_path_dirname() {
let actual = nu!("const name = ('spam/eggs.txt' | path dirname); $name");
assert_eq!(actual.out, "spam");
}

View File

@ -57,3 +57,9 @@ fn checks_tilde_relative_path_exists() {
let actual = nu!("'~' | path exists"); let actual = nu!("'~' | path exists");
assert_eq!(actual.out, "true"); assert_eq!(actual.out, "true");
} }
#[test]
fn const_path_exists() {
let actual = nu!("const exists = ('~' | path exists); $exists");
assert_eq!(actual.out, "true");
}

View File

@ -66,6 +66,26 @@ fn expands_path_with_double_dot() {
}) })
} }
#[test]
fn const_path_expand() {
Playground::setup("const_path_expand", |dirs, sandbox| {
sandbox
.within("menu")
.with_files(vec![EmptyFile("spam.txt")]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
const result = ("menu/./spam.txt" | path expand);
$result
"#
));
let expected = dirs.test.join("menu").join("spam.txt");
assert_eq!(PathBuf::from(actual.out), expected);
})
}
#[cfg(windows)] #[cfg(windows)]
mod windows { mod windows {
use super::*; use super::*;

View File

@ -54,3 +54,10 @@ fn returns_joined_path_when_joining_empty_path() {
assert_eq!(actual.out, "foo.txt"); assert_eq!(actual.out, "foo.txt");
} }
#[test]
fn const_path_join() {
let actual = nu!("const name = ('spam' | path join 'eggs.txt'); $name");
let expected = join_path_sep(&["spam", "eggs.txt"]);
assert_eq!(actual.out, expected);
}

View File

@ -7,6 +7,7 @@ mod parse;
mod split; mod split;
mod type_; mod type_;
use nu_test_support::{nu, pipeline};
use std::path::MAIN_SEPARATOR; use std::path::MAIN_SEPARATOR;
/// Helper function that joins string literals with '/' or '\', based on host OS /// Helper function that joins string literals with '/' or '\', based on host OS
@ -32,3 +33,9 @@ fn joins_path_on_other_than_windows() {
assert_eq!(&actual, "sausage/bacon/spam"); assert_eq!(&actual, "sausage/bacon/spam");
} }
#[test]
fn const_path_relative_to() {
let actual = nu!("'/home/viking' | path relative-to '/home'");
assert_eq!(actual.out, "viking");
}

View File

@ -119,3 +119,15 @@ fn parses_into_correct_number_of_columns() {
assert_eq!(actual.out, expected); assert_eq!(actual.out, expected);
} }
#[test]
fn const_path_parse() {
let actual = nu!("const name = ('spam/eggs.txt' | path parse); $name.parent");
assert_eq!(actual.out, "spam");
let actual = nu!("const name = ('spam/eggs.txt' | path parse); $name.stem");
assert_eq!(actual.out, "eggs");
let actual = nu!("const name = ('spam/eggs.txt' | path parse); $name.extension");
assert_eq!(actual.out, "txt");
}

View File

@ -25,3 +25,13 @@ fn splits_correctly_single_path() {
assert_eq!(actual.out, "spam.txt"); assert_eq!(actual.out, "spam.txt");
} }
#[test]
fn splits_correctly_single_path_const() {
let actual = nu!(r#"
const result = ('home/viking/spam.txt' | path split);
$result | last
"#);
assert_eq!(actual.out, "spam.txt");
}

View File

@ -61,3 +61,22 @@ fn returns_type_of_existing_directory() {
assert_eq!(actual.out, "dir"); assert_eq!(actual.out, "dir");
}) })
} }
#[test]
fn returns_type_of_existing_file_const() {
Playground::setup("path_type_const", |dirs, sandbox| {
sandbox
.within("menu")
.with_files(vec![EmptyFile("spam.txt")]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
const ty = ("menu" | path type);
$ty
"#
));
assert_eq!(actual.out, "dir");
})
}

View File

@ -1,6 +1,7 @@
use nu_protocol::{ use nu_protocol::{
ast::Call, ast::Call,
engine::{EngineState, Stack}, engine::{EngineState, Stack, StateWorkingSet},
eval_const::eval_constant,
FromValue, ShellError, FromValue, ShellError,
}; };
@ -14,6 +15,12 @@ pub trait CallExt {
name: &str, name: &str,
) -> Result<Option<T>, ShellError>; ) -> Result<Option<T>, ShellError>;
fn get_flag_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
name: &str,
) -> Result<Option<T>, ShellError>;
fn rest<T: FromValue>( fn rest<T: FromValue>(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -21,6 +28,12 @@ pub trait CallExt {
starting_pos: usize, starting_pos: usize,
) -> Result<Vec<T>, ShellError>; ) -> Result<Vec<T>, ShellError>;
fn rest_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
starting_pos: usize,
) -> Result<Vec<T>, ShellError>;
fn opt<T: FromValue>( fn opt<T: FromValue>(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -35,6 +48,12 @@ pub trait CallExt {
pos: usize, pos: usize,
) -> Result<T, ShellError>; ) -> Result<T, ShellError>;
fn req_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
pos: usize,
) -> Result<T, ShellError>;
fn req_parser_info<T: FromValue>( fn req_parser_info<T: FromValue>(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -58,6 +77,19 @@ impl CallExt for Call {
} }
} }
fn get_flag_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
name: &str,
) -> Result<Option<T>, ShellError> {
if let Some(expr) = self.get_flag_expr(name) {
let result = eval_constant(working_set, &expr)?;
FromValue::from_value(&result).map(Some)
} else {
Ok(None)
}
}
fn rest<T: FromValue>( fn rest<T: FromValue>(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -74,6 +106,21 @@ impl CallExt for Call {
Ok(output) Ok(output)
} }
fn rest_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
starting_pos: usize,
) -> Result<Vec<T>, ShellError> {
let mut output = vec![];
for expr in self.positional_iter().skip(starting_pos) {
let result = eval_constant(working_set, expr)?;
output.push(FromValue::from_value(&result)?);
}
Ok(output)
}
fn opt<T: FromValue>( fn opt<T: FromValue>(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -107,6 +154,24 @@ impl CallExt for Call {
} }
} }
fn req_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
pos: usize,
) -> Result<T, ShellError> {
if let Some(expr) = self.positional_nth(pos) {
let result = eval_constant(working_set, expr)?;
FromValue::from_value(&result)
} else if self.positional_len() == 0 {
Err(ShellError::AccessEmptyContent { span: self.head })
} else {
Err(ShellError::AccessBeyondEnd {
max_idx: self.positional_len() - 1,
span: self.head,
})
}
}
fn req_parser_info<T: FromValue>( fn req_parser_info<T: FromValue>(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,

View File

@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use nu_protocol::ast::{Call, Expr, PathMember}; use nu_protocol::ast::{Call, Expr, PathMember};
use nu_protocol::engine::{EngineState, Stack}; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet, PWD_ENV};
use nu_protocol::{Config, PipelineData, ShellError, Span, Value, VarId}; use nu_protocol::{Config, PipelineData, ShellError, Span, Value, VarId};
use nu_path::canonicalize_with; use nu_path::canonicalize_with;
@ -159,8 +159,9 @@ pub fn env_to_strings(
/// Shorthand for env_to_string() for PWD with custom error /// Shorthand for env_to_string() for PWD with custom error
pub fn current_dir_str(engine_state: &EngineState, stack: &Stack) -> Result<String, ShellError> { pub fn current_dir_str(engine_state: &EngineState, stack: &Stack) -> Result<String, ShellError> {
if let Some(pwd) = stack.get_env_var(engine_state, "PWD") { if let Some(pwd) = stack.get_env_var(engine_state, PWD_ENV) {
match env_to_string("PWD", &pwd, engine_state, stack) { // TODO: PWD should be string by default, we don't need to run ENV_CONVERSIONS on it
match env_to_string(PWD_ENV, &pwd, engine_state, stack) {
Ok(cwd) => { Ok(cwd) => {
if Path::new(&cwd).is_absolute() { if Path::new(&cwd).is_absolute() {
Ok(cwd) Ok(cwd)
@ -187,11 +188,55 @@ pub fn current_dir_str(engine_state: &EngineState, stack: &Stack) -> Result<Stri
} }
} }
/// Simplified version of current_dir_str() for constant evaluation
pub fn current_dir_str_const(working_set: &StateWorkingSet) -> Result<String, ShellError> {
if let Some(pwd) = working_set.get_env_var(PWD_ENV) {
match pwd {
Value::String { val, span } => {
if Path::new(val).is_absolute() {
Ok(val.clone())
} else {
Err(ShellError::GenericError(
"Invalid current directory".to_string(),
format!("The 'PWD' environment variable must be set to an absolute path. Found: '{val}'"),
Some(*span),
None,
Vec::new()
))
}
}
_ => Err(ShellError::GenericError(
"PWD is not a string".to_string(),
"".to_string(),
None,
Some(
"Cusrrent working directory environment variable 'PWD' must be a string."
.to_string(),
),
Vec::new(),
)),
}
} else {
Err(ShellError::GenericError(
"Current directory not found".to_string(),
"".to_string(),
None,
Some("The environment variable 'PWD' was not found. It is required to define the current directory.".to_string()),
Vec::new(),
))
}
}
/// Calls current_dir_str() and returns the current directory as a PathBuf /// Calls current_dir_str() and returns the current directory as a PathBuf
pub fn current_dir(engine_state: &EngineState, stack: &Stack) -> Result<PathBuf, ShellError> { pub fn current_dir(engine_state: &EngineState, stack: &Stack) -> Result<PathBuf, ShellError> {
current_dir_str(engine_state, stack).map(PathBuf::from) current_dir_str(engine_state, stack).map(PathBuf::from)
} }
/// Version of current_dir() for constant evaluation
pub fn current_dir_const(working_set: &StateWorkingSet) -> Result<PathBuf, ShellError> {
current_dir_str_const(working_set).map(PathBuf::from)
}
/// Get the contents of path environment variable as a list of strings /// Get the contents of path environment variable as a list of strings
/// ///
/// On non-Windows: It will fetch PATH /// On non-Windows: It will fetch PATH

View File

@ -1,5 +1,4 @@
mod deparse; mod deparse;
mod eval;
mod flatten; mod flatten;
mod known_external; mod known_external;
mod lex; mod lex;

View File

@ -12,6 +12,7 @@ use nu_protocol::{
ImportPatternMember, Pipeline, PipelineElement, ImportPatternMember, Pipeline, PipelineElement,
}, },
engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME}, engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME},
eval_const::{eval_constant, value_as_string},
span, Alias, BlockId, Exportable, Module, ModuleId, ParseError, PositionalArg, span, Alias, BlockId, Exportable, Module, ModuleId, ParseError, PositionalArg,
ResolvedImportPattern, Span, Spanned, SyntaxShape, Type, VarId, ResolvedImportPattern, Span, Spanned, SyntaxShape, Type, VarId,
}; };
@ -24,7 +25,6 @@ pub const LIB_DIRS_VAR: &str = "NU_LIB_DIRS";
pub const PLUGIN_DIRS_VAR: &str = "NU_PLUGIN_DIRS"; pub const PLUGIN_DIRS_VAR: &str = "NU_PLUGIN_DIRS";
use crate::{ use crate::{
eval::{eval_constant, value_as_string},
is_math_expression_like, is_math_expression_like,
known_external::KnownExternal, known_external::KnownExternal,
lex, lex,
@ -2585,12 +2585,12 @@ pub fn parse_overlay_new(working_set: &mut StateWorkingSet, call: Box<Call>) ->
Ok(val) => match value_as_string(val, expr.span) { Ok(val) => match value_as_string(val, expr.span) {
Ok(s) => (s, expr.span), Ok(s) => (s, expr.span),
Err(err) => { Err(err) => {
working_set.error(err); working_set.error(err.wrap(working_set, call_span));
return garbage_pipeline(&[call_span]); return garbage_pipeline(&[call_span]);
} }
}, },
Err(err) => { Err(err) => {
working_set.error(err); working_set.error(err.wrap(working_set, call_span));
return garbage_pipeline(&[call_span]); return garbage_pipeline(&[call_span]);
} }
} }
@ -2634,12 +2634,12 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box<Call>) ->
Ok(val) => match value_as_string(val, expr.span) { Ok(val) => match value_as_string(val, expr.span) {
Ok(s) => (s, expr.span), Ok(s) => (s, expr.span),
Err(err) => { Err(err) => {
working_set.error(err); working_set.error(err.wrap(working_set, call_span));
return garbage_pipeline(&[call_span]); return garbage_pipeline(&[call_span]);
} }
}, },
Err(err) => { Err(err) => {
working_set.error(err); working_set.error(err.wrap(working_set, call_span));
return garbage_pipeline(&[call_span]); return garbage_pipeline(&[call_span]);
} }
} }
@ -2660,12 +2660,12 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box<Call>) ->
span: new_name_expression.span, span: new_name_expression.span,
}), }),
Err(err) => { Err(err) => {
working_set.error(err); working_set.error(err.wrap(working_set, call_span));
return garbage_pipeline(&[call_span]); return garbage_pipeline(&[call_span]);
} }
}, },
Err(err) => { Err(err) => {
working_set.error(err); working_set.error(err.wrap(working_set, call_span));
return garbage_pipeline(&[call_span]); return garbage_pipeline(&[call_span]);
} }
} }
@ -2851,12 +2851,12 @@ pub fn parse_overlay_hide(working_set: &mut StateWorkingSet, call: Box<Call>) ->
Ok(val) => match value_as_string(val, expr.span) { Ok(val) => match value_as_string(val, expr.span) {
Ok(s) => (s, expr.span), Ok(s) => (s, expr.span),
Err(err) => { Err(err) => {
working_set.error(err); working_set.error(err.wrap(working_set, call_span));
return garbage_pipeline(&[call_span]); return garbage_pipeline(&[call_span]);
} }
}, },
Err(err) => { Err(err) => {
working_set.error(err); working_set.error(err.wrap(working_set, call_span));
return garbage_pipeline(&[call_span]); return garbage_pipeline(&[call_span]);
} }
} }
@ -3107,7 +3107,7 @@ pub fn parse_const(working_set: &mut StateWorkingSet, spans: &[Span]) -> Pipelin
// Assign the constant value to the variable // Assign the constant value to the variable
working_set.set_variable_const_val(var_id, val); working_set.set_variable_const_val(var_id, val);
} }
Err(err) => working_set.error(err), Err(err) => working_set.error(err.wrap(working_set, rvalue.span)),
} }
} }
@ -3300,7 +3300,7 @@ pub fn parse_source(working_set: &mut StateWorkingSet, spans: &[Span]) -> Pipeli
let val = match eval_constant(working_set, &expr) { let val = match eval_constant(working_set, &expr) {
Ok(val) => val, Ok(val) => val,
Err(err) => { Err(err) => {
working_set.error(err); working_set.error(err.wrap(working_set, span(&spans[1..])));
return Pipeline::from_vec(vec![Expression { return Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call), expr: Expr::Call(call),
span: span(&spans[1..]), span: span(&spans[1..]),
@ -3313,7 +3313,7 @@ pub fn parse_source(working_set: &mut StateWorkingSet, spans: &[Span]) -> Pipeli
let filename = match value_as_string(val, spans[1]) { let filename = match value_as_string(val, spans[1]) {
Ok(s) => s, Ok(s) => s,
Err(err) => { Err(err) => {
working_set.error(err); working_set.error(err.wrap(working_set, span(&spans[1..])));
return Pipeline::from_vec(vec![Expression { return Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call), expr: Expr::Call(call),
span: span(&spans[1..]), span: span(&spans[1..]),
@ -3504,8 +3504,10 @@ pub fn parse_register(working_set: &mut StateWorkingSet, spans: &[Span]) -> Pipe
let arguments = call let arguments = call
.positional_nth(0) .positional_nth(0)
.map(|expr| { .map(|expr| {
let val = eval_constant(working_set, expr)?; let val =
let filename = value_as_string(val, expr.span)?; eval_constant(working_set, expr).map_err(|err| err.wrap(working_set, call.head))?;
let filename =
value_as_string(val, expr.span).map_err(|err| err.wrap(working_set, call.head))?;
let Some(path) = find_in_dirs(&filename, working_set, &cwd, PLUGIN_DIRS_VAR) else { let Some(path) = find_in_dirs(&filename, working_set, &cwd, PLUGIN_DIRS_VAR) else {
return Err(ParseError::RegisteredFileNotFound(filename, expr.span)) return Err(ParseError::RegisteredFileNotFound(filename, expr.span))

View File

@ -1,5 +1,4 @@
use crate::{ use crate::{
eval::{eval_constant, value_as_string},
lex::{lex, lex_signature}, lex::{lex, lex_signature},
lite_parser::{lite_parse, LiteCommand, LiteElement, LitePipeline}, lite_parser::{lite_parse, LiteCommand, LiteElement, LitePipeline},
parse_mut, parse_mut,
@ -16,6 +15,7 @@ use nu_protocol::{
Operator, PathMember, Pattern, Pipeline, PipelineElement, RangeInclusion, RangeOperator, Operator, PathMember, Pattern, Pipeline, PipelineElement, RangeInclusion, RangeOperator,
}, },
engine::StateWorkingSet, engine::StateWorkingSet,
eval_const::{eval_constant, value_as_string},
span, BlockId, DidYouMean, Flag, ParseError, PositionalArg, Signature, Span, Spanned, span, BlockId, DidYouMean, Flag, ParseError, PositionalArg, Signature, Span, Spanned,
SyntaxShape, Type, Unit, VarId, ENV_VARIABLE_ID, IN_VARIABLE_ID, SyntaxShape, Type, Unit, VarId, ENV_VARIABLE_ID, IN_VARIABLE_ID,
}; };
@ -2959,12 +2959,12 @@ pub fn parse_import_pattern(working_set: &mut StateWorkingSet, spans: &[Span]) -
Ok(val) => match value_as_string(val, head_expr.span) { Ok(val) => match value_as_string(val, head_expr.span) {
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) => {
working_set.error(err); working_set.error(err.wrap(working_set, span(spans)));
return garbage(span(spans)); return garbage(span(spans));
} }
}, },
Err(err) => { Err(err) => {
working_set.error(err); working_set.error(err.wrap(working_set, span(spans)));
return garbage(span(spans)); return garbage(span(spans));
} }
}; };

View File

@ -2,7 +2,7 @@ use std::path::PathBuf;
use crate::{ast::Call, Alias, BlockId, Example, PipelineData, ShellError, Signature}; use crate::{ast::Call, Alias, BlockId, Example, PipelineData, ShellError, Signature};
use super::{EngineState, Stack}; use super::{EngineState, Stack, StateWorkingSet};
#[derive(Debug)] #[derive(Debug)]
pub enum CommandType { pub enum CommandType {
@ -34,6 +34,19 @@ pub trait Command: Send + Sync + CommandClone {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError>; ) -> Result<PipelineData, ShellError>;
/// Used by the parser to run command at parse time
///
/// If a command has `is_const()` set to true, it must also implement this method.
#[allow(unused_variables)]
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
Err(ShellError::MissingConstEvalImpl { span: call.head })
}
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
Vec::new() Vec::new()
} }
@ -83,6 +96,11 @@ pub trait Command: Send + Sync + CommandClone {
None None
} }
// Whether can run in const evaluation in the parser
fn is_const(&self) -> bool {
false
}
// If command is a block i.e. def blah [] { }, get the block id // If command is a block i.e. def blah [] { }, get the block id
fn get_block_id(&self) -> Option<BlockId> { fn get_block_id(&self) -> Option<BlockId> {
None None

View File

@ -19,7 +19,7 @@ use std::sync::{
Arc, Mutex, Arc, Mutex,
}; };
static PWD_ENV: &str = "PWD"; pub static PWD_ENV: &str = "PWD";
/// Organizes usage messages for various primitives /// Organizes usage messages for various primitives
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -1090,6 +1090,10 @@ impl<'a> StateWorkingSet<'a> {
} }
} }
pub fn permanent(&self) -> &EngineState {
self.permanent_state
}
pub fn error(&mut self, parse_error: ParseError) { pub fn error(&mut self, parse_error: ParseError) {
self.parse_errors.push(parse_error) self.parse_errors.push(parse_error)
} }

View File

@ -1,16 +1,70 @@
use nu_protocol::{ use crate::{
ast::{Expr, Expression}, ast::{Block, Call, Expr, Expression, PipelineElement},
engine::StateWorkingSet, engine::StateWorkingSet,
ParseError, Record, Span, Value, PipelineData, Record, ShellError, Span, Value,
}; };
fn eval_const_call(
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let decl = working_set.get_decl(call.decl_id);
if !decl.is_const() {
return Err(ShellError::NotAConstCommand(call.head));
}
if !decl.is_known_external() && call.named_iter().any(|(flag, _, _)| flag.item == "help") {
// It would require re-implementing get_full_help() for const evaluation. Assuming that
// getting help messages at parse-time is rare enough, we can simply disallow it.
return Err(ShellError::NotAConstHelp(call.head));
}
decl.run_const(working_set, call, input)
}
fn eval_const_subexpression(
working_set: &StateWorkingSet,
expr: &Expression,
block: &Block,
mut input: PipelineData,
) -> Result<PipelineData, ShellError> {
for pipeline in block.pipelines.iter() {
for element in pipeline.elements.iter() {
let PipelineElement::Expression(_, expr) = element else {
return Err(ShellError::NotAConstant(expr.span));
};
input = eval_constant_with_input(working_set, expr, input)?
}
}
Ok(input)
}
fn eval_constant_with_input(
working_set: &StateWorkingSet,
expr: &Expression,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
match &expr.expr {
Expr::Call(call) => eval_const_call(working_set, call, input),
Expr::Subexpression(block_id) => {
let block = working_set.get_block(*block_id);
eval_const_subexpression(working_set, expr, block, input)
}
_ => eval_constant(working_set, expr).map(|v| PipelineData::Value(v, None)),
}
}
/// Evaluate a constant value at parse time /// Evaluate a constant value at parse time
/// ///
/// Based off eval_expression() in the engine /// Based off eval_expression() in the engine
pub fn eval_constant( pub fn eval_constant(
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
expr: &Expression, expr: &Expression,
) -> Result<Value, ParseError> { ) -> Result<Value, ShellError> {
match &expr.expr { match &expr.expr {
Expr::Bool(b) => Ok(Value::bool(*b, expr.span)), Expr::Bool(b) => Ok(Value::bool(*b, expr.span)),
Expr::Int(i) => Ok(Value::int(*i, expr.span)), Expr::Int(i) => Ok(Value::int(*i, expr.span)),
@ -25,7 +79,7 @@ pub fn eval_constant(
}), }),
Expr::Var(var_id) => match working_set.get_variable(*var_id).const_val.as_ref() { Expr::Var(var_id) => match working_set.get_variable(*var_id).const_val.as_ref() {
Some(val) => Ok(val.clone()), Some(val) => Ok(val.clone()),
None => Err(ParseError::NotAConstant(expr.span)), None => Err(ShellError::NotAConstant(expr.span)),
}, },
Expr::CellPath(cell_path) => Ok(Value::CellPath { Expr::CellPath(cell_path) => Ok(Value::CellPath {
val: cell_path.clone(), val: cell_path.clone(),
@ -37,10 +91,12 @@ pub fn eval_constant(
match value.follow_cell_path(&cell_path.tail, false) { match value.follow_cell_path(&cell_path.tail, false) {
Ok(val) => Ok(val), Ok(val) => Ok(val),
// TODO: Better error conversion // TODO: Better error conversion
Err(shell_error) => Err(ParseError::LabeledError( Err(shell_error) => Err(ShellError::GenericError(
"Error when following cell path".to_string(), "Error when following cell path".to_string(),
format!("{shell_error:?}"), format!("{shell_error:?}"),
expr.span, Some(expr.span),
None,
vec![],
)), )),
} }
} }
@ -112,25 +168,29 @@ pub fn eval_constant(
Expr::Nothing => Ok(Value::Nothing { span: expr.span }), Expr::Nothing => Ok(Value::Nothing { span: expr.span }),
Expr::ValueWithUnit(expr, unit) => { Expr::ValueWithUnit(expr, unit) => {
if let Ok(Value::Int { val, .. }) = eval_constant(working_set, expr) { if let Ok(Value::Int { val, .. }) = eval_constant(working_set, expr) {
unit.item.to_value(val, unit.span).map_err(|_| { unit.item.to_value(val, unit.span)
ParseError::InvalidLiteral(
"literal can not fit in unit".into(),
"literal can not fit in unit".into(),
unit.span,
)
})
} else { } else {
Err(ParseError::NotAConstant(expr.span)) Err(ShellError::NotAConstant(expr.span))
} }
} }
_ => Err(ParseError::NotAConstant(expr.span)), Expr::Call(call) => {
Ok(eval_const_call(working_set, call, PipelineData::empty())?.into_value(expr.span))
}
Expr::Subexpression(block_id) => {
let block = working_set.get_block(*block_id);
Ok(
eval_const_subexpression(working_set, expr, block, PipelineData::empty())?
.into_value(expr.span),
)
}
_ => Err(ShellError::NotAConstant(expr.span)),
} }
} }
/// Get the value as a string /// Get the value as a string
pub fn value_as_string(value: Value, span: Span) -> Result<String, ParseError> { pub fn value_as_string(value: Value, span: Span) -> Result<String, ShellError> {
match value { match value {
Value::String { val, .. } => Ok(val), Value::String { val, .. } => Ok(val),
_ => Err(ParseError::NotAConstant(span)), _ => Err(ShellError::NotAConstant(span)),
} }
} }

View File

@ -4,6 +4,7 @@ pub mod cli_error;
pub mod config; pub mod config;
mod did_you_mean; mod did_you_mean;
pub mod engine; pub mod engine;
pub mod eval_const;
mod example; mod example;
mod exportable; mod exportable;
mod id; mod id;

View File

@ -449,18 +449,6 @@ pub enum ParseError {
#[diagnostic(code(nu::shell::error_reading_file))] #[diagnostic(code(nu::shell::error_reading_file))]
ReadingFile(String, #[label("{0}")] Span), ReadingFile(String, #[label("{0}")] Span),
/// Tried assigning non-constant value to a constant
///
/// ## Resolution
///
/// Only a subset of expressions are allowed to be assigned as a constant during parsing.
#[error("Not a constant.")]
#[diagnostic(
code(nu::parser::not_a_constant),
help("Only a subset of expressions are allowed constants during parsing. Try using the 'const' command or typing the value literally.")
)]
NotAConstant(#[label = "Value is not a parse-time constant"] Span),
#[error("Invalid literal")] // <problem> in <entity>. #[error("Invalid literal")] // <problem> in <entity>.
#[diagnostic()] #[diagnostic()]
InvalidLiteral(String, String, #[label("{0} in {1}")] Span), InvalidLiteral(String, String, #[label("{0} in {1}")] Span),
@ -561,7 +549,6 @@ impl ParseError {
ParseError::ShellOutErrRedirect(s) => *s, ParseError::ShellOutErrRedirect(s) => *s,
ParseError::UnknownOperator(_, _, s) => *s, ParseError::UnknownOperator(_, _, s) => *s,
ParseError::InvalidLiteral(_, _, s) => *s, ParseError::InvalidLiteral(_, _, s) => *s,
ParseError::NotAConstant(s) => *s,
ParseError::LabeledErrorWithHelp { span: s, .. } => *s, ParseError::LabeledErrorWithHelp { span: s, .. } => *s,
} }
} }

View File

@ -2,7 +2,7 @@ use miette::Diagnostic;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::{ast::Operator, Span, Value}; use crate::{ast::Operator, engine::StateWorkingSet, format_error, ParseError, Span, Value};
/// The fundamental error type for the evaluation engine. These cases represent different kinds of errors /// The fundamental error type for the evaluation engine. These cases represent different kinds of errors
/// the evaluator might face, along with helpful spans to label. An error renderer will take this error value /// the evaluator might face, along with helpful spans to label. An error renderer will take this error value
@ -1074,6 +1074,72 @@ pub enum ShellError {
#[label("not a boolean expression")] #[label("not a boolean expression")]
span: Span, span: Span,
}, },
/// An attempt to run a command marked for constant evaluation lacking the const. eval.
/// implementation.
///
/// This is an internal Nushell error, please file an issue.
#[error("Missing const eval implementation")]
#[diagnostic(
code(nu::shell::missing_const_eval_implementation),
help(
"The command lacks an implementation for constant evaluation. \
This is an internal Nushell error, please file an issue https://github.com/nushell/nushell/issues."
)
)]
MissingConstEvalImpl {
#[label("command lacks constant implementation")]
span: Span,
},
/// Tried assigning non-constant value to a constant
///
/// ## Resolution
///
/// Only a subset of expressions are allowed to be assigned as a constant during parsing.
#[error("Not a constant.")]
#[diagnostic(
code(nu::shell::not_a_constant),
help("Only a subset of expressions are allowed constants during parsing. Try using the 'const' command or typing the value literally.")
)]
NotAConstant(#[label = "Value is not a parse-time constant"] Span),
/// Tried running a command that is not const-compatible
///
/// ## Resolution
///
/// Only a subset of builtin commands, and custom commands built only from those commands, can
/// run at parse time.
#[error("Not a const command.")]
#[diagnostic(
code(nu::shell::not_a_const_command),
help("Only a subset of builtin commands, and custom commands built only from those commands, can run at parse time.")
)]
NotAConstCommand(#[label = "This command cannot run at parse time."] Span),
/// Tried getting a help message at parse time.
///
/// ## Resolution
///
/// Help messages are not supported at parse time.
#[error("Help message not a constant.")]
#[diagnostic(
code(nu::shell::not_a_const_help),
help("Help messages are currently not supported to be constants.")
)]
NotAConstHelp(#[label = "Cannot get help message at parse time."] Span),
}
// TODO: Implement as From trait
impl ShellError {
pub fn wrap(self, working_set: &StateWorkingSet, span: Span) -> ParseError {
let msg = format_error(working_set, &self);
ParseError::LabeledError(
msg,
"Encountered error during parse-time evaluation".into(),
span,
)
}
} }
impl From<std::io::Error> for ShellError { impl From<std::io::Error> for ShellError {

View File

@ -352,17 +352,14 @@ fn default_value_constant2() -> TestResult {
} }
#[test] #[test]
fn default_value_not_constant1() -> TestResult { fn default_value_constant3() -> TestResult {
fail_test( run_test(r#"def foo [x = ("foo" | str length)] { $x }; foo"#, "3")
r#"def foo [x = ("foo" | str length)] { $x }; foo"#,
"expected a constant",
)
} }
#[test] #[test]
fn default_value_not_constant2() -> TestResult { fn default_value_not_constant2() -> TestResult {
fail_test( fail_test(
r#"def foo [--x = ("foo" | str length)] { $x }; foo"#, r#"def foo [x = (loop { break })] { $x }; foo"#,
"expected a constant", "expected a constant",
) )
} }

View File

@ -108,12 +108,30 @@ fn const_nothing() {
} }
#[test] #[test]
fn const_unsupported() { fn const_subexpression_supported() {
let inp = &["const x = ('abc' | str length)"]; let inp = &["const x = ('spam')", "$x"];
let actual = nu!(&inp.join("; ")); let actual = nu!(&inp.join("; "));
assert!(actual.err.contains("not_a_constant")); assert_eq!(actual.out, "spam");
}
#[test]
fn const_command_supported() {
let inp = &["const x = ('spam' | str length)", "$x"];
let actual = nu!(&inp.join("; "));
assert_eq!(actual.out, "4");
}
#[test]
fn const_command_unsupported() {
let inp = &["const x = (loop { break })"];
let actual = nu!(&inp.join("; "));
assert!(actual.err.contains("not_a_const_command"));
} }
#[test] #[test]
@ -125,6 +143,12 @@ fn const_in_scope() {
assert_eq!(actual.out, "x"); assert_eq!(actual.out, "x");
} }
#[test]
fn not_a_const_help() {
let actual = nu!("const x = ('abc' | str length -h)");
assert!(actual.err.contains("not_a_const_help"));
}
#[test] #[test]
fn complex_const_export() { fn complex_const_export() {
let inp = &[MODULE_SETUP, "use spam", "$spam.X"]; let inp = &[MODULE_SETUP, "use spam", "$spam.X"];
@ -250,3 +274,22 @@ fn complex_const_overlay_use_hide() {
let actual = nu!(&inp.join("; ")); let actual = nu!(&inp.join("; "));
assert!(actual.err.contains("nu::parser::variable_not_found")); assert!(actual.err.contains("nu::parser::variable_not_found"));
} }
// const implementations of commands without dedicated tests
#[test]
fn describe_const() {
let actual = nu!("const x = ('abc' | describe); $x");
assert_eq!(actual.out, "string");
}
#[test]
fn ignore_const() {
let actual = nu!("const x = (echo spam | ignore); $x == null");
assert_eq!(actual.out, "true");
}
#[test]
fn version_const() {
let actual = nu!("const x = (version); $x");
assert!(actual.err.is_empty());
}