Use only $nu.env.PWD for getting the current directory (#587)

* Use only $nu.env.PWD for getting current directory

Because setting and reading to/from std::env changes the global state
shich is problematic if we call `cd` from multiple threads (e.g., in a
`par-each` block).

With this change, when engine-q starts, it will either inherit existing
PWD env var, or create a new one from `std::env::current_dir()`.
Otherwise, everything that needs the current directory will get it from
`$nu.env.PWD`. Each spawned external command will get its current
directory per-process which should be thread-safe.

One thing left to do is to patch nu-path for this as well since it uses
`std::env::current_dir()` in its expansions.

* Rename nu-path functions

*_with is not *_relative which should be more descriptive and frees
"with" for use in a followup commit.

* Clone stack every each iter; Fix some commands

Cloning the stack each iteration of `each` makes sure we're not reusing
PWD between iterations.

Some fixes in commands to make them use the new PWD.

* Post-rebase cleanup, fmt, clippy

* Change back _relative to _with in nu-path funcs

Didn't use the idea I had for the new "_with".

* Remove leftover current_dir from rebase

* Add cwd sync at merge_delta()

This makes sure the parser and completer always have up-to-date cwd.

* Always pass absolute path to glob in ls

* Do not allow PWD a relative path; Allow recovery

Makes it possible to recover PWD by proceeding with the REPL cycle.

* Clone stack in each also for byte/string stream

* (WIP) Start moving env variables to engine state

* (WIP) Move env vars to engine state (ugly)

Quick and dirty code.

* (WIP) Remove unused mut and args; Fmt

* (WIP) Fix dataframe tests

* (WIP) Fix missing args after rebase

* (WIP) Clone only env vars, not the whole stack

* (WIP) Add env var clone to `for` loop as well

* Minor edits

* Refactor merge_delta() to include stack merging.

Less error-prone than doing it manually.

* Clone env for each `update` command iteration

* Mark env var hidden only when found in eng. state

* Fix clippt warnings

* Add TODO about env var reading

* Do not clone empty environment in loops

* Remove extra cwd collection

* Split current_dir() into str and path; Fix autocd

* Make completions respect PWD env var
This commit is contained in:
Jakub Žádník 2022-01-05 00:30:34 +02:00 committed by GitHub
parent 8f6843c600
commit 74dcd91cc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 424 additions and 177 deletions

View File

@ -104,9 +104,18 @@ impl NuCompleter {
| nu_parser::FlatShape::String => {
let prefix = working_set.get_span_contents(flat.0);
let results = working_set.find_commands_by_prefix(prefix);
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD")
{
match d.as_string() {
Ok(s) => s,
Err(_) => "".to_string(),
}
} else {
"".to_string()
};
let prefix = String::from_utf8_lossy(prefix).to_string();
let results2 = file_path_completion(flat.0, &prefix)
let results2 = file_path_completion(flat.0, &prefix, &cwd)
.into_iter()
.map(move |x| {
(
@ -137,8 +146,17 @@ impl NuCompleter {
| nu_parser::FlatShape::ExternalArg => {
let prefix = working_set.get_span_contents(flat.0);
let prefix = String::from_utf8_lossy(prefix).to_string();
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD")
{
match d.as_string() {
Ok(s) => s,
Err(_) => "".to_string(),
}
} else {
"".to_string()
};
let results = file_path_completion(flat.0, &prefix);
let results = file_path_completion(flat.0, &prefix, &cwd);
return results
.into_iter()
@ -212,6 +230,7 @@ impl Completer for NuCompleter {
fn file_path_completion(
span: nu_protocol::Span,
partial: &str,
cwd: &str,
) -> Vec<(nu_protocol::Span, String)> {
use std::path::{is_separator, Path};
@ -238,7 +257,7 @@ fn file_path_completion(
(base, rest)
};
let base_dir = nu_path::expand_path(&base_dir_name);
let base_dir = nu_path::expand_path_with(&base_dir_name, cwd);
// This check is here as base_dir.read_dir() with base_dir == "" will open the current dir
// which we don't want in this case (if we did, base_dir would already be ".")
if base_dir == Path::new("") {

View File

@ -71,12 +71,16 @@ impl Command for For {
let engine_state = engine_state.clone();
let block = engine_state.get_block(block_id).clone();
let mut stack = stack.collect_captures(&block.captures);
let orig_env_vars = stack.env_vars.clone();
let orig_env_hidden = stack.env_hidden.clone();
match values {
Value::List { vals, .. } => Ok(vals
.into_iter()
.enumerate()
.map(move |(idx, x)| {
stack.with_env(&orig_env_vars, &orig_env_hidden);
stack.add_var(
var_id,
if numbered {
@ -107,6 +111,8 @@ impl Command for For {
.into_range_iter()?
.enumerate()
.map(move |(idx, x)| {
stack.with_env(&orig_env_vars, &orig_env_hidden);
stack.add_var(
var_id,
if numbered {

View File

@ -98,12 +98,12 @@ impl Command for Hide {
return Err(ShellError::NonUtf8(import_pattern.span()));
};
if stack.remove_env_var(&name).is_none() {
if stack.remove_env_var(engine_state, &name).is_none() {
return Err(ShellError::NotFound(call.positional[0].span));
}
}
} else if !import_pattern.hidden.contains(&import_pattern.head.name)
&& stack.remove_env_var(&head_name_str).is_none()
&& stack.remove_env_var(engine_state, &head_name_str).is_none()
{
return Err(ShellError::NotFound(call.positional[0].span));
}

View File

@ -32,7 +32,8 @@ pub fn test_dataframe(cmds: Vec<Box<dyn Command + 'static>>) {
working_set.render()
};
let _ = engine_state.merge_delta(delta);
let cwd = std::env::current_dir().expect("Could not get current working directory.");
let _ = engine_state.merge_delta(delta, None, &cwd);
for example in examples {
// Skip tests that don't have results to compare to
@ -52,7 +53,7 @@ pub fn test_dataframe(cmds: Vec<Box<dyn Command + 'static>>) {
(output, working_set.render())
};
let _ = engine_state.merge_delta(delta);
let _ = engine_state.merge_delta(delta, None, &cwd);
let mut stack = Stack::new();

View File

@ -1,8 +1,10 @@
use nu_protocol::engine::{EngineState, StateWorkingSet};
use std::path::Path;
use crate::*;
pub fn create_default_context() -> EngineState {
pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
let mut engine_state = EngineState::new();
let delta = {
@ -306,7 +308,7 @@ pub fn create_default_context() -> EngineState {
working_set.render()
};
let _ = engine_state.merge_delta(delta);
let _ = engine_state.merge_delta(delta, None, &cwd);
engine_state
}

View File

@ -29,7 +29,8 @@ impl Command for Env {
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();
let mut env_vars: Vec<(String, Value)> =
stack.get_env_vars(engine_state).into_iter().collect();
env_vars.sort_by(|(name1, _), (name2, _)| name1.cmp(name2));
let mut values = vec![];

View File

@ -47,7 +47,8 @@ pub fn test_examples(cmd: impl Command + 'static) {
working_set.render()
};
let _ = engine_state.merge_delta(delta);
let cwd = std::env::current_dir().expect("Could not get current working directory.");
let _ = engine_state.merge_delta(delta, None, &cwd);
for example in examples {
// Skip tests that don't have results to compare to
@ -67,10 +68,19 @@ pub fn test_examples(cmd: impl Command + 'static) {
(output, working_set.render())
};
let _ = engine_state.merge_delta(delta);
let _ = engine_state.merge_delta(delta, None, &cwd);
let mut stack = Stack::new();
// Set up PWD
stack.add_env_var(
"PWD".to_string(),
Value::String {
val: cwd.to_string_lossy().to_string(),
span: Span::test_data(),
},
);
// Set up our initial config to start from
stack.vars.insert(
CONFIG_VARIABLE_ID,

View File

@ -1,3 +1,4 @@
use nu_engine::env::current_dir_str;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
@ -32,7 +33,10 @@ impl Command for Cd {
let (path, span) = match path_val {
Some(v) => {
let path = nu_path::expand_path(v.as_string()?);
let path = nu_path::canonicalize_with(
v.as_string()?,
current_dir_str(engine_state, stack)?,
)?;
(path.to_string_lossy().to_string(), v.span()?)
}
None => {
@ -40,7 +44,6 @@ impl Command for Cd {
(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

View File

@ -1,7 +1,7 @@
use std::env::current_dir;
use std::path::PathBuf;
use super::util::get_interactive_confirmation;
use nu_engine::env::current_dir;
use nu_engine::CallExt;
use nu_path::canonicalize_with;
use nu_protocol::ast::Call;
@ -49,7 +49,7 @@ impl Command for Cp {
let interactive = call.has_flag("interactive");
let force = call.has_flag("force");
let path = current_dir()?;
let path = current_dir(engine_state, stack)?;
let source = path.join(source.as_str());
let destination = path.join(destination.as_str());
@ -135,7 +135,7 @@ impl Command for Cp {
for entry in sources.into_iter().flatten() {
let mut sources = FileStructure::new();
sources.walk_decorate(&entry)?;
sources.walk_decorate(&entry, engine_state, stack)?;
if entry.is_file() {
let sources = sources.paths_applying_with(|(source_file, _depth_level)| {

View File

@ -1,4 +1,5 @@
use chrono::{DateTime, Utc};
use nu_engine::env::current_dir;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
@ -10,6 +11,7 @@ use nu_protocol::{
use std::io::ErrorKind;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
#[derive(Clone)]
pub struct Ls;
@ -63,10 +65,17 @@ impl Command for Ls {
let call_span = call.head;
let pattern = if let Some(mut result) =
let (pattern, prefix) = if let Some(result) =
call.opt::<Spanned<String>>(engine_state, stack, 0)?
{
let path = std::path::Path::new(&result.item);
let path = PathBuf::from(&result.item);
let (mut path, prefix) = if path.is_relative() {
let cwd = current_dir(engine_state, stack)?;
(cwd.join(path), Some(cwd))
} else {
(path, None)
};
if path.is_dir() {
if permission_denied(&path) {
@ -92,16 +101,14 @@ impl Command for Ls {
}
if path.is_dir() {
if !result.item.ends_with(std::path::MAIN_SEPARATOR) {
result.item.push(std::path::MAIN_SEPARATOR);
}
result.item.push('*');
path = path.join("*");
}
}
result.item
(path.to_string_lossy().to_string(), prefix)
} else {
"*".into()
let cwd = current_dir(engine_state, stack)?;
(cwd.join("*").to_string_lossy().to_string(), Some(cwd))
};
let glob = glob::glob(&pattern).map_err(|err| {
@ -144,14 +151,37 @@ impl Command for Ls {
return None;
}
let display_name = if short_names {
path.file_name().and_then(|s| s.to_str())
} else if let Some(pre) = &prefix {
match path.strip_prefix(pre) {
Ok(stripped) => stripped.to_str(),
Err(_) => path.to_str(),
}
} else {
path.to_str()
}
.ok_or_else(|| {
ShellError::SpannedLabeledError(
format!("Invalid file name: {:}", path.to_string_lossy()),
"invalid file name".into(),
call_span,
)
});
match display_name {
Ok(name) => {
let entry =
dir_entry_dict(&path, metadata.as_ref(), call_span, long, short_names);
dir_entry_dict(&path, name, metadata.as_ref(), call_span, long);
match entry {
Ok(value) => Some(value),
Err(err) => Some(Value::Error { error: err }),
}
}
Err(err) => Some(Value::Error { error: err }),
}
}
_ => Some(Value::Nothing { span: call_span }),
})
.into_pipeline_data_with_metadata(
@ -213,7 +243,7 @@ fn path_contains_hidden_folder(path: &Path, folders: &[PathBuf]) -> bool {
#[cfg(unix)]
use std::os::unix::fs::FileTypeExt;
use std::path::{Path, PathBuf};
use std::path::Path;
pub fn get_file_type(md: &std::fs::Metadata) -> &str {
let ft = md.file_type();
@ -243,31 +273,18 @@ pub fn get_file_type(md: &std::fs::Metadata) -> &str {
#[allow(clippy::too_many_arguments)]
pub(crate) fn dir_entry_dict(
filename: &std::path::Path,
filename: &std::path::Path, // absolute path
display_name: &str, // gile name to be displayed
metadata: Option<&std::fs::Metadata>,
span: Span,
long: bool,
short_name: bool,
) -> Result<Value, ShellError> {
let mut cols = vec![];
let mut vals = vec![];
let name = if short_name {
filename.file_name().and_then(|s| s.to_str())
} else {
filename.to_str()
}
.ok_or_else(|| {
ShellError::SpannedLabeledError(
format!("Invalid file name: {:}", filename.to_string_lossy()),
"invalid file name".into(),
span,
)
})?;
cols.push("name".into());
vals.push(Value::String {
val: name.to_string(),
val: display_name.to_string(),
span,
});

View File

@ -1,6 +1,6 @@
use std::collections::VecDeque;
use std::env::current_dir;
use nu_engine::env::current_dir;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
@ -39,7 +39,7 @@ impl Command for Mkdir {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let path = current_dir()?;
let path = current_dir(engine_state, stack)?;
let mut directories = call
.rest::<String>(engine_state, stack, 0)?
.into_iter()

View File

@ -1,7 +1,7 @@
use std::env::current_dir;
use std::path::{Path, PathBuf};
use std::path::Path;
use super::util::get_interactive_confirmation;
use nu_engine::env::current_dir;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
@ -50,7 +50,7 @@ impl Command for Mv {
let interactive = call.has_flag("interactive");
let force = call.has_flag("force");
let path: PathBuf = current_dir()?;
let path = current_dir(engine_state, stack)?;
let source = path.join(spanned_source.item.as_str());
let destination = path.join(destination.as_str());

View File

@ -1,10 +1,10 @@
use std::env::current_dir;
#[cfg(unix)]
use std::os::unix::prelude::FileTypeExt;
use std::path::PathBuf;
use super::util::get_interactive_confirmation;
use nu_engine::env::current_dir;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
@ -86,7 +86,7 @@ fn rm(
));
}
let current_path = current_dir()?;
let current_path = current_dir(engine_state, stack)?;
let mut paths = call
.rest::<String>(engine_state, stack, 0)?
.into_iter()

View File

@ -1,6 +1,8 @@
use std::fs::OpenOptions;
use nu_engine::env::current_dir_str;
use nu_engine::CallExt;
use nu_path::expand_path_with;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape};
@ -39,7 +41,8 @@ impl Command for Touch {
let rest: Vec<String> = call.rest(engine_state, stack, 1)?;
for (index, item) in vec![target].into_iter().chain(rest).enumerate() {
match OpenOptions::new().write(true).create(true).open(&item) {
let path = expand_path_with(&item, current_dir_str(engine_state, stack)?);
match OpenOptions::new().write(true).create(true).open(&path) {
Ok(_) => continue,
Err(err) => {
return Err(ShellError::CreateNotPossible(

View File

@ -1,6 +1,8 @@
use std::path::{Path, PathBuf};
use nu_engine::env::current_dir_str;
use nu_path::canonicalize_with;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::ShellError;
use dialoguer::Input;
@ -39,16 +41,27 @@ impl FileStructure {
.collect()
}
pub fn walk_decorate(&mut self, start_path: &Path) -> Result<(), ShellError> {
pub fn walk_decorate(
&mut self,
start_path: &Path,
engine_state: &EngineState,
stack: &Stack,
) -> Result<(), ShellError> {
self.resources = Vec::<Resource>::new();
self.build(start_path, 0)?;
self.build(start_path, 0, engine_state, stack)?;
self.resources.sort();
Ok(())
}
fn build(&mut self, src: &Path, lvl: usize) -> Result<(), ShellError> {
let source = canonicalize_with(src, std::env::current_dir()?)?;
fn build(
&mut self,
src: &Path,
lvl: usize,
engine_state: &EngineState,
stack: &Stack,
) -> Result<(), ShellError> {
let source = canonicalize_with(src, current_dir_str(engine_state, stack)?)?;
if source.is_dir() {
for entry in std::fs::read_dir(src)? {
@ -56,7 +69,7 @@ impl FileStructure {
let path = entry.path();
if path.is_dir() {
self.build(&path, lvl + 1)?;
self.build(&path, lvl + 1, engine_state, stack)?;
}
self.resources.push(Resource {

View File

@ -71,6 +71,8 @@ impl Command for Each {
let engine_state = engine_state.clone();
let block = engine_state.get_block(block_id).clone();
let mut stack = stack.collect_captures(&block.captures);
let orig_env_vars = stack.env_vars.clone();
let orig_env_hidden = stack.env_hidden.clone();
let span = call.head;
match input {
@ -80,6 +82,8 @@ impl Command for Each {
.into_iter()
.enumerate()
.map(move |(idx, x)| {
stack.with_env(&orig_env_vars, &orig_env_hidden);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
if numbered {
@ -113,6 +117,8 @@ impl Command for Each {
.into_iter()
.enumerate()
.map(move |(idx, x)| {
stack.with_env(&orig_env_vars, &orig_env_hidden);
let x = match x {
Ok(x) => Value::Binary { val: x, span },
Err(err) => return Value::Error { error: err },
@ -151,6 +157,8 @@ impl Command for Each {
.into_iter()
.enumerate()
.map(move |(idx, x)| {
stack.with_env(&orig_env_vars, &orig_env_hidden);
let x = match x {
Ok(x) => Value::String { val: x, span },
Err(err) => return Value::Error { error: err },
@ -192,7 +200,7 @@ impl Command for Each {
for (col, val) in cols.into_iter().zip(vals.into_iter()) {
let block = engine_state.get_block(block_id);
let mut stack = stack.clone();
stack.with_env(&orig_env_vars, &orig_env_hidden);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {

View File

@ -74,9 +74,13 @@ fn update(
let block = engine_state.get_block(block_id).clone();
let mut stack = stack.collect_captures(&block.captures);
let orig_env_vars = stack.env_vars.clone();
let orig_env_hidden = stack.env_hidden.clone();
input.map(
move |mut input| {
stack.with_env(&orig_env_vars, &orig_env_hidden);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, input.clone())

View File

@ -1,7 +1,8 @@
use std::path::Path;
use nu_engine::env::current_dir_str;
use nu_engine::CallExt;
use nu_path::{canonicalize, expand_path};
use nu_path::{canonicalize_with, expand_path};
use nu_protocol::{engine::Command, Example, ShellError, Signature, Span, SyntaxShape, Value};
use super::PathSubcommandArguments;
@ -9,6 +10,7 @@ use super::PathSubcommandArguments;
struct Arguments {
strict: bool,
columns: Option<Vec<String>>,
cwd: String,
}
impl PathSubcommandArguments for Arguments {
@ -55,6 +57,7 @@ impl Command for SubCommand {
let args = Arguments {
strict: call.has_flag("strict"),
columns: call.get_flag(engine_state, stack, "columns")?,
cwd: current_dir_str(engine_state, stack)?,
};
input.map(
@ -107,7 +110,7 @@ impl Command for SubCommand {
}
fn expand(path: &Path, span: Span, args: &Arguments) -> Value {
if let Ok(p) = canonicalize(path) {
if let Ok(p) = canonicalize_with(path, &args.cwd) {
Value::string(p.to_string_lossy(), span)
} else if args.strict {
Value::Error {

View File

@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::env;
use std::io::{BufRead, BufReader, Write};
use std::process::{Command as CommandSys, Stdio};
use std::sync::atomic::Ordering;
@ -113,9 +112,19 @@ impl<'call> ExternalCommand<'call> {
// TODO. We don't have a way to know the current directory
// This should be information from the EvaluationContex or EngineState
let path = env::current_dir()?;
process.current_dir(path);
if let Some(d) = self.env_vars.get("PWD") {
process.current_dir(d);
} else {
return Err(ShellError::SpannedLabeledErrorHelp(
"Current directory not found".to_string(),
"did not find PWD environment variable".to_string(),
head,
concat!(
"The environment variable 'PWD' was not found. ",
"It is required to define the current directory when running an external command."
).to_string(),
));
}
process.envs(&self.env_vars);

View File

@ -62,7 +62,7 @@ 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 = match stack.get_env_var("LS_COLORS") {
let env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
Some(v) => Some(env_to_string("LS_COLORS", v, engine_state, stack, &config)?),
None => None,
};

View File

@ -112,7 +112,7 @@ impl Command for Table {
let config = config.clone();
let ctrlc = ctrlc.clone();
let ls_colors = match stack.get_env_var("LS_COLORS") {
let ls_colors = match stack.get_env_var(engine_state, "LS_COLORS") {
Some(v) => LsColors::from_string(&env_to_string(
"LS_COLORS",
v,

View File

@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::{Config, PipelineData, ShellError, Value};
@ -17,17 +18,15 @@ const ENV_SEP: &str = ":";
/// skip errors. This function is called in the main() so we want to keep running, we cannot just
/// exit.
pub fn convert_env_values(
engine_state: &EngineState,
stack: &mut Stack,
engine_state: &mut EngineState,
stack: &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 {
for (name, val) in &engine_state.env_vars {
if let Some(env_conv) = config.env_conversions.get(name) {
if let Some((block_id, from_span)) = env_conv.from_string {
let val_span = match val.span() {
@ -46,12 +45,8 @@ pub fn convert_env_values(
stack.add_var(*var_id, val.clone());
}
let result = eval_block(
engine_state,
&mut stack,
block,
PipelineData::new(val_span),
);
let result =
eval_block(engine_state, &mut stack, block, PipelineData::new(val_span));
match result {
Ok(data) => {
@ -76,11 +71,10 @@ pub fn convert_env_values(
}
}
new_env_vars.push(new_scope);
for (k, v) in new_scope {
engine_state.env_vars.insert(k, v);
}
stack.env_vars = new_env_vars;
error
}
@ -89,7 +83,7 @@ pub fn env_to_string(
env_name: &str,
value: Value,
engine_state: &EngineState,
stack: &mut Stack,
stack: &Stack,
config: &Config,
) -> Result<String, ShellError> {
if let Some(env_conv) = config.env_conversions.get(env_name) {
@ -128,10 +122,10 @@ pub fn env_to_string(
/// Translate all environment variables from Values to Strings
pub fn env_to_strings(
engine_state: &EngineState,
stack: &mut Stack,
stack: &Stack,
config: &Config,
) -> Result<HashMap<String, String>, ShellError> {
let env_vars = stack.get_env_vars();
let env_vars = stack.get_env_vars(engine_state);
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)?;
@ -140,3 +134,33 @@ pub fn env_to_strings(
Ok(env_vars_str)
}
/// Shorthand for env_to_string() for PWD with custom error
pub fn current_dir_str(engine_state: &EngineState, stack: &Stack) -> Result<String, ShellError> {
let config = stack.get_config()?;
if let Some(pwd) = stack.get_env_var(engine_state, "PWD") {
match env_to_string("PWD", pwd, engine_state, stack, &config) {
Ok(cwd) => {
if Path::new(&cwd).is_absolute() {
Ok(cwd)
} else {
Err(ShellError::LabeledError(
"Invalid current directory".to_string(),
format!("The 'PWD' environment variable must be set to an absolute path. Found: '{}'", cwd)
))
}
}
Err(e) => Err(e),
}
} else {
Err(ShellError::LabeledError(
"Current directory not found".to_string(),
"The environment variable 'PWD' was not found. It is required to define the current directory.".to_string(),
))
}
}
/// 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)
}

View File

@ -8,7 +8,7 @@ use nu_protocol::{
Spanned, Type, Unit, Value, VarId, ENV_VARIABLE_ID,
};
use crate::get_full_help;
use crate::{current_dir_str, get_full_help};
pub fn eval_operator(op: &Expression) -> Result<Operator, ShellError> {
match op {
@ -575,15 +575,9 @@ pub fn eval_variable(
// since the env var PWD doesn't exist on all platforms
// lets just get the current directory
if let Ok(current_dir) = std::env::current_dir() {
if let Some(cwd) = current_dir.to_str() {
let cwd = current_dir_str(engine_state, stack)?;
output_cols.push("cwd".into());
output_vals.push(Value::String {
val: cwd.into(),
span,
})
}
}
output_vals.push(Value::String { val: cwd, span });
if let Some(home_path) = nu_path::home_dir() {
if let Some(home_path_str) = home_path.to_str() {
@ -886,7 +880,7 @@ pub fn eval_variable(
span,
})
} else if var_id == ENV_VARIABLE_ID {
let env_vars = stack.get_env_vars();
let env_vars = stack.get_env_vars(engine_state);
let env_columns = env_vars.keys();
let env_values = env_vars.values();

View File

@ -1,7 +1,7 @@
mod call_ext;
pub mod column;
mod documentation;
mod env;
pub mod env;
mod eval;
pub use call_ext::CallExt;

View File

@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};
fn expand_tilde_with(path: impl AsRef<Path>, home: Option<PathBuf>) -> PathBuf {
fn expand_tilde_with_home(path: impl AsRef<Path>, home: Option<PathBuf>) -> PathBuf {
let path = path.as_ref();
if !path.starts_with("~") {
@ -27,7 +27,7 @@ fn expand_tilde_with(path: impl AsRef<Path>, home: Option<PathBuf>) -> PathBuf {
/// Expand tilde ("~") into a home directory if it is the first path component
pub fn expand_tilde(path: impl AsRef<Path>) -> PathBuf {
// TODO: Extend this to work with "~user" style of home paths
expand_tilde_with(path, dirs_next::home_dir())
expand_tilde_with_home(path, dirs_next::home_dir())
}
#[cfg(test)]
@ -37,17 +37,17 @@ mod tests {
fn check_expanded(s: &str) {
let home = Path::new("/home");
let buf = Some(PathBuf::from(home));
assert!(expand_tilde_with(Path::new(s), buf).starts_with(&home));
assert!(expand_tilde_with_home(Path::new(s), buf).starts_with(&home));
// Tests the special case in expand_tilde for "/" as home
let home = Path::new("/");
let buf = Some(PathBuf::from(home));
assert!(!expand_tilde_with(Path::new(s), buf).starts_with("//"));
assert!(!expand_tilde_with_home(Path::new(s), buf).starts_with("//"));
}
fn check_not_expanded(s: &str) {
let home = PathBuf::from("/home");
let expanded = expand_tilde_with(Path::new(s), Some(home));
let expanded = expand_tilde_with_home(Path::new(s), Some(home));
assert!(expanded == Path::new(s));
}

View File

@ -1,4 +1,4 @@
use super::Command;
use super::{Command, Stack};
use crate::{
ast::Block, BlockId, DeclId, Example, Overlay, OverlayId, ShellError, Signature, Span, Type,
VarId,
@ -9,6 +9,10 @@ use std::{
sync::{atomic::AtomicBool, Arc},
};
use crate::Value;
use std::path::Path;
#[cfg(feature = "plugin")]
use std::path::PathBuf;
@ -140,6 +144,7 @@ pub struct EngineState {
overlays: im::Vector<Overlay>,
pub scope: im::Vector<ScopeFrame>,
pub ctrlc: Option<Arc<AtomicBool>>,
pub env_vars: im::HashMap<String, Value>,
#[cfg(feature = "plugin")]
pub plugin_signatures: Option<PathBuf>,
}
@ -167,6 +172,7 @@ impl EngineState {
overlays: im::vector![],
scope: im::vector![ScopeFrame::new()],
ctrlc: None,
env_vars: im::HashMap::new(),
#[cfg(feature = "plugin")]
plugin_signatures: None,
}
@ -179,7 +185,12 @@ impl EngineState {
///
/// When we want to preserve what the parser has created, we can take its output (the `StateDelta`) and
/// use this function to merge it into the global state.
pub fn merge_delta(&mut self, mut delta: StateDelta) -> Result<(), ShellError> {
pub fn merge_delta(
&mut self,
mut delta: StateDelta,
stack: Option<&mut Stack>,
cwd: impl AsRef<Path>,
) -> Result<(), ShellError> {
// Take the mutable reference and extend the permanent state from the working set
self.files.extend(delta.files);
self.file_contents.extend(delta.file_contents);
@ -216,6 +227,16 @@ impl EngineState {
}
}
if let Some(stack) = stack {
for mut env_scope in stack.env_vars.drain(..) {
for (k, v) in env_scope.drain() {
self.env_vars.insert(k, v);
}
}
}
std::env::set_current_dir(cwd)?;
Ok(())
}
@ -1209,7 +1230,8 @@ mod engine_state_tests {
working_set.render()
};
engine_state.merge_delta(delta)?;
let cwd = std::env::current_dir().expect("Could not get current working directory.");
engine_state.merge_delta(delta, None, &cwd)?;
assert_eq!(engine_state.num_files(), 2);
assert_eq!(&engine_state.files[0].0, "test.nu");

View File

@ -1,5 +1,6 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use crate::engine::EngineState;
use crate::{Config, ShellError, Value, VarId, CONFIG_VARIABLE_ID};
/// A runtime value stack used during evaluation
@ -25,6 +26,9 @@ pub struct Stack {
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, Value>>,
/// Tells which environment variables from engine state are hidden. We don't need to track the
/// env vars in the stack since we can just delete them.
pub env_hidden: HashSet<String>,
}
impl Default for Stack {
@ -38,6 +42,17 @@ impl Stack {
Stack {
vars: HashMap::new(),
env_vars: vec![],
env_hidden: HashSet::new(),
}
}
pub fn with_env(&mut self, env_vars: &[HashMap<String, Value>], env_hidden: &HashSet<String>) {
if env_vars.iter().any(|scope| !scope.is_empty()) {
self.env_vars = env_vars.to_owned();
}
if !env_hidden.is_empty() {
self.env_hidden = env_hidden.clone();
}
}
@ -54,6 +69,9 @@ impl Stack {
}
pub fn add_env_var(&mut self, var: String, value: Value) {
// if the env var was hidden, let's activate it again
self.env_hidden.remove(&var);
if let Some(scope) = self.env_vars.last_mut() {
scope.insert(var, value);
} else {
@ -85,8 +103,15 @@ impl Stack {
}
/// Flatten the env var scope frames into one frame
pub fn get_env_vars(&self) -> HashMap<String, Value> {
let mut result = HashMap::new();
pub fn get_env_vars(&self, engine_state: &EngineState) -> HashMap<String, Value> {
// TODO: We're collecting im::HashMap to HashMap here. It might make sense to make these
// the same data structure.
let mut result: HashMap<String, Value> = engine_state
.env_vars
.iter()
.filter(|(k, _)| !self.env_hidden.contains(*k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
for scope in &self.env_vars {
result.extend(scope.clone());
@ -95,24 +120,37 @@ impl Stack {
result
}
pub fn get_env_var(&self, name: &str) -> Option<Value> {
pub fn get_env_var(&self, engine_state: &EngineState, name: &str) -> Option<Value> {
for scope in self.env_vars.iter().rev() {
if let Some(v) = scope.get(name) {
return Some(v.clone());
}
}
if self.env_hidden.contains(name) {
None
} else {
engine_state.env_vars.get(name).cloned()
}
}
pub fn remove_env_var(&mut self, name: &str) -> Option<Value> {
pub fn remove_env_var(&mut self, engine_state: &EngineState, name: &str) -> Option<Value> {
for scope in self.env_vars.iter_mut().rev() {
if let Some(v) = scope.remove(name) {
return Some(v);
}
}
if self.env_hidden.contains(name) {
// the environment variable is already hidden
None
} else if let Some(val) = engine_state.env_vars.get(name) {
// the environment variable was found in the engine state => mark it as hidden
self.env_hidden.insert(name.to_string());
Some(val.clone())
} else {
None
}
}
pub fn get_config(&self) -> Result<Config, ShellError> {

View File

@ -96,12 +96,9 @@ pub enum ShellError {
VariableNotFoundAtRuntime(#[label = "variable not found"] Span),
#[error("Environment variable not found")]
#[diagnostic(code(nu::shell::variable_not_found), url(docsrs))]
#[diagnostic(code(nu::shell::env_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("Not found.")]
#[diagnostic(code(nu::parser::not_found), url(docsrs))]
NotFound(#[label = "did not find anything under this name"] Span),
@ -185,7 +182,7 @@ pub enum ShellError {
PluginFailedToDecode(String),
#[error("I/O error")]
#[diagnostic(code(nu::shell::io_error), url(docsrs))]
#[diagnostic(code(nu::shell::io_error), url(docsrs), help("{0}"))]
IOError(String),
#[error("Directory not found")]

View File

@ -65,6 +65,9 @@ impl GStat {
}
// This path has to exist
// TODO: If the path is relative, it will be expanded using `std::env::current_dir` and not
// the "PWD" environment variable. We would need a way to read the engine's environment
// variables here.
if !std::path::Path::new(&a_path.item).exists() {
return Err(LabeledError {
label: "error with path".to_string(),

View File

@ -20,6 +20,7 @@ use nu_protocol::{
use reedline::{Completer, CompletionActionHandler, DefaultHinter, LineBuffer, Prompt, Vi};
use std::{
io::Write,
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
@ -97,7 +98,9 @@ fn main() -> Result<()> {
miette_hook(x);
}));
let mut engine_state = create_default_context();
// Get initial current working directory.
let init_cwd = get_init_cwd();
let mut engine_state = create_default_context(&init_cwd);
// TODO: make this conditional in the future
// Ctrl-c protection section
@ -129,7 +132,7 @@ fn main() -> Result<()> {
(output, working_set.render())
};
if let Err(err) = engine_state.merge_delta(delta) {
if let Err(err) = engine_state.merge_delta(delta, None, &init_cwd) {
let working_set = StateWorkingSet::new(&engine_state);
report_error(&working_set, &err);
}
@ -137,7 +140,7 @@ fn main() -> Result<()> {
let mut stack = nu_protocol::engine::Stack::new();
// First, set up env vars as strings only
gather_parent_env_vars(&mut engine_state, &mut stack);
gather_parent_env_vars(&mut engine_state);
// Set up our initial config to start from
stack.vars.insert(
@ -160,7 +163,7 @@ fn main() -> Result<()> {
};
// Translate environment variables from Strings to Values
if let Some(e) = convert_env_values(&engine_state, &mut stack, &config) {
if let Some(e) = convert_env_values(&mut engine_state, &stack, &config) {
let working_set = StateWorkingSet::new(&engine_state);
report_error(&working_set, &e);
std::process::exit(1);
@ -204,7 +207,9 @@ fn main() -> Result<()> {
(output, working_set.render())
};
if let Err(err) = engine_state.merge_delta(delta) {
let cwd = nu_engine::env::current_dir_str(&engine_state, &stack)?;
if let Err(err) = engine_state.merge_delta(delta, Some(&mut stack), &cwd) {
let working_set = StateWorkingSet::new(&engine_state);
report_error(&working_set, &err);
}
@ -255,7 +260,7 @@ fn main() -> Result<()> {
let mut stack = nu_protocol::engine::Stack::new();
// First, set up env vars as strings only
gather_parent_env_vars(&mut engine_state, &mut stack);
gather_parent_env_vars(&mut engine_state);
// Set up our initial config to start from
stack.vars.insert(
@ -331,7 +336,7 @@ fn main() -> Result<()> {
})?;
// Translate environment variables from Strings to Values
if let Some(e) = convert_env_values(&engine_state, &mut stack, &config) {
if let Some(e) = convert_env_values(&mut engine_state, &stack, &config) {
let working_set = StateWorkingSet::new(&engine_state);
report_error(&working_set, &e);
}
@ -429,7 +434,8 @@ fn main() -> Result<()> {
Ok(Signal::Success(s)) => {
let tokens = lex(s.as_bytes(), 0, &[], &[], false);
// Check if this is a single call to a directory, if so auto-cd
let path = nu_path::expand_path(&s);
let cwd = nu_engine::env::current_dir_str(&engine_state, &stack)?;
let path = nu_path::expand_path_with(&s, &cwd);
let orig = s.clone();
if (orig.starts_with('.')
@ -440,8 +446,6 @@ fn main() -> Result<()> {
&& tokens.0.len() == 1
{
// We have an auto-cd
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(
@ -493,7 +497,8 @@ fn main() -> Result<()> {
// 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, quite 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) {
fn gather_parent_env_vars(engine_state: &mut EngineState) {
// Some helper functions
fn get_surround_char(s: &str) -> Option<char> {
if s.contains('"') {
if s.contains('\'') {
@ -517,10 +522,14 @@ 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() {
fn put_env_to_fake_file(
name: &str,
val: &str,
fake_env_file: &mut String,
engine_state: &EngineState,
) {
let (c_name, c_val) =
if let (Some(cn), Some(cv)) = (get_surround_char(&name), get_surround_char(&val)) {
if let (Some(cn), Some(cv)) = (get_surround_char(name), get_surround_char(val)) {
(cn, cv)
} else {
// environment variable with its name or value containing both ' and " is ignored
@ -529,19 +538,53 @@ fn gather_parent_env_vars(engine_state: &mut EngineState, stack: &mut Stack) {
&format!("{}={}", name, val),
"Name or value should not contain both ' and \" at the same time.",
);
continue;
return;
};
fake_env_file.push(c_name);
fake_env_file.push_str(&name);
fake_env_file.push_str(name);
fake_env_file.push(c_name);
fake_env_file.push('=');
fake_env_file.push(c_val);
fake_env_file.push_str(&val);
fake_env_file.push_str(val);
fake_env_file.push(c_val);
fake_env_file.push('\n');
}
let mut fake_env_file = String::new();
// Make sure we always have PWD
if std::env::var("PWD").is_err() {
match std::env::current_dir() {
Ok(cwd) => {
put_env_to_fake_file(
"PWD",
&cwd.to_string_lossy(),
&mut fake_env_file,
engine_state,
);
}
Err(e) => {
// Could not capture current working directory
let working_set = StateWorkingSet::new(engine_state);
report_error(
&working_set,
&ShellError::LabeledError(
"Current directory not found".to_string(),
format!("Retrieving current directory failed: {:?}", e),
),
);
}
}
}
// Write all the env vars into a fake file
for (name, val) in std::env::vars() {
put_env_to_fake_file(&name, &val, &mut fake_env_file, engine_state);
}
// Lex the fake file, assign spans to all environment variables and add them
// to stack
let span_offset = engine_state.next_span_start();
engine_state.add_file(
@ -622,7 +665,8 @@ fn gather_parent_env_vars(engine_state: &mut EngineState, stack: &mut Stack) {
continue;
};
stack.add_env_var(name, value);
// stack.add_env_var(name, value);
engine_state.env_vars.insert(name, value);
}
}
}
@ -714,23 +758,27 @@ fn print_pipeline_data(
Ok(())
}
fn get_prompt_indicators(config: &Config, stack: &Stack) -> (String, String, String, String) {
let prompt_indicator = match stack.get_env_var(PROMPT_INDICATOR) {
fn get_prompt_indicators(
config: &Config,
engine_state: &EngineState,
stack: &Stack,
) -> (String, String, String, String) {
let prompt_indicator = match stack.get_env_var(engine_state, PROMPT_INDICATOR) {
Some(pi) => pi.into_string("", config),
None => "".to_string(),
};
let prompt_vi_insert = match stack.get_env_var(PROMPT_INDICATOR_VI_INSERT) {
let prompt_vi_insert = match stack.get_env_var(engine_state, PROMPT_INDICATOR_VI_INSERT) {
Some(pvii) => pvii.into_string("", config),
None => ": ".to_string(),
};
let prompt_vi_visual = match stack.get_env_var(PROMPT_INDICATOR_VI_VISUAL) {
let prompt_vi_visual = match stack.get_env_var(engine_state, PROMPT_INDICATOR_VI_VISUAL) {
Some(pviv) => pviv.into_string("", config),
None => "v ".to_string(),
};
let prompt_multiline = match stack.get_env_var(PROMPT_MULTILINE_INDICATOR) {
let prompt_multiline = match stack.get_env_var(engine_state, PROMPT_MULTILINE_INDICATOR) {
Some(pm) => pm.into_string("", config),
None => "::: ".to_string(),
};
@ -755,9 +803,9 @@ fn update_prompt<'prompt>(
prompt_vi_insert_string,
prompt_vi_visual_string,
prompt_multiline_string,
) = get_prompt_indicators(config, stack);
) = get_prompt_indicators(config, engine_state, stack);
let prompt_command_block_id = match stack.get_env_var(PROMPT_COMMAND) {
let prompt_command_block_id = match stack.get_env_var(engine_state, PROMPT_COMMAND) {
Some(v) => match v.as_block() {
Ok(b) => b,
Err(_) => {
@ -859,7 +907,16 @@ fn eval_source(
(output, working_set.render())
};
if let Err(err) = engine_state.merge_delta(delta) {
let cwd = match nu_engine::env::current_dir_str(engine_state, stack) {
Ok(p) => PathBuf::from(p),
Err(e) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
get_init_cwd()
}
};
if let Err(err) = engine_state.merge_delta(delta, Some(stack), &cwd) {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
}
@ -929,3 +986,16 @@ pub fn report_error(
let _ = enable_vt_processing();
}
}
fn get_init_cwd() -> PathBuf {
match std::env::current_dir() {
Ok(cwd) => cwd,
Err(_) => match std::env::var("PWD".to_string()) {
Ok(cwd) => PathBuf::from(cwd),
Err(_) => match nu_path::home_dir() {
Some(cwd) => cwd,
None => PathBuf::new(),
},
},
}
}