Slim down configuration readings and nu_cli clean up. (#2559)

We continue refactoring nu_cli and slim down a bit configuration
readings with a naive metadata `modified` field check.
This commit is contained in:
Andrés N. Robalino 2020-09-16 18:22:58 -05:00 committed by GitHub
parent 50cbf91bc5
commit 10d4edc7af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 632 additions and 387 deletions

View File

@ -7,7 +7,7 @@ use crate::prelude::*;
use crate::shell::Helper; use crate::shell::Helper;
use crate::EnvironmentSyncer; use crate::EnvironmentSyncer;
use futures_codec::FramedRead; use futures_codec::FramedRead;
use nu_errors::{ProximateShellError, ShellDiagnostic, ShellError}; use nu_errors::ShellError;
use nu_protocol::hir::{ClassifiedCommand, Expression, InternalCommand, Literal, NamedArguments}; use nu_protocol::hir::{ClassifiedCommand, Expression, InternalCommand, Literal, NamedArguments};
use nu_protocol::{Primitive, ReturnSuccess, UntaggedValue, Value}; use nu_protocol::{Primitive, ReturnSuccess, UntaggedValue, Value};
@ -20,19 +20,6 @@ use std::iter::Iterator;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
pub fn register_plugins(context: &mut Context) -> Result<(), ShellError> {
if let Ok(plugins) = crate::plugin::scan(search_paths()) {
context.add_commands(
plugins
.into_iter()
.filter(|p| !context.is_command_registered(p.name()))
.collect(),
);
}
Ok(())
}
pub fn search_paths() -> Vec<std::path::PathBuf> { pub fn search_paths() -> Vec<std::path::PathBuf> {
use std::env; use std::env;
@ -64,15 +51,8 @@ pub fn search_paths() -> Vec<std::path::PathBuf> {
search_paths search_paths
} }
pub fn create_default_context( pub fn create_default_context(interactive: bool) -> Result<Context, Box<dyn Error>> {
syncer: &mut crate::EnvironmentSyncer,
interactive: bool,
) -> Result<Context, Box<dyn Error>> {
syncer.load_environment();
let mut context = Context::basic()?; let mut context = Context::basic()?;
syncer.sync_env_vars(&mut context);
syncer.sync_path_vars(&mut context);
{ {
use crate::commands::*; use crate::commands::*;
@ -299,282 +279,62 @@ pub async fn run_vec_of_pipelines(
pipelines: Vec<String>, pipelines: Vec<String>,
redirect_stdin: bool, redirect_stdin: bool,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let mut syncer = crate::EnvironmentSyncer::new(); let mut syncer = EnvironmentSyncer::new();
let mut context = create_default_context(&mut syncer, false)?; let mut context = create_default_context(false)?;
let config = syncer.get_config();
let _ = register_plugins(&mut context); context.configure(&config, |_, ctx| {
syncer.load_environment();
syncer.sync_env_vars(ctx);
syncer.sync_path_vars(ctx);
#[cfg(feature = "ctrlc")] if let Err(reason) = syncer.autoenv(ctx) {
{ print_err(reason, &Text::from(""));
let cc = context.ctrl_c.clone();
ctrlc::set_handler(move || {
cc.store(true, Ordering::SeqCst);
})
.expect("Error setting Ctrl-C handler");
if context.ctrl_c.load(Ordering::SeqCst) {
context.ctrl_c.store(false, Ordering::SeqCst);
} }
}
// before we start up, let's run our startup commands let _ = register_plugins(ctx);
if let Ok(config) = nu_data::config::config(Tag::unknown()) { let _ = configure_ctrl_c(ctx);
if let Some(commands) = config.get("startup") { });
match commands {
Value { let _ = run_startup_commands(&mut context, &config).await;
value: UntaggedValue::Table(pipelines),
..
} => {
for pipeline in pipelines {
if let Ok(pipeline_string) = pipeline.as_string() {
let _ = run_pipeline_standalone(
pipeline_string,
false,
&mut context,
false,
)
.await;
}
}
}
_ => {
println!("warning: expected a table of pipeline strings as startup commands");
}
}
}
}
for pipeline in pipelines { for pipeline in pipelines {
run_pipeline_standalone(pipeline, redirect_stdin, &mut context, true).await?; run_pipeline_standalone(pipeline, redirect_stdin, &mut context, true).await?;
} }
Ok(())
}
pub async fn run_pipeline_standalone(
pipeline: String,
redirect_stdin: bool,
context: &mut Context,
exit_on_error: bool,
) -> Result<(), Box<dyn Error>> {
let line = process_line(Ok(pipeline), context, redirect_stdin, false).await;
match line {
LineResult::Success(line) => {
let error_code = {
let errors = context.current_errors.clone();
let errors = errors.lock();
if errors.len() > 0 {
1
} else {
0
}
};
context.maybe_print_errors(Text::from(line));
if error_code != 0 && exit_on_error {
std::process::exit(error_code);
}
}
LineResult::Error(line, err) => {
context.with_host(|_host| {
print_err(err, &Text::from(line.clone()));
});
context.maybe_print_errors(Text::from(line));
if exit_on_error {
std::process::exit(1);
}
}
_ => {}
}
Ok(()) Ok(())
} }
pub fn create_rustyline_configuration() -> (Editor<Helper>, IndexMap<String, Value>) {
#[cfg(windows)]
const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::Circular;
#[cfg(not(windows))]
const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::List;
let config = Config::builder().color_mode(ColorMode::Forced).build();
let mut rl: Editor<_> = Editor::with_config(config);
// add key bindings to move over a whole word with Ctrl+ArrowLeft and Ctrl+ArrowRight
rl.bind_sequence(
KeyPress::ControlLeft,
Cmd::Move(Movement::BackwardWord(1, Word::Vi)),
);
rl.bind_sequence(
KeyPress::ControlRight,
Cmd::Move(Movement::ForwardWord(1, At::AfterEnd, Word::Vi)),
);
// Let's set the defaults up front and then override them later if the user indicates
// defaults taken from here https://github.com/kkawakam/rustyline/blob/2fe886c9576c1ea13ca0e5808053ad491a6fe049/src/config.rs#L150-L167
rl.set_max_history_size(100);
rl.set_history_ignore_dups(true);
rl.set_history_ignore_space(false);
rl.set_completion_type(DEFAULT_COMPLETION_MODE);
rl.set_completion_prompt_limit(100);
rl.set_keyseq_timeout(-1);
rl.set_edit_mode(rustyline::config::EditMode::Emacs);
rl.set_auto_add_history(false);
rl.set_bell_style(rustyline::config::BellStyle::default());
rl.set_color_mode(rustyline::ColorMode::Enabled);
rl.set_tab_stop(8);
if let Err(e) = crate::keybinding::load_keybindings(&mut rl) {
println!("Error loading keybindings: {:?}", e);
}
let config = match config::config(Tag::unknown()) {
Ok(config) => config,
Err(e) => {
eprintln!("Config could not be loaded.");
if let ShellError {
error: ProximateShellError::Diagnostic(ShellDiagnostic { diagnostic }),
..
} = e
{
eprintln!("{}", diagnostic.message);
}
IndexMap::new()
}
};
if let Ok(config) = config::config(Tag::unknown()) {
if let Some(line_editor_vars) = config.get("line_editor") {
for (idx, value) in line_editor_vars.row_entries() {
match idx.as_ref() {
"max_history_size" => {
if let Ok(max_history_size) = value.as_u64() {
rl.set_max_history_size(max_history_size as usize);
}
}
"history_duplicates" => {
// history_duplicates = match value.as_string() {
// Ok(s) if s.to_lowercase() == "alwaysadd" => {
// rustyline::config::HistoryDuplicates::AlwaysAdd
// }
// Ok(s) if s.to_lowercase() == "ignoreconsecutive" => {
// rustyline::config::HistoryDuplicates::IgnoreConsecutive
// }
// _ => rustyline::config::HistoryDuplicates::AlwaysAdd,
// };
if let Ok(history_duplicates) = value.as_bool() {
rl.set_history_ignore_dups(history_duplicates);
}
}
"history_ignore_space" => {
if let Ok(history_ignore_space) = value.as_bool() {
rl.set_history_ignore_space(history_ignore_space);
}
}
"completion_type" => {
let completion_type = match value.as_string() {
Ok(s) if s.to_lowercase() == "circular" => {
rustyline::config::CompletionType::Circular
}
Ok(s) if s.to_lowercase() == "list" => {
rustyline::config::CompletionType::List
}
#[cfg(all(unix, feature = "with-fuzzy"))]
Ok(s) if s.to_lowercase() == "fuzzy" => {
rustyline::config::CompletionType::Fuzzy
}
_ => DEFAULT_COMPLETION_MODE,
};
rl.set_completion_type(completion_type);
}
"completion_prompt_limit" => {
if let Ok(completion_prompt_limit) = value.as_u64() {
rl.set_completion_prompt_limit(completion_prompt_limit as usize);
}
}
"keyseq_timeout_ms" => {
if let Ok(keyseq_timeout_ms) = value.as_u64() {
rl.set_keyseq_timeout(keyseq_timeout_ms as i32);
}
}
"edit_mode" => {
let edit_mode = match value.as_string() {
Ok(s) if s.to_lowercase() == "vi" => rustyline::config::EditMode::Vi,
Ok(s) if s.to_lowercase() == "emacs" => {
rustyline::config::EditMode::Emacs
}
_ => rustyline::config::EditMode::Emacs,
};
rl.set_edit_mode(edit_mode);
// Note: When edit_mode is Emacs, the keyseq_timeout_ms is set to -1
// no matter what you may have configured. This is so that key chords
// can be applied without having to do them in a given timeout. So,
// it essentially turns off the keyseq timeout.
}
"auto_add_history" => {
if let Ok(auto_add_history) = value.as_bool() {
rl.set_auto_add_history(auto_add_history);
}
}
"bell_style" => {
let bell_style = match value.as_string() {
Ok(s) if s.to_lowercase() == "audible" => {
rustyline::config::BellStyle::Audible
}
Ok(s) if s.to_lowercase() == "none" => {
rustyline::config::BellStyle::None
}
Ok(s) if s.to_lowercase() == "visible" => {
rustyline::config::BellStyle::Visible
}
_ => rustyline::config::BellStyle::default(),
};
rl.set_bell_style(bell_style);
}
"color_mode" => {
let color_mode = match value.as_string() {
Ok(s) if s.to_lowercase() == "enabled" => rustyline::ColorMode::Enabled,
Ok(s) if s.to_lowercase() == "forced" => rustyline::ColorMode::Forced,
Ok(s) if s.to_lowercase() == "disabled" => {
rustyline::ColorMode::Disabled
}
_ => rustyline::ColorMode::Enabled,
};
rl.set_color_mode(color_mode);
}
"tab_stop" => {
if let Ok(tab_stop) = value.as_u64() {
rl.set_tab_stop(tab_stop as usize);
}
}
_ => (),
}
}
}
}
(rl, config)
}
/// The entry point for the CLI. Will register all known internal commands, load experimental commands, load plugins, then prepare the prompt and line reader for input. /// The entry point for the CLI. Will register all known internal commands, load experimental commands, load plugins, then prepare the prompt and line reader for input.
pub async fn cli( pub async fn cli(mut context: Context) -> Result<(), Box<dyn Error>> {
mut syncer: EnvironmentSyncer, let mut syncer = EnvironmentSyncer::new();
mut context: Context, let configuration = syncer.get_config();
) -> Result<(), Box<dyn Error>> {
let configuration = nu_data::config::NuConfig::new(); 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) {
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);
});
let _ = run_startup_commands(&mut context, &configuration).await;
let history_path = crate::commands::history::history_path(&configuration); let history_path = crate::commands::history::history_path(&configuration);
let (mut rl, config) = create_rustyline_configuration();
// we are ok if history does not exist
let _ = rl.load_history(&history_path); let _ = rl.load_history(&history_path);
let skip_welcome_message = config let skip_welcome_message = configuration
.get("skip_welcome_message") .var("skip_welcome_message")
.map(|x| x.is_true()) .map(|x| x.is_true())
.unwrap_or(false); .unwrap_or(false);
if !skip_welcome_message { if !skip_welcome_message {
@ -589,44 +349,8 @@ pub async fn cli(
let _ = ansi_term::enable_ansi_support(); let _ = ansi_term::enable_ansi_support();
} }
#[cfg(feature = "ctrlc")]
{
let cc = context.ctrl_c.clone();
ctrlc::set_handler(move || {
cc.store(true, Ordering::SeqCst);
})
.expect("Error setting Ctrl-C handler");
}
let mut ctrlcbreak = false; let mut ctrlcbreak = false;
// before we start up, let's run our startup commands
if let Ok(config) = nu_data::config::config(Tag::unknown()) {
if let Some(commands) = config.get("startup") {
match commands {
Value {
value: UntaggedValue::Table(pipelines),
..
} => {
for pipeline in pipelines {
if let Ok(pipeline_string) = pipeline.as_string() {
let _ = run_pipeline_standalone(
pipeline_string,
false,
&mut context,
false,
)
.await;
}
}
}
_ => {
println!("warning: expected a table of pipeline strings as startup commands");
}
}
}
}
loop { loop {
if context.ctrl_c.load(Ordering::SeqCst) { if context.ctrl_c.load(Ordering::SeqCst) {
context.ctrl_c.store(false, Ordering::SeqCst); context.ctrl_c.store(false, Ordering::SeqCst);
@ -635,12 +359,8 @@ pub async fn cli(
let cwd = context.shell_manager.path(); let cwd = context.shell_manager.path();
let hinter = init_hinter(&config);
rl.set_helper(Some(crate::shell::Helper::new(context.clone(), hinter)));
let colored_prompt = { let colored_prompt = {
if let Some(prompt) = config.get("prompt") { if let Some(prompt) = configuration.var("prompt") {
let prompt_line = prompt.as_string()?; let prompt_line = prompt.as_string()?;
match nu_parser::lite_parse(&prompt_line, 0).map_err(ShellError::from) { match nu_parser::lite_parse(&prompt_line, 0).map_err(ShellError::from) {
@ -729,9 +449,20 @@ pub async fn cli(
// Check the config to see if we need to update the path // 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 // 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 // FIXME: we probably want to be a bit more graceful if we can't set the environment
syncer.reload();
syncer.sync_env_vars(&mut context); context.configure(&configuration, |config, ctx| {
syncer.sync_path_vars(&mut context); if syncer.did_config_change() {
syncer.reload();
syncer.sync_env_vars(ctx);
syncer.sync_path_vars(ctx);
}
if let Err(reason) = syncer.autoenv(ctx) {
print_err(reason, &Text::from(""));
}
let _ = configure_rustyline_editor(&mut rl, config);
});
match line { match line {
LineResult::Success(line) => { LineResult::Success(line) => {
@ -784,15 +515,279 @@ pub async fn cli(
Ok(()) Ok(())
} }
fn init_hinter(config: &IndexMap<String, Value>) -> Option<rustyline::hint::HistoryHinter> { pub fn register_plugins(context: &mut Context) -> Result<(), ShellError> {
// Show hints unless explicitly disabled in config if let Ok(plugins) = crate::plugin::scan(search_paths()) {
if let Some(line_editor_vars) = config.get("line_editor") { context.add_commands(
plugins
.into_iter()
.filter(|p| !context.is_command_registered(p.name()))
.collect(),
);
}
Ok(())
}
fn configure_ctrl_c(_context: &mut Context) -> Result<(), Box<dyn Error>> {
#[cfg(feature = "ctrlc")]
{
let cc = _context.ctrl_c.clone();
ctrlc::set_handler(move || {
cc.store(true, Ordering::SeqCst);
})?;
if _context.ctrl_c.load(Ordering::SeqCst) {
_context.ctrl_c.store(false, Ordering::SeqCst);
}
}
Ok(())
}
async fn run_startup_commands(
context: &mut Context,
config: &dyn nu_data::config::Conf,
) -> Result<(), ShellError> {
if let Some(commands) = config.var("startup") {
match commands {
Value {
value: UntaggedValue::Table(pipelines),
..
} => {
for pipeline in pipelines {
if let Ok(pipeline_string) = pipeline.as_string() {
let _ =
run_pipeline_standalone(pipeline_string, false, context, false).await;
}
}
}
_ => {
return Err(ShellError::untagged_runtime_error(
"expected a table of pipeline strings as startup commands",
))
}
}
}
Ok(())
}
pub async fn run_pipeline_standalone(
pipeline: String,
redirect_stdin: bool,
context: &mut Context,
exit_on_error: bool,
) -> Result<(), Box<dyn Error>> {
let line = process_line(Ok(pipeline), context, redirect_stdin, false).await;
match line {
LineResult::Success(line) => {
let error_code = {
let errors = context.current_errors.clone();
let errors = errors.lock();
if errors.len() > 0 {
1
} else {
0
}
};
context.maybe_print_errors(Text::from(line));
if error_code != 0 && exit_on_error {
std::process::exit(error_code);
}
}
LineResult::Error(line, err) => {
context.with_host(|_host| {
print_err(err, &Text::from(line.clone()));
});
context.maybe_print_errors(Text::from(line));
if exit_on_error {
std::process::exit(1);
}
}
_ => {}
}
Ok(())
}
fn default_rustyline_editor_configuration() -> Editor<Helper> {
#[cfg(windows)]
const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::Circular;
#[cfg(not(windows))]
const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::List;
let config = Config::builder().color_mode(ColorMode::Forced).build();
let mut rl: Editor<_> = Editor::with_config(config);
// add key bindings to move over a whole word with Ctrl+ArrowLeft and Ctrl+ArrowRight
rl.bind_sequence(
KeyPress::ControlLeft,
Cmd::Move(Movement::BackwardWord(1, Word::Vi)),
);
rl.bind_sequence(
KeyPress::ControlRight,
Cmd::Move(Movement::ForwardWord(1, At::AfterEnd, Word::Vi)),
);
// Let's set the defaults up front and then override them later if the user indicates
// defaults taken from here https://github.com/kkawakam/rustyline/blob/2fe886c9576c1ea13ca0e5808053ad491a6fe049/src/config.rs#L150-L167
rl.set_max_history_size(100);
rl.set_history_ignore_dups(true);
rl.set_history_ignore_space(false);
rl.set_completion_type(DEFAULT_COMPLETION_MODE);
rl.set_completion_prompt_limit(100);
rl.set_keyseq_timeout(-1);
rl.set_edit_mode(rustyline::config::EditMode::Emacs);
rl.set_auto_add_history(false);
rl.set_bell_style(rustyline::config::BellStyle::default());
rl.set_color_mode(rustyline::ColorMode::Enabled);
rl.set_tab_stop(8);
if let Err(e) = crate::keybinding::load_keybindings(&mut rl) {
println!("Error loading keybindings: {:?}", e);
}
rl
}
fn configure_rustyline_editor(
rl: &mut Editor<Helper>,
config: &dyn nu_data::config::Conf,
) -> Result<(), ShellError> {
#[cfg(windows)]
const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::Circular;
#[cfg(not(windows))]
const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::List;
if let Some(line_editor_vars) = config.var("line_editor") {
for (idx, value) in line_editor_vars.row_entries() {
match idx.as_ref() {
"max_history_size" => {
if let Ok(max_history_size) = value.as_u64() {
rl.set_max_history_size(max_history_size as usize);
}
}
"history_duplicates" => {
// history_duplicates = match value.as_string() {
// Ok(s) if s.to_lowercase() == "alwaysadd" => {
// rustyline::config::HistoryDuplicates::AlwaysAdd
// }
// Ok(s) if s.to_lowercase() == "ignoreconsecutive" => {
// rustyline::config::HistoryDuplicates::IgnoreConsecutive
// }
// _ => rustyline::config::HistoryDuplicates::AlwaysAdd,
// };
if let Ok(history_duplicates) = value.as_bool() {
rl.set_history_ignore_dups(history_duplicates);
}
}
"history_ignore_space" => {
if let Ok(history_ignore_space) = value.as_bool() {
rl.set_history_ignore_space(history_ignore_space);
}
}
"completion_type" => {
let completion_type = match value.as_string() {
Ok(s) if s.to_lowercase() == "circular" => {
rustyline::config::CompletionType::Circular
}
Ok(s) if s.to_lowercase() == "list" => {
rustyline::config::CompletionType::List
}
#[cfg(all(unix, feature = "with-fuzzy"))]
Ok(s) if s.to_lowercase() == "fuzzy" => {
rustyline::config::CompletionType::Fuzzy
}
_ => DEFAULT_COMPLETION_MODE,
};
rl.set_completion_type(completion_type);
}
"completion_prompt_limit" => {
if let Ok(completion_prompt_limit) = value.as_u64() {
rl.set_completion_prompt_limit(completion_prompt_limit as usize);
}
}
"keyseq_timeout_ms" => {
if let Ok(keyseq_timeout_ms) = value.as_u64() {
rl.set_keyseq_timeout(keyseq_timeout_ms as i32);
}
}
"edit_mode" => {
let edit_mode = match value.as_string() {
Ok(s) if s.to_lowercase() == "vi" => rustyline::config::EditMode::Vi,
Ok(s) if s.to_lowercase() == "emacs" => rustyline::config::EditMode::Emacs,
_ => rustyline::config::EditMode::Emacs,
};
rl.set_edit_mode(edit_mode);
// Note: When edit_mode is Emacs, the keyseq_timeout_ms is set to -1
// no matter what you may have configured. This is so that key chords
// can be applied without having to do them in a given timeout. So,
// it essentially turns off the keyseq timeout.
}
"auto_add_history" => {
if let Ok(auto_add_history) = value.as_bool() {
rl.set_auto_add_history(auto_add_history);
}
}
"bell_style" => {
let bell_style = match value.as_string() {
Ok(s) if s.to_lowercase() == "audible" => {
rustyline::config::BellStyle::Audible
}
Ok(s) if s.to_lowercase() == "none" => rustyline::config::BellStyle::None,
Ok(s) if s.to_lowercase() == "visible" => {
rustyline::config::BellStyle::Visible
}
_ => rustyline::config::BellStyle::default(),
};
rl.set_bell_style(bell_style);
}
"color_mode" => {
let color_mode = match value.as_string() {
Ok(s) if s.to_lowercase() == "enabled" => rustyline::ColorMode::Enabled,
Ok(s) if s.to_lowercase() == "forced" => rustyline::ColorMode::Forced,
Ok(s) if s.to_lowercase() == "disabled" => rustyline::ColorMode::Disabled,
_ => rustyline::ColorMode::Enabled,
};
rl.set_color_mode(color_mode);
}
"tab_stop" => {
if let Ok(tab_stop) = value.as_u64() {
rl.set_tab_stop(tab_stop as usize);
}
}
_ => (),
}
}
}
Ok(())
}
fn nu_line_editor_helper(
context: &mut Context,
config: &dyn nu_data::config::Conf,
) -> crate::shell::Helper {
let hinter = rustyline_hinter(config);
crate::shell::Helper::new(context.clone(), hinter)
}
fn rustyline_hinter(config: &dyn nu_data::config::Conf) -> Option<rustyline::hint::HistoryHinter> {
if let Some(line_editor_vars) = config.var("line_editor") {
for (idx, value) in line_editor_vars.row_entries() { for (idx, value) in line_editor_vars.row_entries() {
if idx == "show_hints" && value.expect_string() == "false" { if idx == "show_hints" && value.expect_string() == "false" {
return None; return None;
} }
} }
} }
Some(rustyline::hint::HistoryHinter {}) Some(rustyline::hint::HistoryHinter {})
} }

View File

@ -37,7 +37,7 @@ pub trait ConfigExtensions: Debug + Send {
} }
pub fn pivot_mode(config: &NuConfig) -> AutoPivotMode { pub fn pivot_mode(config: &NuConfig) -> AutoPivotMode {
let vars = config.vars.lock(); let vars = &config.vars;
if let Some(mode) = vars.get("pivot_mode") { if let Some(mode) = vars.get("pivot_mode") {
let mode = match mode.as_string() { let mode = match mode.as_string() {

View File

@ -1,6 +1,6 @@
use crate::commands::WholeStreamCommand; use crate::commands::WholeStreamCommand;
use crate::prelude::*; use crate::prelude::*;
use nu_data::config::NuConfig; use nu_data::config::{Conf, NuConfig};
use nu_errors::ShellError; use nu_errors::ShellError;
use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; use nu_protocol::{ReturnSuccess, Signature, UntaggedValue};
use std::fs::File; use std::fs::File;
@ -9,9 +9,7 @@ use std::path::PathBuf;
const DEFAULT_LOCATION: &str = "history.txt"; const DEFAULT_LOCATION: &str = "history.txt";
pub fn history_path(config: &NuConfig) -> PathBuf { pub fn history_path(config: &dyn Conf) -> PathBuf {
let vars = config.vars.lock();
let default_path = nu_data::config::user_data() let default_path = nu_data::config::user_data()
.map(|mut p| { .map(|mut p| {
p.push(DEFAULT_LOCATION); p.push(DEFAULT_LOCATION);
@ -19,7 +17,8 @@ pub fn history_path(config: &NuConfig) -> PathBuf {
}) })
.unwrap_or_else(|_| PathBuf::from(DEFAULT_LOCATION)); .unwrap_or_else(|_| PathBuf::from(DEFAULT_LOCATION));
vars.get("history-path") config
.var("history-path")
.map_or(default_path.clone(), |custom_path| { .map_or(default_path.clone(), |custom_path| {
match custom_path.as_string() { match custom_path.as_string() {
Ok(path) => PathBuf::from(path), Ok(path) => PathBuf::from(path),
@ -54,7 +53,7 @@ impl WholeStreamCommand for History {
} }
fn history(args: CommandArgs, _registry: &CommandRegistry) -> Result<OutputStream, ShellError> { fn history(args: CommandArgs, _registry: &CommandRegistry) -> Result<OutputStream, ShellError> {
let config = NuConfig::new(); let config: Box<dyn Conf> = Box::new(NuConfig::new());
let tag = args.call_info.name_tag; let tag = args.call_info.name_tag;
let path = history_path(&config); let path = history_path(&config);
let file = File::open(path); let file = File::open(path);

View File

@ -27,7 +27,7 @@ pub fn header_alignment_from_value(align_value: Option<&Value>) -> nu_table::Ali
} }
pub fn get_color_from_key_and_subkey(config: &NuConfig, key: &str, subkey: &str) -> Option<Value> { pub fn get_color_from_key_and_subkey(config: &NuConfig, key: &str, subkey: &str) -> Option<Value> {
let vars = config.vars.lock(); let vars = &config.vars;
if let Some(config_vars) = vars.get(key) { if let Some(config_vars) = vars.get(key) {
for (kee, value) in config_vars.row_entries() { for (kee, value) in config_vars.row_entries() {
@ -47,7 +47,7 @@ pub fn header_bold_from_value(bold_value: Option<&Value>) -> bool {
} }
pub fn table_mode(config: &NuConfig) -> nu_table::Theme { pub fn table_mode(config: &NuConfig) -> nu_table::Theme {
let vars = config.vars.lock(); let vars = &config.vars;
vars.get("table_mode") vars.get("table_mode")
.map_or(nu_table::Theme::compact(), |mode| match mode.as_string() { .map_or(nu_table::Theme::compact(), |mode| match mode.as_string() {
@ -62,7 +62,7 @@ pub fn table_mode(config: &NuConfig) -> nu_table::Theme {
} }
pub fn disabled_indexes(config: &NuConfig) -> bool { pub fn disabled_indexes(config: &NuConfig) -> bool {
let vars = config.vars.lock(); let vars = &config.vars;
vars.get("disable_table_indexes") vars.get("disable_table_indexes")
.map_or(false, |x| x.as_bool().unwrap_or(false)) .map_or(false, |x| x.as_bool().unwrap_or(false))

View File

@ -210,6 +210,14 @@ impl Context {
} }
} }
pub(crate) fn configure<T>(
&mut self,
config: &dyn nu_data::config::Conf,
block: impl FnOnce(&dyn nu_data::config::Conf, &mut Self) -> T,
) {
block(config, &mut *self);
}
pub(crate) fn with_host<T>(&mut self, block: impl FnOnce(&mut dyn Host) -> T) -> T { pub(crate) fn with_host<T>(&mut self, block: impl FnOnce(&mut dyn Host) -> T) -> T {
let mut host = self.host.lock(); let mut host = self.host.lock();

View File

@ -1,14 +1,13 @@
use crate::context::Context; use crate::context::Context;
use nu_data::config::{Conf, NuConfig};
use crate::env::environment::{Env, Environment}; use crate::env::environment::{Env, Environment};
use nu_source::Text; use nu_data::config::{Conf, NuConfig};
use nu_errors::ShellError;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::sync::Arc; use std::sync::Arc;
pub struct EnvironmentSyncer { pub struct EnvironmentSyncer {
pub env: Arc<Mutex<Box<Environment>>>, pub env: Arc<Mutex<Box<Environment>>>,
pub config: Arc<Box<dyn Conf>>, pub config: Arc<Mutex<Box<dyn Conf>>>,
} }
impl Default for EnvironmentSyncer { impl Default for EnvironmentSyncer {
@ -18,42 +17,60 @@ impl Default for EnvironmentSyncer {
} }
impl EnvironmentSyncer { impl EnvironmentSyncer {
pub fn with_config(config: Box<dyn Conf>) -> Self {
EnvironmentSyncer {
env: Arc::new(Mutex::new(Box::new(Environment::new()))),
config: Arc::new(Mutex::new(config)),
}
}
pub fn new() -> EnvironmentSyncer { pub fn new() -> EnvironmentSyncer {
EnvironmentSyncer { EnvironmentSyncer {
env: Arc::new(Mutex::new(Box::new(Environment::new()))), env: Arc::new(Mutex::new(Box::new(Environment::new()))),
config: Arc::new(Box::new(NuConfig::new())), config: Arc::new(Mutex::new(Box::new(NuConfig::new()))),
} }
} }
#[cfg(test)] #[cfg(test)]
pub fn set_config(&mut self, config: Box<dyn Conf>) { pub fn set_config(&mut self, config: Box<dyn Conf>) {
self.config = Arc::new(config); self.config = Arc::new(Mutex::new(config));
} }
pub fn get_config(&self) -> Box<dyn Conf> { pub fn get_config(&self) -> Box<dyn Conf> {
self.config.clone().clone_box() let config = self.config.lock();
config.clone_box()
} }
pub fn load_environment(&mut self) { pub fn load_environment(&mut self) {
let config = self.config.clone(); let config = self.config.lock();
self.env = Arc::new(Mutex::new(Box::new(Environment::from_config(&*config)))); 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_else(|_| false)
}
pub fn reload(&mut self) { pub fn reload(&mut self) {
self.config.reload(); let mut config = self.config.lock();
config.reload();
let mut environment = self.env.lock(); let mut environment = self.env.lock();
environment.morph(&*self.config); environment.morph(&*config);
}
pub fn autoenv(&self, ctx: &mut Context) -> Result<(), ShellError> {
let mut environment = self.env.lock();
let auto = environment.autoenv(ctx.user_recently_used_autoenv_untrust);
ctx.user_recently_used_autoenv_untrust = false;
auto
} }
pub fn sync_env_vars(&mut self, ctx: &mut Context) { pub fn sync_env_vars(&mut self, ctx: &mut Context) {
let mut environment = self.env.lock(); let mut environment = self.env.lock();
if let Err(e) = environment.autoenv(ctx.user_recently_used_autoenv_untrust) {
crate::cli::print_err(e, &Text::from(""));
}
ctx.user_recently_used_autoenv_untrust = false;
if environment.env().is_some() { if environment.env().is_some() {
for (name, value) in ctx.with_host(|host| host.vars()) { for (name, value) in ctx.with_host(|host| host.vars()) {
if name != "path" && name != "PATH" { if name != "path" && name != "PATH" {
@ -142,6 +159,113 @@ mod tests {
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; 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 = Context::basic()?;
ctx.host = Arc::new(Mutex::new(Box::new(crate::env::host::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.clone()));
// 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] #[test]
fn syncs_env_if_new_env_entry_in_session_is_not_in_configuration_file() -> Result<(), ShellError> fn syncs_env_if_new_env_entry_in_session_is_not_in_configuration_file() -> Result<(), ShellError>
{ {

View File

@ -47,7 +47,8 @@ pub fn nu(env: &IndexMap<String, String>, tag: impl Into<Tag>) -> Result<Value,
UntaggedValue::path(keybinding_path).into_value(&tag), UntaggedValue::path(keybinding_path).into_value(&tag),
); );
let history = crate::commands::history::history_path(&nu_data::config::NuConfig::new()); let config: Box<dyn nu_data::config::Conf> = Box::new(nu_data::config::NuConfig::new());
let history = crate::commands::history::history_path(&config);
nu_dict.insert_value( nu_dict.insert_value(
"history-path", "history-path",
UntaggedValue::path(history).into_value(&tag), UntaggedValue::path(history).into_value(&tag),

View File

@ -204,6 +204,33 @@ pub fn user_data() -> Result<PathBuf, ShellError> {
Ok(std::path::PathBuf::from("/")) Ok(std::path::PathBuf::from("/"))
} }
#[derive(Debug, Clone)]
pub enum Status {
LastModified(std::time::SystemTime),
Unavailable,
}
impl Default for Status {
fn default() -> Self {
Status::Unavailable
}
}
pub fn last_modified(at: &Option<PathBuf>) -> Result<Status, Box<dyn std::error::Error>> {
let filename = default_path()?;
let filename = match at {
None => filename,
Some(ref file) => file.clone(),
};
if let Ok(time) = filename.metadata()?.modified() {
return Ok(Status::LastModified(time));
}
Ok(Status::Unavailable)
}
pub fn read( pub fn read(
tag: impl Into<Tag>, tag: impl Into<Tag>,
at: &Option<PathBuf>, at: &Option<PathBuf>,

View File

@ -2,13 +2,23 @@ use nu_protocol::Value;
use std::fmt::Debug; use std::fmt::Debug;
pub trait Conf: Debug + Send { pub trait Conf: Debug + Send {
fn is_modified(&self) -> Result<bool, Box<dyn std::error::Error>>;
fn var(&self, key: &str) -> Option<Value>;
fn env(&self) -> Option<Value>; fn env(&self) -> Option<Value>;
fn path(&self) -> Option<Value>; fn path(&self) -> Option<Value>;
fn reload(&self); fn reload(&mut self);
fn clone_box(&self) -> Box<dyn Conf>; fn clone_box(&self) -> Box<dyn Conf>;
} }
impl Conf for Box<dyn Conf> { impl Conf for Box<dyn Conf> {
fn is_modified(&self) -> Result<bool, Box<dyn std::error::Error>> {
(**self).is_modified()
}
fn var(&self, key: &str) -> Option<Value> {
(**self).var(key)
}
fn env(&self) -> Option<Value> { fn env(&self) -> Option<Value> {
(**self).env() (**self).env()
} }
@ -17,7 +27,7 @@ impl Conf for Box<dyn Conf> {
(**self).path() (**self).path()
} }
fn reload(&self) { fn reload(&mut self) {
(**self).reload(); (**self).reload();
} }

View File

@ -1,17 +1,24 @@
use crate::config::{read, Conf}; use crate::config::{last_modified, read, Conf, Status};
use indexmap::IndexMap; use indexmap::IndexMap;
use nu_protocol::Value; use nu_protocol::Value;
use nu_source::Tag; use nu_source::Tag;
use parking_lot::Mutex;
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::Arc;
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct NuConfig { pub struct NuConfig {
pub vars: Arc<Mutex<IndexMap<String, Value>>>, pub vars: IndexMap<String, Value>,
pub modified_at: Status,
} }
impl Conf for NuConfig { impl Conf for NuConfig {
fn is_modified(&self) -> Result<bool, Box<dyn std::error::Error>> {
self.is_modified()
}
fn var(&self, key: &str) -> Option<Value> {
self.var(key)
}
fn env(&self) -> Option<Value> { fn env(&self) -> Option<Value> {
self.env() self.env()
} }
@ -20,11 +27,17 @@ impl Conf for NuConfig {
self.path() self.path()
} }
fn reload(&self) { fn reload(&mut self) {
let mut vars = self.vars.lock(); let vars = &mut self.vars;
if let Ok(variables) = read(Tag::unknown(), &None) { if let Ok(variables) = read(Tag::unknown(), &None) {
vars.extend(variables); vars.extend(variables);
self.modified_at = if let Ok(status) = last_modified(&None) {
status
} else {
Status::Unavailable
};
} }
} }
@ -34,6 +47,24 @@ impl Conf for NuConfig {
} }
impl NuConfig { impl NuConfig {
pub fn with(config_file: Option<std::path::PathBuf>) -> NuConfig {
match &config_file {
None => NuConfig::new(),
Some(_) => {
let vars = if let Ok(variables) = read(Tag::unknown(), &config_file) {
variables
} else {
IndexMap::default()
};
NuConfig {
vars,
modified_at: NuConfig::get_last_modified(&config_file),
}
}
}
}
pub fn new() -> NuConfig { pub fn new() -> NuConfig {
let vars = if let Ok(variables) = read(Tag::unknown(), &None) { let vars = if let Ok(variables) = read(Tag::unknown(), &None) {
variables variables
@ -42,12 +73,45 @@ impl NuConfig {
}; };
NuConfig { NuConfig {
vars: Arc::new(Mutex::new(vars)), vars,
modified_at: NuConfig::get_last_modified(&None),
} }
} }
pub fn get_last_modified(config_file: &Option<std::path::PathBuf>) -> Status {
if let Ok(status) = last_modified(config_file) {
status
} else {
Status::Unavailable
}
}
pub fn is_modified(&self) -> Result<bool, Box<dyn std::error::Error>> {
let modified_at = &self.modified_at;
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,
})
}
pub fn var(&self, key: &str) -> Option<Value> {
let vars = &self.vars;
if let Some(value) = vars.get(key) {
return Some(value.clone());
}
None
}
pub fn env(&self) -> Option<Value> { pub fn env(&self) -> Option<Value> {
let vars = self.vars.lock(); let vars = &self.vars;
if let Some(env_vars) = vars.get("env") { if let Some(env_vars) = vars.get("env") {
return Some(env_vars.clone()); return Some(env_vars.clone());
@ -57,7 +121,7 @@ impl NuConfig {
} }
pub fn path(&self) -> Option<Value> { pub fn path(&self) -> Option<Value> {
let vars = self.vars.lock(); let vars = &self.vars;
if let Some(env_vars) = vars.get("path") { if let Some(env_vars) = vars.get("path") {
return Some(env_vars.clone()); return Some(env_vars.clone());

View File

@ -1,17 +1,22 @@
use crate::config::{read, Conf, NuConfig}; use crate::config::{Conf, NuConfig, Status};
use indexmap::IndexMap;
use nu_protocol::Value; use nu_protocol::Value;
use nu_source::Tag;
use parking_lot::Mutex;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct FakeConfig { pub struct FakeConfig {
pub config: NuConfig, pub config: NuConfig,
source_file: Option<PathBuf>,
} }
impl Conf for FakeConfig { impl Conf for FakeConfig {
fn is_modified(&self) -> Result<bool, Box<dyn std::error::Error>> {
self.is_modified()
}
fn var(&self, key: &str) -> Option<Value> {
self.config.var(key)
}
fn env(&self) -> Option<Value> { fn env(&self) -> Option<Value> {
self.config.env() self.config.env()
} }
@ -20,8 +25,8 @@ impl Conf for FakeConfig {
self.config.path() self.config.path()
} }
fn reload(&self) { fn reload(&mut self) {
// no-op self.reload()
} }
fn clone_box(&self) -> Box<dyn Conf> { fn clone_box(&self) -> Box<dyn Conf> {
@ -31,18 +36,31 @@ impl Conf for FakeConfig {
impl FakeConfig { impl FakeConfig {
pub fn new(config_file: &Path) -> FakeConfig { pub fn new(config_file: &Path) -> FakeConfig {
let config_file = PathBuf::from(config_file); let config_file = Some(PathBuf::from(config_file));
let vars = if let Ok(variables) = read(Tag::unknown(), &Some(config_file)) {
variables
} else {
IndexMap::default()
};
FakeConfig { FakeConfig {
config: NuConfig { config: NuConfig::with(config_file.clone()),
vars: Arc::new(Mutex::new(vars)), source_file: config_file,
},
} }
} }
pub fn is_modified(&self) -> Result<bool, Box<dyn std::error::Error>> {
let modified_at = &self.config.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)?;
left != right
}
(_, _) => false,
},
)
}
pub fn reload(&mut self) {
self.config = NuConfig::with(self.source_file.clone());
}
} }

View File

@ -1,7 +1,7 @@
use clap::{App, Arg}; use clap::{App, Arg};
use log::LevelFilter; use log::LevelFilter;
use nu_cli::create_default_context;
use nu_cli::utils::test_bins as binaries; use nu_cli::utils::test_bins as binaries;
use nu_cli::{create_default_context, EnvironmentSyncer};
use std::error::Error; use std::error::Error;
use std::fs::File; use std::fs::File;
use std::io::{prelude::*, BufReader}; use std::io::{prelude::*, BufReader};
@ -160,14 +160,13 @@ fn main() -> Result<(), Box<dyn Error>> {
} }
None => { None => {
let mut syncer = EnvironmentSyncer::new(); let mut context = create_default_context(true)?;
let mut context = create_default_context(&mut syncer, true)?;
if !matches.is_present("skip-plugins") { if !matches.is_present("skip-plugins") {
let _ = nu_cli::register_plugins(&mut context); let _ = nu_cli::register_plugins(&mut context);
} }
futures::executor::block_on(nu_cli::cli(syncer, context))?; futures::executor::block_on(nu_cli::cli(context))?;
} }
} }