From c42b588782287fa84b636d02acbace8453f978fc Mon Sep 17 00:00:00 2001 From: Leonhard Kipp Date: Wed, 31 Mar 2021 07:52:34 +0200 Subject: [PATCH] Refactor nu-cli/env* (#3041) * Revert "History, more test coverage improvements, and refactorings. (#3217)" This reverts commit 8fc8fc89aaf0ab40e7a149f2286eb97515f4ac9b. * Add tests * Refactor .nu-env * Change logic of Config write to logic of read() * Fix reload always appends to old vars * Fix reload always takes last_modified of global config * Add reload_config in evaluation context * Reload config after writing to it in cfg set / cfg set_into * Add --no-history to cli options * Use --no-history in tests * Add comment about maybe_print_errors * Get ctrl_exit var from context.global_config * Use context.global_config in command "config" * Add Readme in engine how env vars are now handled * Update docs from autoenv command * Move history_path from engine to nu_data * Move load history out of if * No let before return * Add import for indexmap --- Cargo.lock | 90 +-- crates/nu-cli/src/cli.rs | 328 ++++------ crates/nu-cli/src/env.rs | 3 - .../src/env/directory_specific_environment.rs | 259 -------- crates/nu-cli/src/env/environment.rs | 274 -------- crates/nu-cli/src/env/environment_syncer.rs | 617 ------------------ crates/nu-cli/src/lib.rs | 6 - crates/nu-cli/src/line_editor.rs | 4 +- crates/nu-cli/src/shell/helper.rs | 5 +- crates/nu-command/src/commands/autoenv.rs | 63 +- .../nu-command/src/commands/autoenv_trust.rs | 2 +- .../src/commands/autoenv_untrust.rs | 2 +- .../src/commands/autoview/command.rs | 16 +- .../src/commands/classified/external.rs | 6 +- .../nu-command/src/commands/config/command.rs | 31 +- crates/nu-command/src/commands/config/set.rs | 7 +- .../src/commands/config/set_into.rs | 13 +- .../src/commands/default_context.rs | 5 +- crates/nu-command/src/commands/enter.rs | 2 + crates/nu-command/src/commands/history.rs | 6 +- crates/nu-command/src/commands/math/avg.rs | 14 +- crates/nu-command/src/commands/math/ceil.rs | 10 +- crates/nu-command/src/commands/math/floor.rs | 10 +- crates/nu-command/src/commands/math/max.rs | 14 +- crates/nu-command/src/commands/math/median.rs | 14 +- crates/nu-command/src/commands/math/min.rs | 14 +- crates/nu-command/src/commands/math/mode.rs | 14 +- .../nu-command/src/commands/math/product.rs | 14 +- crates/nu-command/src/commands/math/sum.rs | 14 +- .../nu-command/src/commands/run_external.rs | 2 +- crates/nu-command/src/commands/save.rs | 2 + crates/nu-command/src/examples.rs | 12 +- crates/nu-command/src/utils/test_bins.rs | 12 + crates/nu-data/Cargo.toml | 2 + crates/nu-data/src/config.rs | 28 +- crates/nu-data/src/config/conf.rs | 21 +- crates/nu-data/src/config/config_trust.rs | 44 ++ crates/nu-data/src/config/local_config.rs | 150 +++++ crates/nu-data/src/config/nuconfig.rs | 132 ++-- crates/nu-data/src/config/path.rs | 37 +- crates/nu-data/src/config/tests.rs | 5 - crates/nu-engine/Cargo.toml | 2 + crates/nu-engine/README.md | 16 + .../nu-engine/src/basic_evaluation_context.rs | 26 + crates/nu-engine/src/basic_shell_manager.rs | 16 + crates/nu-engine/src/command_args.rs | 10 +- crates/nu-engine/src/config_holder.rs | 53 ++ crates/nu-engine/src/env/basic_host.rs | 28 +- crates/nu-engine/src/env/environment.rs | 30 - crates/nu-engine/src/env/mod.rs | 1 - crates/nu-engine/src/evaluate/internal.rs | 28 +- crates/nu-engine/src/evaluate/scope.rs | 88 +++ crates/nu-engine/src/evaluate/variables.rs | 41 +- crates/nu-engine/src/evaluation_context.rs | 235 +++++-- .../src/filesystem/filesystem_shell.rs | 66 +- crates/nu-engine/src/lib.rs | 11 +- crates/nu-engine/src/print.rs | 18 + crates/nu-engine/src/runnable_context.rs | 30 +- crates/nu-engine/src/script.rs | 40 +- crates/nu-engine/src/shell/help_shell.rs | 5 + crates/nu-engine/src/shell/mod.rs | 1 + crates/nu-engine/src/shell/shell_manager.rs | 22 +- crates/nu-engine/src/shell/value_shell.rs | 5 + crates/nu-protocol/src/config_path.rs | 19 + crates/nu-protocol/src/lib.rs | 2 + crates/nu-protocol/src/return_value.rs | 12 +- src/main.rs | 23 +- tests/shell/environment/env.rs | 45 ++ tests/shell/environment/mod.rs | 1 + tests/shell/environment/nu_env.rs | 324 ++++++++- 70 files changed, 1615 insertions(+), 1887 deletions(-) delete mode 100644 crates/nu-cli/src/env.rs delete mode 100644 crates/nu-cli/src/env/directory_specific_environment.rs delete mode 100644 crates/nu-cli/src/env/environment.rs delete mode 100644 crates/nu-cli/src/env/environment_syncer.rs create mode 100644 crates/nu-data/src/config/config_trust.rs create mode 100644 crates/nu-data/src/config/local_config.rs create mode 100644 crates/nu-engine/README.md create mode 100644 crates/nu-engine/src/basic_evaluation_context.rs create mode 100644 crates/nu-engine/src/basic_shell_manager.rs create mode 100644 crates/nu-engine/src/config_holder.rs delete mode 100644 crates/nu-engine/src/env/environment.rs create mode 100644 crates/nu-engine/src/print.rs create mode 100644 crates/nu-protocol/src/config_path.rs create mode 100644 tests/shell/environment/env.rs diff --git a/Cargo.lock b/Cargo.lock index 7807929b05..c37be4ccce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,7 +190,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" dependencies = [ "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -281,7 +281,7 @@ checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -326,7 +326,7 @@ checksum = "36ea56748e10732c49404c153638a15ec3d6211ec5ff35d9bb20e13b93576adf" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -564,9 +564,9 @@ checksum = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" [[package]] name = "byte-unit" -version = "4.0.9" +version = "4.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c8758c32833faaae35b24a73d332e62d0528e89076ae841c63940e37008b153" +checksum = "b9520900471c3a9bbcfe0fd4c7b6bcfeff41b20a76cf91c59b7474b09be1ee27" dependencies = [ "utf8-width", ] @@ -579,9 +579,9 @@ checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58" [[package]] name = "byteorder" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" @@ -780,6 +780,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "common-path" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101" + [[package]] name = "concurrent-queue" version = "1.2.2" @@ -1104,7 +1110,7 @@ dependencies = [ "proc-macro2", "quote 1.0.9", "smallvec 1.6.1", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -1114,7 +1120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" dependencies = [ "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -1146,7 +1152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8f45d9ad417bcef4817d614a501ab55cdd96a6fdb24f49aab89a54acfd66b19" dependencies = [ "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -1254,7 +1260,7 @@ checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -1276,7 +1282,7 @@ checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -1576,7 +1582,7 @@ checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", "synstructure", ] @@ -1855,7 +1861,7 @@ dependencies = [ "proc-macro-hack", "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -2013,7 +2019,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -2206,7 +2212,7 @@ dependencies = [ "markup5ever", "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -3340,6 +3346,7 @@ dependencies = [ "bigdecimal", "byte-unit", "chrono", + "common-path", "derive-new", "directories-next", "dirs-next", @@ -3358,6 +3365,7 @@ dependencies = [ "num-traits 0.2.14", "query_interface", "serde 1.0.124", + "sha2 0.9.3", "toml", "users", ] @@ -3366,6 +3374,7 @@ dependencies = [ name = "nu-engine" version = "0.29.0" dependencies = [ + "ansi_term 0.12.1", "async-recursion", "async-trait", "bytes 0.5.6", @@ -3373,6 +3382,7 @@ dependencies = [ "derive-new", "dirs-next", "dunce", + "dyn-clone", "encoding_rs", "filesize", "fs_extra", @@ -4015,9 +4025,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "open" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2033f93630dd4b04768ecf5e16bcd3002a89e1e1dbef375bf290dd67e2b7a4d" +checksum = "a7e9f1bdf15cd1f5a00cc9002a733a6ee6d0ff562491852d59652471c4a389f7" dependencies = [ "which", "winapi 0.3.9", @@ -4219,7 +4229,7 @@ dependencies = [ "proc-macro-hack", "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -4257,7 +4267,7 @@ checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -4268,7 +4278,7 @@ checksum = "758669ae3558c6f74bd2a18b41f7ac0b5a195aea6639d6a9b5e5d1ad5ba24c0b" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -4403,7 +4413,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", "version_check", ] @@ -4517,7 +4527,7 @@ checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -4939,7 +4949,7 @@ dependencies = [ "proc-macro2", "quote 1.0.9", "rust-embed-utils", - "syn 1.0.62", + "syn 1.0.63", "walkdir", ] @@ -5235,7 +5245,7 @@ checksum = "1800f7693e94e186f5e25a28291ae1570da908aff7d97a095dec1e56ff99069b" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -5346,7 +5356,7 @@ checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -5511,7 +5521,7 @@ checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -5585,7 +5595,7 @@ dependencies = [ "quote 1.0.9", "serde 1.0.124", "serde_derive", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -5601,7 +5611,7 @@ dependencies = [ "serde_derive", "serde_json", "sha1 0.6.0", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -5740,9 +5750,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "123a78a3596b24fee53a6464ce52d8ecbf62241e6294c7e7fe12086cd161f512" +checksum = "8fd9bc7ccc2688b3344c2f48b9b546648b25ce0b20fc717ee7fa7981a8ca9717" dependencies = [ "proc-macro2", "quote 1.0.9", @@ -5766,7 +5776,7 @@ checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", "unicode-xid 0.2.1", ] @@ -5913,7 +5923,7 @@ checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -5991,7 +6001,7 @@ dependencies = [ "proc-macro2", "quote 1.0.9", "standback", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -6151,7 +6161,7 @@ checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -6340,13 +6350,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a9bd1db7706f2373a190b0d067146caa39350c486f3d455b0e33b431f94c07" +checksum = "41768be5b9f3489491825f56f01f25290aa1d3e7cc97e182d4d34360493ba6fa" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -6686,7 +6696,7 @@ dependencies = [ "log 0.4.14", "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", "wasm-bindgen-shared", ] @@ -6736,7 +6746,7 @@ checksum = "cc053ec74d454df287b9374ee8abb36ffd5acb95ba87da3ba5b7d3fe20eb401e" dependencies = [ "proc-macro2", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 6c070cfc54..ede076899d 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -1,6 +1,6 @@ use crate::line_editor::configure_ctrl_c; use nu_command::commands::default_context::create_default_context; -use nu_engine::{evaluation_context, run_block, script::run_script_standalone, EvaluationContext}; +use nu_engine::{maybe_print_errors, run_block, script::run_script_standalone, EvaluationContext}; #[allow(unused_imports)] pub(crate) use nu_engine::script::{process_script, LineResult}; @@ -13,8 +13,7 @@ use crate::line_editor::{ #[allow(unused_imports)] use nu_data::config; -use nu_data::config::{Conf, NuConfig}; -use nu_source::{AnchorLocation, Tag, Text}; +use nu_source::{Tag, Text}; use nu_stream::InputStream; use std::ffi::{OsStr, OsString}; #[allow(unused_imports)] @@ -23,10 +22,9 @@ use std::sync::atomic::Ordering; #[cfg(feature = "rustyline-support")] use rustyline::{self, error::ReadlineError}; -use crate::EnvironmentSyncer; use nu_errors::ShellError; use nu_parser::ParserScope; -use nu_protocol::{hir::ExternalRedirection, UntaggedValue, Value}; +use nu_protocol::{hir::ExternalRedirection, ConfigPath, UntaggedValue, Value}; use log::trace; use std::error::Error; @@ -35,10 +33,9 @@ use std::path::PathBuf; pub struct Options { pub config: Option, - pub history: Option, - pub save_history: bool, pub stdin: bool, pub scripts: Vec, + pub save_history: bool, } impl Default for Options { @@ -51,20 +48,9 @@ impl Options { pub fn new() -> Self { Self { config: None, - history: None, - save_history: true, stdin: false, scripts: vec![], - } - } - - pub fn history(&self, block: impl FnOnce(&std::path::Path)) { - if !self.save_history { - return; - } - - if let Some(file) = &self.history { - block(&file) + save_history: true, } } } @@ -137,25 +123,17 @@ pub fn search_paths() -> Vec { search_paths } -pub async fn run_script_file(mut options: Options) -> Result<(), Box> { - let mut context = create_default_context(false)?; - let mut syncer = create_environment_syncer(&context, &mut options); - let config = syncer.get_config(); +pub async fn run_script_file(options: Options) -> Result<(), Box> { + let context = create_default_context(false)?; - context.configure(&config, |_, ctx| { - syncer.load_environment(); - syncer.sync_env_vars(ctx); - syncer.sync_path_vars(ctx); + if let Some(cfg) = options.config { + load_cfg_as_global_cfg(&context, PathBuf::from(cfg)).await; + } else { + load_global_cfg(&context).await; + } - if let Err(reason) = syncer.autoenv(ctx) { - ctx.with_host(|host| host.print_err(reason, &Text::from(""))); - } - - let _ = register_plugins(ctx); - let _ = configure_ctrl_c(ctx); - }); - - let _ = run_startup_commands(&mut context, &config).await; + let _ = register_plugins(&context); + let _ = configure_ctrl_c(&context); let script = options .scripts @@ -167,76 +145,18 @@ pub async fn run_script_file(mut options: Options) -> Result<(), Box> Ok(()) } -fn create_environment_syncer( - context: &EvaluationContext, - options: &mut Options, -) -> EnvironmentSyncer { - let configuration = match &options.config { - Some(config_file) => { - let location = Some(AnchorLocation::File( - config_file.to_string_lossy().to_string(), - )); - - let tag = Tag::unknown().anchored(location); - - context.scope.add_var( - "config-path", - UntaggedValue::filepath(PathBuf::from(&config_file)).into_value(tag), - ); - - NuConfig::with(Some(config_file.into())) - } - None => NuConfig::new(), - }; - - let history_path = configuration.history_path(); - options.history = Some(history_path.clone()); - - let location = Some(AnchorLocation::File( - history_path.to_string_lossy().to_string(), - )); - - let tag = Tag::unknown().anchored(location); - - context.scope.add_var( - "history-path", - UntaggedValue::filepath(history_path).into_value(tag), - ); - - EnvironmentSyncer::with_config(Box::new(configuration)) -} - #[cfg(feature = "rustyline-support")] -pub async fn cli( - mut context: EvaluationContext, - mut options: Options, -) -> Result<(), Box> { - let mut syncer = create_environment_syncer(&context, &mut options); +pub async fn cli(context: EvaluationContext, options: Options) -> Result<(), Box> { + let _ = configure_ctrl_c(&context); - let configuration = syncer.get_config(); - - let mut rl = default_rustyline_editor_configuration(); - - context.configure(&configuration, |config, ctx| { - syncer.load_environment(); - syncer.sync_env_vars(ctx); - syncer.sync_path_vars(ctx); - - if let Err(reason) = syncer.autoenv(ctx) { - ctx.with_host(|host| host.print_err(reason, &Text::from(""))); - } - - let _ = configure_ctrl_c(ctx); - let _ = configure_rustyline_editor(&mut rl, config); - - let helper = Some(nu_line_editor_helper(ctx, config)); - rl.set_helper(helper); - }); - - // start time for command duration + // start time for running startup scripts (this metric includes loading of the cfg, but w/e) let startup_commands_start_time = std::time::Instant::now(); - // run the startup commands - let _ = run_startup_commands(&mut context, &configuration).await; + + if let Some(cfg) = options.config { + load_cfg_as_global_cfg(&context, PathBuf::from(cfg)).await; + } else { + load_global_cfg(&context).await; + } // Store cmd duration in an env var context.scope.add_env_var( "CMD_DURATION", @@ -247,20 +167,43 @@ pub async fn cli( startup_commands_start_time.elapsed() ); + //Configure rustyline + let mut rl = default_rustyline_editor_configuration(); + let history_path = if let Some(cfg) = &context.configs.lock().global_config { + let _ = configure_rustyline_editor(&mut rl, cfg); + let helper = Some(nu_line_editor_helper(&context, cfg)); + rl.set_helper(helper); + nu_data::config::path::history_path(cfg) + } else { + nu_data::config::path::default_history_path() + }; + + // Don't load history if it's not necessary + if options.save_history { + let _ = rl.load_history(&history_path); + } + + //set vars from cfg if present + let (skip_welcome_message, prompt) = if let Some(cfg) = &context.configs.lock().global_config { + ( + cfg.var("skip_welcome_message") + .map(|x| x.is_true()) + .unwrap_or(false), + cfg.var("prompt"), + ) + } else { + (false, None) + }; + + //Check whether dir we start in contains local cfg file and if so load it. + load_local_cfg_if_present(&context).await; + // Give ourselves a scope to work in context.scope.enter_scope(); - options.history(|file| { - let _ = rl.load_history(&file); - }); - let mut session_text = String::new(); let mut line_start: usize = 0; - let skip_welcome_message = configuration - .var("skip_welcome_message") - .map(|x| x.is_true()) - .unwrap_or(false); if !skip_welcome_message { println!( "Welcome to Nushell {} (type 'help' for more info)", @@ -284,11 +227,10 @@ pub async fn cli( let cwd = context.shell_manager.path(); let colored_prompt = { - if let Some(prompt) = configuration.var("prompt") { + if let Some(prompt) = &prompt { let prompt_line = prompt.as_string()?; context.scope.enter_scope(); - let (mut prompt_block, err) = nu_parser::parse(&prompt_line, 0, &context.scope); prompt_block.set_redirect(ExternalRedirection::Stdout); @@ -305,10 +247,7 @@ pub async fn cli( Ok(result) => match result.collect_string(Tag::unknown()).await { Ok(string_result) => { let errors = context.get_errors(); - evaluation_context::maybe_print_errors( - &context, - Text::from(prompt_line), - ); + maybe_print_errors(&context, Text::from(prompt_line)); context.clear_errors(); if !errors.is_empty() { @@ -381,54 +320,50 @@ pub async fn cli( .scope .add_env_var("CMD_DURATION", format!("{:?}", cmd_start_time.elapsed())); - // Check the config to see if we need to update the path - // TODO: make sure config is cached so we don't path this load every call - // FIXME: we probably want to be a bit more graceful if we can't set the environment - - context.configure(&configuration, |config, ctx| { - if syncer.did_config_change() { - syncer.reload(); - syncer.sync_env_vars(ctx); - syncer.sync_path_vars(ctx); - } - - if let Err(reason) = syncer.autoenv(ctx) { - ctx.with_host(|host| host.print_err(reason, &Text::from(""))); - } - - let _ = configure_rustyline_editor(&mut rl, config); - }); - match line { LineResult::Success(line) => { - options.history(|file| { + if options.save_history { rl.add_history_entry(&line); - let _ = rl.save_history(&file); - }); - - evaluation_context::maybe_print_errors(&context, Text::from(session_text.clone())); + let _ = rl.save_history(&history_path); + } + maybe_print_errors(&context, Text::from(session_text.clone())); } LineResult::ClearHistory => { - options.history(|file| { + if options.save_history { rl.clear_history(); - let _ = rl.save_history(&file); - }); + let _ = rl.save_history(&history_path); + } } - LineResult::Error(line, reason) => { - options.history(|file| { + LineResult::Error(line, err) => { + if options.save_history { rl.add_history_entry(&line); - let _ = rl.save_history(&file); - }); + let _ = rl.save_history(&history_path); + } - context.with_host(|host| host.print_err(reason, &Text::from(session_text.clone()))); + context + .host + .lock() + .print_err(err, &Text::from(session_text.clone())); + + // I am not so sure, we don't need maybe_print_errors here (as we printed an err + // above), because maybe_print_errors also clears the errors. + // TODO Analyze where above err comes from, and whether we need to clear + // context.errors here + // Or just be consistent and return errors always in context.errors... + maybe_print_errors(&context, Text::from(session_text.clone())); } LineResult::CtrlC => { - let config_ctrlc_exit = configuration - .var("ctrlc_exit") - .map(|s| s.value.is_true()) + let config_ctrlc_exit = context + .configs + .lock() + .global_config + .as_ref() + .map(|cfg| cfg.var("ctrlc_exit")) + .flatten() + .map(|ctrl_c| ctrl_c.is_true()) .unwrap_or(false); // default behavior is to allow CTRL-C spamming similar to other shells if !config_ctrlc_exit { @@ -436,10 +371,9 @@ pub async fn cli( } if ctrlcbreak { - options.history(|file| { - let _ = rl.save_history(&file); - }); - + if options.save_history { + let _ = rl.save_history(&history_path); + } std::process::exit(0); } else { context.with_host(|host| host.stdout("CTRL-C pressed (again to quit)")); @@ -463,14 +397,56 @@ pub async fn cli( } // we are ok if we can not save history - options.history(|file| { - let _ = rl.save_history(&file); - }); + if options.save_history { + let _ = rl.save_history(&history_path); + } Ok(()) } -pub fn register_plugins(context: &mut EvaluationContext) -> Result<(), ShellError> { +pub async fn load_local_cfg_if_present(context: &EvaluationContext) { + trace!("Loading local cfg if present"); + match config::loadable_cfg_exists_in_dir(PathBuf::from(context.shell_manager.path())) { + Ok(Some(cfg_path)) => { + if let Err(err) = context.load_config(&ConfigPath::Local(cfg_path)).await { + context.host.lock().print_err(err, &Text::from("")) + } + } + Err(e) => { + //Report error while checking for local cfg file + context.host.lock().print_err(e, &Text::from("")) + } + Ok(None) => { + //No local cfg file present in start dir + } + } +} + +async fn load_cfg_as_global_cfg(context: &EvaluationContext, path: PathBuf) { + if let Err(err) = context.load_config(&ConfigPath::Global(path.clone())).await { + context.host.lock().print_err(err, &Text::from("")); + } else { + //TODO current commands assume to find path to global cfg file under config-path + //TODO use newly introduced nuconfig::file_path instead + context.scope.add_var( + "config-path", + UntaggedValue::filepath(path).into_untagged_value(), + ); + } +} + +pub async fn load_global_cfg(context: &EvaluationContext) { + match config::default_path() { + Ok(path) => { + load_cfg_as_global_cfg(context, path).await; + } + Err(e) => { + context.host.lock().print_err(e, &Text::from("")); + } + } +} + +pub fn register_plugins(context: &EvaluationContext) -> Result<(), ShellError> { if let Ok(plugins) = nu_engine::plugin::build_plugin::scan(search_paths()) { context.add_commands( plugins @@ -483,34 +459,6 @@ pub fn register_plugins(context: &mut EvaluationContext) -> Result<(), ShellErro Ok(()) } -async fn run_startup_commands( - context: &mut EvaluationContext, - config: &dyn nu_data::config::Conf, -) -> Result<(), ShellError> { - if let Some(commands) = config.var("startup") { - match commands { - Value { - value: UntaggedValue::Table(pipelines), - .. - } => { - let mut script_file = String::new(); - for pipeline in pipelines { - script_file.push_str(&pipeline.as_string()?); - script_file.push('\n'); - } - let _ = run_script_standalone(script_file, false, context, false).await; - } - _ => { - return Err(ShellError::untagged_runtime_error( - "expected a table of pipeline strings as startup commands", - )); - } - } - } - - Ok(()) -} - pub async fn parse_and_eval(line: &str, ctx: &EvaluationContext) -> Result { // FIXME: do we still need this? let line = if let Some(s) = line.strip_suffix('\n') { @@ -528,8 +476,6 @@ pub async fn parse_and_eval(line: &str, ctx: &EvaluationContext) -> Result String { #[cfg(test)] mod tests { - use nu_engine::EvaluationContext; + use nu_engine::basic_evaluation_context; #[quickcheck] fn quickcheck_parse(data: String) -> bool { let (tokens, err) = nu_parser::lex(&data, 0); let (lite_block, err2) = nu_parser::parse_block(tokens); if err.is_none() && err2.is_none() { - let context = EvaluationContext::basic().unwrap(); + let context = basic_evaluation_context().unwrap(); let _ = nu_parser::classify_block(&lite_block, &context.scope); } true diff --git a/crates/nu-cli/src/env.rs b/crates/nu-cli/src/env.rs deleted file mode 100644 index e9a8eb7ec5..0000000000 --- a/crates/nu-cli/src/env.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod directory_specific_environment; -pub(crate) mod environment; -pub(crate) mod environment_syncer; diff --git a/crates/nu-cli/src/env/directory_specific_environment.rs b/crates/nu-cli/src/env/directory_specific_environment.rs deleted file mode 100644 index 4a3deceaae..0000000000 --- a/crates/nu-cli/src/env/directory_specific_environment.rs +++ /dev/null @@ -1,259 +0,0 @@ -use indexmap::{IndexMap, IndexSet}; -use nu_command::commands::autoenv; -use nu_errors::ShellError; -use serde::Deserialize; -use std::env::*; -use std::process::Command; - -use std::{ - ffi::OsString, - fmt::Debug, - path::{Path, PathBuf}, -}; - -//Tests reside in /nushell/tests/shell/pipeline/commands/internal.rs - -type EnvKey = String; -type EnvVal = OsString; -#[derive(Debug, Default)] -pub struct DirectorySpecificEnvironment { - pub last_seen_directory: PathBuf, - //If an environment var has been added from a .nu in a directory, we track it here so we can remove it when the user leaves the directory. - //If setting the var overwrote some value, we save the old value in an option so we can restore it later. - added_vars: IndexMap>>, - - //We track directories that we have read .nu-env from. This is different from the keys in added_vars since sometimes a file only wants to run scripts. - visited_dirs: IndexSet, - exitscripts: IndexMap>, -} - -#[derive(Deserialize, Debug, Default)] -pub struct NuEnvDoc { - pub env: Option>, - pub scriptvars: Option>, - pub scripts: Option>>, - pub entryscripts: Option>, - pub exitscripts: Option>, -} - -impl DirectorySpecificEnvironment { - pub fn new() -> DirectorySpecificEnvironment { - let root_dir = if cfg!(target_os = "windows") { - PathBuf::from("c:\\") - } else { - PathBuf::from("/") - }; - DirectorySpecificEnvironment { - last_seen_directory: root_dir, - added_vars: IndexMap::new(), - visited_dirs: IndexSet::new(), - exitscripts: IndexMap::new(), - } - } - - fn toml_if_trusted(&mut self, nu_env_file: &Path) -> Result { - let content = std::fs::read(&nu_env_file)?; - - if autoenv::file_is_trusted(&nu_env_file, &content)? { - let mut doc: NuEnvDoc = toml::de::from_slice(&content) - .map_err(|e| ShellError::untagged_runtime_error(format!("{:?}", e)))?; - - if let Some(scripts) = doc.scripts.as_ref() { - for (k, v) in scripts { - if k == "entryscripts" { - doc.entryscripts = Some(v.clone()); - } else if k == "exitscripts" { - doc.exitscripts = Some(v.clone()); - } - } - } - return Ok(doc); - } - Err(ShellError::untagged_runtime_error( - format!("{:?} is untrusted. Run 'autoenv trust {:?}' to trust it.\nThis needs to be done after each change to the file.", nu_env_file, nu_env_file.parent().unwrap_or_else(|| &Path::new(""))))) - } - - pub fn maintain_autoenv(&mut self) -> Result<(), ShellError> { - let mut dir = current_dir()?; - - if self.last_seen_directory == dir { - return Ok(()); - } - - //We track which keys we set as we go up the directory hierarchy, so that we don't overwrite a value we set in a subdir. - let mut added_keys = IndexSet::new(); - - let mut new_visited_dirs = IndexSet::new(); - let mut popped = true; - while popped { - let nu_env_file = dir.join(".nu-env"); - if nu_env_file.exists() && !self.visited_dirs.contains(&dir) { - let nu_env_doc = self.toml_if_trusted(&nu_env_file)?; - - //add regular variables from the [env section] - if let Some(env) = nu_env_doc.env { - for (env_key, env_val) in env { - self.maybe_add_key(&mut added_keys, &dir, &env_key, &env_val); - } - } - - //Add variables that need to evaluate scripts to run, from [scriptvars] section - if let Some(sv) = nu_env_doc.scriptvars { - for (key, script) in sv { - self.maybe_add_key( - &mut added_keys, - &dir, - &key, - value_from_script(&script)?.as_str(), - ); - } - } - - if let Some(es) = nu_env_doc.entryscripts { - for s in es { - run(s.as_str(), None)?; - } - } - - if let Some(es) = nu_env_doc.exitscripts { - self.exitscripts.insert(dir.clone(), es); - } - } - new_visited_dirs.insert(dir.clone()); - popped = dir.pop(); - } - - //Time to clear out vars set by directories that we have left. - let mut new_vars = IndexMap::new(); - for (dir, dirmap) in self.added_vars.drain(..) { - if new_visited_dirs.contains(&dir) { - new_vars.insert(dir, dirmap); - } else { - for (k, v) in dirmap { - if let Some(v) = v { - std::env::set_var(k, v); - } else { - std::env::remove_var(k); - } - } - } - } - - //Run exitscripts, can not be done in same loop as new vars as some files can contain only exitscripts - let mut new_exitscripts = IndexMap::new(); - for (dir, scripts) in self.exitscripts.drain(..) { - if new_visited_dirs.contains(&dir) { - new_exitscripts.insert(dir, scripts); - } else { - for s in scripts { - run(s.as_str(), Some(&dir))?; - } - } - } - - self.visited_dirs = new_visited_dirs; - self.exitscripts = new_exitscripts; - self.added_vars = new_vars; - self.last_seen_directory = current_dir()?; - Ok(()) - } - - pub fn maybe_add_key( - &mut self, - seen_vars: &mut IndexSet, - dir: &Path, - key: &str, - val: &str, - ) { - //This condition is to make sure variables in parent directories don't overwrite variables set by subdirectories. - if !seen_vars.contains(key) { - seen_vars.insert(key.to_string()); - self.added_vars - .entry(PathBuf::from(dir)) - .or_insert(IndexMap::new()) - .insert(key.to_string(), var_os(key)); - - std::env::set_var(key, val); - } - } - - // If the user recently ran autoenv untrust on a file, we clear the environment variables it set and make sure to not run any possible exitscripts. - pub fn clear_recently_untrusted_file(&mut self) -> Result<(), ShellError> { - // Figure out which file was untrusted - // Remove all vars set by it - let current_trusted_files: IndexSet = autoenv::read_trusted()? - .files - .iter() - .map(|(k, _)| PathBuf::from(k)) - .collect(); - - // We figure out which file(s) the user untrusted by taking the set difference of current trusted files in .config/nu/nu-env.toml and the files tracked by self.added_env_vars - // If a file is in self.added_env_vars but not in nu-env.toml, it was just untrusted. - let untrusted_files: IndexSet = self - .added_vars - .iter() - .filter_map(|(path, _)| { - if !current_trusted_files.contains(path) { - return Some(path.clone()); - } - None - }) - .collect(); - - for path in untrusted_files { - if let Some(added_keys) = self.added_vars.get(&path) { - for (key, _) in added_keys { - remove_var(key); - } - } - self.exitscripts.remove(&path); - self.added_vars.remove(&path); - } - - Ok(()) - } -} - -fn run(cmd: &str, dir: Option<&PathBuf>) -> Result<(), ShellError> { - if cfg!(target_os = "windows") { - if let Some(dir) = dir { - let command = format!("cd {} & {}", dir.to_string_lossy(), cmd); - Command::new("cmd") - .args(&["/C", command.as_str()]) - .output()? - } else { - Command::new("cmd").args(&["/C", cmd]).output()? - } - } else if let Some(dir) = dir { - // FIXME: When nu scripting is added, cding like might not be a good idea. If nu decides to execute entryscripts when entering the dir this way, it will cause troubles. - // For now only standard shell scripts are used, so this is an issue for the future. - Command::new("sh") - .arg("-c") - .arg(format!("cd {:?}; {}", dir, cmd)) - .output()? - } else { - Command::new("sh").arg("-c").arg(&cmd).output()? - }; - Ok(()) -} -fn value_from_script(cmd: &str) -> Result { - let command = if cfg!(target_os = "windows") { - Command::new("cmd").args(&["/C", cmd]).output()? - } else { - Command::new("sh").arg("-c").arg(&cmd).output()? - }; - if command.stdout.is_empty() { - return Err(ShellError::untagged_runtime_error(format!( - "{:?} did not return any output", - cmd - ))); - } - let response = std::str::from_utf8(&command.stdout[..command.stdout.len()]).map_err(|e| { - ShellError::untagged_runtime_error(format!( - "Couldn't parse stdout from command {:?}: {:?}", - command, e - )) - })?; - - Ok(response.trim().to_string()) -} diff --git a/crates/nu-cli/src/env/environment.rs b/crates/nu-cli/src/env/environment.rs deleted file mode 100644 index 37bf638ad5..0000000000 --- a/crates/nu-cli/src/env/environment.rs +++ /dev/null @@ -1,274 +0,0 @@ -use crate::env::directory_specific_environment::*; -use indexmap::{indexmap, IndexSet}; -use nu_data::config::Conf; -use nu_engine::Env; -use nu_errors::ShellError; -use nu_protocol::{UntaggedValue, Value}; - -#[derive(Debug, Default)] -pub struct Environment { - environment_vars: Option, - path_vars: Option, - pub autoenv: DirectorySpecificEnvironment, -} - -impl Environment { - pub fn new() -> Environment { - Environment { - environment_vars: None, - path_vars: None, - autoenv: DirectorySpecificEnvironment::new(), - } - } - - pub fn from_config(configuration: &T) -> Environment { - let env = configuration.env(); - let path = configuration.path(); - Environment { - environment_vars: env, - path_vars: path, - autoenv: DirectorySpecificEnvironment::new(), - } - } - - pub fn autoenv(&mut self, reload_trusted: bool) -> Result<(), ShellError> { - self.autoenv.maintain_autoenv()?; - if reload_trusted { - self.autoenv.clear_recently_untrusted_file()?; - } - Ok(()) - } - - pub fn morph(&mut self, configuration: &T) { - self.environment_vars = configuration.env(); - self.path_vars = configuration.path(); - } -} - -impl Env for Environment { - fn env(&self) -> Option { - if let Some(vars) = &self.environment_vars { - return Some(vars.clone()); - } - - None - } - - fn path(&self) -> Option { - if let Some(vars) = &self.path_vars { - return Some(vars.clone()); - } - - None - } - - fn add_env(&mut self, key: &str, value: &str) { - let value = UntaggedValue::string(value); - - let new_envs = { - if let Some(Value { - value: UntaggedValue::Row(ref envs), - ref tag, - }) = self.environment_vars - { - let mut new_envs = envs.clone(); - - if !new_envs.contains_key(key) { - new_envs.insert_data_at_key(key, value.into_value(tag.clone())); - } - - Value { - value: UntaggedValue::Row(new_envs), - tag: tag.clone(), - } - } else { - UntaggedValue::Row(indexmap! { key.into() => value.into_untagged_value() }.into()) - .into_untagged_value() - } - }; - - self.environment_vars = Some(new_envs); - } - - fn add_path(&mut self, paths: std::ffi::OsString) { - let new_paths = { - if let Some(Value { - value: UntaggedValue::Table(ref current_paths), - ref tag, - }) = self.path_vars - { - let mut new_paths = current_paths.clone(); - - let new_path_candidates = std::env::split_paths(&paths).map(|path| { - UntaggedValue::string(path.to_string_lossy()).into_value(tag.clone()) - }); - - new_paths.extend(new_path_candidates); - - let paths: IndexSet = new_paths.into_iter().collect(); - - Value { - value: UntaggedValue::Table(paths.into_iter().collect()), - tag: tag.clone(), - } - } else { - let p = paths.into_string().unwrap_or_else(|_| String::from("")); - let p = UntaggedValue::string(p).into_untagged_value(); - UntaggedValue::Table(vec![p]).into_untagged_value() - } - }; - - self.path_vars = Some(new_paths); - } -} - -#[cfg(test)] -mod tests { - use super::{Env, Environment}; - use nu_data::config::{tests::FakeConfig, Conf}; - use nu_protocol::UntaggedValue; - use nu_test_support::fs::Stub::FileWithContent; - use nu_test_support::playground::Playground; - - #[test] - fn picks_up_environment_variables_from_configuration() { - Playground::setup("environment_test_1", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "configuration.toml", - r#" - [env] - mosquetero_1 = "Andrés N. Robalino" - mosquetero_2 = "Jonathan Turner" - mosquetero_3 = "Yehuda katz" - mosquetero_4 = "Jason Gedge" - "#, - )]); - - let mut file = dirs.test().clone(); - file.push("configuration.toml"); - - let fake_config = FakeConfig::new(&file); - let actual = Environment::from_config(&fake_config); - - assert_eq!(actual.env(), fake_config.env()); - }); - } - - #[test] - fn picks_up_path_variables_from_configuration() { - Playground::setup("environment_test_2", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "configuration.toml", - r#" - path = ["/Users/andresrobalino/.volta/bin", "/users/mosqueteros/bin"] - "#, - )]); - - let mut file = dirs.test().clone(); - file.push("configuration.toml"); - - let fake_config = FakeConfig::new(&file); - let actual = Environment::from_config(&fake_config); - - assert_eq!(actual.path(), fake_config.path()); - }); - } - - #[test] - fn updates_env_variable() { - Playground::setup("environment_test_3", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "configuration.toml", - r#" - [env] - SHELL = "/usr/bin/you_already_made_the_nu_choice" - "#, - )]); - - let mut file = dirs.test().clone(); - file.push("configuration.toml"); - - let fake_config = FakeConfig::new(&file); - let mut actual = Environment::from_config(&fake_config); - - actual.add_env("USER", "NUNO"); - - assert_eq!( - actual.env(), - Some( - UntaggedValue::row( - indexmap! { - "USER".into() => UntaggedValue::string("NUNO").into_untagged_value(), - "SHELL".into() => UntaggedValue::string("/usr/bin/you_already_made_the_nu_choice").into_untagged_value(), - } - ).into_untagged_value() - ) - ); - }); - } - - #[test] - fn does_not_update_env_variable_if_it_exists() { - Playground::setup("environment_test_4", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "configuration.toml", - r#" - [env] - SHELL = "/usr/bin/you_already_made_the_nu_choice" - "#, - )]); - - let mut file = dirs.test().clone(); - file.push("configuration.toml"); - - let fake_config = FakeConfig::new(&file); - let mut actual = Environment::from_config(&fake_config); - - actual.add_env("SHELL", "/usr/bin/sh"); - - assert_eq!( - actual.env(), - Some( - UntaggedValue::row( - indexmap! { - "SHELL".into() => UntaggedValue::string("/usr/bin/you_already_made_the_nu_choice").into_untagged_value(), - } - ).into_untagged_value() - ) - ); - }); - } - - #[test] - fn updates_path_variable() { - Playground::setup("environment_test_5", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "configuration.toml", - r#" - path = ["/Users/andresrobalino/.volta/bin", "/users/mosqueteros/bin"] - "#, - )]); - - let mut file = dirs.test().clone(); - file.push("configuration.toml"); - - let fake_config = FakeConfig::new(&file); - let mut actual = Environment::from_config(&fake_config); - - actual.add_path(std::ffi::OsString::from("/path/to/be/added")); - - assert_eq!( - actual.path(), - Some( - UntaggedValue::table(&[ - UntaggedValue::string("/Users/andresrobalino/.volta/bin") - .into_untagged_value(), - UntaggedValue::string("/users/mosqueteros/bin").into_untagged_value(), - UntaggedValue::string("/path/to/be/added").into_untagged_value(), - ]) - .into_untagged_value() - ) - ); - }); - } -} diff --git a/crates/nu-cli/src/env/environment_syncer.rs b/crates/nu-cli/src/env/environment_syncer.rs deleted file mode 100644 index 3f02314225..0000000000 --- a/crates/nu-cli/src/env/environment_syncer.rs +++ /dev/null @@ -1,617 +0,0 @@ -use crate::env::environment::Environment; -use nu_data::config::{Conf, NuConfig}; -use nu_engine::Env; -use nu_engine::EvaluationContext; -use nu_errors::ShellError; -use parking_lot::Mutex; -use std::sync::{atomic::Ordering, Arc}; - -pub struct EnvironmentSyncer { - pub env: Arc>>, - pub config: Arc>>, -} - -impl Default for EnvironmentSyncer { - fn default() -> Self { - Self::new() - } -} - -impl EnvironmentSyncer { - pub fn with_config(config: Box) -> Self { - EnvironmentSyncer { - env: Arc::new(Mutex::new(Box::new(Environment::new()))), - config: Arc::new(Mutex::new(config)), - } - } - - pub fn new() -> EnvironmentSyncer { - EnvironmentSyncer { - env: Arc::new(Mutex::new(Box::new(Environment::new()))), - config: Arc::new(Mutex::new(Box::new(NuConfig::new()))), - } - } - - #[cfg(test)] - pub fn set_config(&mut self, config: Box) { - self.config = Arc::new(Mutex::new(config)); - } - - pub fn get_config(&self) -> Box { - let config = self.config.lock(); - - config.clone_box() - } - - pub fn load_environment(&mut self) { - let config = self.config.lock(); - - self.env = Arc::new(Mutex::new(Box::new(Environment::from_config(&*config)))); - } - - pub fn did_config_change(&mut self) -> bool { - let config = self.config.lock(); - config.is_modified().unwrap_or(false) - } - - pub fn reload(&mut self) { - let mut config = self.config.lock(); - config.reload(); - - let mut environment = self.env.lock(); - environment.morph(&*config); - } - - pub fn autoenv(&self, ctx: &mut EvaluationContext) -> Result<(), ShellError> { - let mut environment = self.env.lock(); - let recently_used = ctx - .user_recently_used_autoenv_untrust - .load(Ordering::SeqCst); - let auto = environment.autoenv(recently_used); - ctx.user_recently_used_autoenv_untrust - .store(false, Ordering::SeqCst); - auto - } - - pub fn sync_env_vars(&mut self, ctx: &mut EvaluationContext) { - let mut environment = self.env.lock(); - - if environment.env().is_some() { - for (name, value) in ctx.with_host(|host| host.vars()) { - if name != "path" && name != "PATH" { - // account for new env vars present in the current session - // that aren't loaded from config. - environment.add_env(&name, &value); - - // clear the env var from the session - // we are about to replace them - ctx.with_host(|host| host.env_rm(std::ffi::OsString::from(name))); - } - } - - if let Some(variables) = environment.env() { - for var in variables.row_entries() { - if let Ok(string) = var.1.as_string() { - ctx.with_host(|host| { - host.env_set( - std::ffi::OsString::from(var.0), - std::ffi::OsString::from(&string), - ) - }); - - ctx.scope.add_env_var_to_base(var.0, string); - } - } - } - } - } - - pub fn sync_path_vars(&mut self, ctx: &mut EvaluationContext) { - let mut environment = self.env.lock(); - - if environment.path().is_some() { - let native_paths = ctx.with_host(|host| host.env_get(std::ffi::OsString::from("PATH"))); - - if let Some(native_paths) = native_paths { - environment.add_path(native_paths); - - ctx.with_host(|host| { - host.env_rm(std::ffi::OsString::from("PATH")); - }); - } - - if let Some(new_paths) = environment.path() { - let prepared = std::env::join_paths( - new_paths - .table_entries() - .map(|p| p.as_string()) - .filter_map(Result::ok), - ); - - if let Ok(paths_ready) = prepared { - ctx.with_host(|host| { - host.env_set( - std::ffi::OsString::from("PATH"), - std::ffi::OsString::from(&paths_ready), - ); - }); - - ctx.scope - .add_env_var_to_base("PATH", paths_ready.to_string_lossy().to_string()); - } - } - } - } - - #[cfg(test)] - pub fn clear_env_vars(&mut self, ctx: &mut EvaluationContext) { - for (key, _value) in ctx.with_host(|host| host.vars()) { - if key != "path" && key != "PATH" { - ctx.with_host(|host| host.env_rm(std::ffi::OsString::from(key))); - } - } - } - - #[cfg(test)] - pub fn clear_path_var(&mut self, ctx: &mut EvaluationContext) { - ctx.with_host(|host| host.env_rm(std::ffi::OsString::from("PATH"))); - } -} - -#[cfg(test)] -mod tests { - use super::EnvironmentSyncer; - use indexmap::IndexMap; - use nu_data::config::tests::FakeConfig; - use nu_engine::Env; - use nu_engine::EvaluationContext; - use nu_errors::ShellError; - use nu_test_support::fs::Stub::FileWithContent; - use nu_test_support::playground::Playground; - use parking_lot::Mutex; - use std::path::PathBuf; - use std::sync::Arc; - - // This test fails on Linux. - // It's possible it has something to do with the fake configuration - // TODO: More tests. - #[cfg(not(target_os = "linux"))] - #[test] - fn syncs_env_if_new_env_entry_is_added_to_an_existing_configuration() -> Result<(), ShellError> - { - let mut ctx = EvaluationContext::basic()?; - ctx.host = Arc::new(Mutex::new(Box::new(nu_engine::FakeHost::new()))); - - let mut expected = IndexMap::new(); - expected.insert( - "SHELL".to_string(), - "/usr/bin/you_already_made_the_nu_choice".to_string(), - ); - - Playground::setup("syncs_env_from_config_updated_test_1", |dirs, sandbox| { - sandbox.with_files(vec![ - FileWithContent( - "configuration.toml", - r#" - [env] - SHELL = "/usr/bin/you_already_made_the_nu_choice" - "#, - ), - FileWithContent( - "updated_configuration.toml", - r#" - [env] - SHELL = "/usr/bin/you_already_made_the_nu_choice" - USER = "NUNO" - "#, - ), - ]); - - let file = dirs.test().join("configuration.toml"); - let new_file = dirs.test().join("updated_configuration.toml"); - - let fake_config = FakeConfig::new(&file); - let mut actual = EnvironmentSyncer::with_config(Box::new(fake_config)); - - // Here, the environment variables from the current session - // are cleared since we will load and set them from the - // configuration file - actual.clear_env_vars(&mut ctx); - - // Nu loads the environment variables from the configuration file - actual.load_environment(); - actual.sync_env_vars(&mut ctx); - - { - let environment = actual.env.lock(); - let mut vars = IndexMap::new(); - environment - .env() - .expect("No variables in the environment.") - .row_entries() - .for_each(|(name, value)| { - vars.insert( - name.to_string(), - value.as_string().expect("Couldn't convert to string"), - ); - }); - - for k in expected.keys() { - assert!(vars.contains_key(k)); - } - } - - assert!(!actual.did_config_change()); - - // Replacing the newer configuration file to the existing one. - let new_config_contents = std::fs::read_to_string(new_file).expect("Failed"); - std::fs::write(&file, &new_config_contents).expect("Failed"); - - // A change has happened - assert!(actual.did_config_change()); - - // Syncer should reload and add new envs - actual.reload(); - actual.sync_env_vars(&mut ctx); - - expected.insert("USER".to_string(), "NUNO".to_string()); - - { - let environment = actual.env.lock(); - let mut vars = IndexMap::new(); - environment - .env() - .expect("No variables in the environment.") - .row_entries() - .for_each(|(name, value)| { - vars.insert( - name.to_string(), - value.as_string().expect("Couldn't convert to string"), - ); - }); - - for k in expected.keys() { - assert!(vars.contains_key(k)); - } - } - }); - - Ok(()) - } - - #[test] - fn syncs_env_if_new_env_entry_in_session_is_not_in_configuration_file() -> Result<(), ShellError> - { - let mut ctx = EvaluationContext::basic()?; - ctx.host = Arc::new(Mutex::new(Box::new(nu_engine::FakeHost::new()))); - - let mut expected = IndexMap::new(); - expected.insert( - "SHELL".to_string(), - "/usr/bin/you_already_made_the_nu_choice".to_string(), - ); - expected.insert("USER".to_string(), "NUNO".to_string()); - - Playground::setup("syncs_env_test_1", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "configuration.toml", - r#" - [env] - SHELL = "/usr/bin/you_already_made_the_nu_choice" - "#, - )]); - - let mut file = dirs.test().clone(); - file.push("configuration.toml"); - - let fake_config = FakeConfig::new(&file); - let mut actual = EnvironmentSyncer::new(); - actual.set_config(Box::new(fake_config)); - - // Here, the environment variables from the current session - // are cleared since we will load and set them from the - // configuration file (if any) - actual.clear_env_vars(&mut ctx); - - // We explicitly simulate and add the USER variable to the current - // session's environment variables with the value "NUNO". - ctx.with_host(|test_host| { - test_host.env_set( - std::ffi::OsString::from("USER"), - std::ffi::OsString::from("NUNO"), - ) - }); - - // Nu loads the environment variables from the configuration file (if any) - actual.load_environment(); - - // By this point, Nu has already loaded the environment variables - // stored in the configuration file. Before continuing we check - // if any new environment variables have been added from the ones loaded - // in the configuration file. - // - // Nu sees the missing "USER" variable and accounts for it. - actual.sync_env_vars(&mut ctx); - - // Confirms session environment variables are replaced from Nu configuration file - // including the newer one accounted for. - ctx.with_host(|test_host| { - let var_user = test_host - .env_get(std::ffi::OsString::from("USER")) - .expect("Couldn't get USER var from host.") - .into_string() - .expect("Couldn't convert to string."); - - let var_shell = test_host - .env_get(std::ffi::OsString::from("SHELL")) - .expect("Couldn't get SHELL var from host.") - .into_string() - .expect("Couldn't convert to string."); - - let mut found = IndexMap::new(); - found.insert("SHELL".to_string(), var_shell); - found.insert("USER".to_string(), var_user); - - for k in found.keys() { - assert!(expected.contains_key(k)); - } - }); - - // Now confirm in-memory environment variables synced appropriately - // including the newer one accounted for. - let environment = actual.env.lock(); - - let mut vars = IndexMap::new(); - environment - .env() - .expect("No variables in the environment.") - .row_entries() - .for_each(|(name, value)| { - vars.insert( - name.to_string(), - value.as_string().expect("Couldn't convert to string"), - ); - }); - for k in expected.keys() { - assert!(vars.contains_key(k)); - } - }); - Ok(()) - } - - #[test] - fn nu_envs_have_higher_priority_and_does_not_get_overwritten() -> Result<(), ShellError> { - let mut ctx = EvaluationContext::basic()?; - ctx.host = Arc::new(Mutex::new(Box::new(nu_engine::FakeHost::new()))); - - let mut expected = IndexMap::new(); - expected.insert( - "SHELL".to_string(), - "/usr/bin/you_already_made_the_nu_choice".to_string(), - ); - - Playground::setup("syncs_env_test_2", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "configuration.toml", - r#" - [env] - SHELL = "/usr/bin/you_already_made_the_nu_choice" - "#, - )]); - - let mut file = dirs.test().clone(); - file.push("configuration.toml"); - - let fake_config = FakeConfig::new(&file); - let mut actual = EnvironmentSyncer::new(); - actual.set_config(Box::new(fake_config)); - - actual.clear_env_vars(&mut ctx); - - ctx.with_host(|test_host| { - test_host.env_set( - std::ffi::OsString::from("SHELL"), - std::ffi::OsString::from("/usr/bin/sh"), - ) - }); - - actual.load_environment(); - actual.sync_env_vars(&mut ctx); - - ctx.with_host(|test_host| { - let var_shell = test_host - .env_get(std::ffi::OsString::from("SHELL")) - .expect("Couldn't get SHELL var from host.") - .into_string() - .expect("Couldn't convert to string."); - - let mut found = IndexMap::new(); - found.insert("SHELL".to_string(), var_shell); - - for k in found.keys() { - assert!(expected.contains_key(k)); - } - }); - - let environment = actual.env.lock(); - - let mut vars = IndexMap::new(); - environment - .env() - .expect("No variables in the environment.") - .row_entries() - .for_each(|(name, value)| { - vars.insert( - name.to_string(), - value.as_string().expect("couldn't convert to string"), - ); - }); - for k in expected.keys() { - assert!(vars.contains_key(k)); - } - }); - - Ok(()) - } - - #[test] - fn syncs_path_if_new_path_entry_in_session_is_not_in_configuration_file( - ) -> Result<(), ShellError> { - let mut ctx = EvaluationContext::basic()?; - ctx.host = Arc::new(Mutex::new(Box::new(nu_engine::FakeHost::new()))); - - let expected = std::env::join_paths(vec![ - PathBuf::from("/Users/andresrobalino/.volta/bin"), - PathBuf::from("/Users/mosqueteros/bin"), - PathBuf::from("/path/to/be/added"), - ]) - .expect("Couldn't join paths.") - .into_string() - .expect("Couldn't convert to string."); - - Playground::setup("syncs_path_test_1", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "configuration.toml", - r#" - path = ["/Users/andresrobalino/.volta/bin", "/Users/mosqueteros/bin"] - "#, - )]); - - let mut file = dirs.test().clone(); - file.push("configuration.toml"); - - let fake_config = FakeConfig::new(&file); - let mut actual = EnvironmentSyncer::new(); - actual.set_config(Box::new(fake_config)); - - // Here, the environment variables from the current session - // are cleared since we will load and set them from the - // configuration file (if any) - actual.clear_path_var(&mut ctx); - - // We explicitly simulate and add the PATH variable to the current - // session with the path "/path/to/be/added". - ctx.with_host(|test_host| { - test_host.env_set( - std::ffi::OsString::from("PATH"), - std::env::join_paths(vec![PathBuf::from("/path/to/be/added")]) - .expect("Couldn't join paths."), - ) - }); - - // Nu loads the path variables from the configuration file (if any) - actual.load_environment(); - - // By this point, Nu has already loaded environment path variable - // stored in the configuration file. Before continuing we check - // if any new paths have been added from the ones loaded in the - // configuration file. - // - // Nu sees the missing "/path/to/be/added" and accounts for it. - actual.sync_path_vars(&mut ctx); - - ctx.with_host(|test_host| { - let actual = test_host - .env_get(std::ffi::OsString::from("PATH")) - .expect("Couldn't get PATH var from host.") - .into_string() - .expect("Couldn't convert to string."); - - assert_eq!(actual, expected); - }); - - let environment = actual.env.lock(); - - let paths = std::env::join_paths( - &environment - .path() - .expect("No path variable in the environment.") - .table_entries() - .map(|value| value.as_string().expect("Couldn't convert to string")) - .map(PathBuf::from) - .collect::>(), - ) - .expect("Couldn't join paths.") - .into_string() - .expect("Couldn't convert to string."); - - assert_eq!(paths, expected); - }); - - Ok(()) - } - - #[test] - fn nu_paths_have_higher_priority_and_new_paths_get_appended_to_the_end( - ) -> Result<(), ShellError> { - let mut ctx = EvaluationContext::basic()?; - ctx.host = Arc::new(Mutex::new(Box::new(nu_engine::FakeHost::new()))); - - let expected = std::env::join_paths(vec![ - PathBuf::from("/Users/andresrobalino/.volta/bin"), - PathBuf::from("/Users/mosqueteros/bin"), - PathBuf::from("/path/to/be/added"), - ]) - .expect("Couldn't join paths.") - .into_string() - .expect("Couldn't convert to string."); - - Playground::setup("syncs_path_test_2", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "configuration.toml", - r#" - path = ["/Users/andresrobalino/.volta/bin", "/Users/mosqueteros/bin"] - "#, - )]); - - let mut file = dirs.test().clone(); - file.push("configuration.toml"); - - let fake_config = FakeConfig::new(&file); - let mut actual = EnvironmentSyncer::new(); - actual.set_config(Box::new(fake_config)); - - actual.clear_path_var(&mut ctx); - - ctx.with_host(|test_host| { - test_host.env_set( - std::ffi::OsString::from("PATH"), - std::env::join_paths(vec![PathBuf::from("/path/to/be/added")]) - .expect("Couldn't join paths."), - ) - }); - - actual.load_environment(); - actual.sync_path_vars(&mut ctx); - - ctx.with_host(|test_host| { - let actual = test_host - .env_get(std::ffi::OsString::from("PATH")) - .expect("Couldn't get PATH var from host.") - .into_string() - .expect("Couldn't convert to string."); - - assert_eq!(actual, expected); - }); - - let environment = actual.env.lock(); - - let paths = std::env::join_paths( - &environment - .path() - .expect("No path variable in the environment.") - .table_entries() - .map(|value| value.as_string().expect("Couldn't convert to string")) - .map(PathBuf::from) - .collect::>(), - ) - .expect("Couldn't join paths.") - .into_string() - .expect("Couldn't convert to string."); - - assert_eq!(paths, expected); - }); - - Ok(()) - } -} diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index 809bac120c..a90039144d 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -1,9 +1,5 @@ #![recursion_limit = "2048"] -#[cfg(test)] -#[macro_use] -extern crate indexmap; - #[macro_use] mod prelude; @@ -16,7 +12,6 @@ extern crate quickcheck_macros; mod cli; #[cfg(feature = "rustyline-support")] mod completion; -mod env; mod format; #[cfg(feature = "rustyline-support")] mod keybinding; @@ -30,7 +25,6 @@ pub use crate::cli::cli; pub use crate::cli::{parse_and_eval, register_plugins, run_script_file}; pub use crate::cli::{NuScript, Options}; -pub use crate::env::environment_syncer::EnvironmentSyncer; pub use nu_command::commands::default_context::create_default_context; pub use nu_data::config; pub use nu_data::dict::TaggedListBuilder; diff --git a/crates/nu-cli/src/line_editor.rs b/crates/nu-cli/src/line_editor.rs index 4875b0534e..06624df8ad 100644 --- a/crates/nu-cli/src/line_editor.rs +++ b/crates/nu-cli/src/line_editor.rs @@ -202,7 +202,7 @@ pub fn configure_rustyline_editor( #[cfg(feature = "rustyline-support")] pub fn nu_line_editor_helper( - context: &mut EvaluationContext, + context: &EvaluationContext, config: &dyn nu_data::config::Conf, ) -> crate::shell::Helper { let hinter = rustyline_hinter(config); @@ -224,7 +224,7 @@ pub fn rustyline_hinter( Some(rustyline::hint::HistoryHinter {}) } -pub fn configure_ctrl_c(_context: &mut EvaluationContext) -> Result<(), Box> { +pub fn configure_ctrl_c(_context: &EvaluationContext) -> Result<(), Box> { #[cfg(feature = "ctrlc")] { let cc = _context.ctrl_c.clone(); diff --git a/crates/nu-cli/src/shell/helper.rs b/crates/nu-cli/src/shell/helper.rs index 0ea2827ab0..2c56915530 100644 --- a/crates/nu-cli/src/shell/helper.rs +++ b/crates/nu-cli/src/shell/helper.rs @@ -149,6 +149,7 @@ impl rustyline::Helper for Helper {} #[cfg(test)] mod tests { use super::*; + use nu_engine::basic_evaluation_context; use rustyline::completion::Completer; use rustyline::line_buffer::LineBuffer; @@ -162,7 +163,7 @@ mod tests { buffer.insert_str(0, text); buffer.set_pos(text.len() - 1); - let helper = Helper::new(EvaluationContext::basic().unwrap(), None); + let helper = Helper::new(basic_evaluation_context().unwrap(), None); helper.update(&mut buffer, "cd ".len(), &replacement); @@ -182,7 +183,7 @@ mod tests { buffer.insert_str(0, text); buffer.set_pos(text.len() - 30); - let helper = Helper::new(EvaluationContext::basic().unwrap(), None); + let helper = Helper::new(basic_evaluation_context().unwrap(), None); helper.update(&mut buffer, "cd ".len(), &replacement); diff --git a/crates/nu-command/src/commands/autoenv.rs b/crates/nu-command/src/commands/autoenv.rs index c2fb974674..ee79f53dd4 100644 --- a/crates/nu-command/src/commands/autoenv.rs +++ b/crates/nu-command/src/commands/autoenv.rs @@ -2,49 +2,8 @@ use crate::prelude::*; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; -use serde::Deserialize; -use serde::Serialize; -use sha2::{Digest, Sha256}; -use std::io::Read; -use std::path::{Path, PathBuf}; pub struct Autoenv; -#[derive(Deserialize, Serialize, Debug, Default)] -pub struct Trusted { - pub files: IndexMap>, -} -impl Trusted { - pub fn new() -> Self { - Trusted { - files: IndexMap::new(), - } - } -} -pub fn file_is_trusted(nu_env_file: &Path, content: &[u8]) -> Result { - let contentdigest = Sha256::digest(&content).as_slice().to_vec(); - let nufile = std::fs::canonicalize(nu_env_file)?; - - let trusted = read_trusted()?; - - Ok(trusted.files.get(&nufile.to_string_lossy().to_string()) == Some(&contentdigest)) -} - -pub fn read_trusted() -> Result { - let config_path = config::default_path_for(&Some(PathBuf::from("nu-env.toml")))?; - - let mut file = std::fs::OpenOptions::new() - .read(true) - .create(true) - .write(true) - .open(config_path) - .map_err(|_| ShellError::untagged_runtime_error("Couldn't open nu-env.toml"))?; - let mut doc = String::new(); - file.read_to_string(&mut doc)?; - - let allowed = toml::de::from_str(doc.as_str()).unwrap_or_else(|_| Trusted::new()); - Ok(allowed) -} - #[async_trait] impl WholeStreamCommand for Autoenv { fn name(&self) -> &str { @@ -56,11 +15,12 @@ impl WholeStreamCommand for Autoenv { fn extra_usage(&self) -> &str { // "Mark a .nu-env file in a directory as trusted. Needs to be re-run after each change to the file or its filepath." - r#"Create a file called .nu-env in any directory and run 'autoenv trust' to let nushell read it when entering the directory. -The file can contain several optional sections: - env: environment variables to set when visiting the directory. The variables are unset after leaving the directory and any overwritten values are restored. - scriptvars: environment variables that should be set to the return value of a script. After they have been set, they behave in the same way as variables set in the env section. - scripts: scripts to run when entering the directory or leaving it."# + r#"Create a file called .nu-env in any directory and run 'autoenv trust' to let nushell load it when entering the directory. +The .nu-env file has the same format as your $HOME/nu/config.toml file. By loading a .nu-env file the following applies: + - environment variables (section \"[env]\") are loaded from the .nu-env file. Those env variables are only existend in this directory (and children directories) + - the \"startup\" commands are run when entering the directory + - the \"on_exit\" commands are run when leaving the directory +"# } fn signature(&self) -> Signature { @@ -76,15 +36,12 @@ The file can contain several optional sections: vec![Example { description: "Example .nu-env file", example: r#"cat .nu-env + startup = ["echo ...entering the directory", "echo 1 2 3"] + on_exit = ["echo ...leaving the directory"] + [env] mykey = "myvalue" - - [scriptvars] - myscript = "echo myval" - - [scripts] - entryscripts = ["touch hello.txt", "touch hello2.txt"] - exitscripts = ["touch bye.txt"]"#, + "#, result: None, }] } diff --git a/crates/nu-command/src/commands/autoenv_trust.rs b/crates/nu-command/src/commands/autoenv_trust.rs index 665ca05fa0..b4e1e58c59 100644 --- a/crates/nu-command/src/commands/autoenv_trust.rs +++ b/crates/nu-command/src/commands/autoenv_trust.rs @@ -1,5 +1,5 @@ -use super::autoenv::read_trusted; use crate::prelude::*; +use nu_data::config::read_trusted; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; use nu_protocol::SyntaxShape; diff --git a/crates/nu-command/src/commands/autoenv_untrust.rs b/crates/nu-command/src/commands/autoenv_untrust.rs index 38cfc5c172..2a8f636577 100644 --- a/crates/nu-command/src/commands/autoenv_untrust.rs +++ b/crates/nu-command/src/commands/autoenv_untrust.rs @@ -1,5 +1,5 @@ -use super::autoenv::Trusted; use crate::prelude::*; +use nu_data::config::Trusted; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; use nu_protocol::SyntaxShape; diff --git a/crates/nu-command/src/commands/autoview/command.rs b/crates/nu-command/src/commands/autoview/command.rs index b07f214348..42bc628e97 100644 --- a/crates/nu-command/src/commands/autoview/command.rs +++ b/crates/nu-command/src/commands/autoview/command.rs @@ -2,7 +2,7 @@ use crate::commands::autoview::options::{ConfigExtensions, NuConfig as AutoViewC use crate::prelude::*; use crate::primitive::get_color_config; use nu_data::value::format_leaf; -use nu_engine::{UnevaluatedCallInfo, WholeStreamCommand}; +use nu_engine::{ConfigHolder, RunnableContext, UnevaluatedCallInfo, WholeStreamCommand}; use nu_errors::ShellError; use nu_protocol::hir::{self, Expression, ExternalRedirection, Literal, SpannedExpression}; use nu_protocol::{Primitive, Signature, UntaggedValue, Value}; @@ -27,16 +27,7 @@ impl WholeStreamCommand for Command { } async fn run(&self, args: CommandArgs) -> Result { - autoview(RunnableContext { - input: args.input, - scope: args.scope.clone(), - shell_manager: args.shell_manager, - host: args.host, - ctrl_c: args.ctrl_c, - current_errors: args.current_errors, - name: args.call_info.name_tag, - }) - .await + autoview(RunnableContext::from_command_args(args)).await } fn examples(&self) -> Vec { @@ -60,6 +51,7 @@ pub struct RunnableContextWithoutInput { pub host: Arc>>, pub current_errors: Arc>>, pub ctrl_c: Arc, + pub configs: Arc>, pub scope: Scope, pub name: Tag, } @@ -70,6 +62,7 @@ impl RunnableContextWithoutInput { shell_manager: context.shell_manager, host: context.host, ctrl_c: context.ctrl_c, + configs: context.configs, current_errors: context.current_errors, scope: context.scope, name: context.name, @@ -288,6 +281,7 @@ fn create_default_command_args(context: &RunnableContextWithoutInput) -> RawComm RawCommandArgs { host: context.host.clone(), ctrl_c: context.ctrl_c.clone(), + configs: context.configs.clone(), current_errors: context.current_errors.clone(), shell_manager: context.shell_manager.clone(), call_info: UnevaluatedCallInfo { diff --git a/crates/nu-command/src/commands/classified/external.rs b/crates/nu-command/src/commands/classified/external.rs index 465db14877..c6936901d3 100644 --- a/crates/nu-command/src/commands/classified/external.rs +++ b/crates/nu-command/src/commands/classified/external.rs @@ -509,7 +509,7 @@ mod tests { #[cfg(feature = "which")] use futures::executor::block_on; #[cfg(feature = "which")] - use nu_engine::EvaluationContext; + use nu_engine::basic_evaluation_context; #[cfg(feature = "which")] use nu_errors::ShellError; #[cfg(feature = "which")] @@ -534,7 +534,7 @@ mod tests { let input = InputStream::empty(); let mut ctx = - EvaluationContext::basic().expect("There was a problem creating a basic context."); + basic_evaluation_context().expect("There was a problem creating a basic context."); assert!( run_external_command(cmd, &mut ctx, input, ExternalRedirection::Stdout) @@ -548,7 +548,7 @@ mod tests { // async fn failure_run() -> Result<(), ShellError> { // let cmd = ExternalBuilder::for_name("fail").build(); - // let mut ctx = crate::cli::EvaluationContext::basic().expect("There was a problem creating a basic context."); + // let mut ctx = crate::cli::basic_evaluation_context().expect("There was a problem creating a basic context."); // let stream = run_external_command(cmd, &mut ctx, None, false) // .await? // .expect("There was a problem running the external command."); diff --git a/crates/nu-command/src/commands/config/command.rs b/crates/nu-command/src/commands/config/command.rs index 196dc05a18..6d4423225f 100644 --- a/crates/nu-command/src/commands/config/command.rs +++ b/crates/nu-command/src/commands/config/command.rs @@ -2,7 +2,7 @@ use crate::prelude::*; use nu_engine::CommandArgs; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; -use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value}; +use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; use nu_stream::OutputStream; pub struct Command; @@ -22,20 +22,21 @@ impl WholeStreamCommand for Command { } async fn run(&self, args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); + let name = args.call_info.name_tag; - let path = match args.scope.get_var("config-path") { - Some(Value { - value: UntaggedValue::Primitive(Primitive::FilePath(path)), - .. - }) => Some(path), - _ => nu_data::config::default_path().ok(), - }; - let result = nu_data::config::read(&name, &path)?; - - Ok(futures::stream::iter(vec![ReturnSuccess::value( - UntaggedValue::Row(result.into()).into_value(name), - )]) - .to_output_stream()) + if let Some(global_cfg) = &args.configs.lock().global_config { + let result = global_cfg.vars.clone(); + Ok(futures::stream::iter(vec![ReturnSuccess::value( + UntaggedValue::Row(result.into()).into_value(name), + )]) + .to_output_stream()) + } else { + Ok( + futures::stream::iter(vec![ReturnSuccess::value(UntaggedValue::Error( + ShellError::untagged_runtime_error("No global config found!"), + ))]) + .to_output_stream(), + ) + } } } diff --git a/crates/nu-command/src/commands/config/set.rs b/crates/nu-command/src/commands/config/set.rs index abe09865ec..44296f3552 100644 --- a/crates/nu-command/src/commands/config/set.rs +++ b/crates/nu-command/src/commands/config/set.rs @@ -2,7 +2,7 @@ use crate::prelude::*; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, + ColumnPath, ConfigPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, }; pub struct SubCommand; @@ -61,6 +61,7 @@ impl WholeStreamCommand for SubCommand { pub async fn set(args: CommandArgs) -> Result { let name = args.call_info.name_tag.clone(); + let ctx = EvaluationContext::from_args(&args); let scope = args.scope.clone(); let ( Arguments { @@ -93,6 +94,10 @@ pub async fn set(args: CommandArgs) -> Result { .. }) => { config::write(&changes.entries, &path)?; + ctx.reload_config(&ConfigPath::Global( + path.expect("Global config path is always some"), + )) + .await?; Ok(OutputStream::one(ReturnSuccess::value( UntaggedValue::Row(changes).into_value(name), diff --git a/crates/nu-command/src/commands/config/set_into.rs b/crates/nu-command/src/commands/config/set_into.rs index 9cb6873283..4a99b0745d 100644 --- a/crates/nu-command/src/commands/config/set_into.rs +++ b/crates/nu-command/src/commands/config/set_into.rs @@ -1,7 +1,9 @@ use crate::prelude::*; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; -use nu_protocol::{Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; +use nu_protocol::{ + ConfigPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, +}; use nu_source::Tagged; pub struct SubCommand; @@ -44,6 +46,7 @@ impl WholeStreamCommand for SubCommand { pub async fn set_into(args: CommandArgs) -> Result { let name = args.call_info.name_tag.clone(); + let ctx = EvaluationContext::from_args(&args); let scope = args.scope.clone(); let (Arguments { set_into: v }, input) = args.process().await?; @@ -73,6 +76,10 @@ pub async fn set_into(args: CommandArgs) -> Result { result.insert(key, value.clone()); config::write(&result, &path)?; + ctx.reload_config(&ConfigPath::Global( + path.expect("Global config path is always some"), + )) + .await?; OutputStream::one(ReturnSuccess::value( UntaggedValue::Row(result.into()).into_value(name), @@ -84,6 +91,10 @@ pub async fn set_into(args: CommandArgs) -> Result { result.insert(key, value); config::write(&result, &path)?; + ctx.reload_config(&ConfigPath::Global( + path.expect("Global config path is always some"), + )) + .await?; OutputStream::one(ReturnSuccess::value( UntaggedValue::Row(result.into()).into_value(name), diff --git a/crates/nu-command/src/commands/default_context.rs b/crates/nu-command/src/commands/default_context.rs index 2c4facf5a0..06590f7e92 100644 --- a/crates/nu-command/src/commands/default_context.rs +++ b/crates/nu-command/src/commands/default_context.rs @@ -1,9 +1,10 @@ +use crate::prelude::*; +use nu_engine::basic_evaluation_context; use nu_engine::whole_stream_command; -use nu_engine::EvaluationContext; use std::error::Error; pub fn create_default_context(interactive: bool) -> Result> { - let context = EvaluationContext::basic()?; + let context = basic_evaluation_context()?; { use crate::commands::*; diff --git a/crates/nu-command/src/commands/enter.rs b/crates/nu-command/src/commands/enter.rs index 405a8c61bb..ead7f32900 100644 --- a/crates/nu-command/src/commands/enter.rs +++ b/crates/nu-command/src/commands/enter.rs @@ -81,6 +81,7 @@ async fn enter(raw_args: CommandArgs) -> Result { let shell_manager = raw_args.shell_manager.clone(); let head = raw_args.call_info.args.head.clone(); let ctrl_c = raw_args.ctrl_c.clone(); + let configs = raw_args.configs.clone(); let current_errors = raw_args.current_errors.clone(); let host = raw_args.host.clone(); let tag = raw_args.call_info.name_tag.clone(); @@ -132,6 +133,7 @@ async fn enter(raw_args: CommandArgs) -> Result { let new_args = RawCommandArgs { host, ctrl_c, + configs, current_errors, shell_manager, call_info: UnevaluatedCallInfo { diff --git a/crates/nu-command/src/commands/history.rs b/crates/nu-command/src/commands/history.rs index 0df581c0c2..bf16d1fc81 100644 --- a/crates/nu-command/src/commands/history.rs +++ b/crates/nu-command/src/commands/history.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use nu_data::config::{path::history as history_path, NuConfig}; +use nu_data::config::{Conf, NuConfig}; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; @@ -33,11 +33,11 @@ impl WholeStreamCommand for History { } async fn history(args: CommandArgs) -> Result { - let config = NuConfig::new(); + let config: Box = Box::new(NuConfig::new()); let tag = args.call_info.name_tag.clone(); let (Arguments { clear }, _) = args.process().await?; - let path = history_path(&config); + let path = nu_data::config::path::history_path(&config); match clear { Some(_) => { diff --git a/crates/nu-command/src/commands/math/avg.rs b/crates/nu-command/src/commands/math/avg.rs index 1408e3549e..edcc134621 100644 --- a/crates/nu-command/src/commands/math/avg.rs +++ b/crates/nu-command/src/commands/math/avg.rs @@ -29,19 +29,7 @@ impl WholeStreamCommand for SubCommand { } async fn run(&self, args: CommandArgs) -> Result { - run_with_function( - RunnableContext { - input: args.input, - scope: args.scope.clone(), - shell_manager: args.shell_manager, - host: args.host, - ctrl_c: args.ctrl_c, - current_errors: args.current_errors, - name: args.call_info.name_tag, - }, - average, - ) - .await + run_with_function(RunnableContext::from_command_args(args), average).await } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/commands/math/ceil.rs b/crates/nu-command/src/commands/math/ceil.rs index cc66bdc855..ac62b6e261 100644 --- a/crates/nu-command/src/commands/math/ceil.rs +++ b/crates/nu-command/src/commands/math/ceil.rs @@ -23,15 +23,7 @@ impl WholeStreamCommand for SubCommand { async fn run(&self, args: CommandArgs) -> Result { run_with_numerical_functions_on_stream( - RunnableContext { - input: args.input, - scope: args.scope.clone(), - shell_manager: args.shell_manager, - host: args.host, - ctrl_c: args.ctrl_c, - current_errors: args.current_errors, - name: args.call_info.name_tag, - }, + RunnableContext::from_command_args(args), ceil_big_int, ceil_big_decimal, ceil_default, diff --git a/crates/nu-command/src/commands/math/floor.rs b/crates/nu-command/src/commands/math/floor.rs index 50776b1f26..1b2736d3e6 100644 --- a/crates/nu-command/src/commands/math/floor.rs +++ b/crates/nu-command/src/commands/math/floor.rs @@ -23,15 +23,7 @@ impl WholeStreamCommand for SubCommand { async fn run(&self, args: CommandArgs) -> Result { run_with_numerical_functions_on_stream( - RunnableContext { - input: args.input, - scope: args.scope.clone(), - shell_manager: args.shell_manager, - host: args.host, - ctrl_c: args.ctrl_c, - current_errors: args.current_errors, - name: args.call_info.name_tag, - }, + RunnableContext::from_command_args(args), floor_big_int, floor_big_decimal, floor_default, diff --git a/crates/nu-command/src/commands/math/max.rs b/crates/nu-command/src/commands/math/max.rs index 8e8e0222a7..1ed3802f98 100644 --- a/crates/nu-command/src/commands/math/max.rs +++ b/crates/nu-command/src/commands/math/max.rs @@ -22,19 +22,7 @@ impl WholeStreamCommand for SubCommand { } async fn run(&self, args: CommandArgs) -> Result { - run_with_function( - RunnableContext { - input: args.input, - scope: args.scope.clone(), - shell_manager: args.shell_manager, - host: args.host, - ctrl_c: args.ctrl_c, - current_errors: args.current_errors, - name: args.call_info.name_tag, - }, - maximum, - ) - .await + run_with_function(RunnableContext::from_command_args(args), maximum).await } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/commands/math/median.rs b/crates/nu-command/src/commands/math/median.rs index d73e564ddf..0211c4b9de 100644 --- a/crates/nu-command/src/commands/math/median.rs +++ b/crates/nu-command/src/commands/math/median.rs @@ -26,19 +26,7 @@ impl WholeStreamCommand for SubCommand { } async fn run(&self, args: CommandArgs) -> Result { - run_with_function( - RunnableContext { - input: args.input, - scope: args.scope.clone(), - shell_manager: args.shell_manager, - host: args.host, - ctrl_c: args.ctrl_c, - current_errors: args.current_errors, - name: args.call_info.name_tag, - }, - median, - ) - .await + run_with_function(RunnableContext::from_command_args(args), median).await } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/commands/math/min.rs b/crates/nu-command/src/commands/math/min.rs index 1578a89975..e2580059b9 100644 --- a/crates/nu-command/src/commands/math/min.rs +++ b/crates/nu-command/src/commands/math/min.rs @@ -22,19 +22,7 @@ impl WholeStreamCommand for SubCommand { } async fn run(&self, args: CommandArgs) -> Result { - run_with_function( - RunnableContext { - input: args.input, - scope: args.scope.clone(), - shell_manager: args.shell_manager, - host: args.host, - ctrl_c: args.ctrl_c, - current_errors: args.current_errors, - name: args.call_info.name_tag, - }, - minimum, - ) - .await + run_with_function(RunnableContext::from_command_args(args), minimum).await } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/commands/math/mode.rs b/crates/nu-command/src/commands/math/mode.rs index 3c87af3c67..c10f4700ad 100644 --- a/crates/nu-command/src/commands/math/mode.rs +++ b/crates/nu-command/src/commands/math/mode.rs @@ -22,19 +22,7 @@ impl WholeStreamCommand for SubCommand { } async fn run(&self, args: CommandArgs) -> Result { - run_with_function( - RunnableContext { - input: args.input, - scope: args.scope.clone(), - shell_manager: args.shell_manager, - host: args.host, - ctrl_c: args.ctrl_c, - current_errors: args.current_errors, - name: args.call_info.name_tag, - }, - mode, - ) - .await + run_with_function(RunnableContext::from_command_args(args), mode).await } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/commands/math/product.rs b/crates/nu-command/src/commands/math/product.rs index 0216fff1f8..19778d43b4 100644 --- a/crates/nu-command/src/commands/math/product.rs +++ b/crates/nu-command/src/commands/math/product.rs @@ -22,19 +22,7 @@ impl WholeStreamCommand for SubCommand { } async fn run(&self, args: CommandArgs) -> Result { - run_with_function( - RunnableContext { - input: args.input, - scope: args.scope.clone(), - shell_manager: args.shell_manager, - host: args.host, - ctrl_c: args.ctrl_c, - current_errors: args.current_errors, - name: args.call_info.name_tag, - }, - product, - ) - .await + run_with_function(RunnableContext::from_command_args(args), product).await } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/commands/math/sum.rs b/crates/nu-command/src/commands/math/sum.rs index 08105340bd..47a70db966 100644 --- a/crates/nu-command/src/commands/math/sum.rs +++ b/crates/nu-command/src/commands/math/sum.rs @@ -23,19 +23,7 @@ impl WholeStreamCommand for SubCommand { } async fn run(&self, args: CommandArgs) -> Result { - run_with_function( - RunnableContext { - input: args.input, - scope: args.scope.clone(), - shell_manager: args.shell_manager, - host: args.host, - ctrl_c: args.ctrl_c, - current_errors: args.current_errors, - name: args.call_info.name_tag, - }, - summation, - ) - .await + run_with_function(RunnableContext::from_command_args(args), summation).await } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/commands/run_external.rs b/crates/nu-command/src/commands/run_external.rs index 9f4790da62..8a5e404e94 100644 --- a/crates/nu-command/src/commands/run_external.rs +++ b/crates/nu-command/src/commands/run_external.rs @@ -83,9 +83,9 @@ impl WholeStreamCommand for RunExternalCommand { EvaluationContext { scope: args.scope.clone(), host: args.host.clone(), - user_recently_used_autoenv_untrust: Arc::new(AtomicBool::new(false)), shell_manager: args.shell_manager.clone(), ctrl_c: args.ctrl_c.clone(), + configs: args.configs.clone(), current_errors: Arc::new(Mutex::new(vec![])), windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())), } diff --git a/crates/nu-command/src/commands/save.rs b/crates/nu-command/src/commands/save.rs index b2d17effec..74b540be8f 100644 --- a/crates/nu-command/src/commands/save.rs +++ b/crates/nu-command/src/commands/save.rs @@ -168,6 +168,7 @@ async fn save(raw_args: CommandArgs) -> Result { let scope = raw_args.scope.clone(); let host = raw_args.host.clone(); let ctrl_c = raw_args.ctrl_c.clone(); + let configs = raw_args.configs.clone(); let current_errors = raw_args.current_errors.clone(); let shell_manager = raw_args.shell_manager.clone(); @@ -215,6 +216,7 @@ async fn save(raw_args: CommandArgs) -> Result { let new_args = RawCommandArgs { host, ctrl_c, + configs, current_errors, shell_manager: shell_manager.clone(), call_info: UnevaluatedCallInfo { diff --git a/crates/nu-command/src/examples.rs b/crates/nu-command/src/examples.rs index 0d59e2c09c..c44da09467 100644 --- a/crates/nu-command/src/examples.rs +++ b/crates/nu-command/src/examples.rs @@ -6,12 +6,14 @@ mod stub_generate; use double_echo::Command as DoubleEcho; use double_ls::Command as DoubleLs; +use stub_generate::{mock_path, Command as StubOpen}; + +use nu_engine::basic_evaluation_context; use nu_errors::ShellError; use nu_parser::ParserScope; use nu_protocol::hir::ClassifiedBlock; use nu_protocol::{ShellTypeName, Value}; use nu_source::AnchorLocation; -use stub_generate::{mock_path, Command as StubOpen}; use crate::commands::{ Append, BuildString, Each, Echo, First, Get, Keep, Last, Let, Nth, Select, StrCollect, Wrap, @@ -24,7 +26,7 @@ use futures::executor::block_on; pub fn test_examples(cmd: Command) -> Result<(), ShellError> { let examples = cmd.examples(); - let base_context = EvaluationContext::basic()?; + let base_context = basic_evaluation_context()?; base_context.add_commands(vec![ // Command Doubles @@ -90,7 +92,7 @@ pub fn test_examples(cmd: Command) -> Result<(), ShellError> { pub fn test(cmd: impl WholeStreamCommand + 'static) -> Result<(), ShellError> { let examples = cmd.examples(); - let base_context = EvaluationContext::basic()?; + let base_context = basic_evaluation_context()?; base_context.add_commands(vec![ whole_stream_command(Echo {}), @@ -147,7 +149,7 @@ pub fn test(cmd: impl WholeStreamCommand + 'static) -> Result<(), ShellError> { pub fn test_anchors(cmd: Command) -> Result<(), ShellError> { let examples = cmd.examples(); - let base_context = EvaluationContext::basic()?; + let base_context = basic_evaluation_context()?; base_context.add_commands(vec![ // Minimal restricted commands to aid in testing @@ -228,10 +230,8 @@ async fn evaluate_block( ctx: &mut EvaluationContext, ) -> Result, ShellError> { let input_stream = InputStream::empty(); - let env = ctx.get_env(); ctx.scope.enter_scope(); - ctx.scope.add_env(env); let result = run_block(&block.block, ctx, input_stream).await; diff --git a/crates/nu-command/src/utils/test_bins.rs b/crates/nu-command/src/utils/test_bins.rs index 2c5ed1ea8a..54824109f4 100644 --- a/crates/nu-command/src/utils/test_bins.rs +++ b/crates/nu-command/src/utils/test_bins.rs @@ -1,5 +1,17 @@ use std::io::{self, BufRead, Write}; +/// Echo's value of env keys from args +/// Example: nu --testbin env_echo FOO BAR +/// If it it's not present echo's nothing +pub fn echo_env() { + let args = args(); + for arg in args { + if let Ok(v) = std::env::var(arg) { + println!("{}", v); + } + } +} + pub fn cococo() { let args: Vec = args(); diff --git a/crates/nu-data/Cargo.toml b/crates/nu-data/Cargo.toml index a8bf186497..ab380985f4 100644 --- a/crates/nu-data/Cargo.toml +++ b/crates/nu-data/Cargo.toml @@ -26,6 +26,8 @@ num-traits = "0.2.14" query_interface = "0.3.5" serde = { version = "1.0.123", features = ["derive"] } toml = "0.5.8" +sha2 = "0.9.3" +common-path = "1.0.0" nu-errors = { version = "0.29.0", path = "../nu-errors" } nu-protocol = { version = "0.29.0", path = "../nu-protocol" } diff --git a/crates/nu-data/src/config.rs b/crates/nu-data/src/config.rs index 8057b5c1e1..47358f4619 100644 --- a/crates/nu-data/src/config.rs +++ b/crates/nu-data/src/config.rs @@ -1,18 +1,23 @@ mod conf; +mod config_trust; +mod local_config; mod nuconfig; pub mod path; -pub mod tests; - pub use conf::Conf; +pub use config_trust::is_file_trusted; +pub use config_trust::read_trusted; +pub use config_trust::Trusted; +pub use local_config::loadable_cfg_exists_in_dir; +pub use local_config::LocalConfigDiff; pub use nuconfig::NuConfig; use indexmap::IndexMap; use log::trace; use nu_errors::{CoerceInto, ShellError}; use nu_protocol::{ - Dictionary, Primitive, ShellTypeName, TaggedDictBuilder, UnspannedPathMember, UntaggedValue, - Value, + ConfigPath, Dictionary, Primitive, ShellTypeName, TaggedDictBuilder, UnspannedPathMember, + UntaggedValue, Value, }; use nu_source::{SpannedItem, Tag, TaggedItem}; use std::fs::{self, OpenOptions}; @@ -186,7 +191,7 @@ pub fn default_path_for(file: &Option) -> Result { let file: &Path = file .as_ref() .map(AsRef::as_ref) - .unwrap_or_else(|| self::path::DEFAULT_CONFIG_LOCATION.as_ref()); + .unwrap_or_else(|| "config.toml".as_ref()); filename.push(file); Ok(filename) @@ -297,14 +302,11 @@ pub fn config(tag: impl Into) -> Result, ShellError } pub fn write(config: &IndexMap, at: &Option) -> Result<(), ShellError> { - let filename = &mut default_path()?; + let filename = default_path()?; + let filename = match at { None => filename, - Some(file) => { - filename.pop(); - filename.push(file); - filename - } + Some(ref file) => file.clone(), }; let contents = value_to_toml_value( @@ -325,3 +327,7 @@ fn touch(path: &Path) -> io::Result<()> { Err(e) => Err(e), } } + +pub fn cfg_path_to_scope_tag(cfg_path: &ConfigPath) -> String { + cfg_path.get_path().to_string_lossy().to_string() +} diff --git a/crates/nu-data/src/config/conf.rs b/crates/nu-data/src/config/conf.rs index 3496c4e369..784184eaaa 100644 --- a/crates/nu-data/src/config/conf.rs +++ b/crates/nu-data/src/config/conf.rs @@ -1,22 +1,17 @@ +use nu_errors::ShellError; use nu_protocol::Value; -use std::any::Any; -use std::fmt::Debug; +use std::{fmt::Debug, path::PathBuf}; pub trait Conf: Debug + Send { - fn as_any(&self) -> &dyn Any; fn is_modified(&self) -> Result>; fn var(&self, key: &str) -> Option; fn env(&self) -> Option; - fn path(&self) -> Option; - fn reload(&mut self); + fn path(&self) -> Result>, ShellError>; fn clone_box(&self) -> Box; + fn reload(&mut self); } impl Conf for Box { - fn as_any(&self) -> &dyn Any { - self - } - fn is_modified(&self) -> Result> { (**self).is_modified() } @@ -29,10 +24,6 @@ impl Conf for Box { (**self).env() } - fn path(&self) -> Option { - (**self).path() - } - fn reload(&mut self) { (**self).reload(); } @@ -40,4 +31,8 @@ impl Conf for Box { fn clone_box(&self) -> Box { (**self).clone_box() } + + fn path(&self) -> Result>, ShellError> { + (**self).path() + } } diff --git a/crates/nu-data/src/config/config_trust.rs b/crates/nu-data/src/config/config_trust.rs new file mode 100644 index 0000000000..fc69031959 --- /dev/null +++ b/crates/nu-data/src/config/config_trust.rs @@ -0,0 +1,44 @@ +use serde::Deserialize; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::{io::Read, path::Path, path::PathBuf}; + +use indexmap::IndexMap; +use nu_errors::ShellError; + +#[derive(Deserialize, Serialize, Debug, Default)] +pub struct Trusted { + pub files: IndexMap>, +} + +impl Trusted { + pub fn new() -> Self { + Trusted { + files: IndexMap::new(), + } + } +} + +pub fn is_file_trusted(nu_env_file: &Path, content: &[u8]) -> Result { + let contentdigest = Sha256::digest(&content).as_slice().to_vec(); + let nufile = std::fs::canonicalize(nu_env_file)?; + + let trusted = read_trusted()?; + Ok(trusted.files.get(&nufile.to_string_lossy().to_string()) == Some(&contentdigest)) +} + +pub fn read_trusted() -> Result { + let config_path = crate::config::default_path_for(&Some(PathBuf::from("nu-env.toml")))?; + + let mut file = std::fs::OpenOptions::new() + .read(true) + .create(true) + .write(true) + .open(config_path) + .map_err(|_| ShellError::untagged_runtime_error("Couldn't open nu-env.toml"))?; + let mut doc = String::new(); + file.read_to_string(&mut doc)?; + + let allowed = toml::de::from_str(doc.as_str()).unwrap_or_else(|_| Trusted::new()); + Ok(allowed) +} diff --git a/crates/nu-data/src/config/local_config.rs b/crates/nu-data/src/config/local_config.rs new file mode 100644 index 0000000000..0ee20d7117 --- /dev/null +++ b/crates/nu-data/src/config/local_config.rs @@ -0,0 +1,150 @@ +use nu_errors::ShellError; +use std::path::{Path, PathBuf}; + +static LOCAL_CFG_FILE_NAME: &str = ".nu-env"; + +pub struct LocalConfigDiff { + pub cfgs_to_load: Vec, + pub cfgs_to_unload: Vec, +} + +/// Finds all local configs between `from` up to `to`. +/// Every config seen while going up the filesystem (e.G. from `/foo` to `/foo/bar`) is returned +/// as a config to load +/// Every config seen while going down the filesystem (e.G. from `/foo/bar` to `/foo/bar`) is +/// returned as a config to unload +/// If both paths are unrelated to each other, (e.G. windows paths as: `C:/foo` and `D:/bar`) +/// this function first walks `from` completly down the filesystem and then it walks up until `to`. +/// +/// Both paths are required to be absolute. +impl LocalConfigDiff { + pub fn between(from: PathBuf, to: PathBuf) -> (LocalConfigDiff, Vec) { + let common_prefix = common_path::common_path(&from, &to); + let (cfgs_to_unload, err_down) = walk_down(&from, &common_prefix); + let (cfgs_to_load, err_up) = walk_up(&common_prefix, &to); + + ( + LocalConfigDiff { + cfgs_to_load, + cfgs_to_unload, + }, + err_down.into_iter().chain(err_up).collect(), + ) + } +} + +///Walks from the first parameter down the filesystem to the second parameter. Marking all +///configs found in directories on the way as to remove. +///If to is None, this method walks from the first paramter down to the beginning of the +///filesystem +///Returns tuple of (configs to remove, errors from io). +fn walk_down( + from_inclusive: &Path, + to_exclusive: &Option, +) -> (Vec, Vec) { + let mut all_err = vec![]; + let mut all_cfgs_to_unload = vec![]; + for dir in from_inclusive.ancestors().take_while(|cur_path| { + if let Some(until_path) = to_exclusive { + //Stop before `to_exclusive` + *cur_path != until_path + } else { + //No end, walk all the way down + true + } + }) { + match local_cfg_should_be_unloaded(dir.to_path_buf()) { + Ok(Some(cfg)) => all_cfgs_to_unload.push(cfg), + Err(e) => all_err.push(e), + _ => {} + } + } + + (all_cfgs_to_unload, all_err) +} + +///Walks from the first parameter up the filesystem to the second parameter, returns all configs +///found in directories on the way to load. +///Returns combined errors from checking directories on the way +///If from is None, this method walks from the beginning of the second parameter up to the +///second parameter +fn walk_up( + from_exclusive: &Option, + to_inclusive: &Path, +) -> (Vec, Vec) { + let mut all_err = vec![]; + let mut all_cfgs_to_load = vec![]; + + //skip all paths until (inclusive) from (or 0 if from is None) + let skip_ahead = from_exclusive + .as_ref() + .map(|p| p.ancestors().count()) + .unwrap_or(0); + //We have to traverse ancestors in reverse order (apply lower directories first) + //ancestors() does not yield iter with .rev() method. So we store all ancestors + //and then iterate over the vec + let dirs: Vec<_> = to_inclusive.ancestors().map(Path::to_path_buf).collect(); + for dir in dirs.iter().rev().skip(skip_ahead) { + match loadable_cfg_exists_in_dir(dir.clone()) { + Ok(Some(cfg)) => all_cfgs_to_load.push(cfg), + Err(e) => all_err.push(e), + _ => {} + } + } + + (all_cfgs_to_load, all_err) +} + +fn is_existent_local_cfg(cfg_file_path: &Path) -> Result { + if !cfg_file_path.exists() || cfg_file_path.parent() == super::default_path()?.parent() { + //Don't treat global cfg as local one + Ok(false) + } else { + Ok(true) + } +} + +fn is_trusted_local_cfg_content(cfg_file_path: &Path, content: &[u8]) -> Result { + //This checks whether user used `autoenv trust` to mark this cfg as secure + if !super::is_file_trusted(&cfg_file_path, &content)? { + //Notify user about present config, but not trusted + Err(ShellError::untagged_runtime_error( + format!("{:?} is untrusted. Run 'autoenv trust {:?}' to trust it.\nThis needs to be done after each change to the file.", + cfg_file_path, cfg_file_path.parent().unwrap_or_else(|| &Path::new(""))))) + } else { + Ok(true) + } +} + +fn local_cfg_should_be_unloaded>(cfg_dir: P) -> Result, ShellError> { + let mut cfg = cfg_dir.as_ref().to_path_buf(); + cfg.push(LOCAL_CFG_FILE_NAME); + if is_existent_local_cfg(&cfg)? { + //No need to compute whether content is good. If it is not loaded before, unloading does + //nothing + Ok(Some(cfg)) + } else { + Ok(None) + } +} + +/// Checks whether a local_cfg exists in cfg_dir and returns: +/// Ok(Some(cfg_path)) if cfg exists and is good to load +/// Ok(None) if no cfg exists +/// Err(error) if cfg exits, but is not good to load +pub fn loadable_cfg_exists_in_dir(mut cfg_dir: PathBuf) -> Result, ShellError> { + cfg_dir.push(LOCAL_CFG_FILE_NAME); + let cfg_path = cfg_dir; + + if !is_existent_local_cfg(&cfg_path)? { + return Ok(None); + } + + let content = std::fs::read(&cfg_path)?; + + if !is_trusted_local_cfg_content(&cfg_path, &content)? { + return Ok(None); + } + + Ok(Some(cfg_path)) +} diff --git a/crates/nu-data/src/config/nuconfig.rs b/crates/nu-data/src/config/nuconfig.rs index c8048c82d1..be08d67941 100644 --- a/crates/nu-data/src/config/nuconfig.rs +++ b/crates/nu-data/src/config/nuconfig.rs @@ -1,23 +1,18 @@ use crate::config::{last_modified, read, Conf, Status}; use indexmap::IndexMap; +use nu_errors::ShellError; use nu_protocol::Value; use nu_source::Tag; -use std::any::Any; -use std::fmt::Debug; -use std::path::PathBuf; +use std::{fmt::Debug, path::PathBuf}; #[derive(Debug, Clone, Default)] pub struct NuConfig { - pub source_file: Option, pub vars: IndexMap, + pub file_path: PathBuf, pub modified_at: Status, } impl Conf for NuConfig { - fn as_any(&self) -> &dyn Any { - self - } - fn is_modified(&self) -> Result> { self.is_modified() } @@ -30,17 +25,15 @@ impl Conf for NuConfig { self.env() } - fn path(&self) -> Option { + fn path(&self) -> Result>, ShellError> { self.path() } fn reload(&mut self) { - let vars = &mut self.vars; + if let Ok(variables) = read(Tag::unknown(), &Some(self.file_path.clone())) { + self.vars = variables; - if let Ok(variables) = read(Tag::unknown(), &self.source_file) { - vars.extend(variables); - - self.modified_at = if let Ok(status) = last_modified(&None) { + self.modified_at = if let Ok(status) = last_modified(&Some(self.file_path.clone())) { status } else { Status::Unavailable @@ -54,25 +47,20 @@ impl Conf for NuConfig { } impl NuConfig { - pub fn with(config_file: Option) -> NuConfig { - match &config_file { - None => NuConfig::new(), - Some(_) => { - let source_file = config_file.map(std::path::PathBuf::from); + pub fn load(cfg_file_path: Option) -> Result { + let vars = read(Tag::unknown(), &cfg_file_path)?; + let modified_at = NuConfig::get_last_modified(&cfg_file_path); + let file_path = if let Some(file_path) = cfg_file_path { + file_path + } else { + crate::config::default_path()? + }; - let vars = if let Ok(variables) = read(Tag::unknown(), &source_file) { - variables - } else { - IndexMap::default() - }; - - NuConfig { - source_file: source_file.clone(), - vars, - modified_at: NuConfig::get_last_modified(&source_file), - } - } - } + Ok(NuConfig { + file_path, + vars, + modified_at, + }) } pub fn new() -> NuConfig { @@ -81,18 +69,19 @@ impl NuConfig { } else { IndexMap::default() }; + let path = if let Ok(path) = crate::config::default_path() { + path + } else { + PathBuf::new() + }; NuConfig { - source_file: None, vars, modified_at: NuConfig::get_last_modified(&None), + file_path: path, } } - pub fn history_path(&self) -> PathBuf { - super::path::history(self) - } - pub fn get_last_modified(config_file: &Option) -> Status { if let Ok(status) = last_modified(config_file) { status @@ -104,17 +93,15 @@ impl NuConfig { pub fn is_modified(&self) -> Result> { let modified_at = &self.modified_at; - Ok( - match (NuConfig::get_last_modified(&self.source_file), modified_at) { - (Status::LastModified(left), Status::LastModified(right)) => { - let left = left.duration_since(std::time::UNIX_EPOCH)?; - let right = (*right).duration_since(std::time::UNIX_EPOCH)?; + Ok(match (NuConfig::get_last_modified(&None), modified_at) { + (Status::LastModified(left), Status::LastModified(right)) => { + let left = left.duration_since(std::time::UNIX_EPOCH)?; + let right = (*right).duration_since(std::time::UNIX_EPOCH)?; - left != right - } - (_, _) => false, - }, - ) + left != right + } + (_, _) => false, + }) } pub fn var(&self, key: &str) -> Option { @@ -127,6 +114,19 @@ impl NuConfig { None } + /// Return environment variables as map + pub fn env_map(&self) -> IndexMap { + let mut result = IndexMap::new(); + if let Some(variables) = self.env() { + for var in variables.row_entries() { + if let Ok(value) = var.1.as_string() { + result.insert(var.0.clone(), value); + } + } + } + result + } + pub fn env(&self) -> Option { let vars = &self.vars; @@ -137,17 +137,43 @@ impl NuConfig { None } - pub fn path(&self) -> Option { + pub fn path(&self) -> Result>, ShellError> { let vars = &self.vars; - if let Some(env_vars) = vars.get("path") { - return Some(env_vars.clone()); + if let Some(path) = vars.get("path").or_else(|| vars.get("PATH")) { + path + .table_entries() + .map(|p| { + p.as_string().map(PathBuf::from).map_err(|_| { + ShellError::untagged_runtime_error("Could not format path entry as string!\nPath entry from config won't be added") + }) + }) + .collect::, ShellError>>().map(Some) + } else { + Ok(None) } + } - if let Some(env_vars) = vars.get("PATH") { - return Some(env_vars.clone()); + fn load_scripts_if_present(&self, scripts_name: &str) -> Result, ShellError> { + if let Some(array) = self.var(scripts_name) { + if !array.is_table() { + Err(ShellError::untagged_runtime_error(format!( + "expected an array of strings as {} commands", + scripts_name + ))) + } else { + array.table_entries().map(Value::as_string).collect() + } + } else { + Ok(vec![]) } + } - None + pub fn exit_scripts(&self) -> Result, ShellError> { + self.load_scripts_if_present("on_exit") + } + + pub fn startup_scripts(&self) -> Result, ShellError> { + self.load_scripts_if_present("startup") } } diff --git a/crates/nu-data/src/config/path.rs b/crates/nu-data/src/config/path.rs index 7781787c14..a6f982685e 100644 --- a/crates/nu-data/src/config/path.rs +++ b/crates/nu-data/src/config/path.rs @@ -1,32 +1,25 @@ -use crate::config::NuConfig; +use crate::config::Conf; use std::path::PathBuf; -pub const DEFAULT_CONFIG_LOCATION: &str = "config.toml"; -const DEFAULT_HISTORY_LOCATION: &str = "history.txt"; +const DEFAULT_LOCATION: &str = "history.txt"; -pub fn history(config: &NuConfig) -> PathBuf { - let default_path = crate::config::user_data() +pub fn default_history_path() -> PathBuf { + crate::config::user_data() .map(|mut p| { - p.push(DEFAULT_HISTORY_LOCATION); + p.push(DEFAULT_LOCATION); p }) - .unwrap_or_else(|_| PathBuf::from(DEFAULT_HISTORY_LOCATION)); + .unwrap_or_else(|_| PathBuf::from(DEFAULT_LOCATION)) +} - let path = &config.var("history-path"); +pub fn history_path(config: &dyn Conf) -> PathBuf { + let default_history_path = default_history_path(); - path.as_ref().map_or(default_path.clone(), |custom_path| { - match custom_path.as_string() { + config.var("history-path").map_or( + default_history_path.clone(), + |custom_path| match custom_path.as_string() { Ok(path) => PathBuf::from(path), - Err(_) => default_path, - } - }) -} - -pub fn source_file(config: &NuConfig) -> PathBuf { - match &config.source_file { - Some(path) => PathBuf::from(path), - None => { - crate::config::default_path().unwrap_or_else(|_| PathBuf::from(DEFAULT_CONFIG_LOCATION)) - } - } + Err(_) => default_history_path, + }, + ) } diff --git a/crates/nu-data/src/config/tests.rs b/crates/nu-data/src/config/tests.rs index 2430253971..9a89355375 100644 --- a/crates/nu-data/src/config/tests.rs +++ b/crates/nu-data/src/config/tests.rs @@ -1,6 +1,5 @@ use crate::config::{Conf, NuConfig, Status}; use nu_protocol::Value; -use std::any::Any; use std::path::{Path, PathBuf}; #[derive(Debug, Clone)] @@ -10,10 +9,6 @@ pub struct FakeConfig { } impl Conf for FakeConfig { - fn as_any(&self) -> &dyn Any { - self - } - fn is_modified(&self) -> Result> { self.is_modified() } diff --git a/crates/nu-engine/Cargo.toml b/crates/nu-engine/Cargo.toml index cb6fb1d032..d72ea992fb 100644 --- a/crates/nu-engine/Cargo.toml +++ b/crates/nu-engine/Cargo.toml @@ -20,6 +20,8 @@ nu-ansi-term = { version = "0.29.0", path = "../nu-ansi-term" } trash = { version = "1.3.0", optional = true } which = { version = "4.0.2", optional = true } codespan-reporting = "0.11.0" +dyn-clone = "1.0.4" +ansi_term = "0.12.1" async-recursion = "0.3.2" async-trait = "0.1.42" bytes = "0.5.6" diff --git a/crates/nu-engine/README.md b/crates/nu-engine/README.md new file mode 100644 index 0000000000..bcf58e0300 --- /dev/null +++ b/crates/nu-engine/README.md @@ -0,0 +1,16 @@ +# Nu-Engine +Nu-engine handles most of the core logic of nushell. For example, engine handles: + - Passing of data between commands + - Evaluating a commands return values + - Loading of user configurations + +## Top level introduction +The following topics shall give the reader a top level understanding how various topics are handled in nushell. + +### How are environment variables handled? +Environment variables (or short envs) are stored in the `Scope` of the `EvaluationContext`. That means that environment variables are scoped by default and we don't use `std::env` to store envs (but make exceptions where convenient). + +Nushell handles environment variables and their lifetime the following: +- At startup all existing environment variables are read and put into `Scope`. (Nushell reads existing environment variables platform independent by asking the `Host`. They will most likly come from `std::env::*`) +- Envs can also be loaded from config files. Each loaded config produces a new `ScopeFrame` with the envs of the loaded config. +- Nu-Script files and internal commands read and write env variables from / to the `Scope`. External scripts and binaries can't interact with the `Scope`. Therefore all env variables are read from the `Scope` and put into the external binaries environment-variables-memory area. diff --git a/crates/nu-engine/src/basic_evaluation_context.rs b/crates/nu-engine/src/basic_evaluation_context.rs new file mode 100644 index 0000000000..133bda09c6 --- /dev/null +++ b/crates/nu-engine/src/basic_evaluation_context.rs @@ -0,0 +1,26 @@ +use crate::EvaluationContext; +use crate::Scope; +use crate::{basic_shell_manager, config_holder::ConfigHolder}; +use crate::{env::basic_host::BasicHost, Host}; +use indexmap::IndexMap; +use parking_lot::Mutex; +use std::error::Error; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +pub fn basic_evaluation_context() -> Result> { + let scope = Scope::new(); + let mut host = BasicHost {}; + let env_vars = host.vars().iter().cloned().collect::>(); + scope.add_env(env_vars); + + Ok(EvaluationContext { + scope, + host: Arc::new(parking_lot::Mutex::new(Box::new(host))), + current_errors: Arc::new(Mutex::new(vec![])), + ctrl_c: Arc::new(AtomicBool::new(false)), + configs: Arc::new(Mutex::new(ConfigHolder::new())), + shell_manager: basic_shell_manager::basic_shell_manager()?, + windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())), + }) +} diff --git a/crates/nu-engine/src/basic_shell_manager.rs b/crates/nu-engine/src/basic_shell_manager.rs new file mode 100644 index 0000000000..c0257b2df6 --- /dev/null +++ b/crates/nu-engine/src/basic_shell_manager.rs @@ -0,0 +1,16 @@ +use crate::filesystem::filesystem_shell::{FilesystemShell, FilesystemShellMode}; +use crate::shell::shell_manager::ShellManager; + +use parking_lot::Mutex; +use std::error::Error; +use std::sync::atomic::AtomicUsize; +use std::sync::Arc; + +pub fn basic_shell_manager() -> Result> { + Ok(ShellManager { + current_shell: Arc::new(AtomicUsize::new(0)), + shells: Arc::new(Mutex::new(vec![Box::new(FilesystemShell::basic( + FilesystemShellMode::Cli, + )?)])), + }) +} diff --git a/crates/nu-engine/src/command_args.rs b/crates/nu-engine/src/command_args.rs index 0f071a8bf8..ba394c559c 100644 --- a/crates/nu-engine/src/command_args.rs +++ b/crates/nu-engine/src/command_args.rs @@ -1,9 +1,9 @@ -use crate::call_info::UnevaluatedCallInfo; use crate::deserializer::ConfigDeserializer; use crate::env::host::Host; use crate::evaluate::scope::Scope; use crate::evaluation_context::EvaluationContext; use crate::shell::shell_manager::ShellManager; +use crate::{call_info::UnevaluatedCallInfo, config_holder::ConfigHolder}; use derive_new::new; use getset::Getters; use nu_errors::ShellError; @@ -22,6 +22,7 @@ use std::sync::Arc; pub struct CommandArgs { pub host: Arc>>, pub ctrl_c: Arc, + pub configs: Arc>, pub current_errors: Arc>>, pub shell_manager: ShellManager, pub call_info: UnevaluatedCallInfo, @@ -35,6 +36,7 @@ pub struct RawCommandArgs { pub host: Arc>>, pub ctrl_c: Arc, pub current_errors: Arc>>, + pub configs: Arc>, pub shell_manager: ShellManager, pub scope: Scope, pub call_info: UnevaluatedCallInfo, @@ -45,6 +47,7 @@ impl RawCommandArgs { CommandArgs { host: self.host, ctrl_c: self.ctrl_c, + configs: self.configs, current_errors: self.current_errors, shell_manager: self.shell_manager, call_info: self.call_info, @@ -65,6 +68,7 @@ impl CommandArgs { let ctx = EvaluationContext::from_args(&self); let host = self.host.clone(); let ctrl_c = self.ctrl_c.clone(); + let configs = self.configs.clone(); let shell_manager = self.shell_manager.clone(); let input = self.input; let call_info = self.call_info.evaluate(&ctx).await?; @@ -73,6 +77,7 @@ impl CommandArgs { Ok(EvaluatedWholeStreamCommandArgs::new( host, ctrl_c, + configs, shell_manager, call_info, input, @@ -106,6 +111,7 @@ impl EvaluatedWholeStreamCommandArgs { pub fn new( host: Arc>, ctrl_c: Arc, + configs: Arc>, shell_manager: ShellManager, call_info: CallInfo, input: impl Into, @@ -115,6 +121,7 @@ impl EvaluatedWholeStreamCommandArgs { args: EvaluatedCommandArgs { host, ctrl_c, + configs, shell_manager, call_info, scope, @@ -145,6 +152,7 @@ impl EvaluatedWholeStreamCommandArgs { pub struct EvaluatedCommandArgs { pub host: Arc>, pub ctrl_c: Arc, + pub configs: Arc>, pub shell_manager: ShellManager, pub call_info: CallInfo, pub scope: Scope, diff --git a/crates/nu-engine/src/config_holder.rs b/crates/nu-engine/src/config_holder.rs new file mode 100644 index 0000000000..ff3d8fee59 --- /dev/null +++ b/crates/nu-engine/src/config_holder.rs @@ -0,0 +1,53 @@ +use std::path::Path; + +use nu_data::config::NuConfig; +use nu_protocol::ConfigPath; + +/// ConfigHolder holds information which configs have been loaded. +#[derive(Clone)] +pub struct ConfigHolder { + pub global_config: Option, + pub local_configs: Vec, +} + +impl Default for ConfigHolder { + fn default() -> Self { + Self::new() + } +} + +impl ConfigHolder { + pub fn new() -> ConfigHolder { + ConfigHolder { + global_config: None, + local_configs: vec![], + } + } + + pub fn add_local_cfg(&mut self, cfg: NuConfig) { + self.local_configs.push(cfg); + } + + pub fn set_global_cfg(&mut self, cfg: NuConfig) { + self.global_config = Some(cfg); + } + + pub fn remove_cfg(&mut self, cfg_path: &ConfigPath) { + match cfg_path { + ConfigPath::Global(_) => self.global_config = None, + ConfigPath::Local(p) => self.remove_local_cfg(p), + } + } + + fn remove_local_cfg>(&mut self, cfg_path: P) { + // Remove the first loaded local config with specified cfg_path + if let Some(index) = self + .local_configs + .iter() + .rev() + .position(|cfg| cfg.file_path == cfg_path.as_ref()) + { + self.local_configs.remove(index); + } + } +} diff --git a/crates/nu-engine/src/env/basic_host.rs b/crates/nu-engine/src/env/basic_host.rs index 602cb43b2f..e9cbb14a0e 100644 --- a/crates/nu-engine/src/env/basic_host.rs +++ b/crates/nu-engine/src/env/basic_host.rs @@ -7,21 +7,6 @@ use std::ffi::OsString; #[derive(Debug)] pub struct BasicHost; -pub fn print_err(err: ShellError, source: &Text) { - if let Some(diag) = err.into_diagnostic() { - let source = source.to_string(); - let mut files = codespan_reporting::files::SimpleFiles::new(); - files.add("shell", source); - - let writer = termcolor::StandardStream::stderr(termcolor::ColorChoice::Auto); - let config = codespan_reporting::term::Config::default(); - - let _ = std::panic::catch_unwind(move || { - let _ = codespan_reporting::term::emit(&mut writer.lock(), &config, &files, &diag); - }); - } -} - impl Host for BasicHost { fn stdout(&mut self, out: &str) { match out { @@ -38,7 +23,18 @@ impl Host for BasicHost { } fn print_err(&mut self, err: ShellError, source: &Text) { - print_err(err, source); + if let Some(diag) = err.into_diagnostic() { + let source = source.to_string(); + let mut files = codespan_reporting::files::SimpleFiles::new(); + files.add("shell", source); + + let writer = termcolor::StandardStream::stderr(termcolor::ColorChoice::Auto); + let config = codespan_reporting::term::Config::default(); + + let _ = std::panic::catch_unwind(move || { + let _ = codespan_reporting::term::emit(&mut writer.lock(), &config, &files, &diag); + }); + } } #[allow(unused_variables)] diff --git a/crates/nu-engine/src/env/environment.rs b/crates/nu-engine/src/env/environment.rs deleted file mode 100644 index 5d5fff6a57..0000000000 --- a/crates/nu-engine/src/env/environment.rs +++ /dev/null @@ -1,30 +0,0 @@ -use nu_protocol::Value; -use std::ffi::OsString; - -use std::fmt::Debug; - -pub trait Env: Debug + Send { - fn env(&self) -> Option; - fn path(&self) -> Option; - - fn add_env(&mut self, key: &str, value: &str); - fn add_path(&mut self, new_path: OsString); -} - -impl Env for Box { - fn env(&self) -> Option { - (**self).env() - } - - fn path(&self) -> Option { - (**self).path() - } - - fn add_env(&mut self, key: &str, value: &str) { - (**self).add_env(key, value); - } - - fn add_path(&mut self, new_path: OsString) { - (**self).add_path(new_path); - } -} diff --git a/crates/nu-engine/src/env/mod.rs b/crates/nu-engine/src/env/mod.rs index ab078fd7bf..38a48cfe53 100644 --- a/crates/nu-engine/src/env/mod.rs +++ b/crates/nu-engine/src/env/mod.rs @@ -1,3 +1,2 @@ pub(crate) mod basic_host; -pub(crate) mod environment; pub(crate) mod host; diff --git a/crates/nu-engine/src/evaluate/internal.rs b/crates/nu-engine/src/evaluate/internal.rs index f0b0bdc895..7573f40b18 100644 --- a/crates/nu-engine/src/evaluate/internal.rs +++ b/crates/nu-engine/src/evaluate/internal.rs @@ -1,7 +1,7 @@ use crate::call_info::UnevaluatedCallInfo; use crate::command_args::RawCommandArgs; use crate::evaluation_context::EvaluationContext; -use crate::filesystem::filesystem_shell::FilesystemShell; +use crate::filesystem::filesystem_shell::{FilesystemShell, FilesystemShellMode}; use crate::shell::help_shell::HelpShell; use crate::shell::value_shell::ValueShell; use futures::StreamExt; @@ -11,7 +11,6 @@ use nu_protocol::hir::{ExternalRedirection, InternalCommand}; use nu_protocol::{CommandAction, Primitive, ReturnSuccess, UntaggedValue, Value}; use nu_source::{PrettyDebug, Span, Tag}; use nu_stream::{trace_stream, InputStream, ToInputStream}; -use std::sync::atomic::Ordering; use std::sync::Arc; pub(crate) async fn run_internal_command( @@ -28,12 +27,6 @@ pub(crate) async fn run_internal_command( let internal_command = context.scope.expect_command(&command.name); - if command.name == "autoenv untrust" { - context - .user_recently_used_autoenv_untrust - .store(true, Ordering::SeqCst); - } - let result = { context .run_command( @@ -75,6 +68,7 @@ pub(crate) async fn run_internal_command( let new_args = RawCommandArgs { host: context.host.clone(), ctrl_c: context.ctrl_c.clone(), + configs: context.configs.clone(), current_errors: context.current_errors.clone(), shell_manager: context.shell_manager.clone(), call_info: UnevaluatedCallInfo { @@ -172,8 +166,13 @@ pub(crate) async fn run_internal_command( InputStream::from_stream(futures::stream::iter(vec![])) } CommandAction::EnterShell(location) => { + let mode = if context.shell_manager.is_interactive() { + FilesystemShellMode::Cli + } else { + FilesystemShellMode::Script + }; context.shell_manager.insert_at_current(Box::new( - match FilesystemShell::with_location(location) { + match FilesystemShell::with_location(location, mode) { Ok(v) => v, Err(err) => { context.error(err.into()); @@ -220,6 +219,17 @@ pub(crate) async fn run_internal_command( } InputStream::empty() } + CommandAction::UnloadConfig(cfg_path) => { + context.unload_config(&cfg_path).await; + InputStream::empty() + } + CommandAction::LoadConfig(cfg_path) => { + if let Err(e) = context.load_config(&cfg_path).await { + InputStream::one(UntaggedValue::Error(e).into_untagged_value()) + } else { + InputStream::empty() + } + } }, Ok(ReturnSuccess::Value(Value { diff --git a/crates/nu-engine/src/evaluate/scope.rs b/crates/nu-engine/src/evaluate/scope.rs index 7bab541ea8..b34beb8fbd 100644 --- a/crates/nu-engine/src/evaluate/scope.rs +++ b/crates/nu-engine/src/evaluate/scope.rs @@ -177,6 +177,16 @@ impl Scope { output } + pub fn get_env(&self, name: &str) -> Option { + for frame in self.frames.lock().iter().rev() { + if let Some(v) = frame.env.get(name) { + return Some(v.clone()); + } + } + + None + } + pub fn get_var(&self, name: &str) -> Option { for frame in self.frames.lock().iter().rev() { if let Some(v) = frame.vars.get(name) { @@ -224,6 +234,66 @@ impl Scope { frame.env.insert(name.into(), value); } } + + pub fn set_exit_scripts(&self, scripts: Vec) { + if let Some(frame) = self.frames.lock().last_mut() { + frame.exitscripts = scripts + } + } + + pub fn enter_scope_with_tag(&self, tag: String) { + self.frames.lock().push(ScopeFrame::with_tag(tag)); + } + + //Removes the scopeframe with tag. + pub fn exit_scope_with_tag(&self, tag: &str) { + let mut frames = self.frames.lock(); + let tag = Some(tag); + if let Some(i) = frames.iter().rposition(|f| f.tag.as_deref() == tag) { + frames.remove(i); + } + } + + pub fn get_exitscripts_of_frame_with_tag(&self, tag: &str) -> Option> { + let frames = self.frames.lock(); + let tag = Some(tag); + frames.iter().find_map(|f| { + if f.tag.as_deref() == tag { + Some(f.exitscripts.clone()) + } else { + None + } + }) + } + + pub fn get_frame_with_tag(&self, tag: &str) -> Option { + let frames = self.frames.lock(); + let tag = Some(tag); + frames.iter().rev().find_map(|f| { + if f.tag.as_deref() == tag { + Some(f.clone()) + } else { + None + } + }) + } + + pub fn update_frame_with_tag(&self, frame: ScopeFrame, tag: &str) -> Result<(), ShellError> { + let mut frames = self.frames.lock(); + let tag = Some(tag); + for f in frames.iter_mut().rev() { + if f.tag.as_deref() == tag { + *f = frame; + return Ok(()); + } + } + + // Frame not found, return err + Err(ShellError::untagged_runtime_error(format!( + "Can't update frame with tag {:?}. No such frame present!", + tag + ))) + } } impl ParserScope for Scope { @@ -286,6 +356,15 @@ pub struct ScopeFrame { pub commands: IndexMap, pub custom_commands: IndexMap, pub aliases: IndexMap>>, + ///Optional tag to better identify this scope frame later + pub tag: Option, + pub exitscripts: Vec, +} + +impl Default for ScopeFrame { + fn default() -> Self { + ScopeFrame::new() + } } impl ScopeFrame { @@ -324,6 +403,15 @@ impl ScopeFrame { commands: IndexMap::new(), custom_commands: IndexMap::new(), aliases: IndexMap::new(), + tag: None, + exitscripts: Vec::new(), } } + + pub fn with_tag(tag: String) -> ScopeFrame { + let mut scope = ScopeFrame::new(); + scope.tag = Some(tag); + + scope + } } diff --git a/crates/nu-engine/src/evaluate/variables.rs b/crates/nu-engine/src/evaluate/variables.rs index cc047da802..31d8aa883b 100644 --- a/crates/nu-engine/src/evaluate/variables.rs +++ b/crates/nu-engine/src/evaluate/variables.rs @@ -1,24 +1,13 @@ use crate::evaluate::scope::Scope; use indexmap::IndexMap; -use nu_data::config::NuConfig; +use nu_data::config::path::history_path; use nu_errors::ShellError; use nu_protocol::{Primitive, TaggedDictBuilder, UntaggedValue, Value}; use nu_source::{Spanned, Tag}; pub fn nu(scope: &Scope, tag: impl Into) -> Result { - let tag = tag.into(); - let env = &scope.get_env_vars(); - - let config = if let Some(Value { - value: UntaggedValue::Primitive(Primitive::FilePath(path)), - .. - }) = scope.get_var("config-path") - { - NuConfig::with(Some(path).map(|p| p.into_os_string())) - } else { - NuConfig::new() - }; + let tag = tag.into(); let mut nu_dict = TaggedDictBuilder::new(&tag); @@ -30,10 +19,16 @@ pub fn nu(scope: &Scope, tag: impl Into) -> Result { } nu_dict.insert_value("env", dict.into_value()); - nu_dict.insert_value( - "config", - UntaggedValue::row(config.vars.clone()).into_value(&tag), - ); + let config_file = match scope.get_var("config-path") { + Some(Value { + value: UntaggedValue::Primitive(Primitive::FilePath(path)), + .. + }) => Some(path), + _ => None, + }; + + let config = nu_data::config::read(&tag, &config_file)?; + nu_dict.insert_value("config", UntaggedValue::row(config).into_value(&tag)); let mut table = vec![]; for v in env.iter() { @@ -55,9 +50,15 @@ pub fn nu(scope: &Scope, tag: impl Into) -> Result { let temp = std::env::temp_dir(); nu_dict.insert_value("temp-dir", UntaggedValue::filepath(temp).into_value(&tag)); + let config = if let Some(path) = config_file { + path + } else { + nu_data::config::default_path()? + }; + nu_dict.insert_value( "config-path", - UntaggedValue::filepath(nu_data::config::path::source_file(&config)).into_value(&tag), + UntaggedValue::filepath(config).into_value(&tag), ); #[cfg(feature = "rustyline-support")] @@ -69,9 +70,11 @@ pub fn nu(scope: &Scope, tag: impl Into) -> Result { ); } + let config: Box = Box::new(nu_data::config::NuConfig::new()); + let history = history_path(&config); nu_dict.insert_value( "history-path", - UntaggedValue::filepath(nu_data::config::path::history(&config)).into_value(&tag), + UntaggedValue::filepath(history).into_value(&tag), ); Ok(nu_dict.into_value()) diff --git a/crates/nu-engine/src/evaluation_context.rs b/crates/nu-engine/src/evaluation_context.rs index ef1167d930..8667016b6a 100644 --- a/crates/nu-engine/src/evaluation_context.rs +++ b/crates/nu-engine/src/evaluation_context.rs @@ -1,17 +1,18 @@ -use crate::call_info::UnevaluatedCallInfo; -use crate::command_args::CommandArgs; -use crate::env::{basic_host::BasicHost, host::Host}; -use crate::evaluate::scope::Scope; +use crate::env::host::Host; +use crate::evaluate::scope::{Scope, ScopeFrame}; use crate::shell::shell_manager::ShellManager; use crate::whole_stream_command::Command; -use indexmap::IndexMap; +use crate::{call_info::UnevaluatedCallInfo, config_holder::ConfigHolder}; +use crate::{command_args::CommandArgs, script}; +use log::trace; +use nu_data::config::{self, Conf, NuConfig}; use nu_errors::ShellError; -use nu_protocol::hir; -use nu_source::{Tag, Text}; +use nu_protocol::{hir, ConfigPath}; +use nu_source::{Span, Tag}; use nu_stream::{InputStream, OutputStream}; use parking_lot::Mutex; use std::sync::atomic::AtomicBool; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; #[derive(Clone)] pub struct EvaluationContext { @@ -19,7 +20,7 @@ pub struct EvaluationContext { pub host: Arc>>, pub current_errors: Arc>>, pub ctrl_c: Arc, - pub user_recently_used_autoenv_untrust: Arc, + pub configs: Arc>, pub shell_manager: ShellManager, /// Windows-specific: keep track of previous cwd on each drive @@ -27,26 +28,14 @@ pub struct EvaluationContext { } impl EvaluationContext { - pub fn basic() -> Result { - Ok(EvaluationContext { - scope: Scope::new(), - host: Arc::new(parking_lot::Mutex::new(Box::new(BasicHost))), - current_errors: Arc::new(Mutex::new(vec![])), - ctrl_c: Arc::new(AtomicBool::new(false)), - user_recently_used_autoenv_untrust: Arc::new(AtomicBool::new(false)), - shell_manager: ShellManager::basic()?, - windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())), - }) - } - pub fn from_args(args: &CommandArgs) -> EvaluationContext { EvaluationContext { scope: args.scope.clone(), host: args.host.clone(), current_errors: args.current_errors.clone(), ctrl_c: args.ctrl_c.clone(), + configs: args.configs.clone(), shell_manager: args.shell_manager.clone(), - user_recently_used_autoenv_untrust: Arc::new(AtomicBool::new(false)), windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())), } } @@ -117,6 +106,7 @@ impl EvaluationContext { CommandArgs { host: self.host.clone(), ctrl_c: self.ctrl_c.clone(), + configs: self.configs.clone(), current_errors: self.current_errors.clone(), shell_manager: self.shell_manager.clone(), call_info: self.call_info(args, name_tag), @@ -125,26 +115,187 @@ impl EvaluationContext { } } - pub fn get_env(&self) -> IndexMap { - let mut output = IndexMap::new(); - for (var, value) in self.host.lock().vars() { - output.insert(var, value); + /// Loads config under cfg_path. + /// If an error occurs while loading the config: + /// The config is not loaded + /// The error is returned + /// After successfull loading of the config the startup scripts are run + /// as normal scripts (Errors are printed out, ...) + /// After executing the startup scripts, true is returned to indicate successfull loading + /// of the config + // + // The rational here is that, we should not partially load any config + // that might be damaged. However, startup scripts might fail for various reasons. + // A failure there is not as crucial as wrong config files. + pub async fn load_config(&self, cfg_path: &ConfigPath) -> Result<(), ShellError> { + trace!("Loading cfg {:?}", cfg_path); + + let cfg = NuConfig::load(Some(cfg_path.get_path().clone()))?; + let exit_scripts = cfg.exit_scripts()?; + let startup_scripts = cfg.startup_scripts()?; + let cfg_paths = cfg.path()?; + + let joined_paths = cfg_paths + .map(|mut cfg_paths| { + //existing paths are prepended to path + if let Some(env_paths) = self.scope.get_env("PATH") { + let mut env_paths = std::env::split_paths(&env_paths).collect::>(); + //No duplicates! Remove env_paths already existing in cfg_paths + env_paths.retain(|env_path| !cfg_paths.contains(env_path)); + //env_paths entries are appended at the end + //nu config paths have a higher priority + cfg_paths.extend(env_paths); + } + cfg_paths + }) + .map(|paths| { + std::env::join_paths(paths) + .map(|s| s.to_string_lossy().to_string()) + .map_err(|e| { + ShellError::labeled_error( + &format!("Error while joining paths from config: {:?}", e), + "Config path error", + Span::unknown(), + ) + }) + }) + .transpose()?; + + let tag = config::cfg_path_to_scope_tag(cfg_path); + + self.scope.enter_scope_with_tag(tag); + self.scope.add_env(cfg.env_map()); + if let Some(path) = joined_paths { + self.scope.add_env_var("PATH", path); + } + self.scope.set_exit_scripts(exit_scripts); + + match cfg_path { + ConfigPath::Global(_) => self.configs.lock().set_global_cfg(cfg), + ConfigPath::Local(_) => { + self.configs.lock().add_local_cfg(cfg); + } + } + + if !startup_scripts.is_empty() { + self.run_scripts(startup_scripts, cfg_path.get_path().parent()) + .await; + } + + Ok(()) + } + + /// Reloads config with a path of cfg_path. + /// If an error occurs while reloading the config: + /// The config is not reloaded + /// The error is returned + pub async fn reload_config(&self, cfg_path: &ConfigPath) -> Result<(), ShellError> { + trace!("Reloading cfg {:?}", cfg_path); + + let mut configs = self.configs.lock(); + let cfg = match cfg_path { + ConfigPath::Global(path) => { + configs.global_config.iter_mut().find(|cfg| &cfg.file_path == path).ok_or_else(|| + ShellError::labeled_error( + &format!("Error reloading global config with path of {}. No such global config present.", path.display()), + "Config path error", + Span::unknown(), + ) + )? + } + ConfigPath::Local(path) => { + configs.local_configs.iter_mut().find(|cfg| &cfg.file_path == path).ok_or_else(|| + ShellError::labeled_error( + &format!("Error reloading local config with path of {}. No such local config present.", path.display()), + "Config path error", + Span::unknown(), + ) + )? + } + }; + + cfg.reload(); + + let exit_scripts = cfg.exit_scripts()?; + let cfg_paths = cfg.path()?; + + let joined_paths = cfg_paths + .map(|mut cfg_paths| { + //existing paths are prepended to path + if let Some(env_paths) = self.scope.get_env("PATH") { + let mut env_paths = std::env::split_paths(&env_paths).collect::>(); + //No duplicates! Remove env_paths already existing in cfg_paths + env_paths.retain(|env_path| !cfg_paths.contains(env_path)); + //env_paths entries are appended at the end + //nu config paths have a higher priority + cfg_paths.extend(env_paths); + } + cfg_paths + }) + .map(|paths| { + std::env::join_paths(paths) + .map(|s| s.to_string_lossy().to_string()) + .map_err(|e| { + ShellError::labeled_error( + &format!("Error while joining paths from config: {:?}", e), + "Config path error", + Span::unknown(), + ) + }) + }) + .transpose()?; + + let tag = config::cfg_path_to_scope_tag(cfg_path); + let mut frame = ScopeFrame::new(); + + frame.env = cfg.env_map(); + if let Some(path) = joined_paths { + frame.env.insert("PATH".to_string(), path); + } + frame.exitscripts = exit_scripts; + + self.scope.update_frame_with_tag(frame, &tag)?; + + Ok(()) + } + + /// Runs all exit_scripts before unloading the config with path of cfg_path + /// If an error occurs while running exit scripts: + /// The error is added to `self.current_errors` + /// If no config with path of `cfg_path` is present, this method does nothing + pub async fn unload_config(&self, cfg_path: &ConfigPath) { + trace!("UnLoading cfg {:?}", cfg_path); + + let tag = config::cfg_path_to_scope_tag(cfg_path); + + //Run exitscripts with scope frame and cfg still applied + if let Some(scripts) = self.scope.get_exitscripts_of_frame_with_tag(&tag) { + self.run_scripts(scripts, cfg_path.get_path().parent()) + .await; + } + + //Unload config + self.configs.lock().remove_cfg(&cfg_path); + self.scope.exit_scope_with_tag(&tag); + } + + /// Runs scripts with cwd of dir. If dir is None, this method does nothing. + /// Each error is added to `self.current_errors` + pub async fn run_scripts(&self, scripts: Vec, dir: Option<&Path>) { + if let Some(dir) = dir { + for script in scripts { + match script::run_script_in_dir(script.clone(), dir, &self).await { + Ok(_) => {} + Err(e) => { + let err = ShellError::untagged_runtime_error(format!( + "Err while executing exitscript. Err was\n{:?}", + e + )); + let text = script.into(); + self.host.lock().print_err(err, &text); + } + } + } } - output - } -} - -pub fn maybe_print_errors(context: &EvaluationContext, source: Text) -> bool { - let errors = context.current_errors.clone(); - let mut errors = errors.lock(); - - if errors.len() > 0 { - let error = errors[0].clone(); - *errors = vec![]; - - context.host.lock().print_err(error, &source); - true - } else { - false } } diff --git a/crates/nu-engine/src/filesystem/filesystem_shell.rs b/crates/nu-engine/src/filesystem/filesystem_shell.rs index c85b4c7d47..a7550d760c 100644 --- a/crates/nu-engine/src/filesystem/filesystem_shell.rs +++ b/crates/nu-engine/src/filesystem/filesystem_shell.rs @@ -10,7 +10,8 @@ use futures::stream::BoxStream; use futures::StreamExt; use futures_codec::FramedRead; use futures_util::TryStreamExt; -use nu_protocol::{TaggedDictBuilder, Value}; +use nu_data::config::LocalConfigDiff; +use nu_protocol::{CommandAction, ConfigPath, TaggedDictBuilder, Value}; use nu_source::{Span, Tag}; use nu_stream::{Interruptible, OutputStream, ToOutputStream}; use std::collections::HashMap; @@ -27,9 +28,16 @@ use nu_errors::ShellError; use nu_protocol::{Primitive, ReturnSuccess, UntaggedValue}; use nu_source::Tagged; +#[derive(Eq, PartialEq, Clone, Copy)] +pub enum FilesystemShellMode { + Cli, + Script, +} + pub struct FilesystemShell { pub(crate) path: String, pub(crate) last_path: String, + pub(crate) mode: FilesystemShellMode, } impl std::fmt::Debug for FilesystemShell { @@ -43,12 +51,13 @@ impl Clone for FilesystemShell { FilesystemShell { path: self.path.clone(), last_path: self.path.clone(), + mode: self.mode, } } } impl FilesystemShell { - pub fn basic() -> Result { + pub fn basic(mode: FilesystemShellMode) -> Result { let path = match std::env::current_dir() { Ok(path) => path, Err(_) => PathBuf::from("/"), @@ -57,15 +66,23 @@ impl FilesystemShell { Ok(FilesystemShell { path: path.to_string_lossy().to_string(), last_path: path.to_string_lossy().to_string(), + mode, }) } - pub fn with_location(path: String) -> Result { + pub fn with_location( + path: String, + mode: FilesystemShellMode, + ) -> Result { let path = canonicalize(std::env::current_dir()?, &path)?; let path = path.display().to_string(); let last_path = path.clone(); - Ok(FilesystemShell { path, last_path }) + Ok(FilesystemShell { + path, + last_path, + mode, + }) } } @@ -255,6 +272,43 @@ impl Shell for FilesystemShell { path.to_string_lossy().to_string(), )); + //Loading local configs in script mode, makes scripts behave different on different + //filesystems and might therefore surprise users. That's why we only load them in cli mode. + if self.mode == FilesystemShellMode::Cli { + match dunce::canonicalize(self.path()) { + Err(e) => { + let err = ShellError::untagged_runtime_error(format!( + "Could not get absolute path from current fs shell. The error was: {:?}", + e + )); + stream.push_back(ReturnSuccess::value( + UntaggedValue::Error(err).into_value(Tag::unknown()), + )); + } + Ok(current_pwd) => { + let (changes, errs) = LocalConfigDiff::between(current_pwd, path); + + for err in errs { + stream.push_back(ReturnSuccess::value( + UntaggedValue::Error(err).into_value(Tag::unknown()), + )); + } + + for unload_cfg in changes.cfgs_to_unload { + stream.push_back(ReturnSuccess::action(CommandAction::UnloadConfig( + ConfigPath::Local(unload_cfg), + ))); + } + + for load_cfg in changes.cfgs_to_load { + stream.push_back(ReturnSuccess::action(CommandAction::LoadConfig( + ConfigPath::Local(load_cfg), + ))); + } + } + }; + } + Ok(stream.into()) } @@ -777,6 +831,10 @@ impl Shell for FilesystemShell { )), } } + + fn is_interactive(&self) -> bool { + self.mode == FilesystemShellMode::Cli + } } struct TaggedPathBuf<'a>(&'a PathBuf, &'a Tag); diff --git a/crates/nu-engine/src/lib.rs b/crates/nu-engine/src/lib.rs index 9816023462..eb806690ff 100644 --- a/crates/nu-engine/src/lib.rs +++ b/crates/nu-engine/src/lib.rs @@ -1,25 +1,31 @@ +pub mod basic_evaluation_context; +pub mod basic_shell_manager; mod call_info; mod command_args; +mod config_holder; pub mod deserializer; pub mod documentation; mod env; mod evaluate; -pub mod evaluation_context; +mod evaluation_context; mod example; pub mod filesystem; mod maybe_text_codec; pub mod plugin; +mod print; mod runnable_context; pub mod script; pub mod shell; mod whole_stream_command; +pub use crate::basic_evaluation_context::basic_evaluation_context; +pub use crate::basic_shell_manager::basic_shell_manager; pub use crate::call_info::UnevaluatedCallInfo; pub use crate::command_args::{ CommandArgs, EvaluatedCommandArgs, EvaluatedWholeStreamCommandArgs, RawCommandArgs, }; +pub use crate::config_holder::ConfigHolder; pub use crate::documentation::{generate_docs, get_brief_help, get_documentation, get_full_help}; -pub use crate::env::environment::Env; pub use crate::env::host::FakeHost; pub use crate::env::host::Host; pub use crate::evaluate::block::run_block; @@ -31,6 +37,7 @@ pub use crate::filesystem::dir_info::{DirBuilder, DirInfo, FileInfo}; pub use crate::filesystem::filesystem_shell::FilesystemShell; pub use crate::filesystem::path; pub use crate::maybe_text_codec::{MaybeTextCodec, StringOrBinary}; +pub use crate::print::maybe_print_errors; pub use crate::runnable_context::RunnableContext; pub use crate::shell::help_shell::{command_dict, HelpShell}; pub use crate::shell::painter::Painter; diff --git a/crates/nu-engine/src/print.rs b/crates/nu-engine/src/print.rs new file mode 100644 index 0000000000..87f36e93a3 --- /dev/null +++ b/crates/nu-engine/src/print.rs @@ -0,0 +1,18 @@ +use nu_source::Text; + +use crate::EvaluationContext; + +pub fn maybe_print_errors(context: &EvaluationContext, source: Text) -> bool { + let errors = context.current_errors.clone(); + let mut errors = errors.lock(); + + if errors.len() > 0 { + let error = errors[0].clone(); + *errors = vec![]; + + context.host.lock().print_err(error, &source); + true + } else { + false + } +} diff --git a/crates/nu-engine/src/runnable_context.rs b/crates/nu-engine/src/runnable_context.rs index 2c6b459f9d..5db935888b 100644 --- a/crates/nu-engine/src/runnable_context.rs +++ b/crates/nu-engine/src/runnable_context.rs @@ -1,4 +1,5 @@ -use crate::{Command, Host, Scope, ShellManager}; +use crate::{Command, CommandArgs, EvaluationContext}; +use crate::{ConfigHolder, Host, Scope, ShellManager}; use nu_errors::ShellError; use nu_source::Tag; use nu_stream::InputStream; @@ -10,12 +11,39 @@ pub struct RunnableContext { pub shell_manager: ShellManager, pub host: Arc>>, pub ctrl_c: Arc, + pub configs: Arc>, pub current_errors: Arc>>, pub scope: Scope, pub name: Tag, } impl RunnableContext { + pub fn from_command_args(args: CommandArgs) -> Self { + RunnableContext { + input: args.input, + scope: args.scope.clone(), + shell_manager: args.shell_manager, + host: args.host, + ctrl_c: args.ctrl_c, + configs: args.configs, + current_errors: args.current_errors, + name: args.call_info.name_tag, + } + } + + pub fn from_evaluation_context(input: InputStream, ctx: &EvaluationContext) -> Self { + RunnableContext { + input, + shell_manager: ctx.shell_manager.clone(), + host: ctx.host.clone(), + ctrl_c: ctx.ctrl_c.clone(), + configs: ctx.configs.clone(), + current_errors: ctx.current_errors.clone(), + scope: ctx.scope.clone(), + name: Tag::unknown(), + } + } + pub fn get_command(&self, name: &str) -> Option { self.scope.get_command(name) } diff --git a/crates/nu-engine/src/script.rs b/crates/nu-engine/src/script.rs index 10de802c61..29ad593302 100644 --- a/crates/nu-engine/src/script.rs +++ b/crates/nu-engine/src/script.rs @@ -1,5 +1,4 @@ -use crate::path::canonicalize; -use crate::run_block; +use crate::{maybe_print_errors, path::canonicalize, run_block}; use crate::{MaybeTextCodec, StringOrBinary}; use futures::StreamExt; use futures_codec::FramedRead; @@ -11,7 +10,7 @@ use nu_protocol::hir::{ use nu_protocol::{Primitive, ReturnSuccess, UntaggedValue, Value}; use nu_stream::{InputStream, ToInputStream}; -use crate::{evaluation_context, EvaluationContext}; +use crate::EvaluationContext; use log::{debug, trace}; use nu_source::{Span, Tag, Text}; use std::iter::Iterator; @@ -36,6 +35,22 @@ fn chomp_newline(s: &str) -> &str { } } +pub async fn run_script_in_dir( + script: String, + dir: &Path, + ctx: &EvaluationContext, +) -> Result<(), Box> { + //Save path before to switch back to it after executing script + let path_before = ctx.shell_manager.path(); + + ctx.shell_manager + .set_path(dir.to_string_lossy().to_string()); + run_script_standalone(script, false, ctx, false).await?; + ctx.shell_manager.set_path(path_before); + + Ok(()) +} + /// Process the line by parsing the text to turn it into commands, classify those commands so that we understand what is being called in the pipeline, and then run this pipeline pub async fn process_script( script_text: &str, @@ -168,9 +183,7 @@ pub async fn process_script( }; trace!("{:#?}", block); - let env = ctx.get_env(); - ctx.scope.add_env_to_base(env); let result = run_block(&block, ctx, input_stream).await; match result { @@ -229,6 +242,10 @@ pub async fn run_script_standalone( context: &EvaluationContext, exit_on_error: bool, ) -> Result<(), Box> { + context + .shell_manager + .enter_script_mode() + .map_err(Box::new)?; let line = process_script(&script_text, context, redirect_stdin, 0, false).await; match line { @@ -244,16 +261,19 @@ pub async fn run_script_standalone( } }; - evaluation_context::maybe_print_errors(&context, Text::from(line)); + maybe_print_errors(&context, Text::from(line)); if error_code != 0 && exit_on_error { std::process::exit(error_code); } } LineResult::Error(line, err) => { - context.with_host(|host| host.print_err(err, &Text::from(line.clone()))); + context + .host + .lock() + .print_err(err, &Text::from(line.clone())); - evaluation_context::maybe_print_errors(&context, Text::from(line)); + maybe_print_errors(&context, Text::from(line)); if exit_on_error { std::process::exit(1); } @@ -261,5 +281,9 @@ pub async fn run_script_standalone( _ => {} } + + //exit script mode shell + context.shell_manager.remove_at_current(); + Ok(()) } diff --git a/crates/nu-engine/src/shell/help_shell.rs b/crates/nu-engine/src/shell/help_shell.rs index ef0fb21e9e..9bc9a0c909 100644 --- a/crates/nu-engine/src/shell/help_shell.rs +++ b/crates/nu-engine/src/shell/help_shell.rs @@ -244,4 +244,9 @@ impl Shell for HelpShell { "save on help shell is not supported", )) } + + fn is_interactive(&self) -> bool { + //Help shell is always interactive + true + } } diff --git a/crates/nu-engine/src/shell/mod.rs b/crates/nu-engine/src/shell/mod.rs index d747fe392a..8d96ab7ca5 100644 --- a/crates/nu-engine/src/shell/mod.rs +++ b/crates/nu-engine/src/shell/mod.rs @@ -19,6 +19,7 @@ pub(crate) mod shell_manager; pub(crate) mod value_shell; pub trait Shell: std::fmt::Debug { + fn is_interactive(&self) -> bool; fn name(&self) -> String; fn homedir(&self) -> Option; diff --git a/crates/nu-engine/src/shell/shell_manager.rs b/crates/nu-engine/src/shell/shell_manager.rs index 00c063cdb0..af3e16418b 100644 --- a/crates/nu-engine/src/shell/shell_manager.rs +++ b/crates/nu-engine/src/shell/shell_manager.rs @@ -1,12 +1,10 @@ -use crate::command_args::EvaluatedWholeStreamCommandArgs; -use crate::maybe_text_codec::StringOrBinary; +use crate::shell::Shell; +use crate::{command_args::EvaluatedWholeStreamCommandArgs, FilesystemShell}; +use crate::{filesystem::filesystem_shell::FilesystemShellMode, maybe_text_codec::StringOrBinary}; use futures::Stream; use nu_stream::OutputStream; -use crate::filesystem::filesystem_shell::FilesystemShell; use crate::shell::shell_args::{CdArgs, CopyArgs, LsArgs, MkdirArgs, MvArgs, RemoveArgs}; -use crate::shell::Shell; - use encoding_rs::Encoding; use nu_errors::ShellError; use nu_source::{Span, Tag}; @@ -22,11 +20,11 @@ pub struct ShellManager { } impl ShellManager { - pub fn basic() -> Result { - Ok(ShellManager { - current_shell: Arc::new(AtomicUsize::new(0)), - shells: Arc::new(Mutex::new(vec![Box::new(FilesystemShell::basic()?)])), - }) + pub fn enter_script_mode(&self) -> Result<(), std::io::Error> { + //New fs_shell starting from current path + let fs_shell = FilesystemShell::with_location(self.path(), FilesystemShellMode::Script)?; + self.insert_at_current(Box::new(fs_shell)); + Ok(()) } pub fn insert_at_current(&self, shell: Box) { @@ -64,6 +62,10 @@ impl ShellManager { self.shells.lock().is_empty() } + pub fn is_interactive(&self) -> bool { + self.shells.lock()[self.current_shell()].is_interactive() + } + pub fn path(&self) -> String { self.shells.lock()[self.current_shell()].path() } diff --git a/crates/nu-engine/src/shell/value_shell.rs b/crates/nu-engine/src/shell/value_shell.rs index 86d31b211f..85fd849f41 100644 --- a/crates/nu-engine/src/shell/value_shell.rs +++ b/crates/nu-engine/src/shell/value_shell.rs @@ -254,4 +254,9 @@ impl Shell for ValueShell { "save on help shell is not supported", )) } + + fn is_interactive(&self) -> bool { + //Value shell is always interactive + true + } } diff --git a/crates/nu-protocol/src/config_path.rs b/crates/nu-protocol/src/config_path.rs new file mode 100644 index 0000000000..ccbdefc313 --- /dev/null +++ b/crates/nu-protocol/src/config_path.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// Specifies a path to a configuration file and its type +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum ConfigPath { + /// Path to the global configuration file + Global(PathBuf), + /// Path to a local configuration file + Local(PathBuf), +} + +impl ConfigPath { + pub fn get_path(&self) -> &PathBuf { + match self { + ConfigPath::Global(p) | ConfigPath::Local(p) => p, + } + } +} diff --git a/crates/nu-protocol/src/lib.rs b/crates/nu-protocol/src/lib.rs index 67720c85ab..33d4ab4fdb 100644 --- a/crates/nu-protocol/src/lib.rs +++ b/crates/nu-protocol/src/lib.rs @@ -2,6 +2,7 @@ mod macros; mod call_info; +pub mod config_path; pub mod hir; mod maybe_owned; mod return_value; @@ -12,6 +13,7 @@ mod type_shape; pub mod value; pub use crate::call_info::{CallInfo, EvaluatedArgs}; +pub use crate::config_path::ConfigPath; pub use crate::maybe_owned::MaybeOwned; pub use crate::return_value::{CommandAction, ReturnSuccess, ReturnValue}; pub use crate::signature::{NamedType, PositionalType, Signature}; diff --git a/crates/nu-protocol/src/return_value.rs b/crates/nu-protocol/src/return_value.rs index 9e1f8fdca6..65909951d2 100644 --- a/crates/nu-protocol/src/return_value.rs +++ b/crates/nu-protocol/src/return_value.rs @@ -1,4 +1,4 @@ -use crate::value::Value; +use crate::{value::Value, ConfigPath}; use nu_errors::ShellError; use nu_source::{DbgDocBldr, DebugDocBuilder, PrettyDebug}; use serde::{Deserialize, Serialize}; @@ -22,6 +22,10 @@ pub enum CommandAction { EnterHelpShell(Value), /// Add plugins from path given AddPlugins(String), + /// Unload the config specified by PathBuf if present + UnloadConfig(ConfigPath), + /// Load the config specified by PathBuf + LoadConfig(ConfigPath), /// Go to the previous shell in the shell ring buffer PreviousShell, /// Go to the next shell in the shell ring buffer @@ -51,6 +55,12 @@ impl PrettyDebug for CommandAction { CommandAction::PreviousShell => DbgDocBldr::description("previous shell"), CommandAction::NextShell => DbgDocBldr::description("next shell"), CommandAction::LeaveShell(_) => DbgDocBldr::description("leave shell"), + CommandAction::UnloadConfig(cfg) => { + DbgDocBldr::description(format!("unload config {:?}", cfg)) + } + CommandAction::LoadConfig(cfg) => { + DbgDocBldr::description(format!("load config {:?}", cfg)) + } } } } diff --git a/src/main.rs b/src/main.rs index 877ae21cfb..3f0f8a51fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,13 @@ fn main() -> Result<(), Box> { .hidden(true) .takes_value(true), ) + .arg( + Arg::with_name("no-history") + .hidden(true) + .long("no-history") + .multiple(false) + .takes_value(false), + ) .arg( Arg::with_name("loglevel") .short("l") @@ -36,7 +43,9 @@ fn main() -> Result<(), Box> { .hidden(true) .long("testbin") .value_name("TESTBIN") - .possible_values(&["cococo", "iecho", "fail", "nonu", "chop", "repeater"]) + .possible_values(&[ + "echo_env", "cococo", "iecho", "fail", "nonu", "chop", "repeater", + ]) .takes_value(true), ) .arg( @@ -64,13 +73,6 @@ fn main() -> Result<(), Box> { .multiple(false) .takes_value(false), ) - .arg( - Arg::with_name("no-history") - .hidden(true) - .long("no-history") - .multiple(false) - .takes_value(false), - ) .arg( Arg::with_name("script") .help("the nu script to run") @@ -86,6 +88,7 @@ fn main() -> Result<(), Box> { if let Some(bin) = matches.value_of("testbin") { match bin { + "echo_env" => binaries::echo_env(), "cococo" => binaries::cococo(), "iecho" => binaries::iecho(), "fail" => binaries::fail(), @@ -163,10 +166,10 @@ fn main() -> Result<(), Box> { } None => { - let mut context = create_default_context(true)?; + let context = create_default_context(true)?; if !matches.is_present("skip-plugins") { - let _ = nu_cli::register_plugins(&mut context); + let _ = nu_cli::register_plugins(&context); } #[cfg(feature = "rustyline-support")] diff --git a/tests/shell/environment/env.rs b/tests/shell/environment/env.rs new file mode 100644 index 0000000000..bce60c6404 --- /dev/null +++ b/tests/shell/environment/env.rs @@ -0,0 +1,45 @@ +use super::support::Trusted; + +use nu_test_support::fs::Stub::FileWithContent; +use nu_test_support::nu; +use nu_test_support::playground::Playground; + +use serial_test::serial; + +#[test] +fn passes_let_env_env_var_to_external_process() { + let actual = nu!(cwd: ".", r#" + let-env FOO = foo + nu --testbin echo_env FOO + "#); + assert_eq!(actual.out, "foo"); +} + +#[test] +fn passes_with_env_env_var_to_external_process() { + let actual = nu!(cwd: ".", r#" + with-env [FOO foo] {nu --testbin echo_env FOO} + "#); + assert_eq!(actual.out, "foo"); +} + +#[test] +#[serial] +fn passes_env_from_local_cfg_to_external_process() { + Playground::setup("autoenv_dir", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + ".nu-env", + r#"[env] + FOO = "foo" + "#, + )]); + + let actual = Trusted::in_path(&dirs, || { + nu!(cwd: dirs.test(), r#" + nu --testbin echo_env FOO + "#) + }); + + assert_eq!(actual.out, "foo"); + }) +} diff --git a/tests/shell/environment/mod.rs b/tests/shell/environment/mod.rs index b3a0b763fb..01a0a36572 100644 --- a/tests/shell/environment/mod.rs +++ b/tests/shell/environment/mod.rs @@ -1,4 +1,5 @@ mod configuration; +mod env; mod in_sync; mod nu_env; diff --git a/tests/shell/environment/nu_env.rs b/tests/shell/environment/nu_env.rs index 69aecd335d..fcc2f37e97 100644 --- a/tests/shell/environment/nu_env.rs +++ b/tests/shell/environment/nu_env.rs @@ -6,17 +6,8 @@ use nu_test_support::{nu, pipeline}; use serial_test::serial; -// Windows uses a different command to create an empty file -// so we need to have different content on windows. -const SCRIPTS: &str = if cfg!(target_os = "windows") { - r#"[scripts] - entryscripts = ["echo nul > hello.txt"] - exitscripts = ["echo nul > bye.txt"]"# -} else { - r#"[scripts] - entryscripts = ["touch hello.txt"] - exitscripts = ["touch bye.txt"]"# -}; +const SCRIPTS: &str = r#"startup = ["touch hello.txt"] + on_exit = ["touch bye.txt"]"#; #[test] #[serial] @@ -26,13 +17,13 @@ fn picks_up_env_keys_when_entering_trusted_directory() { ".nu-env", &format!( "{}\n{}", + SCRIPTS, r#"[env] testkey = "testvalue" [scriptvars] myscript = "echo myval" - "#, - SCRIPTS + "# ), )]); @@ -46,19 +37,20 @@ fn picks_up_env_keys_when_entering_trusted_directory() { #[test] #[serial] +#[ignore] fn picks_up_script_vars_when_entering_trusted_directory() { Playground::setup("autoenv_test_2", |dirs, sandbox| { sandbox.with_files(vec![FileWithContent( ".nu-env", &format!( "{}\n{}", + SCRIPTS, r#"[env] testkey = "testvalue" [scriptvars] myscript = "echo myval" - "#, - SCRIPTS + "# ), )]); @@ -66,6 +58,8 @@ fn picks_up_script_vars_when_entering_trusted_directory() { let actual = Trusted::in_path(&dirs, || nu!(cwd: dirs.test(), "echo $nu.env.myscript")); + // scriptvars are not supported + // and why is myval expected when myscript is "echo myval" assert_eq!(actual.out, expected); }) } @@ -102,13 +96,10 @@ fn entering_a_trusted_directory_runs_entry_scripts() { ".nu-env", &format!( "{}\n{}", + SCRIPTS, r#"[env] testkey = "testvalue" - - [scriptvars] - myscript = "echo myval" - "#, - SCRIPTS + "# ), )]); @@ -132,13 +123,13 @@ fn leaving_a_trusted_directory_runs_exit_scripts() { ".nu-env", &format!( "{}\n{}", + SCRIPTS, r#"[env] testkey = "testvalue" [scriptvars] myscript = "echo myval" - "#, - SCRIPTS + "# ), )]); @@ -161,13 +152,13 @@ fn entry_scripts_are_called_when_revisiting_a_trusted_directory() { ".nu-env", &format!( "{}\n{}", + SCRIPTS, r#"[env] testkey = "testvalue" [scriptvars] myscript = "echo myval" - "#, - SCRIPTS + "# ), )]); @@ -194,13 +185,13 @@ fn given_a_trusted_directory_with_entry_scripts_when_entering_a_subdirectory_ent ".nu-env", &format!( "{}\n{}", + SCRIPTS, r#"[env] testkey = "testvalue" [scriptvars] myscript = "echo myval" - "#, - SCRIPTS + "# ), )]); @@ -225,13 +216,13 @@ fn given_a_trusted_directory_with_exit_scripts_when_entering_a_subdirectory_exit ".nu-env", &format!( "{}\n{}", + SCRIPTS, r#"[env] testkey = "testvalue" [scriptvars] myscript = "echo myval" - "#, - SCRIPTS + "# ), )]); @@ -308,6 +299,40 @@ fn given_a_hierachy_of_trusted_directories_nested_ones_should_overwrite_variable }) } +#[test] +#[serial] +#[cfg(not(windows))] //TODO figure out why this test doesn't work on windows +fn local_config_should_not_be_added_when_running_scripts() { + Playground::setup("autoenv_test_10", |dirs, sandbox| { + sandbox.mkdir("foo"); + sandbox.with_files(vec![ + FileWithContent( + ".nu-env", + r#"[env] + organization = "nu""#, + ), + FileWithContent( + "foo/.nu-env", + r#"[env] + organization = "foo""#, + ), + FileWithContent( + "script.nu", + r#"cd foo + echo $nu.env.organization"#, + ), + ]); + + let actual = Trusted::in_path(&dirs, || { + nu!(cwd: dirs.test(), r#" + do { autoenv trust foo ; = $nothing } # Silence autoenv trust message from output + nu script.nu + "#) + }); + + assert_eq!(actual.out, "nu"); + }) +} #[test] #[serial] fn given_a_hierachy_of_trusted_directories_going_back_restores_overwritten_variables() { @@ -340,3 +365,246 @@ fn given_a_hierachy_of_trusted_directories_going_back_restores_overwritten_varia assert_eq!(actual.out, "nushell"); }) } + +#[cfg(feature = "directories-support")] +#[cfg(feature = "which-support")] +#[test] +#[serial] +fn local_config_env_var_present_and_removed_correctly() { + use nu_test_support::fs::Stub::FileWithContent; + Playground::setup("autoenv_test", |dirs, sandbox| { + sandbox.mkdir("foo"); + sandbox.mkdir("foo/bar"); + sandbox.with_files(vec![FileWithContent( + "foo/.nu-env", + r#"[env] + testkey = "testvalue" + "#, + )]); + //Assert testkey is not present before entering directory + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo; + echo $nu.env.testkey"# + ); + assert!(actual.err.contains("Unknown")); + //Assert testkey is present in foo + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo; cd foo + echo $nu.env.testkey"# + ); + assert_eq!(actual.out, "testvalue"); + //Assert testkey is present also in subdirectories + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo; cd foo + cd bar + echo $nu.env.testkey"# + ); + assert_eq!(actual.out, "testvalue"); + //Assert testkey is present also when jumping over foo + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo; cd foo/bar + echo $nu.env.testkey"# + ); + assert_eq!(actual.out, "testvalue"); + //Assert testkey removed after leaving foo + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo; cd foo + cd .. + echo $nu.env.testkey"# + ); + assert!(actual.err.contains("Unknown")); + }); +} + +#[cfg(feature = "directories-support")] +#[cfg(feature = "which-support")] +#[test] +#[serial] +fn local_config_env_var_gets_overwritten() { + use nu_test_support::fs::Stub::FileWithContent; + Playground::setup("autoenv_test", |dirs, sandbox| { + sandbox.mkdir("foo"); + sandbox.mkdir("foo/bar"); + sandbox.with_files(vec![ + FileWithContent( + "foo/.nu-env", + r#"[env] + overwrite_me = "foo" + "#, + ), + FileWithContent( + "foo/bar/.nu-env", + r#"[env] + overwrite_me = "bar" + "#, + ), + ]); + //Assert overwrite_me is not present before entering directory + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo; + echo $nu.env.overwrite_me"# + ); + assert!(actual.err.contains("Unknown")); + //Assert overwrite_me is foo in foo + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo; cd foo + echo $nu.env.overwrite_me"# + ); + assert_eq!(actual.out, "foo"); + //Assert overwrite_me is bar in bar + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo | = $nothing + autoenv trust foo/bar | = $nothing + cd foo + cd bar + echo $nu.env.overwrite_me"# + ); + assert_eq!(actual.out, "bar"); + //Assert overwrite_me is present also when jumping over foo + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo; autoenv trust foo/bar; cd foo/bar + echo $nu.env.overwrite_me + "# + ); + assert_eq!(actual.out, "bar"); + //Assert overwrite_me removed after leaving bar + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo; autoenv trust foo/bar; cd foo + cd bar + cd .. + echo $nu.env.overwrite_me"# + ); + assert_eq!(actual.out, "foo"); + }); +} + +#[cfg(feature = "directories-support")] +#[cfg(feature = "which-support")] +#[test] +#[serial] +fn autoenv_test_entry_scripts() { + use nu_test_support::fs::Stub::FileWithContent; + Playground::setup("autoenv_test", |dirs, sandbox| { + sandbox.mkdir("foo/bar"); + + // Windows uses a different command to create an empty file so we need to have different content on windows. + let nu_env = if cfg!(target_os = "windows") { + r#"startup = ["echo nul > hello.txt"]"# + } else { + r#"startup = ["touch hello.txt"]"# + }; + + sandbox.with_files(vec![FileWithContent("foo/.nu-env", nu_env)]); + + // Make sure entryscript is run when entering directory + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo + cd foo + ls | where name == "hello.txt" | get name"# + ); + assert!(actual.out.contains("hello.txt")); + + // Make sure entry scripts are also run when jumping over directory + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo + cd foo/bar + ls .. | where name == "../hello.txt" | get name"# + ); + assert!(actual.out.contains("hello.txt")); + + // Entryscripts should not run after changing to a subdirectory. + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo | = $nothing + cd foo + rm hello.txt + cd bar + ls .. | where name == "../hello.txt" | length"# + ); + assert!(actual.out.contains("0")); + }); +} + +#[cfg(feature = "directories-support")] +#[cfg(feature = "which-support")] +#[test] +#[serial] +fn autoenv_test_exit_scripts() { + use nu_test_support::fs::Stub::FileWithContent; + Playground::setup("autoenv_test", |dirs, sandbox| { + sandbox.mkdir("foo/bar"); + + // Windows uses a different command to create an empty file so we need to have different content on windows. + let nu_env = r#"on_exit = ["touch bye.txt"]"#; + + sandbox.with_files(vec![FileWithContent("foo/.nu-env", nu_env)]); + + // Make sure exitscript is run + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo | = $nothing + cd foo + cd .. + ls foo | where name =~ "bye.txt" | length + rm foo/bye.txt; cd . + "# + ); + assert_eq!(actual.out, "1"); + + // Entering a subdir should not trigger exitscripts + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo | = $nothing + cd foo + cd bar + ls .. | where name =~ "bye.txt" | length"# + ); + assert_eq!(actual.out, "0"); + + // Also run exitscripts when jumping over directory + let actual = nu!( + cwd: dirs.test(), + r#"autoenv trust foo | = $nothing + cd foo/bar + cd ../.. + ls foo | where name =~ "bye.txt" | length + rm foo/bye.txt; cd ."# + ); + assert_eq!(actual.out, "1"); + }); +} + +#[test] +#[serial] +#[cfg(unix)] +fn prepends_path_from_local_config() { + //If this test fails for you, make sure that your environment from which you start nu + //contains some env vars + Playground::setup("autoenv_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + ".nu-env", + r#" + path = ["/hi", "/nushell"] + "#, + )]); + + let expected = "[\"/hi\",\"/nushell\","; + + let actual = Trusted::in_path(&dirs, || nu!(cwd: dirs.test(), "echo $nu.path | to json")); + // assert_eq!("", actual.out); + assert!(actual.out.starts_with(expected)); + assert!(actual.out.len() > expected.len()); + }) +}