Allow NU_LIBS_DIR and friends to be const (#8310)

# Description

Allow NU_LIBS_DIR and friends to be const they can be updated within the
same parse pass. This will allow us to remove having multiple config
files eventually.

Small implementation detail: I've changed `call.parser_info` to a
hashmap with string keys, so the information can have names rather than
indices, and we don't have to worry too much about the order in which we
put things into it.

Closes https://github.com/nushell/nushell/issues/8422

# User-Facing Changes

In a single file, users can now do stuff like
```
const NU_LIBS_DIR = ['/some/path/here']
source script.nu
```
and the source statement will use the value of NU_LIBS_DIR declared the
line before.

Currently, if there is no `NU_LIBS_DIR` const, then we fallback to using
the value of the `NU_LIBS_DIR` env-var, so there are no breaking changes
(unless someone named a const NU_LIBS_DIR for some reason).


![2023-03-04-014103_hyprshot](https://user-images.githubusercontent.com/13265529/222885263-135cdd0d-7884-438b-b2ed-c3979fa44463.png)

# Tests + Formatting

~~TODO: write tests~~ Done

# After Submitting

~~TODO: update docs~~ Will do when we update default_env.nu/merge
default_env.nu into default_config.nu.
This commit is contained in:
StevenDoesStuffs 2023-03-17 07:23:29 -05:00 committed by GitHub
parent 7095d8994e
commit 400a9d3b1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 265 additions and 117 deletions

View File

@ -6,7 +6,7 @@ use nu_protocol::{
PipelineData, Span, Type, Value,
};
use reedline::Suggestion;
use std::sync::Arc;
use std::{collections::HashMap, sync::Arc};
use super::completer::map_value_completions;
@ -66,7 +66,7 @@ impl Completer for CustomCompletion {
],
redirect_stdout: true,
redirect_stderr: true,
parser_info: vec![],
parser_info: HashMap::new(),
},
PipelineData::empty(),
);

View File

@ -1,4 +1,4 @@
use nu_engine::{eval_block, find_in_dirs_env, redirect_env, CallExt};
use nu_engine::{eval_block, find_in_dirs_env, get_dirs_var_from_call, redirect_env, CallExt};
use nu_parser::trim_quotes_str;
use nu_protocol::ast::{Call, Expr};
use nu_protocol::engine::{Command, EngineState, Stack};
@ -66,23 +66,24 @@ impl Command for OverlayUse {
let mut name_arg: Spanned<String> = call.req(engine_state, caller_stack, 0)?;
name_arg.item = trim_quotes_str(&name_arg.item).to_string();
let maybe_origin_module_id = if let Some(overlay_expr) = call.parser_info_nth(0) {
if let Expr::Overlay(module_id) = overlay_expr.expr {
module_id
let maybe_origin_module_id =
if let Some(overlay_expr) = call.get_parser_info("overlay_expr") {
if let Expr::Overlay(module_id) = overlay_expr.expr {
module_id
} else {
return Err(ShellError::NushellFailedSpanned {
msg: "Not an overlay".to_string(),
label: "requires an overlay (path or a string)".to_string(),
span: overlay_expr.span,
});
}
} else {
return Err(ShellError::NushellFailedSpanned {
msg: "Not an overlay".to_string(),
label: "requires an overlay (path or a string)".to_string(),
span: overlay_expr.span,
msg: "Missing positional".to_string(),
label: "missing required overlay".to_string(),
span: call.head,
});
}
} else {
return Err(ShellError::NushellFailedSpanned {
msg: "Missing positional".to_string(),
label: "missing required overlay".to_string(),
span: call.head,
});
};
};
let overlay_name = if let Some(name) = call.opt(engine_state, caller_stack, 1)? {
name
@ -113,7 +114,12 @@ impl Command for OverlayUse {
// Evaluate the export-env block (if any) and keep its environment
if let Some(block_id) = module.env_block {
let maybe_path = find_in_dirs_env(&name_arg.item, engine_state, caller_stack)?;
let maybe_path = find_in_dirs_env(
&name_arg.item,
engine_state,
caller_stack,
get_dirs_var_from_call(call),
)?;
let block = engine_state.get_block(block_id);
let mut callee_stack = caller_stack.gather_captures(&block.captures);

View File

@ -1,4 +1,4 @@
use nu_engine::{eval_block, find_in_dirs_env, redirect_env};
use nu_engine::{eval_block, find_in_dirs_env, get_dirs_var_from_call, redirect_env};
use nu_protocol::ast::{Call, Expr, Expression};
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
@ -48,7 +48,7 @@ impl Command for Use {
let import_pattern = if let Some(Expression {
expr: Expr::ImportPattern(pat),
..
}) = call.parser_info_nth(0)
}) = call.get_parser_info("import_pattern")
{
pat
} else {
@ -72,9 +72,12 @@ impl Command for Use {
let module_arg_str = String::from_utf8_lossy(
engine_state.get_span_contents(&import_pattern.head.span),
);
let maybe_parent = if let Some(path) =
find_in_dirs_env(&module_arg_str, engine_state, caller_stack)?
{
let maybe_parent = if let Some(path) = find_in_dirs_env(
&module_arg_str,
engine_state,
caller_stack,
get_dirs_var_from_call(call),
)? {
path.parent().map(|p| p.to_path_buf()).or(None)
} else {
None

View File

@ -45,7 +45,7 @@ impl Command for Source {
) -> Result<PipelineData, ShellError> {
// Note: this hidden positional is the block_id that corresponded to the 0th position
// it is put here by the parser
let block_id: i64 = call.req_parser_info(engine_state, stack, 0)?;
let block_id: i64 = call.req_parser_info(engine_state, stack, "block_id")?;
let block = engine_state.get_block(block_id as usize).clone();
eval_block_with_early_return(

View File

@ -1,6 +1,8 @@
use std::path::PathBuf;
use nu_engine::{eval_block_with_early_return, find_in_dirs_env, redirect_env, CallExt};
use nu_engine::{
eval_block_with_early_return, find_in_dirs_env, get_dirs_var_from_call, redirect_env, CallExt,
};
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
@ -42,12 +44,15 @@ impl Command for SourceEnv {
// Note: this hidden positional is the block_id that corresponded to the 0th position
// it is put here by the parser
let block_id: i64 = call.req_parser_info(engine_state, caller_stack, 0)?;
let block_id: i64 = call.req_parser_info(engine_state, caller_stack, "block_id")?;
// Set the currently evaluated directory (file-relative PWD)
let mut parent = if let Some(path) =
find_in_dirs_env(&source_filename.item, engine_state, caller_stack)?
{
let mut parent = if let Some(path) = find_in_dirs_env(
&source_filename.item,
engine_state,
caller_stack,
get_dirs_var_from_call(call),
)? {
PathBuf::from(&path)
} else {
return Err(ShellError::FileNotFound(source_filename.span));

View File

@ -1,4 +1,4 @@
use nu_engine::{find_in_dirs_env, CallExt};
use nu_engine::{find_in_dirs_env, get_dirs_var_from_call, CallExt};
use nu_parser::{parse, parse_module_block, unescape_unquote_string};
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet};
@ -106,7 +106,12 @@ impl Command for NuCheck {
_ => {
if let Some(path_str) = path {
// look up the path as relative to FILE_PWD or inside NU_LIB_DIRS (same process as source-env)
let path = match find_in_dirs_env(&path_str.item, engine_state, stack) {
let path = match find_in_dirs_env(
&path_str.item,
engine_state,
stack,
get_dirs_var_from_call(call),
) {
Ok(path) => {
if let Some(path) = path {
path

View File

@ -39,7 +39,7 @@ pub trait CallExt {
&self,
engine_state: &EngineState,
stack: &mut Stack,
pos: usize,
name: &str,
) -> Result<T, ShellError>;
}
@ -111,9 +111,9 @@ impl CallExt for Call {
&self,
engine_state: &EngineState,
stack: &mut Stack,
pos: usize,
name: &str,
) -> Result<T, ShellError> {
if let Some(expr) = self.parser_info_nth(pos) {
if let Some(expr) = self.get_parser_info(name) {
let result = eval_expression(engine_state, stack, expr)?;
FromValue::from_value(&result)
} else if self.parser_info.is_empty() {

View File

@ -1,9 +1,9 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use nu_protocol::ast::PathMember;
use nu_protocol::ast::{Call, Expr, PathMember};
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::{PipelineData, ShellError, Span, Value};
use nu_protocol::{PipelineData, ShellError, Span, Value, VarId};
use nu_path::canonicalize_with;
@ -18,8 +18,6 @@ const ENV_PATH_NAME: &str = "PATH";
const ENV_CONVERSIONS: &str = "ENV_CONVERSIONS";
static LIB_DIRS_ENV: &str = "NU_LIB_DIRS";
enum ConversionResult {
Ok(Value),
ConversionError(ShellError), // Failure during the conversion itself
@ -224,6 +222,17 @@ pub fn path_str(
env_to_string(pathname, &pathval, engine_state, stack)
}
pub const DIR_VAR_PARSER_INFO: &str = "dirs_var";
pub fn get_dirs_var_from_call(call: &Call) -> Option<VarId> {
call.get_parser_info(DIR_VAR_PARSER_INFO).and_then(|x| {
if let Expr::Var(id) = x.expr {
Some(id)
} else {
None
}
})
}
/// This helper function is used to find files during eval
///
/// First, the actual current working directory is selected as
@ -239,6 +248,7 @@ pub fn find_in_dirs_env(
filename: &str,
engine_state: &EngineState,
stack: &Stack,
dirs_var: Option<VarId>,
) -> Result<Option<PathBuf>, ShellError> {
// Choose whether to use file-relative or PWD-relative path
let cwd = if let Some(pwd) = stack.get_env_var(engine_state, "FILE_PWD") {
@ -262,36 +272,32 @@ pub fn find_in_dirs_env(
current_dir_str(engine_state, stack)?
};
if let Ok(p) = canonicalize_with(filename, &cwd) {
Ok(Some(p))
} else {
let path = Path::new(filename);
if path.is_relative() {
if let Some(lib_dirs) = stack.get_env_var(engine_state, LIB_DIRS_ENV) {
if let Ok(dirs) = lib_dirs.as_list() {
for lib_dir in dirs {
if let Ok(dir) = lib_dir.as_path() {
// make sure the dir is absolute path
if let Ok(dir_abs) = canonicalize_with(dir, &cwd) {
if let Ok(path) = canonicalize_with(filename, dir_abs) {
return Ok(Some(path));
}
}
}
}
Ok(None)
} else {
Ok(None)
}
} else {
Ok(None)
}
} else {
Ok(None)
Ok((|| -> Option<PathBuf> {
if let Ok(p) = canonicalize_with(filename, &cwd) {
return Some(p);
}
}
let path = Path::new(filename);
if !path.is_relative() {
return None;
}
let lib_dirs = dirs_var.and_then(|dirs_var| engine_state.find_constant(dirs_var, &[]));
// TODO: remove (see #8310)
let lib_dirs_fallback = stack.get_env_var(engine_state, "NU_LIB_DIRS");
let lib_dirs = lib_dirs.or(lib_dirs_fallback.as_ref())?;
lib_dirs
.as_list()
.ok()?
.iter()
.map(|lib_dir| -> Option<PathBuf> {
let dir = lib_dir.as_path().ok()?;
let dir_abs = canonicalize_with(dir, &cwd).ok()?;
canonicalize_with(filename, dir_abs).ok()
})
.find(Option::is_some)
.flatten()
})())
}
fn get_converted_value(

View File

@ -828,7 +828,7 @@ pub fn eval_element_with_input(
],
redirect_stdout: false,
redirect_stderr: false,
parser_info: vec![],
parser_info: HashMap::new(),
},
input,
)
@ -899,7 +899,7 @@ pub fn eval_element_with_input(
],
redirect_stdout: false,
redirect_stderr: false,
parser_info: vec![],
parser_info: HashMap::new(),
},
input,
)

View File

@ -1,4 +1,5 @@
use log::trace;
use nu_engine::get_dirs_var_from_call;
use nu_path::canonicalize_with;
use nu_protocol::{
ast::{
@ -7,13 +8,14 @@ use nu_protocol::{
},
engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME},
span, Alias, BlockId, Exportable, Module, PositionalArg, Span, Spanned, SyntaxShape, Type,
VarId,
};
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
static LIB_DIRS_ENV: &str = "NU_LIB_DIRS";
pub const LIB_DIRS_VAR: &str = "NU_LIB_DIRS";
#[cfg(feature = "plugin")]
static PLUGIN_DIRS_ENV: &str = "NU_PLUGIN_DIRS";
pub const PLUGIN_DIRS_VAR: &str = "NU_PLUGIN_DIRS";
use crate::{
eval::{eval_constant, value_as_string},
@ -962,7 +964,7 @@ pub fn parse_old_alias(
decl_id,
redirect_stdout: true,
redirect_stderr: false,
parser_info: vec![],
parser_info: HashMap::new(),
}));
return (
Pipeline::from_vec(vec![Expression {
@ -1263,7 +1265,7 @@ pub fn parse_export_in_module(
arguments: vec![],
redirect_stdout: true,
redirect_stderr: false,
parser_info: vec![],
parser_info: HashMap::new(),
});
let exportables = if let Some(kw_span) = spans.get(1) {
@ -2087,7 +2089,7 @@ pub fn parse_module(
],
redirect_stdout: true,
redirect_stderr: false,
parser_info: vec![],
parser_info: HashMap::new(),
});
(
@ -2229,9 +2231,12 @@ pub fn parse_use(
unescape_unquote_string(&import_pattern.head.name, import_pattern.head.span);
if err.is_none() {
if let Some(module_path) =
find_in_dirs(&module_filename, working_set, &cwd, LIB_DIRS_ENV)
{
if let Some(module_path) = find_in_dirs_with_id(
&module_filename,
working_set,
&cwd,
get_dirs_var_from_call(&call),
) {
if let Some(i) = working_set
.parsed_module_files
.iter()
@ -2444,7 +2449,7 @@ pub fn parse_use(
};
let mut call = call;
call.add_parser_info(import_pattern_expr);
call.set_parser_info("import_pattern".to_string(), import_pattern_expr);
(
Pipeline::from_vec(vec![Expression {
@ -2665,7 +2670,7 @@ pub fn parse_hide(
};
let mut call = call;
call.add_parser_info(import_pattern_expr);
call.set_parser_info("import_pattern".to_string(), import_pattern_expr);
(
Pipeline::from_vec(vec![Expression {
@ -2889,9 +2894,12 @@ pub fn parse_overlay_use(
if let Ok(module_filename) =
String::from_utf8(trim_quotes(overlay_name.as_bytes()).to_vec())
{
if let Some(module_path) =
find_in_dirs(&module_filename, working_set, &cwd, LIB_DIRS_ENV)
{
if let Some(module_path) = find_in_dirs_with_id(
&module_filename,
working_set,
&cwd,
get_dirs_var_from_call(&call),
) {
let overlay_name = if let Some(stem) = module_path.file_stem() {
stem.to_string_lossy().to_string()
} else {
@ -2982,16 +2990,19 @@ pub fn parse_overlay_use(
// Change the call argument to include the Overlay expression with the module ID
let mut call = call;
call.add_parser_info(Expression {
expr: Expr::Overlay(if is_module_updated {
Some(origin_module_id)
} else {
None
}),
span: overlay_name_span,
ty: Type::Any,
custom_completion: None,
});
call.set_parser_info(
"overlay_expr".to_string(),
Expression {
expr: Expr::Overlay(if is_module_updated {
Some(origin_module_id)
} else {
None
}),
span: overlay_name_span,
ty: Type::Any,
custom_completion: None,
},
);
let pipeline = Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
@ -3160,7 +3171,7 @@ pub fn parse_let_or_const(
],
redirect_stdout: true,
redirect_stderr: false,
parser_info: vec![],
parser_info: HashMap::new(),
});
return (
@ -3282,7 +3293,7 @@ pub fn parse_mut(
],
redirect_stdout: true,
redirect_stderr: false,
parser_info: vec![],
parser_info: HashMap::new(),
});
return (
@ -3411,7 +3422,7 @@ pub fn parse_source(
}
};
if let Some(path) = find_in_dirs(&filename, working_set, &cwd, LIB_DIRS_ENV) {
if let Some(path) = find_in_dirs(&filename, working_set, &cwd, LIB_DIRS_VAR) {
if let Ok(contents) = std::fs::read(&path) {
// Change currently parsed directory
let prev_currently_parsed_cwd = if let Some(parent) = path.parent() {
@ -3457,12 +3468,15 @@ pub fn parse_source(
// FIXME: Adding this expression to the positional creates a syntax highlighting error
// after writing `source example.nu`
call_with_block.add_parser_info(Expression {
expr: Expr::Int(block_id as i64),
span: spans[1],
ty: Type::Any,
custom_completion: None,
});
call_with_block.set_parser_info(
"block_id".to_string(),
Expression {
expr: Expr::Int(block_id as i64),
span: spans[1],
ty: Type::Any,
custom_completion: None,
},
);
return (
Pipeline::from_vec(vec![Expression {
@ -3672,7 +3686,7 @@ pub fn parse_register(
if let Some(err) = err {
Err(err)
} else {
let path = if let Some(p) = find_in_dirs(&name, working_set, &cwd, PLUGIN_DIRS_ENV)
let path = if let Some(p) = find_in_dirs(&name, working_set, &cwd, PLUGIN_DIRS_VAR)
{
p
} else {
@ -3817,12 +3831,67 @@ pub fn parse_register(
/// a) the directory of a file currently being parsed
/// b) current working directory (PWD)
///
/// Then, if the file is not found in the actual cwd, NU_LIB_DIRS is checked.
/// If there is a relative path in NU_LIB_DIRS, it is assumed to be relative to the actual cwd
/// Then, if the file is not found in the actual cwd, dirs_var is checked.
/// If dirs_var is an Expr::Var, then we look for a const with that VarId,
/// and if dirs_var is an Expr::String, then we look for an environment with that name.
/// If there is a relative path in dirs_var, it is assumed to be relative to the actual cwd
/// determined in the first step.
///
/// Always returns an absolute path
pub fn find_in_dirs_with_id(
filename: &str,
working_set: &StateWorkingSet,
cwd: &str,
dirs_var_id: Option<VarId>,
) -> Option<PathBuf> {
// Choose whether to use file-relative or PWD-relative path
let actual_cwd = if let Some(currently_parsed_cwd) = &working_set.currently_parsed_cwd {
currently_parsed_cwd.as_path()
} else {
Path::new(cwd)
};
if let Ok(p) = canonicalize_with(filename, actual_cwd) {
return Some(p);
}
let path = Path::new(filename);
if !path.is_relative() {
return None;
}
working_set
.find_constant(dirs_var_id?)?
.as_list()
.ok()?
.iter()
.map(|lib_dir| -> Option<PathBuf> {
let dir = lib_dir.as_path().ok()?;
let dir_abs = canonicalize_with(dir, actual_cwd).ok()?;
canonicalize_with(filename, dir_abs).ok()
})
.find(Option::is_some)
.flatten()
}
pub fn find_dirs_var(working_set: &StateWorkingSet, var_name: &str) -> Option<VarId> {
working_set
.find_variable(format!("${}", var_name).as_bytes())
.filter(|var_id| working_set.find_constant(*var_id).is_some())
}
pub fn find_in_dirs(
filename: &str,
working_set: &StateWorkingSet,
cwd: &str,
dirs_var_name: &str,
) -> Option<PathBuf> {
find_dirs_var(working_set, dirs_var_name)
.and_then(|var_id| find_in_dirs_with_id(filename, working_set, cwd, Some(var_id)))
.or_else(|| find_in_dirs_old(filename, working_set, cwd, dirs_var_name))
}
// TODO: remove (see #8310)
pub fn find_in_dirs_old(
filename: &str,
working_set: &StateWorkingSet,
cwd: &str,

View File

@ -7,6 +7,7 @@ use crate::{
ParseError, Token, TokenContents,
};
use nu_engine::DIR_VAR_PARSER_INFO;
use nu_protocol::{
ast::{
Argument, Assignment, Bits, Block, Boolean, Call, CellPath, Comparison, Expr, Expression,
@ -19,10 +20,10 @@ use nu_protocol::{
};
use crate::parse_keywords::{
is_unaliasable_parser_keyword, parse_alias, parse_def, parse_def_predecl,
find_dirs_var, is_unaliasable_parser_keyword, parse_alias, parse_def, parse_def_predecl,
parse_export_in_block, parse_extern, parse_for, parse_hide, parse_keyword, parse_let_or_const,
parse_module, parse_old_alias, parse_overlay_hide, parse_overlay_new, parse_overlay_use,
parse_source, parse_use, parse_where, parse_where_expr,
parse_source, parse_use, parse_where, parse_where_expr, LIB_DIRS_VAR,
};
use itertools::Itertools;
@ -781,6 +782,25 @@ pub struct ParsedInternalCall {
pub error: Option<ParseError>,
}
fn attach_parser_info_builtin(working_set: &StateWorkingSet, name: &str, call: &mut Call) {
match name {
"use" | "overlay use" | "source-env" | "nu-check" => {
if let Some(var_id) = find_dirs_var(working_set, LIB_DIRS_VAR) {
call.set_parser_info(
DIR_VAR_PARSER_INFO.to_owned(),
Expression {
expr: Expr::Var(var_id),
span: call.head,
ty: Type::Any,
custom_completion: None,
},
);
}
}
_ => {}
}
}
pub fn parse_internal_call(
working_set: &mut StateWorkingSet,
command_span: Span,
@ -800,6 +820,10 @@ pub fn parse_internal_call(
let signature = decl.signature();
let output = signature.output_type.clone();
if decl.is_builtin() {
attach_parser_info_builtin(working_set, decl.name(), &mut call);
}
// The index into the positional parameter in the definition
let mut positional_idx = 0;
@ -5312,7 +5336,7 @@ pub fn parse_expression(
arguments,
redirect_stdout: true,
redirect_stderr: false,
parser_info: vec![],
parser_info: HashMap::new(),
}));
(
@ -6158,7 +6182,7 @@ fn wrap_expr_with_collect(working_set: &mut StateWorkingSet, expr: &Expression)
decl_id,
redirect_stdout: true,
redirect_stderr: false,
parser_info: vec![],
parser_info: HashMap::new(),
})),
span,
ty: Type::String,

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::Expression;
@ -19,7 +21,7 @@ pub struct Call {
pub redirect_stdout: bool,
pub redirect_stderr: bool,
/// this field is used by the parser to pass additional command-specific information
pub parser_info: Vec<Expression>,
pub parser_info: HashMap<String, Expression>,
}
impl Call {
@ -30,7 +32,7 @@ impl Call {
arguments: vec![],
redirect_stdout: true,
redirect_stderr: false,
parser_info: vec![],
parser_info: HashMap::new(),
}
}
@ -70,10 +72,6 @@ impl Call {
self.arguments.push(Argument::Positional(positional));
}
pub fn add_parser_info(&mut self, info: Expression) {
self.parser_info.push(info);
}
pub fn add_unknown(&mut self, unknown: Expression) {
self.arguments.push(Argument::Unknown(unknown));
}
@ -106,8 +104,12 @@ impl Call {
self.positional_iter().count()
}
pub fn parser_info_nth(&self, i: usize) -> Option<&Expression> {
self.parser_info.get(i)
pub fn get_parser_info(&self, name: &str) -> Option<&Expression> {
self.parser_info.get(name)
}
pub fn set_parser_info(&mut self, name: String, val: Expression) -> Option<Expression> {
self.parser_info.insert(name, val)
}
pub fn has_flag(&self, flag_name: &str) -> bool {

View File

@ -140,6 +140,34 @@ fn nu_lib_dirs_relative_repl() {
})
}
// TODO: add absolute path tests after we expand const capabilities (see #8310)
#[test]
fn const_nu_lib_dirs_relative() {
Playground::setup("const_nu_lib_dirs_relative", |dirs, sandbox| {
sandbox
.mkdir("scripts")
.with_files(vec![FileWithContentToBeTrimmed(
"scripts/foo.nu",
r#"
let-env FOO = "foo"
"#,
)])
.with_files(vec![FileWithContentToBeTrimmed(
"main.nu",
r#"
const NU_LIB_DIRS = [ 'scripts' ]
source-env foo.nu
$env.FOO
"#,
)]);
let outcome = nu!(cwd: dirs.test(), "source main.nu");
assert!(outcome.err.is_empty());
assert_eq!(outcome.out, "foo");
})
}
#[test]
fn nu_lib_dirs_relative_script() {
Playground::setup("nu_lib_dirs_relative_script", |dirs, sandbox| {