diff --git a/crates/nu-cli/src/commands/default_context.rs b/crates/nu-cli/src/commands/default_context.rs index 0c1c459c20..d4194eb10d 100644 --- a/crates/nu-cli/src/commands/default_context.rs +++ b/crates/nu-cli/src/commands/default_context.rs @@ -18,6 +18,7 @@ pub fn add_cli_context(mut engine_state: EngineState) -> EngineState { CommandlineSetCursor, History, HistorySession, + HistoryImport, Keybindings, KeybindingsDefault, KeybindingsList, diff --git a/crates/nu-cli/src/commands/history/fields.rs b/crates/nu-cli/src/commands/history/fields.rs new file mode 100644 index 0000000000..4207b1409a --- /dev/null +++ b/crates/nu-cli/src/commands/history/fields.rs @@ -0,0 +1,10 @@ +// Each const is named after a HistoryItem field, and the value is the field name to be displayed to +// the user (or accept during import). +pub const COMMAND_LINE: &str = "command"; +pub const ID: &str = "item_id"; +pub const START_TIMESTAMP: &str = "start_timestamp"; +pub const HOSTNAME: &str = "hostname"; +pub const CWD: &str = "cwd"; +pub const EXIT_STATUS: &str = "exist_status"; +pub const DURATION: &str = "duration"; +pub const SESSION_ID: &str = "session_id"; diff --git a/crates/nu-cli/src/commands/history/history_.rs b/crates/nu-cli/src/commands/history/history_.rs index cdf85eea72..5a600f6da1 100644 --- a/crates/nu-cli/src/commands/history/history_.rs +++ b/crates/nu-cli/src/commands/history/history_.rs @@ -5,6 +5,8 @@ use reedline::{ SqliteBackedHistory, }; +use crate::{commands::history::fields, config_files::get_history_path}; + #[derive(Clone)] pub struct History; @@ -44,89 +46,78 @@ impl Command for History { }; // todo for sqlite history this command should be an alias to `open ~/.config/nushell/history.sqlite3 | get history` - if let Some(config_path) = nu_path::config_dir() { - let clear = call.has_flag(engine_state, stack, "clear")?; - let long = call.has_flag(engine_state, stack, "long")?; - let signals = engine_state.signals().clone(); + let Some(history_path) = get_history_path(history.file_format) else { + return Err(ShellError::ConfigDirNotFound { span: Some(head) }); + }; + let clear = call.has_flag(engine_state, stack, "clear")?; + let long = call.has_flag(engine_state, stack, "long")?; + let signals = engine_state.signals().clone(); - let mut history_path = config_path; - history_path.push("nushell"); - match history.file_format { - HistoryFileFormat::Sqlite => { - history_path.push("history.sqlite3"); - } - HistoryFileFormat::PlainText => { - history_path.push("history.txt"); - } - } - - if clear { - let _ = std::fs::remove_file(history_path); - // TODO: FIXME also clear the auxiliary files when using sqlite - Ok(PipelineData::empty()) - } else { - let history_reader: Option> = match history.file_format { - HistoryFileFormat::Sqlite => { - SqliteBackedHistory::with_file(history_path.clone(), None, None) - .map(|inner| { - let boxed: Box = Box::new(inner); - boxed - }) - .ok() - } - - HistoryFileFormat::PlainText => FileBackedHistory::with_file( - history.max_size as usize, - history_path.clone(), - ) - .map(|inner| { - let boxed: Box = Box::new(inner); - boxed - }) - .ok(), - }; - - match history.file_format { - HistoryFileFormat::PlainText => Ok(history_reader - .and_then(|h| { - h.search(SearchQuery::everything(SearchDirection::Forward, None)) - .ok() - }) - .map(move |entries| { - entries.into_iter().enumerate().map(move |(idx, entry)| { - Value::record( - record! { - "command" => Value::string(entry.command_line, head), - "index" => Value::int(idx as i64, head), - }, - head, - ) - }) - }) - .ok_or(ShellError::FileNotFound { - file: history_path.display().to_string(), - span: head, - })? - .into_pipeline_data(head, signals)), - HistoryFileFormat::Sqlite => Ok(history_reader - .and_then(|h| { - h.search(SearchQuery::everything(SearchDirection::Forward, None)) - .ok() - }) - .map(move |entries| { - entries.into_iter().enumerate().map(move |(idx, entry)| { - create_history_record(idx, entry, long, head) - }) - }) - .ok_or(ShellError::FileNotFound { - file: history_path.display().to_string(), - span: head, - })? - .into_pipeline_data(head, signals)), - } - } + if clear { + let _ = std::fs::remove_file(history_path); + // TODO: FIXME also clear the auxiliary files when using sqlite + Ok(PipelineData::empty()) } else { - Err(ShellError::ConfigDirNotFound { span: Some(head) }) + let history_reader: Option> = match history.file_format { + HistoryFileFormat::Sqlite => { + SqliteBackedHistory::with_file(history_path.clone(), None, None) + .map(|inner| { + let boxed: Box = Box::new(inner); + boxed + }) + .ok() + } + + HistoryFileFormat::PlainText => { + FileBackedHistory::with_file(history.max_size as usize, history_path.clone()) + .map(|inner| { + let boxed: Box = Box::new(inner); + boxed + }) + .ok() + } + }; + + match history.file_format { + HistoryFileFormat::PlainText => Ok(history_reader + .and_then(|h| { + h.search(SearchQuery::everything(SearchDirection::Forward, None)) + .ok() + }) + .map(move |entries| { + entries.into_iter().enumerate().map(move |(idx, entry)| { + Value::record( + record! { + fields::COMMAND_LINE => Value::string(entry.command_line, head), + // TODO: This name is inconsistent with create_history_record. + "index" => Value::int(idx as i64, head), + }, + head, + ) + }) + }) + .ok_or(ShellError::FileNotFound { + file: history_path.display().to_string(), + span: head, + })? + .into_pipeline_data(head, signals)), + HistoryFileFormat::Sqlite => Ok(history_reader + .and_then(|h| { + h.search(SearchQuery::everything(SearchDirection::Forward, None)) + .ok() + }) + .map(move |entries| { + entries + .into_iter() + .enumerate() + .map(move |(idx, entry)| create_history_record(idx, entry, long, head)) + }) + .ok_or(ShellError::FileNotFound { + file: history_path.display().to_string(), + span: head, + })? + .into_pipeline_data(head, signals)), + } } } @@ -215,14 +206,14 @@ fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span) if long { Value::record( record! { - "item_id" => item_id_value, - "start_timestamp" => start_timestamp_value, - "command" => command_value, - "session_id" => session_id_value, - "hostname" => hostname_value, - "cwd" => cwd_value, - "duration" => duration_value, - "exit_status" => exit_status_value, + fields::ID => item_id_value, + fields::START_TIMESTAMP => start_timestamp_value, + fields::COMMAND_LINE => command_value, + fields::SESSION_ID => session_id_value, + fields::HOSTNAME => hostname_value, + fields::CWD => cwd_value, + fields::DURATION => duration_value, + fields::EXIT_STATUS => exit_status_value, "idx" => index_value, }, head, @@ -230,11 +221,11 @@ fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span) } else { Value::record( record! { - "start_timestamp" => start_timestamp_value, - "command" => command_value, - "cwd" => cwd_value, - "duration" => duration_value, - "exit_status" => exit_status_value, + fields::START_TIMESTAMP => start_timestamp_value, + fields::COMMAND_LINE => command_value, + fields::CWD => cwd_value, + fields::DURATION => duration_value, + fields::EXIT_STATUS => exit_status_value, }, head, ) diff --git a/crates/nu-cli/src/commands/history/history_import.rs b/crates/nu-cli/src/commands/history/history_import.rs new file mode 100644 index 0000000000..1abf5bea35 --- /dev/null +++ b/crates/nu-cli/src/commands/history/history_import.rs @@ -0,0 +1,318 @@ +use nu_engine::command_prelude::*; +use nu_protocol::HistoryFileFormat; + +use reedline::{ + FileBackedHistory, History, HistoryItem, HistoryItemId, ReedlineError, SearchQuery, + SqliteBackedHistory, +}; + +use crate::{ + commands::history::fields, + config_files::{HISTORY_FILE_SQLITE, HISTORY_FILE_TXT}, +}; + +#[derive(Clone)] +pub struct HistoryImport; + +impl Command for HistoryImport { + fn name(&self) -> &str { + "history import" + } + + fn usage(&self) -> &str { + "Import command line history" + } + + fn extra_usage(&self) -> &str { + r#"Can import history from input, either successive command lines or more detailed records. If providing records, available fields are: + command_line, id, start_timestamp, hostname, cwd, duration, exit_status. + +If no input is provided, will import all history items from existing history in the other format: if current history is stored in sqlite, it will store it in plain text and vice versa."# + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("history import") + .category(Category::History) + .input_output_types(vec![ + (Type::Nothing, Type::Nothing), + (Type::List(Box::new(Type::String)), Type::Nothing), + (Type::table(), Type::Nothing), + ]) + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "history import", + description: + "Append all items from history in the other format to the current history", + result: None, + }, + Example { + example: "echo foo | history import", + description: "Append `foo` to the current history", + result: None, + }, + Example { + example: "[[ command_line cwd ]; [ foo /home ]] | history import", + description: "Append `foo` ran from `/home` to the current history", + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let ok = Ok(Value::nothing(call.head).into_pipeline_data()); + + let Some(history) = engine_state.history_config() else { + return ok; + }; + let Some(config_path) = nu_path::nu_config_dir() else { + return Err(ShellError::ConfigDirNotFound { + span: Some(call.head), + }); + }; + + let new_sqlite_backend = || { + SqliteBackedHistory::with_file(config_path.join(HISTORY_FILE_SQLITE), None, None) + .map_err(error_from_reedline) + }; + + let new_file_backend = || { + FileBackedHistory::with_file( + history.max_size as usize, + config_path.join(HISTORY_FILE_TXT), + ) + .map_err(error_from_reedline) + }; + + match input { + PipelineData::Empty => { + let mut sqlite = new_sqlite_backend()?; + let mut plaintext = new_file_backend()?; + let (src, dst): (&dyn History, &mut dyn History) = match history.file_format { + HistoryFileFormat::Sqlite => (&plaintext, &mut sqlite), + HistoryFileFormat::PlainText => (&sqlite, &mut plaintext), + }; + + let items = src + .search(SearchQuery::everything( + reedline::SearchDirection::Forward, + None, + )) + .map_err(error_from_reedline)? + .into_iter() + .map(Ok); + import(dst, items) + } + _ => { + let input = input.into_iter().map(item_from_value); + match history.file_format { + HistoryFileFormat::Sqlite => import(&mut new_sqlite_backend()?, input), + HistoryFileFormat::PlainText => import(&mut new_file_backend()?, input), + } + } + }?; + + ok + } +} + +fn import( + dst: &mut dyn History, + src: impl Iterator>, +) -> Result<(), ShellError> { + for item in src { + dst.save(item?).map_err(error_from_reedline)?; + } + Ok(()) +} + +fn error_from_reedline(e: ReedlineError) -> ShellError { + // TODO: Should we add a new ShellError variant? + ShellError::IOError { + msg: format!("reedline error: {e}"), + } +} + +fn item_from_value(v: Value) -> Result { + let span = v.span(); + match v { + Value::Record { val, .. } => item_from_record(val.into_owned(), span), + Value::String { val, .. } => Ok(HistoryItem { + command_line: val, + id: None, + start_timestamp: None, + session_id: None, + hostname: None, + cwd: None, + duration: None, + exit_status: None, + more_info: None, + }), + _ => Err(ShellError::UnsupportedInput { + msg: "Only list and record inputs are supported".to_owned(), + input: v.get_type().to_string(), + msg_span: span, + input_span: span, + }), + } +} + +fn item_from_record(mut rec: Record, span: Span) -> Result { + let cmd = match rec.remove(fields::COMMAND_LINE) { + Some(v) => v.as_str()?.to_owned(), + None => { + return Err(ShellError::NotFound { + span: Span::unknown(), + }) + } + }; + + fn get( + rec: &mut Record, + field: &'static str, + f: impl FnOnce(Value) -> Result, + ) -> Result, ShellError> { + rec.remove(field).map(f).transpose() + } + + let item = HistoryItem { + command_line: cmd, + id: get(&mut rec, fields::ID, |v| { + Ok(HistoryItemId::new(v.as_int()?)) + })?, + start_timestamp: get(&mut rec, fields::START_TIMESTAMP, |v| { + Ok(v.as_date()?.to_utc()) + })?, + hostname: get(&mut rec, fields::HOSTNAME, |v| Ok(v.as_str()?.to_owned()))?, + cwd: get(&mut rec, fields::CWD, |v| Ok(v.as_str()?.to_owned()))?, + exit_status: get(&mut rec, fields::EXIT_STATUS, |v| v.as_i64())?, + duration: get(&mut rec, fields::DURATION, duration_from_value)?, + more_info: None, + // TODO: Currently reedline doesn't let you create session IDs. + session_id: None, + }; + + if !rec.is_empty() { + let cols = rec.columns().map(|s| s.as_str()).collect::>(); + return Err(ShellError::TypeMismatch { + err_message: format!("unsupported column names: {}", cols.join(", ")), + span, + }); + } + Ok(item) +} + +fn duration_from_value(v: Value) -> Result { + chrono::Duration::nanoseconds(v.as_duration()?) + .to_std() + .map_err(|_| ShellError::IOError { + msg: "negative duration not supported".to_string(), + }) +} + +#[cfg(test)] +mod tests { + use chrono::DateTime; + + use super::*; + + #[test] + fn test_item_from_value_string() -> Result<(), ShellError> { + let item = item_from_value(Value::string("foo", Span::unknown()))?; + assert_eq!( + item, + HistoryItem { + command_line: "foo".to_string(), + id: None, + start_timestamp: None, + session_id: None, + hostname: None, + cwd: None, + duration: None, + exit_status: None, + more_info: None + } + ); + Ok(()) + } + + #[test] + fn test_item_from_value_record() { + let span = Span::unknown(); + let rec = new_record(&[ + ("command_line", Value::string("foo", span)), + ("id", Value::int(1, span)), + ( + "start_timestamp", + Value::date( + DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00").unwrap(), + span, + ), + ), + ("hostname", Value::string("localhost", span)), + ("cwd", Value::string("/home/test", span)), + ("duration", Value::duration(100_000_000, span)), + ("exit_status", Value::int(42, span)), + ]); + let item = item_from_value(rec).unwrap(); + assert_eq!( + item, + HistoryItem { + command_line: "foo".to_string(), + id: Some(HistoryItemId::new(1)), + start_timestamp: Some( + DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00") + .unwrap() + .to_utc() + ), + hostname: Some("localhost".to_string()), + cwd: Some("/home/test".to_string()), + duration: Some(std::time::Duration::from_nanos(100_000_000)), + exit_status: Some(42), + + session_id: None, + more_info: None + } + ); + } + + #[test] + fn test_item_from_value_record_extra_field() { + let span = Span::unknown(); + let rec = new_record(&[ + ("command_line", Value::string("foo", span)), + ("id_nonexistent", Value::int(1, span)), + ]); + assert!(item_from_value(rec).is_err()); + } + + #[test] + fn test_item_from_value_record_bad_type() { + let span = Span::unknown(); + let rec = new_record(&[ + ("command_line", Value::string("foo", span)), + ("id", Value::string("one".to_string(), span)), + ]); + assert!(item_from_value(rec).is_err()); + } + + fn new_record(rec: &[(&'static str, Value)]) -> Value { + let span = Span::unknown(); + let rec = Record::from_raw_cols_vals( + rec.iter().map(|(col, _)| col.to_string()).collect(), + rec.iter().map(|(_, val)| val.clone()).collect(), + span, + span, + ) + .unwrap(); + Value::record(rec, span) + } +} diff --git a/crates/nu-cli/src/commands/history/mod.rs b/crates/nu-cli/src/commands/history/mod.rs index be7d1fc11f..c36b560307 100644 --- a/crates/nu-cli/src/commands/history/mod.rs +++ b/crates/nu-cli/src/commands/history/mod.rs @@ -1,5 +1,8 @@ +mod fields; mod history_; +mod history_import; mod history_session; pub use history_::History; +pub use history_import::HistoryImport; pub use history_session::HistorySession; diff --git a/crates/nu-cli/src/commands/mod.rs b/crates/nu-cli/src/commands/mod.rs index f63724e95f..4a9dd9ef21 100644 --- a/crates/nu-cli/src/commands/mod.rs +++ b/crates/nu-cli/src/commands/mod.rs @@ -7,7 +7,7 @@ mod keybindings_list; mod keybindings_listen; pub use commandline::{Commandline, CommandlineEdit, CommandlineGetCursor, CommandlineSetCursor}; -pub use history::{History, HistorySession}; +pub use history::{History, HistoryImport, HistorySession}; pub use keybindings::Keybindings; pub use keybindings_default::KeybindingsDefault; pub use keybindings_list::KeybindingsList; diff --git a/crates/nu-cli/src/config_files.rs b/crates/nu-cli/src/config_files.rs index 3a02f75e86..1447b880bd 100644 --- a/crates/nu-cli/src/config_files.rs +++ b/crates/nu-cli/src/config_files.rs @@ -16,8 +16,8 @@ const PLUGIN_FILE: &str = "plugin.msgpackz"; #[cfg(feature = "plugin")] const OLD_PLUGIN_FILE: &str = "plugin.nu"; -const HISTORY_FILE_TXT: &str = "history.txt"; -const HISTORY_FILE_SQLITE: &str = "history.sqlite3"; +pub const HISTORY_FILE_TXT: &str = "history.txt"; +pub const HISTORY_FILE_SQLITE: &str = "history.sqlite3"; #[cfg(feature = "plugin")] pub fn read_plugin_file( @@ -240,9 +240,8 @@ pub fn eval_config_contents( } } -pub(crate) fn get_history_path(storage_path: &str, mode: HistoryFileFormat) -> Option { - nu_path::config_dir().map(|mut history_path| { - history_path.push(storage_path); +pub(crate) fn get_history_path(mode: HistoryFileFormat) -> Option { + nu_path::nu_config_dir().map(|mut history_path| { history_path.push(match mode { HistoryFileFormat::PlainText => HISTORY_FILE_TXT, HistoryFileFormat::Sqlite => HISTORY_FILE_SQLITE, diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index 817950a445..3d3ac2d837 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -52,7 +52,6 @@ use sysinfo::System; pub fn evaluate_repl( engine_state: &mut EngineState, stack: Stack, - nushell_path: &str, prerun_command: Option>, load_std_lib: Option>, entire_start_time: Instant, @@ -99,7 +98,7 @@ pub fn evaluate_repl( unique_stack.add_env_var("LAST_EXIT_CODE".into(), Value::int(0, Span::unknown())); - let mut line_editor = get_line_editor(engine_state, nushell_path, use_color)?; + let mut line_editor = get_line_editor(engine_state, use_color)?; let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4())); if let Some(s) = prerun_command { @@ -200,7 +199,7 @@ pub fn evaluate_repl( } Err(_) => { // line_editor is lost in the error case so reconstruct a new one - line_editor = get_line_editor(engine_state, nushell_path, use_color)?; + line_editor = get_line_editor(engine_state, use_color)?; } } } @@ -208,11 +207,7 @@ pub fn evaluate_repl( Ok(()) } -fn get_line_editor( - engine_state: &mut EngineState, - nushell_path: &str, - use_color: bool, -) -> Result { +fn get_line_editor(engine_state: &mut EngineState, use_color: bool) -> Result { let mut start_time = std::time::Instant::now(); let mut line_editor = Reedline::create(); @@ -223,7 +218,7 @@ fn get_line_editor( if let Some(history) = engine_state.history_config() { start_time = std::time::Instant::now(); - line_editor = setup_history(nushell_path, engine_state, line_editor, history)?; + line_editor = setup_history(engine_state, line_editor, history)?; perf!("setup history", start_time, use_color); } @@ -1037,7 +1032,6 @@ fn flush_engine_state_repl_buffer(engine_state: &mut EngineState, line_editor: & /// Setup history management for Reedline /// fn setup_history( - nushell_path: &str, engine_state: &mut EngineState, line_editor: Reedline, history: HistoryConfig, @@ -1049,7 +1043,7 @@ fn setup_history( None }; - if let Some(path) = crate::config_files::get_history_path(nushell_path, history.file_format) { + if let Some(path) = crate::config_files::get_history_path(history.file_format) { return update_line_editor_history( engine_state, path, @@ -1317,8 +1311,7 @@ fn trailing_slash_looks_like_path() { fn are_session_ids_in_sync() { let engine_state = &mut EngineState::new(); let history = engine_state.history_config().unwrap(); - let history_path = - crate::config_files::get_history_path("nushell", history.file_format).unwrap(); + let history_path = crate::config_files::get_history_path(history.file_format).unwrap(); let line_editor = reedline::Reedline::create(); let history_session_id = reedline::Reedline::create_history_session_id(); let line_editor = update_line_editor_history( diff --git a/crates/nu-cli/tests/commands/history_import.rs b/crates/nu-cli/tests/commands/history_import.rs new file mode 100644 index 0000000000..33e90e4ecd --- /dev/null +++ b/crates/nu-cli/tests/commands/history_import.rs @@ -0,0 +1,161 @@ +use nu_test_support::{nu, Outcome}; +use reedline::{ + FileBackedHistory, History, HistoryItem, HistoryItemId, ReedlineError, SearchQuery, + SqliteBackedHistory, +}; +use tempfile::TempDir; + +struct Test { + cfg_dir: TempDir, +} + +impl Test { + fn new(history_format: &'static str) -> Self { + let cfg_dir = tempfile::Builder::new() + .prefix("history_import_test") + .tempdir() + .unwrap(); + std::fs::create_dir(&cfg_dir).unwrap(); + // Assigning to $env.config.history.file_format seems to work only in startup + // configuration. + std::fs::write( + cfg_dir.path().join("env.nu"), + format!("$env.config.history.file_format = {history_format}"), + ) + .unwrap(); + Self { cfg_dir } + } + + fn nu(&self, cmd: &'static str) -> Outcome { + let env = [( + "XDG_CONFIG_HOME".to_string(), + self.cfg_dir.path().to_str().unwrap().to_string(), + )]; + let env_config = self.cfg_dir.path().join("env.nu"); + nu!(envs: env, env_config: env_config, cmd) + } + + fn open_plaintext(&self) -> Result { + FileBackedHistory::with_file(100, self.cfg_dir.path().join("nushell").join("history.txt")) + } + + fn open_sqlite(&self) -> Result { + SqliteBackedHistory::with_file( + self.cfg_dir.path().join("nushell").join("history.sqlite3"), + None, + None, + ) + } +} + +fn query_all(history: impl History) -> Result, ReedlineError> { + history.search(SearchQuery::everything( + reedline::SearchDirection::Forward, + None, + )) +} + +fn save_all(mut history: impl History, items: Vec) -> Result<(), ReedlineError> { + for item in items { + history.save(item)?; + } + Ok(()) +} + +const EMPTY_ITEM: HistoryItem = HistoryItem { + command_line: String::new(), + id: None, + start_timestamp: None, + session_id: None, + hostname: None, + cwd: None, + duration: None, + exit_status: None, + more_info: None, +}; + +#[test] +fn history_import_pipe() { + let test = Test::new("plaintext"); + let outcome = test.nu("echo bar | history import"); + assert!(outcome.status.success()); + assert_eq!( + query_all(test.open_plaintext().unwrap()).unwrap(), + vec![HistoryItem { + id: Some(HistoryItemId::new(0)), + command_line: "bar".to_string(), + ..EMPTY_ITEM + }] + ); +} + +#[test] +fn history_import_plain_to_sqlite() { + let test = Test::new("sqlite"); + save_all( + test.open_plaintext().unwrap(), + vec![ + HistoryItem { + id: Some(HistoryItemId::new(0)), + command_line: "foo".to_string(), + ..EMPTY_ITEM + }, + HistoryItem { + id: Some(HistoryItemId::new(1)), + command_line: "bar".to_string(), + ..EMPTY_ITEM + }, + ], + ) + .unwrap(); + let outcome = test.nu("history import"); + assert!(outcome.status.success()); + assert_eq!( + query_all(test.open_sqlite().unwrap()).unwrap(), + vec![HistoryItem { + id: Some(HistoryItemId::new(0)), + command_line: "bar".to_string(), + ..EMPTY_ITEM + }] + ); +} + +#[test] +fn history_import_sqlite_to_plain() { + let test = Test::new("plaintext"); + save_all( + test.open_sqlite().unwrap(), + vec![ + HistoryItem { + id: Some(HistoryItemId::new(0)), + command_line: "foo".to_string(), + hostname: Some("host".to_string()), + ..EMPTY_ITEM + }, + HistoryItem { + id: Some(HistoryItemId::new(1)), + command_line: "bar".to_string(), + cwd: Some("/home/test".to_string()), + ..EMPTY_ITEM + }, + ], + ) + .unwrap(); + let outcome = test.nu("history import"); + assert!(outcome.status.success()); + assert_eq!( + query_all(test.open_plaintext().unwrap()).unwrap(), + vec![ + HistoryItem { + id: Some(HistoryItemId::new(0)), + command_line: "foo".to_string(), + ..EMPTY_ITEM + }, + HistoryItem { + id: Some(HistoryItemId::new(1)), + command_line: "bar".to_string(), + ..EMPTY_ITEM + }, + ] + ); +} diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index 087791302e..9eb18e3280 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -1,2 +1,3 @@ +mod history_import; mod keybindings_list; mod nu_highlight; diff --git a/crates/nu-path/src/helpers.rs b/crates/nu-path/src/helpers.rs index a6e35bddfe..141a9af6b2 100644 --- a/crates/nu-path/src/helpers.rs +++ b/crates/nu-path/src/helpers.rs @@ -2,6 +2,11 @@ use omnipath::WinPathExt; use std::path::PathBuf; +#[deprecated( + note = "prefer using nu_config_dir() instead of config_dir() joined with NUSHELL_FOLDER" +)] +pub const NUSHELL_FOLDER: &str = "nushell"; + pub fn home_dir() -> Option { dirs::home_dir() } @@ -34,6 +39,15 @@ pub fn config_dir() -> Option { } } +/// Return the nushell config directory. +pub fn nu_config_dir() -> Option { + config_dir().map(|mut p| { + #[allow(deprecated)] + p.push(NUSHELL_FOLDER); + p + }) +} + pub fn get_canonicalized_path(path: Option) -> Option { let path = path?; Some(canonicalize(&path).unwrap_or(path)) diff --git a/crates/nu-path/src/lib.rs b/crates/nu-path/src/lib.rs index 2cb415264a..9ddba94e3e 100644 --- a/crates/nu-path/src/lib.rs +++ b/crates/nu-path/src/lib.rs @@ -11,7 +11,12 @@ mod trailing_slash; pub use components::components; pub use expansions::{canonicalize_with, expand_path_with, expand_to_real_path, locate_in_dirs}; -pub use helpers::{cache_dir, config_dir, data_dir, get_canonicalized_path, home_dir}; +pub use helpers::{ + cache_dir, config_dir, data_dir, get_canonicalized_path, home_dir, nu_config_dir, +}; pub use path::*; pub use tilde::expand_tilde; pub use trailing_slash::{has_trailing_slash, strip_trailing_slash}; + +#[allow(deprecated)] +pub use helpers::NUSHELL_FOLDER; diff --git a/crates/nu-test-support/src/macros.rs b/crates/nu-test-support/src/macros.rs index 6fd3a8c307..d968dcbcee 100644 --- a/crates/nu-test-support/src/macros.rs +++ b/crates/nu-test-support/src/macros.rs @@ -234,7 +234,7 @@ macro_rules! nu_with_plugins { } use crate::{Outcome, NATIVE_PATH_ENV_VAR}; -use nu_path::{AbsolutePath, AbsolutePathBuf, Path}; +use nu_path::{AbsolutePath, AbsolutePathBuf, Path, PathBuf}; use std::{ ffi::OsStr, process::{Command, Stdio}, @@ -248,6 +248,10 @@ pub struct NuOpts { pub envs: Option>, pub collapse_output: Option, pub use_ir: Option, + // Note: At the time this was added, passing in a file path was more convenient. However, + // passing in file contents seems like a better API - consider this when adding new uses of + // this field. + pub env_config: Option, } pub fn nu_run_test(opts: NuOpts, commands: impl AsRef, with_std: bool) -> Outcome { @@ -278,8 +282,14 @@ pub fn nu_run_test(opts: NuOpts, commands: impl AsRef, with_std: bool) -> O command.envs(envs); } - // Ensure that the user's config doesn't interfere with the tests - command.arg("--no-config-file"); + match opts.env_config { + Some(path) => command.arg("--env-config").arg(path), + // TODO: This seems unnecessary: the code that runs for integration tests + // (run_commands) loads startup configs only if they are specified via flags explicitly or + // the shell is started as logging shell (which it is not in this case). + None => command.arg("--no-config-file"), + }; + if !with_std { command.arg("--no-std-lib"); } diff --git a/src/config_files.rs b/src/config_files.rs index cf72819600..00dbf9d290 100644 --- a/src/config_files.rs +++ b/src/config_files.rs @@ -3,6 +3,8 @@ use log::warn; use nu_cli::read_plugin_file; use nu_cli::{eval_config_contents, eval_source}; use nu_path::canonicalize_with; +#[allow(deprecated)] +use nu_path::NUSHELL_FOLDER; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, report_error, report_error_new, Config, ParseError, PipelineData, Spanned, @@ -17,7 +19,6 @@ use std::{ sync::Arc, }; -pub(crate) const NUSHELL_FOLDER: &str = "nushell"; const CONFIG_FILE: &str = "config.nu"; const ENV_FILE: &str = "env.nu"; const LOGINSHELL_FILE: &str = "login.nu"; @@ -50,6 +51,7 @@ pub(crate) fn read_config_file( } } } else if let Some(mut config_path) = nu_path::config_dir() { + #[allow(deprecated)] config_path.push(NUSHELL_FOLDER); // Create config directory if it does not exist @@ -135,6 +137,7 @@ pub(crate) fn read_loginshell_file(engine_state: &mut EngineState, stack: &mut S // read and execute loginshell file if exists if let Some(mut config_path) = nu_path::config_dir() { + #[allow(deprecated)] config_path.push(NUSHELL_FOLDER); config_path.push(LOGINSHELL_FILE); @@ -268,6 +271,7 @@ pub(crate) fn setup_config( ); let result = catch_unwind(AssertUnwindSafe(|| { #[cfg(feature = "plugin")] + #[allow(deprecated)] read_plugin_file(engine_state, plugin_file, NUSHELL_FOLDER); read_config_file(engine_state, stack, env_file, true); @@ -301,6 +305,7 @@ pub(crate) fn set_config_path( let config_path = match config_file { Some(s) => canonicalize_with(&s.item, cwd).ok(), None => nu_path::config_dir().map(|mut p| { + #[allow(deprecated)] p.push(NUSHELL_FOLDER); let mut p = canonicalize_with(&p, cwd).unwrap_or(p); p.push(default_config_name); diff --git a/src/run.rs b/src/run.rs index 10a5043b25..652cc05b44 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,5 +1,4 @@ #[cfg(feature = "plugin")] -use crate::config_files::NUSHELL_FOLDER; use crate::{ command, config_files::{self, setup_config}, @@ -8,6 +7,8 @@ use log::trace; #[cfg(feature = "plugin")] use nu_cli::read_plugin_file; use nu_cli::{evaluate_commands, evaluate_file, evaluate_repl, EvaluateCommandsOpts}; +#[allow(deprecated)] +use nu_path::NUSHELL_FOLDER; use nu_protocol::{ engine::{EngineState, Stack}, report_error_new, PipelineData, Spanned, @@ -37,6 +38,7 @@ pub(crate) fn run_commands( // if the --no-config-file(-n) flag is passed, do not load plugin, env, or config files if parsed_nu_cli_args.no_config_file.is_none() { #[cfg(feature = "plugin")] + #[allow(deprecated)] read_plugin_file(engine_state, parsed_nu_cli_args.plugin_file, NUSHELL_FOLDER); perf!("read plugins", start_time, use_color); @@ -125,6 +127,7 @@ pub(crate) fn run_file( if parsed_nu_cli_args.no_config_file.is_none() { let start_time = std::time::Instant::now(); #[cfg(feature = "plugin")] + #[allow(deprecated)] read_plugin_file(engine_state, parsed_nu_cli_args.plugin_file, NUSHELL_FOLDER); perf!("read plugins", start_time, use_color); @@ -216,7 +219,6 @@ pub(crate) fn run_repl( let ret_val = evaluate_repl( engine_state, stack, - config_files::NUSHELL_FOLDER, parsed_nu_cli_args.execute, parsed_nu_cli_args.no_std_lib, entire_start_time,