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::engine::{Command, EngineState, Stack};
use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet};
use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value,
};
@ -27,6 +27,10 @@ impl Command for Describe {
.category(Category::Core)
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,
@ -34,39 +38,16 @@ impl Command for Describe {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
run(call, input)
}
let no_collect: bool = call.has_flag("no-collect");
let description = match input {
PipelineData::ExternalStream { .. } => "raw input".into(),
PipelineData::ListStream(_, _) => {
if no_collect {
"stream".into()
} else {
let value = input.into_value(head);
let base_description = match value {
Value::CustomValue { val, .. } => val.value_string(),
_ => value.get_type().to_string(),
};
format!("{base_description} (stream)")
}
}
_ => {
let value = input.into_value(head);
match value {
Value::CustomValue { val, .. } => val.value_string(),
_ => value.get_type().to_string(),
}
}
};
Ok(Value::String {
val: description,
span: head,
}
.into_pipeline_data())
fn run_const(
&self,
_working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
run(call, input)
}
fn examples(&self) -> Vec<Example> {
@ -96,6 +77,42 @@ impl Command for Describe {
}
}
fn run(call: &Call, input: PipelineData) -> Result<PipelineData, ShellError> {
let head = call.head;
let no_collect: bool = call.has_flag("no-collect");
let description = match input {
PipelineData::ExternalStream { .. } => "raw input".into(),
PipelineData::ListStream(_, _) => {
if no_collect {
"stream".into()
} else {
let value = input.into_value(head);
let base_description = match value {
Value::CustomValue { val, .. } => val.value_string(),
_ => value.get_type().to_string(),
};
format!("{base_description} (stream)")
}
}
_ => {
let value = input.into_value(head);
match value {
Value::CustomValue { val, .. } => val.value_string(),
_ => value.get_type().to_string(),
}
}
};
Ok(Value::String {
val: description,
span: head,
}
.into_pipeline_data())
}
#[cfg(test)]
mod test {
#[test]

View File

@ -1,6 +1,6 @@
use nu_engine::CallExt;
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, ListStream, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
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."#
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,
@ -38,22 +42,18 @@ little reason to use this over just writing the values as-is."#
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
call.rest(engine_state, stack, 0).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,
),
let args = call.rest(engine_state, stack, 0);
run(engine_state, args, call)
}
// 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> {
@ -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)]
mod test {
#[test]

View File

@ -1,5 +1,5 @@
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};
#[derive(Clone)]
@ -24,6 +24,10 @@ impl Command for Ignore {
vec!["silent", "quiet", "out-null"]
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,
@ -35,6 +39,16 @@ impl Command for Ignore {
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> {
vec![Example {
description: "Ignore the output of an echo command",

View File

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

View File

@ -3,7 +3,7 @@ use std::path::Path;
use super::PathSubcommandArguments;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{
engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value,
@ -45,6 +45,10 @@ impl Command for SubCommand {
"Get the final component of a path."
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
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)]
fn examples(&self) -> Vec<Example> {
vec![

View File

@ -2,7 +2,7 @@ use std::path::Path;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{
engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value,
@ -53,6 +53,10 @@ impl Command for SubCommand {
"Get the parent directory of a path."
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
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)]
fn examples(&self) -> Vec<Example> {
vec![

View File

@ -1,9 +1,9 @@
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_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{
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`."#
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
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)]
fn examples(&self) -> Vec<Example> {
vec![

View File

@ -1,9 +1,9 @@
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_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{
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."
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
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)]
fn examples(&self) -> Vec<Example> {
vec![

View File

@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{
engine::Command, Category, Example, PipelineData, Record, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value,
@ -46,6 +46,10 @@ impl Command for SubCommand {
the output of 'path parse' and 'path split' subcommands."#
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,
@ -53,29 +57,24 @@ the output of 'path parse' and 'path split' subcommands."#
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let args = Arguments {
append: call.rest(engine_state, stack, 0)?,
};
let metadata = input.metadata();
run(call, &args, input)
}
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 run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let args = Arguments {
append: call.rest_const(working_set, 0)?,
};
run(call, &args, input)
}
#[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 {
match v {
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_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{
engine::Command, Category, Example, PipelineData, Record, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value,
@ -48,6 +48,10 @@ impl Command for SubCommand {
On Windows, an extra 'prefix' column is added."#
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
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)]
fn examples(&self) -> Vec<Example> {
vec![

View File

@ -3,7 +3,7 @@ use std::path::Path;
use nu_engine::CallExt;
use nu_path::expand_to_real_path;
use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{
engine::Command, Category, Example, PipelineData, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value,
@ -52,6 +52,10 @@ absolute or both relative. The argument path needs to be a parent of the input
path."#
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
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)]
fn examples(&self) -> Vec<Example> {
vec![

View File

@ -1,7 +1,7 @@
use std::path::{Component, Path};
use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{
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."
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
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)]
fn examples(&self) -> Vec<Example> {
vec![

View File

@ -2,7 +2,7 @@ use std::path::Path;
use nu_path::expand_tilde;
use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
use nu_protocol::{
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."#
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
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> {
vec![
Example {

View File

@ -3,7 +3,7 @@ use nu_cmd_base::input_handler::{operate, CmdArgument};
use nu_engine::CallExt;
use nu_protocol::ast::Call;
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::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value};
use unicode_segmentation::UnicodeSegmentation;
@ -62,6 +62,10 @@ impl Command for SubCommand {
vec!["size", "count"]
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,
@ -70,11 +74,17 @@ impl Command for SubCommand {
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
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())
run(cell_paths, engine_state, call, input)
}
fn run_const(
&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> {
@ -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 {
match input {
Value::String { val, .. } => Value::int(

View File

@ -34,3 +34,9 @@ fn echo_range_handles_exclusive_down() {
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"]);
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"]);
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");
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)]
mod windows {
use super::*;

View File

@ -54,3 +54,10 @@ fn returns_joined_path_when_joining_empty_path() {
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 type_;
use nu_test_support::{nu, pipeline};
use std::path::MAIN_SEPARATOR;
/// 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");
}
#[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);
}
#[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");
}
#[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");
})
}
#[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::{
ast::Call,
engine::{EngineState, Stack},
engine::{EngineState, Stack, StateWorkingSet},
eval_const::eval_constant,
FromValue, ShellError,
};
@ -14,6 +15,12 @@ pub trait CallExt {
name: &str,
) -> Result<Option<T>, ShellError>;
fn get_flag_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
name: &str,
) -> Result<Option<T>, ShellError>;
fn rest<T: FromValue>(
&self,
engine_state: &EngineState,
@ -21,6 +28,12 @@ pub trait CallExt {
starting_pos: usize,
) -> Result<Vec<T>, ShellError>;
fn rest_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
starting_pos: usize,
) -> Result<Vec<T>, ShellError>;
fn opt<T: FromValue>(
&self,
engine_state: &EngineState,
@ -35,6 +48,12 @@ pub trait CallExt {
pos: usize,
) -> Result<T, ShellError>;
fn req_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
pos: usize,
) -> Result<T, ShellError>;
fn req_parser_info<T: FromValue>(
&self,
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>(
&self,
engine_state: &EngineState,
@ -74,6 +106,21 @@ impl CallExt for Call {
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>(
&self,
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>(
&self,
engine_state: &EngineState,

View File

@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf};
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_path::canonicalize_with;
@ -159,8 +159,9 @@ pub fn env_to_strings(
/// Shorthand for env_to_string() for PWD with custom error
pub fn current_dir_str(engine_state: &EngineState, stack: &Stack) -> Result<String, ShellError> {
if let Some(pwd) = stack.get_env_var(engine_state, "PWD") {
match env_to_string("PWD", &pwd, engine_state, stack) {
if let Some(pwd) = stack.get_env_var(engine_state, PWD_ENV) {
// 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) => {
if Path::new(&cwd).is_absolute() {
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
pub fn current_dir(engine_state: &EngineState, stack: &Stack) -> Result<PathBuf, ShellError> {
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
///
/// On non-Windows: It will fetch PATH

View File

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

View File

@ -12,6 +12,7 @@ use nu_protocol::{
ImportPatternMember, Pipeline, PipelineElement,
},
engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME},
eval_const::{eval_constant, value_as_string},
span, Alias, BlockId, Exportable, Module, ModuleId, ParseError, PositionalArg,
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";
use crate::{
eval::{eval_constant, value_as_string},
is_math_expression_like,
known_external::KnownExternal,
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(s) => (s, expr.span),
Err(err) => {
working_set.error(err);
working_set.error(err.wrap(working_set, call_span));
return garbage_pipeline(&[call_span]);
}
},
Err(err) => {
working_set.error(err);
working_set.error(err.wrap(working_set, 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(s) => (s, expr.span),
Err(err) => {
working_set.error(err);
working_set.error(err.wrap(working_set, call_span));
return garbage_pipeline(&[call_span]);
}
},
Err(err) => {
working_set.error(err);
working_set.error(err.wrap(working_set, 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,
}),
Err(err) => {
working_set.error(err);
working_set.error(err.wrap(working_set, call_span));
return garbage_pipeline(&[call_span]);
}
},
Err(err) => {
working_set.error(err);
working_set.error(err.wrap(working_set, 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(s) => (s, expr.span),
Err(err) => {
working_set.error(err);
working_set.error(err.wrap(working_set, call_span));
return garbage_pipeline(&[call_span]);
}
},
Err(err) => {
working_set.error(err);
working_set.error(err.wrap(working_set, 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
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) {
Ok(val) => val,
Err(err) => {
working_set.error(err);
working_set.error(err.wrap(working_set, span(&spans[1..])));
return Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
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]) {
Ok(s) => s,
Err(err) => {
working_set.error(err);
working_set.error(err.wrap(working_set, span(&spans[1..])));
return Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
span: span(&spans[1..]),
@ -3504,8 +3504,10 @@ pub fn parse_register(working_set: &mut StateWorkingSet, spans: &[Span]) -> Pipe
let arguments = call
.positional_nth(0)
.map(|expr| {
let val = eval_constant(working_set, expr)?;
let filename = value_as_string(val, expr.span)?;
let val =
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 {
return Err(ParseError::RegisteredFileNotFound(filename, expr.span))

View File

@ -1,5 +1,4 @@
use crate::{
eval::{eval_constant, value_as_string},
lex::{lex, lex_signature},
lite_parser::{lite_parse, LiteCommand, LiteElement, LitePipeline},
parse_mut,
@ -16,6 +15,7 @@ use nu_protocol::{
Operator, PathMember, Pattern, Pipeline, PipelineElement, RangeInclusion, RangeOperator,
},
engine::StateWorkingSet,
eval_const::{eval_constant, value_as_string},
span, BlockId, DidYouMean, Flag, ParseError, PositionalArg, Signature, Span, Spanned,
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(s) => (working_set.find_module(s.as_bytes()), s.into_bytes()),
Err(err) => {
working_set.error(err);
working_set.error(err.wrap(working_set, span(spans)));
return garbage(span(spans));
}
},
Err(err) => {
working_set.error(err);
working_set.error(err.wrap(working_set, 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 super::{EngineState, Stack};
use super::{EngineState, Stack, StateWorkingSet};
#[derive(Debug)]
pub enum CommandType {
@ -34,6 +34,19 @@ pub trait Command: Send + Sync + CommandClone {
input: PipelineData,
) -> 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> {
Vec::new()
}
@ -83,6 +96,11 @@ pub trait Command: Send + Sync + CommandClone {
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
fn get_block_id(&self) -> Option<BlockId> {
None

View File

@ -19,7 +19,7 @@ use std::sync::{
Arc, Mutex,
};
static PWD_ENV: &str = "PWD";
pub static PWD_ENV: &str = "PWD";
/// Organizes usage messages for various primitives
#[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) {
self.parse_errors.push(parse_error)
}

View File

@ -1,16 +1,70 @@
use nu_protocol::{
ast::{Expr, Expression},
use crate::{
ast::{Block, Call, Expr, Expression, PipelineElement},
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
///
/// Based off eval_expression() in the engine
pub fn eval_constant(
working_set: &StateWorkingSet,
expr: &Expression,
) -> Result<Value, ParseError> {
) -> Result<Value, ShellError> {
match &expr.expr {
Expr::Bool(b) => Ok(Value::bool(*b, 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() {
Some(val) => Ok(val.clone()),
None => Err(ParseError::NotAConstant(expr.span)),
None => Err(ShellError::NotAConstant(expr.span)),
},
Expr::CellPath(cell_path) => Ok(Value::CellPath {
val: cell_path.clone(),
@ -37,10 +91,12 @@ pub fn eval_constant(
match value.follow_cell_path(&cell_path.tail, false) {
Ok(val) => Ok(val),
// TODO: Better error conversion
Err(shell_error) => Err(ParseError::LabeledError(
Err(shell_error) => Err(ShellError::GenericError(
"Error when following cell path".to_string(),
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::ValueWithUnit(expr, unit) => {
if let Ok(Value::Int { val, .. }) = eval_constant(working_set, expr) {
unit.item.to_value(val, unit.span).map_err(|_| {
ParseError::InvalidLiteral(
"literal can not fit in unit".into(),
"literal can not fit in unit".into(),
unit.span,
)
})
unit.item.to_value(val, unit.span)
} 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
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 {
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;
mod did_you_mean;
pub mod engine;
pub mod eval_const;
mod example;
mod exportable;
mod id;

View File

@ -449,18 +449,6 @@ pub enum ParseError {
#[diagnostic(code(nu::shell::error_reading_file))]
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>.
#[diagnostic()]
InvalidLiteral(String, String, #[label("{0} in {1}")] Span),
@ -561,7 +549,6 @@ impl ParseError {
ParseError::ShellOutErrRedirect(s) => *s,
ParseError::UnknownOperator(_, _, s) => *s,
ParseError::InvalidLiteral(_, _, s) => *s,
ParseError::NotAConstant(s) => *s,
ParseError::LabeledErrorWithHelp { span: s, .. } => *s,
}
}

View File

@ -2,7 +2,7 @@ use miette::Diagnostic;
use serde::{Deserialize, Serialize};
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 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")]
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 {

View File

@ -75,7 +75,7 @@ fn scope_variable() -> TestResult {
fn scope_command_defaults(#[case] var: &str, #[case] exp_result: &str) -> TestResult {
run_test(
&format!(
r#"def t1 [a:int b?:float=1.23 --flag1:string --flag2:float=4.56] {{ true }};
r#"def t1 [a:int b?:float=1.23 --flag1:string --flag2:float=4.56] {{ true }};
let rslt = (scope commands | where name == 't1' | get signatures.0.any | where parameter_name == '{var}' | get parameter_default.0);
$"<($rslt)> ($rslt | describe)""#
),
@ -352,17 +352,14 @@ fn default_value_constant2() -> TestResult {
}
#[test]
fn default_value_not_constant1() -> TestResult {
fail_test(
r#"def foo [x = ("foo" | str length)] { $x }; foo"#,
"expected a constant",
)
fn default_value_constant3() -> TestResult {
run_test(r#"def foo [x = ("foo" | str length)] { $x }; foo"#, "3")
}
#[test]
fn default_value_not_constant2() -> TestResult {
fail_test(
r#"def foo [--x = ("foo" | str length)] { $x }; foo"#,
r#"def foo [x = (loop { break })] { $x }; foo"#,
"expected a constant",
)
}

View File

@ -108,12 +108,30 @@ fn const_nothing() {
}
#[test]
fn const_unsupported() {
let inp = &["const x = ('abc' | str length)"];
fn const_subexpression_supported() {
let inp = &["const x = ('spam')", "$x"];
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]
@ -125,6 +143,12 @@ fn const_in_scope() {
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]
fn complex_const_export() {
let inp = &[MODULE_SETUP, "use spam", "$spam.X"];
@ -250,3 +274,22 @@ fn complex_const_overlay_use_hide() {
let actual = nu!(&inp.join("; "));
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());
}