From 6a0f4045584d84f181ab73edc35996b86fa58b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1dn=C3=ADk?= Date: Fri, 17 Dec 2021 03:04:54 +0200 Subject: [PATCH] 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 --- crates/nu-cli/src/prompt.rs | 9 +- crates/nu-command/src/core_commands/use_.rs | 11 +- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/env/env_command.rs | 61 ++++++++ crates/nu-command/src/env/let_env.rs | 5 +- crates/nu-command/src/env/mod.rs | 2 + crates/nu-command/src/env/with_env.rs | 51 +------ crates/nu-command/src/filesystem/cd.rs | 16 +-- crates/nu-command/src/system/run_external.rs | 14 +- crates/nu-command/src/viewers/griddle.rs | 6 +- crates/nu-command/src/viewers/table.rs | 9 +- crates/nu-engine/src/env.rs | 131 ++++++++++++++++++ crates/nu-engine/src/eval.rs | 10 +- crates/nu-engine/src/lib.rs | 2 + crates/nu-protocol/src/config.rs | 55 +++++++- crates/nu-protocol/src/engine/engine_state.rs | 3 +- crates/nu-protocol/src/engine/stack.rs | 14 +- crates/nu-protocol/src/shell_error.rs | 15 +- src/main.rs | 126 +++++++++++++---- src/tests.rs | 5 - 20 files changed, 414 insertions(+), 132 deletions(-) create mode 100644 crates/nu-command/src/env/env_command.rs create mode 100644 crates/nu-engine/src/env.rs diff --git a/crates/nu-cli/src/prompt.rs b/crates/nu-cli/src/prompt.rs index 5e748f8a35..488a922c34 100644 --- a/crates/nu-cli/src/prompt.rs +++ b/crates/nu-cli/src/prompt.rs @@ -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; } diff --git a/crates/nu-command/src/core_commands/use_.rs b/crates/nu-command/src/core_commands/use_.rs index 20df8b2d92..91d4d070b0 100644 --- a/crates/nu-command/src/core_commands/use_.rs +++ b/crates/nu-command/src/core_commands/use_.rs @@ -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); } diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 8660b97ee4..3b1c104262 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -226,6 +226,7 @@ pub fn create_default_context() -> EngineState { bind_command! { LetEnv, WithEnv, + Env, }; // Math diff --git a/crates/nu-command/src/env/env_command.rs b/crates/nu-command/src/env/env_command.rs new file mode 100644 index 0000000000..2c00e25411 --- /dev/null +++ b/crates/nu-command/src/env/env_command.rs @@ -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 { + 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()) + } +} diff --git a/crates/nu-command/src/env/let_env.rs b/crates/nu-command/src/env/let_env.rs index 201b0e6059..b234a3c94a 100644 --- a/crates/nu-command/src/env/let_env.rs +++ b/crates/nu-command/src/env/let_env.rs @@ -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)) diff --git a/crates/nu-command/src/env/mod.rs b/crates/nu-command/src/env/mod.rs index 48e8293dfd..55f1c90bd6 100644 --- a/crates/nu-command/src/env/mod.rs +++ b/crates/nu-command/src/env/mod.rs @@ -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; diff --git a/crates/nu-command/src/env/with_env.rs b/crates/nu-command/src/env/with_env.rs index 3f59c1f917..c9c7ae5648 100644 --- a/crates/nu-command/src/env/with_env.rs +++ b/crates/nu-command/src/env/with_env.rs @@ -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 { - 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 = HashMap::new(); + let mut env: HashMap = 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) diff --git a/crates/nu-command/src/filesystem/cd.rs b/crates/nu-command/src/filesystem/cd.rs index ea6a00bedb..0625127956 100644 --- a/crates/nu-command/src/filesystem/cd.rs +++ b/crates/nu-command/src/filesystem/cd.rs @@ -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 { - let path: Option = call.opt(engine_state, stack, 0)?; + let path_val: Option = 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)) } } diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index 044c1df015..543fbe0a24 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -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 = call.req(engine_state, stack, 0)?; let args: Vec = 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) diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index b3750e92a8..7a7ec8a047 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -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 = 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 { diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index 6b86a796ed..1f53410533 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -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(), }; diff --git a/crates/nu-engine/src/env.rs b/crates/nu-engine/src/env.rs new file mode 100644 index 0000000000..f87afeb32f --- /dev/null +++ b/crates/nu-engine/src/env.rs @@ -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 { + 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 { + 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, 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) +} diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index a3a82120bf..9df53e46a5 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -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 { diff --git a/crates/nu-engine/src/lib.rs b/crates/nu-engine/src/lib.rs index ab957a4638..db7b4cf357 100644 --- a/crates/nu-engine/src/lib.rs +++ b/crates/nu-engine/src/lib.rs @@ -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}; diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index 0b1df20dcd..a8a96280cc 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -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 { + 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, } 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; + } _ => {} } } diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 9c3717999d..0a56d4e0f4 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -489,8 +489,7 @@ impl EngineState { "".into() } - #[allow(unused)] - pub(crate) fn add_file(&mut self, filename: String, contents: Vec) -> usize { + pub fn add_file(&mut self, filename: String, contents: Vec) -> usize { let next_span_start = self.next_span_start(); let next_span_end = next_span_start + contents.len(); diff --git a/crates/nu-protocol/src/engine/stack.rs b/crates/nu-protocol/src/engine/stack.rs index 99bd321a36..32312ec7aa 100644 --- a/crates/nu-protocol/src/engine/stack.rs +++ b/crates/nu-protocol/src/engine/stack.rs @@ -24,7 +24,7 @@ pub struct Stack { /// Variables pub vars: HashMap, /// Environment variables arranged as a stack to be able to recover values from parent scopes - pub env_vars: Vec>, + pub env_vars: Vec>, } 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 { + pub fn get_env_vars(&self) -> HashMap { 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 { + pub fn get_env_var(&self, name: &str) -> Option { 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 { + pub fn remove_env_var(&mut self, name: &str) -> Option { 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()); } } } diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index a11e19b7cd..8e6c8f767e 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -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), diff --git a/src/main.rs b/src/main.rs index 94acf9d63c..4039e40572 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 } diff --git a/src/tests.rs b/src/tests.rs index b3e4b999ad..c998cfa63a 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -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(