Treating environment variables as Values (#497)

* Proof of concept treating env vars as Values

* Refactor env var collection and method name

* Remove unnecessary pub

* Move env translations into a new file

* Fix LS_COLORS to support any Value

* Fix spans during env var translation

* Add span to env var in cd

* Improve error diagnostics

* Fix non-string env vars failing string conversion

* Make PROMPT_COMMAND a Block instead of String

* Record host env vars to a fake file

This will give spans to env vars that would otherwise be without one.
Makes errors less confusing.

* Add 'env' command to list env vars

It will list also their values translated to strings

* Sort env command by name; Add env var type

* Remove obsolete test
This commit is contained in:
Jakub Žádník 2021-12-17 03:04:54 +02:00 committed by GitHub
parent 342584e5f8
commit 6a0f404558
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 414 additions and 132 deletions

View File

@ -8,7 +8,6 @@ use {
/// Nushell prompt definition
#[derive(Clone)]
pub struct NushellPrompt {
prompt_command: String,
prompt_string: String,
// These are part of the struct definition in case we want to allow
// further customization to the shell status
@ -27,7 +26,6 @@ impl Default for NushellPrompt {
impl NushellPrompt {
pub fn new() -> NushellPrompt {
NushellPrompt {
prompt_command: "".to_string(),
prompt_string: "".to_string(),
default_prompt_indicator: "".to_string(),
default_vi_insert_prompt_indicator: ": ".to_string(),
@ -36,12 +34,7 @@ impl NushellPrompt {
}
}
pub fn is_new_prompt(&self, prompt_command: &str) -> bool {
self.prompt_command != prompt_command
}
pub fn update_prompt(&mut self, prompt_command: String, prompt_string: String) {
self.prompt_command = prompt_command;
pub fn update_prompt(&mut self, prompt_string: String) {
self.prompt_string = prompt_string;
}

View File

@ -88,15 +88,8 @@ impl Command for Use {
// TODO: Add string conversions (e.g. int to string)
// TODO: Later expand env to take all Values
let val = if let Ok(s) =
eval_block(engine_state, stack, block, PipelineData::new(call.head))?
.into_value(Span::unknown())
.as_string()
{
s
} else {
return Err(ShellError::EnvVarNotAString(import_pattern.span()));
};
let val = eval_block(engine_state, stack, block, PipelineData::new(call.head))?
.into_value(Span::unknown());
stack.add_env_var(name, val);
}

View File

@ -226,6 +226,7 @@ pub fn create_default_context() -> EngineState {
bind_command! {
LetEnv,
WithEnv,
Env,
};
// Math

View File

@ -0,0 +1,61 @@
use nu_engine::env_to_string;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{Category, IntoPipelineData, PipelineData, Signature, Value};
#[derive(Clone)]
pub struct Env;
impl Command for Env {
fn name(&self) -> &str {
"env"
}
fn usage(&self) -> &str {
"Display current environment"
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("env").category(Category::Env)
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let span = call.head;
let config = stack.get_config().unwrap_or_default();
let mut env_vars: Vec<(String, Value)> = stack.get_env_vars().into_iter().collect();
env_vars.sort_by(|(name1, _), (name2, _)| name1.cmp(name2));
let mut values = vec![];
for (name, val) in env_vars {
let mut cols = vec![];
let mut vals = vec![];
let raw = env_to_string(&name, val.clone(), engine_state, stack, &config)?;
let val_type = val.get_type();
cols.push("name".into());
vals.push(Value::string(name, span));
cols.push("type".into());
vals.push(Value::string(format!("{}", val_type), span));
cols.push("value".into());
vals.push(val);
cols.push("raw".into());
vals.push(Value::string(raw, span));
values.push(Value::Record { cols, vals, span });
}
Ok(Value::List { vals: values, span }.into_pipeline_data())
}
}

View File

@ -20,7 +20,7 @@ impl Command for LetEnv {
.required("var_name", SyntaxShape::String, "variable name")
.required(
"initial_value",
SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::String)),
SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::Any)),
"equals sign followed by value",
)
.category(Category::Env)
@ -42,9 +42,6 @@ impl Command for LetEnv {
.expect("internal error: missing keyword");
let rhs = eval_expression(engine_state, stack, keyword_expr)?;
let rhs = rhs.as_string()?;
//println!("Adding: {:?} to {}", rhs, var_id);
stack.add_env_var(env_var, rhs);
Ok(PipelineData::new(call.head))

View File

@ -1,5 +1,7 @@
mod env_command;
mod let_env;
mod with_env;
pub use env_command::Env;
pub use let_env::LetEnv;
pub use with_env::WithEnv;

View File

@ -1,7 +1,4 @@
use std::{
collections::HashMap,
convert::{TryFrom, TryInto},
};
use std::collections::HashMap;
use nu_engine::{eval_block, CallExt};
use nu_protocol::{
@ -73,34 +70,6 @@ impl Command for WithEnv {
}
}
#[derive(Debug, Clone)]
pub enum EnvVar {
Proper(String),
Nothing,
}
impl TryFrom<&Value> for EnvVar {
type Error = ShellError;
fn try_from(value: &Value) -> Result<Self, Self::Error> {
if matches!(value, Value::Nothing { .. }) {
Ok(EnvVar::Nothing)
} else if let Ok(s) = value.as_string() {
if s.is_empty() {
Ok(EnvVar::Nothing)
} else {
Ok(EnvVar::Proper(s))
}
} else {
Err(ShellError::CantConvert(
"string".into(),
value.get_type().to_string(),
value.span()?,
))
}
}
}
fn with_env(
engine_state: &EngineState,
stack: &mut Stack,
@ -116,7 +85,7 @@ fn with_env(
let block = engine_state.get_block(block_id).clone();
let mut stack = stack.collect_captures(&block.captures);
let mut env: HashMap<String, EnvVar> = HashMap::new();
let mut env: HashMap<String, Value> = HashMap::new();
match &variable {
Value::List { vals: table, .. } => {
@ -125,7 +94,7 @@ fn with_env(
match &table[0] {
Value::Record { cols, vals, .. } => {
for (k, v) in cols.iter().zip(vals.iter()) {
env.insert(k.to_string(), v.try_into()?);
env.insert(k.to_string(), v.clone());
}
}
x => {
@ -140,15 +109,16 @@ fn with_env(
// primitive values([X Y W Z])
for row in table.chunks(2) {
if row.len() == 2 {
env.insert(row[0].as_string()?, (&row[1]).try_into()?);
env.insert(row[0].as_string()?, (&row[1]).clone());
}
// TODO: else error?
}
}
}
// when get object by `open x.json` or `from json`
Value::Record { cols, vals, .. } => {
for (k, v) in cols.iter().zip(vals) {
env.insert(k.clone(), v.try_into()?);
env.insert(k.clone(), v.clone());
}
}
x => {
@ -161,14 +131,7 @@ fn with_env(
};
for (k, v) in env {
match v {
EnvVar::Nothing => {
stack.remove_env_var(&k);
}
EnvVar::Proper(s) => {
stack.add_env_var(k, s);
}
}
stack.add_env_var(k, v);
}
eval_block(engine_state, &mut stack, &block, input)

View File

@ -1,7 +1,7 @@
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{Category, PipelineData, Signature, SyntaxShape};
use nu_protocol::{Category, PipelineData, Signature, SyntaxShape, Value};
#[derive(Clone)]
pub struct Cd;
@ -28,23 +28,23 @@ impl Command for Cd {
call: &Call,
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let path: Option<String> = call.opt(engine_state, stack, 0)?;
let path_val: Option<Value> = call.opt(engine_state, stack, 0)?;
let path = match path {
Some(path) => {
let path = nu_path::expand_path(path);
path.to_string_lossy().to_string()
let (path, span) = match path_val {
Some(v) => {
let path = nu_path::expand_path(v.as_string()?);
(path.to_string_lossy().to_string(), v.span()?)
}
None => {
let path = nu_path::expand_tilde("~");
path.to_string_lossy().to_string()
(path.to_string_lossy().to_string(), call.head)
}
};
let _ = std::env::set_current_dir(&path);
//FIXME: this only changes the current scope, but instead this environment variable
//should probably be a block that loads the information from the state in the overlay
stack.add_env_var("PWD".into(), path);
stack.add_env_var("PWD".into(), Value::String { val: path, span });
Ok(PipelineData::new(call.head))
}
}

View File

@ -7,6 +7,7 @@ use std::process::{Command as CommandSys, Stdio};
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use nu_engine::env_to_strings;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::{ast::Call, engine::Command, ShellError, Signature, SyntaxShape, Value};
use nu_protocol::{Category, Config, IntoInterruptiblePipelineData, PipelineData, Span, Spanned};
@ -51,9 +52,10 @@ impl Command for External {
let mut name: Spanned<String> = call.req(engine_state, stack, 0)?;
let args: Vec<String> = call.rest(engine_state, stack, 1)?;
let last_expression = call.has_flag("last_expression");
let env_vars = stack.get_env_vars();
// Translate environment variables from Values to Strings
let config = stack.get_config().unwrap_or_default();
let env_vars_str = env_to_strings(engine_state, stack, &config)?;
// Check if this is a single call to a directory, if so auto-cd
let path = nu_path::expand_path(&name.item);
@ -73,7 +75,13 @@ impl Command for External {
//FIXME: this only changes the current scope, but instead this environment variable
//should probably be a block that loads the information from the state in the overlay
stack.add_env_var("PWD".into(), name.item.clone());
stack.add_env_var(
"PWD".into(),
Value::String {
val: name.item.clone(),
span: Span::unknown(),
},
);
return Ok(PipelineData::new(call.head));
}
@ -81,7 +89,7 @@ impl Command for External {
name,
args,
last_expression,
env_vars,
env_vars: env_vars_str,
call,
};
command.run_with_input(engine_state, input, config)

View File

@ -1,6 +1,7 @@
// use super::icons::{icon_for_file, iconify_style_ansi_to_nu};
use super::icons::icon_for_file;
use lscolors::{LsColors, Style};
use nu_engine::env_to_string;
use nu_engine::CallExt;
use nu_protocol::{
ast::{Call, PathMember},
@ -61,7 +62,10 @@ prints out the list properly."#
let color_param: bool = call.has_flag("color");
let separator_param: Option<String> = call.get_flag(engine_state, stack, "separator")?;
let config = stack.get_config().unwrap_or_default();
let env_str = stack.get_env_var("LS_COLORS");
let env_str = match stack.get_env_var("LS_COLORS") {
Some(v) => Some(env_to_string("LS_COLORS", v, engine_state, stack, &config)?),
None => None,
};
let use_grid_icons = config.use_grid_icons;
match input {

View File

@ -1,5 +1,6 @@
use lscolors::{LsColors, Style};
use nu_color_config::{get_color_config, style_primitive};
use nu_engine::env_to_string;
use nu_protocol::ast::{Call, PathMember};
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
@ -74,7 +75,13 @@ impl Command for Table {
let ctrlc = ctrlc.clone();
let ls_colors = match stack.get_env_var("LS_COLORS") {
Some(s) => LsColors::from_string(&s),
Some(v) => LsColors::from_string(&env_to_string(
"LS_COLORS",
v,
engine_state,
stack,
&config,
)?),
None => LsColors::default(),
};

131
crates/nu-engine/src/env.rs Normal file
View File

@ -0,0 +1,131 @@
use std::collections::HashMap;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::{Config, PipelineData, ShellError, Value};
use crate::eval_block;
#[cfg(windows)]
const ENV_SEP: &str = ";";
#[cfg(not(windows))]
const ENV_SEP: &str = ":";
/// Translate environment variables from Strings to Values. Requires config to be already set up in
/// case the user defined custom env conversions in config.nu.
///
/// It returns Option instead of Result since we do want to translate all the values we can and
/// skip errors. This function is called in the main() so we want to keep running, we cannot just
/// exit.
pub fn env_to_values(
engine_state: &EngineState,
stack: &mut Stack,
config: &Config,
) -> Option<ShellError> {
let mut new_env_vars = vec![];
let mut error = None;
for scope in &stack.env_vars {
let mut new_scope = HashMap::new();
for (name, val) in scope {
if let Some(conv) = config.env_conversions.get(name) {
let span = match val.span() {
Ok(sp) => sp,
Err(e) => {
error = error.or(Some(e));
continue;
}
};
let block = engine_state.get_block(conv.from_string.0);
if let Some(var) = block.signature.get_positional(0) {
let mut stack = stack.collect_captures(&block.captures);
if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, val.clone());
}
let result =
eval_block(engine_state, &mut stack, block, PipelineData::new(span));
match result {
Ok(data) => {
let val = data.into_value(span);
new_scope.insert(name.to_string(), val);
}
Err(e) => error = error.or(Some(e)),
}
} else {
error = error.or_else(|| {
Some(ShellError::MissingParameter(
"block input".into(),
conv.from_string.1,
))
});
}
} else {
new_scope.insert(name.to_string(), val.clone());
}
}
new_env_vars.push(new_scope);
}
stack.env_vars = new_env_vars;
error
}
/// Translate one environment variable from Value to String
pub fn env_to_string(
env_name: &str,
value: Value,
engine_state: &EngineState,
stack: &mut Stack,
config: &Config,
) -> Result<String, ShellError> {
if let Some(conv) = config.env_conversions.get(env_name) {
let block = engine_state.get_block(conv.to_string.0);
if let Some(var) = block.signature.get_positional(0) {
let span = value.span()?;
let mut stack = stack.collect_captures(&block.captures);
if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, value);
}
Ok(
// This one is OK to fail: We want to know if custom conversion is working
eval_block(engine_state, &mut stack, block, PipelineData::new(span))?
.into_value(span)
.as_string()?,
)
} else {
Err(ShellError::MissingParameter(
"block input".into(),
conv.to_string.1,
))
}
} else {
// Do not fail here. Must sicceed, otherwise setting a non-string env var would constantly
// throw errors when running externals etc.
Ok(value.into_string(ENV_SEP, config))
}
}
/// Translate all environment variables from Values to Strings
pub fn env_to_strings(
engine_state: &EngineState,
stack: &mut Stack,
config: &Config,
) -> Result<HashMap<String, String>, ShellError> {
let env_vars = stack.get_env_vars();
let mut env_vars_str = HashMap::new();
for (env_name, val) in env_vars {
let val_str = env_to_string(&env_name, val, engine_state, stack, config)?;
env_vars_str.insert(env_name, val_str);
}
Ok(env_vars_str)
}

View File

@ -472,13 +472,7 @@ pub fn eval_variable(
let env_vars = stack.get_env_vars();
let env_columns: Vec<_> = env_vars.keys().map(|x| x.to_string()).collect();
let env_values: Vec<_> = env_vars
.values()
.map(|x| Value::String {
val: x.to_string(),
span,
})
.collect();
let env_values: Vec<_> = env_vars.values().cloned().collect();
output_cols.push("env".into());
output_vals.push(Value::Record {
@ -852,7 +846,7 @@ pub fn eval_variable(
}
}
pub fn compute(size: i64, unit: Unit, span: Span) -> Value {
fn compute(size: i64, unit: Unit, span: Span) -> Value {
match unit {
Unit::Byte => Value::Filesize { val: size, span },
Unit::Kilobyte => Value::Filesize {

View File

@ -1,7 +1,9 @@
mod call_ext;
mod documentation;
mod env;
mod eval;
pub use call_ext::CallExt;
pub use documentation::{generate_docs, get_brief_help, get_documentation, get_full_help};
pub use env::*;
pub use eval::{eval_block, eval_expression, eval_operator};

View File

@ -1,9 +1,50 @@
use crate::{ShellError, Value};
use crate::{BlockId, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
const ANIMATE_PROMPT_DEFAULT: bool = false;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EnvConversion {
pub from_string: (BlockId, Span),
pub to_string: (BlockId, Span),
}
impl EnvConversion {
pub fn from_record(value: &Value) -> Result<Self, ShellError> {
let record = value.as_record()?;
let mut conv_map = HashMap::new();
for (k, v) in record.0.iter().zip(record.1) {
if (k == "from_string") || (k == "to_string") {
conv_map.insert(k.as_str(), (v.as_block()?, v.span()?));
} else {
return Err(ShellError::UnsupportedConfigValue(
"'from_string' and 'to_string' fields".into(),
k.into(),
value.span()?,
));
}
}
match (conv_map.get("from_string"), conv_map.get("to_string")) {
(None, _) => Err(ShellError::MissingConfigValue(
"'from_string' field".into(),
value.span()?,
)),
(_, None) => Err(ShellError::MissingConfigValue(
"'to_string' field".into(),
value.span()?,
)),
(Some(from), Some(to)) => Ok(EnvConversion {
from_string: *from,
to_string: *to,
}),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Config {
pub filesize_metric: bool,
@ -16,6 +57,7 @@ pub struct Config {
pub float_precision: i64,
pub filesize_format: String,
pub use_ansi_coloring: bool,
pub env_conversions: HashMap<String, EnvConversion>,
}
impl Default for Config {
@ -31,6 +73,7 @@ impl Default for Config {
float_precision: 4,
filesize_format: "auto".into(),
use_ansi_coloring: true,
env_conversions: HashMap::new(), // TODO: Add default conversoins
}
}
}
@ -129,6 +172,16 @@ impl Value {
"filesize_format" => {
config.filesize_format = value.as_string()?.to_lowercase();
}
"env_conversions" => {
let (env_vars, conversions) = value.as_record()?;
let mut env_conversions = HashMap::new();
for (env_var, record) in env_vars.iter().zip(conversions) {
env_conversions.insert(env_var.into(), EnvConversion::from_record(record)?);
}
config.env_conversions = env_conversions;
}
_ => {}
}
}

View File

@ -489,8 +489,7 @@ impl EngineState {
"<unknown>".into()
}
#[allow(unused)]
pub(crate) fn add_file(&mut self, filename: String, contents: Vec<u8>) -> usize {
pub fn add_file(&mut self, filename: String, contents: Vec<u8>) -> usize {
let next_span_start = self.next_span_start();
let next_span_end = next_span_start + contents.len();

View File

@ -24,7 +24,7 @@ pub struct Stack {
/// Variables
pub vars: HashMap<VarId, Value>,
/// Environment variables arranged as a stack to be able to recover values from parent scopes
pub env_vars: Vec<HashMap<String, String>>,
pub env_vars: Vec<HashMap<String, Value>>,
}
impl Default for Stack {
@ -53,7 +53,7 @@ impl Stack {
self.vars.insert(var_id, value);
}
pub fn add_env_var(&mut self, var: String, value: String) {
pub fn add_env_var(&mut self, var: String, value: Value) {
if let Some(scope) = self.env_vars.last_mut() {
scope.insert(var, value);
} else {
@ -85,7 +85,7 @@ impl Stack {
}
/// Flatten the env var scope frames into one frame
pub fn get_env_vars(&self) -> HashMap<String, String> {
pub fn get_env_vars(&self) -> HashMap<String, Value> {
let mut result = HashMap::new();
for scope in &self.env_vars {
@ -95,17 +95,17 @@ impl Stack {
result
}
pub fn get_env_var(&self, name: &str) -> Option<String> {
pub fn get_env_var(&self, name: &str) -> Option<Value> {
for scope in self.env_vars.iter().rev() {
if let Some(v) = scope.get(name) {
return Some(v.to_string());
return Some(v.clone());
}
}
None
}
pub fn remove_env_var(&mut self, name: &str) -> Option<String> {
pub fn remove_env_var(&mut self, name: &str) -> Option<Value> {
for scope in self.env_vars.iter_mut().rev() {
if let Some(v) = scope.remove(name) {
return Some(v);
@ -135,7 +135,7 @@ impl Stack {
for (i, scope) in self.env_vars.iter().rev().enumerate() {
println!("env vars, scope {} (from the last);", i);
for (var, val) in scope {
println!(" {}: {:?}", var, val);
println!(" {}: {:?}", var, val.clone().debug_value());
}
}
}

View File

@ -99,10 +99,9 @@ pub enum ShellError {
#[diagnostic(code(nu::shell::variable_not_found), url(docsrs))]
EnvVarNotFoundAtRuntime(#[label = "environment variable not found"] Span),
#[error("Environment variable is not a string")]
#[diagnostic(code(nu::shell::variable_not_found), url(docsrs))]
EnvVarNotAString(#[label = "does not evaluate to a string"] Span),
// #[error("Environment variable is not a string")]
// #[diagnostic(code(nu::shell::variable_not_found), url(docsrs))]
// EnvVarNotAString(#[label = "does not evaluate to a string"] Span),
#[error("Not found.")]
#[diagnostic(code(nu::parser::not_found), url(docsrs))]
NotFound(#[label = "did not find anything under this name"] Span),
@ -235,6 +234,14 @@ pub enum ShellError {
#[diagnostic(code(nu::shell::downcast_not_possible), url(docsrs))]
DowncastNotPossible(String, #[label("{0}")] Span),
#[error("Unsupported config value")]
#[diagnostic(code(nu::shell::unsupported_config_value), url(docsrs))]
UnsupportedConfigValue(String, String, #[label = "expected {0}, got {1}"] Span),
#[error("Missing config value")]
#[diagnostic(code(nu::shell::missing_config_value), url(docsrs))]
MissingConfigValue(String, #[label = "missing {0}"] Span),
#[error("{0}")]
#[diagnostic()]
SpannedLabeledError(String, String, #[label("{1}")] Span),

View File

@ -8,8 +8,8 @@ use dialoguer::{
use miette::{IntoDiagnostic, Result};
use nu_cli::{CliError, NuCompleter, NuHighlighter, NuValidator, NushellPrompt};
use nu_command::create_default_context;
use nu_engine::eval_block;
use nu_parser::parse;
use nu_engine::{env_to_values, eval_block};
use nu_parser::{lex, parse, Token, TokenContents};
use nu_protocol::{
ast::Call,
engine::{EngineState, Stack, StateWorkingSet},
@ -126,9 +126,8 @@ fn main() -> Result<()> {
let mut stack = nu_protocol::engine::Stack::new();
for (k, v) in std::env::vars() {
stack.add_env_var(k, v);
}
// First, set up env vars as strings only
gather_parent_env_vars(&mut engine_state, &mut stack);
// Set up our initial config to start from
stack.vars.insert(
@ -150,6 +149,13 @@ fn main() -> Result<()> {
}
};
// Translate environment variables from Strings to Values
if let Some(e) = env_to_values(&engine_state, &mut stack, &config) {
let working_set = StateWorkingSet::new(&engine_state);
report_error(&working_set, &e);
std::process::exit(1);
}
match eval_block(
&engine_state,
&mut stack,
@ -239,9 +245,8 @@ fn main() -> Result<()> {
let mut nu_prompt = NushellPrompt::new();
let mut stack = nu_protocol::engine::Stack::new();
for (k, v) in std::env::vars() {
stack.add_env_var(k, v);
}
// First, set up env vars as strings only
gather_parent_env_vars(&mut engine_state, &mut stack);
// Set up our initial config to start from
stack.vars.insert(
@ -269,6 +274,23 @@ fn main() -> Result<()> {
}
}
// Get the config
let config = match stack.get_config() {
Ok(config) => config,
Err(e) => {
let working_set = StateWorkingSet::new(&engine_state);
report_error(&working_set, &e);
Config::default()
}
};
// Translate environment variables from Strings to Values
if let Some(e) = env_to_values(&engine_state, &mut stack, &config) {
let working_set = StateWorkingSet::new(&engine_state);
report_error(&working_set, &e);
}
let history_path = if let Some(mut history_path) = nu_path::config_dir() {
history_path.push("nushell");
history_path.push("history.txt");
@ -385,6 +407,67 @@ fn main() -> Result<()> {
}
}
// This fill collect environment variables from std::env and adds them to a stack.
//
// In order to ensure the values have spans, it first creates a dummy file, writes the collected
// env vars into it (in a NAME=value format, similar to the output of the Unix 'env' tool), then
// uses the file to get the spans. The file stays in memory, no filesystem IO is done.
fn gather_parent_env_vars(engine_state: &mut EngineState, stack: &mut Stack) {
let mut fake_env_file = String::new();
for (name, val) in std::env::vars() {
fake_env_file.push_str(&name);
fake_env_file.push('=');
fake_env_file.push_str(&val);
fake_env_file.push('\n');
}
let span_offset = engine_state.next_span_start();
engine_state.add_file(
"Host Environment Variables".to_string(),
fake_env_file.as_bytes().to_vec(),
);
let (tokens, _) = lex(fake_env_file.as_bytes(), span_offset, &[], &[], true);
for token in tokens {
if let Token {
contents: TokenContents::Item,
span: full_span,
} = token
{
let contents = engine_state.get_span_contents(&full_span);
let (parts, _) = lex(contents, full_span.start, &[], &[b'='], true);
let name = if let Some(Token {
contents: TokenContents::Item,
span,
}) = parts.get(0)
{
String::from_utf8_lossy(engine_state.get_span_contents(span)).to_string()
} else {
// Skip this env var if it does not have a name
continue;
};
let value = if let Some(Token {
contents: TokenContents::Item,
span,
}) = parts.get(2)
{
Value::String {
val: String::from_utf8_lossy(engine_state.get_span_contents(span)).to_string(),
span: *span,
}
} else {
Value::String {
val: "".to_string(),
span: Span::new(full_span.end, full_span.end),
}
};
stack.add_env_var(name, value);
}
}
}
fn print_pipeline_data(
input: PipelineData,
engine_state: &EngineState,
@ -447,33 +530,22 @@ fn update_prompt<'prompt>(
nu_prompt: &'prompt mut NushellPrompt,
default_prompt: &'prompt DefaultPrompt,
) -> &'prompt dyn Prompt {
let prompt_command = match stack.get_env_var(env_variable) {
Some(prompt) => prompt,
let block_id = match stack.get_env_var(env_variable) {
Some(v) => match v.as_block() {
Ok(b) => b,
Err(_) => return default_prompt as &dyn Prompt,
},
None => return default_prompt as &dyn Prompt,
};
// Checking if the PROMPT_COMMAND is the same to avoid evaluating constantly
// the same command, thus saturating the contents in the EngineState
if !nu_prompt.is_new_prompt(prompt_command.as_str()) {
return nu_prompt as &dyn Prompt;
}
let block = {
let mut working_set = StateWorkingSet::new(engine_state);
let (output, err) = parse(&mut working_set, None, prompt_command.as_bytes(), false);
if let Some(err) = err {
report_error(&working_set, &err);
return default_prompt as &dyn Prompt;
}
output
};
let block = engine_state.get_block(block_id);
let mut stack = stack.clone();
let evaluated_prompt = match eval_block(
engine_state,
&mut stack,
&block,
block,
PipelineData::new(Span::unknown()),
) {
Ok(pipeline_data) => {
@ -486,7 +558,7 @@ fn update_prompt<'prompt>(
}
};
nu_prompt.update_prompt(prompt_command, evaluated_prompt);
nu_prompt.update_prompt(evaluated_prompt);
nu_prompt as &dyn Prompt
}

View File

@ -1066,11 +1066,6 @@ fn shorthand_env_3() -> TestResult {
run_test(r#"FOO=BAZ BAR=MOO $nu.env.FOO"#, "BAZ")
}
#[test]
fn shorthand_env_4() -> TestResult {
fail_test(r#"FOO=BAZ FOO= $nu.env.FOO"#, "did you mean")
}
#[test]
fn update_cell_path_1() -> TestResult {
run_test(