Add CLI flag to disable history (#11550)

# Description
Adds a CLI flag for nushell that disables reading and writing to the
history file. This will be useful for future testing and possibly our
users as well. To borrow `fish` shell's terminology, this allows users
to start nushell in "private" mode.

# User-Facing Changes
Breaking API change for `nu-protocol` (changed `Config`).
This commit is contained in:
Ian Manske 2024-01-17 15:40:59 +00:00 committed by GitHub
parent a4199ea312
commit 55bf4d847f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 163 additions and 106 deletions

View File

@ -44,6 +44,10 @@ impl Command for History {
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
let Some(history) = engine_state.history_config() else {
return Ok(PipelineData::empty());
};
// todo for sqlite history this command should be an alias to `open ~/.config/nushell/history.sqlite3 | get 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() { if let Some(config_path) = nu_path::config_dir() {
let clear = call.has_flag(engine_state, stack, "clear")?; let clear = call.has_flag(engine_state, stack, "clear")?;
@ -52,7 +56,7 @@ impl Command for History {
let mut history_path = config_path; let mut history_path = config_path;
history_path.push("nushell"); history_path.push("nushell");
match engine_state.config.history_file_format { match history.file_format {
HistoryFileFormat::Sqlite => { HistoryFileFormat::Sqlite => {
history_path.push("history.sqlite3"); history_path.push("history.sqlite3");
} }
@ -66,8 +70,7 @@ impl Command for History {
// TODO: FIXME also clear the auxiliary files when using sqlite // TODO: FIXME also clear the auxiliary files when using sqlite
Ok(PipelineData::empty()) Ok(PipelineData::empty())
} else { } else {
let history_reader: Option<Box<dyn ReedlineHistory>> = let history_reader: Option<Box<dyn ReedlineHistory>> = match history.file_format {
match engine_state.config.history_file_format {
HistoryFileFormat::Sqlite => { HistoryFileFormat::Sqlite => {
SqliteBackedHistory::with_file(history_path, None, None) SqliteBackedHistory::with_file(history_path, None, None)
.map(|inner| { .map(|inner| {
@ -77,18 +80,17 @@ impl Command for History {
.ok() .ok()
} }
HistoryFileFormat::PlainText => FileBackedHistory::with_file( HistoryFileFormat::PlainText => {
engine_state.config.max_history_size as usize, FileBackedHistory::with_file(history.max_size as usize, history_path)
history_path,
)
.map(|inner| { .map(|inner| {
let boxed: Box<dyn ReedlineHistory> = Box::new(inner); let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
boxed boxed
}) })
.ok(), .ok()
}
}; };
match engine_state.config.history_file_format { match history.file_format {
HistoryFileFormat::PlainText => Ok(history_reader HistoryFileFormat::PlainText => Ok(history_reader
.and_then(|h| { .and_then(|h| {
h.search(SearchQuery::everything(SearchDirection::Forward, None)) h.search(SearchQuery::everything(SearchDirection::Forward, None))

View File

@ -17,8 +17,8 @@ use nu_protocol::{
config::NuCursorShape, config::NuCursorShape,
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
eval_const::create_nu_constant, eval_const::create_nu_constant,
report_error, report_error_new, HistoryFileFormat, PipelineData, ShellError, Span, Spanned, report_error, report_error_new, HistoryConfig, HistoryFileFormat, PipelineData, ShellError,
Value, NU_VARIABLE_ID, Span, Spanned, Value, NU_VARIABLE_ID,
}; };
use nu_utils::utils::perf; use nu_utils::utils::perf;
use reedline::{ use reedline::{
@ -28,7 +28,7 @@ use reedline::{
use std::{ use std::{
env::temp_dir, env::temp_dir,
io::{self, IsTerminal, Write}, io::{self, IsTerminal, Write},
path::Path, path::PathBuf,
sync::atomic::Ordering, sync::atomic::Ordering,
time::Instant, time::Instant,
}; };
@ -109,23 +109,27 @@ pub fn evaluate_repl(
use_color, use_color,
); );
if let Some(history) = engine_state.history_config() {
start_time = std::time::Instant::now();
// Setup history_isolation aka "history per session" // Setup history_isolation aka "history per session"
let history_isolation = engine_state.get_config().history_isolation; let history_session_id = if history.isolation {
let history_session_id = if history_isolation {
Reedline::create_history_session_id() Reedline::create_history_session_id()
} else { } else {
None None
}; };
start_time = std::time::Instant::now(); if let Some(path) = crate::config_files::get_history_path(nushell_path, history.file_format)
let history_path = crate::config_files::get_history_path( {
nushell_path, line_editor = update_line_editor_history(
engine_state.config.history_file_format, engine_state,
); path,
if let Some(history_path) = history_path.as_deref() { history,
line_editor = line_editor,
update_line_editor_history(engine_state, history_path, line_editor, history_session_id)? history_session_id,
)?
}; };
perf( perf(
"setup history", "setup history",
start_time, start_time,
@ -134,6 +138,7 @@ pub fn evaluate_repl(
column!(), column!(),
use_color, use_color,
); );
}
if let Some(s) = prerun_command { if let Some(s) = prerun_command {
eval_source( eval_source(
@ -313,8 +318,9 @@ pub fn evaluate_repl(
use_color, use_color,
); );
if let Some(history) = engine_state.history_config() {
start_time = std::time::Instant::now(); start_time = std::time::Instant::now();
if config.sync_history_on_enter { if history.sync_on_enter {
if let Err(e) = line_editor.sync_history() { if let Err(e) = line_editor.sync_history() {
warn!("Failed to sync history: {}", e); warn!("Failed to sync history: {}", e);
} }
@ -327,6 +333,7 @@ pub fn evaluate_repl(
column!(), column!(),
use_color, use_color,
); );
}
start_time = std::time::Instant::now(); start_time = std::time::Instant::now();
// Changing the line editor based on the found keybindings // Changing the line editor based on the found keybindings
@ -418,8 +425,10 @@ pub fn evaluate_repl(
match input { match input {
Ok(Signal::Success(s)) => { Ok(Signal::Success(s)) => {
let hostname = System::host_name(); let hostname = System::host_name();
let history_supports_meta = let history_supports_meta = matches!(
matches!(config.history_file_format, HistoryFileFormat::Sqlite); engine_state.history_config().map(|h| h.file_format),
Some(HistoryFileFormat::Sqlite)
);
if history_supports_meta && !s.is_empty() && line_editor.has_last_command_context() if history_supports_meta && !s.is_empty() && line_editor.has_last_command_context()
{ {
line_editor line_editor
@ -715,17 +724,14 @@ fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reed
fn update_line_editor_history( fn update_line_editor_history(
engine_state: &mut EngineState, engine_state: &mut EngineState,
history_path: &Path, history_path: PathBuf,
history: HistoryConfig,
line_editor: Reedline, line_editor: Reedline,
history_session_id: Option<HistorySessionId>, history_session_id: Option<HistorySessionId>,
) -> Result<Reedline, ErrReport> { ) -> Result<Reedline, ErrReport> {
let config = engine_state.get_config(); let history: Box<dyn reedline::History> = match history.file_format {
let history: Box<dyn reedline::History> = match engine_state.config.history_file_format {
HistoryFileFormat::PlainText => Box::new( HistoryFileFormat::PlainText => Box::new(
FileBackedHistory::with_file( FileBackedHistory::with_file(history.max_size as usize, history_path)
config.max_history_size as usize,
history_path.to_path_buf(),
)
.into_diagnostic()?, .into_diagnostic()?,
), ),
HistoryFileFormat::Sqlite => Box::new( HistoryFileFormat::Sqlite => Box::new(
@ -834,14 +840,18 @@ fn trailing_slash_looks_like_path() {
#[test] #[test]
fn are_session_ids_in_sync() { fn are_session_ids_in_sync() {
let engine_state = &mut EngineState::new(); let engine_state = &mut EngineState::new();
let history_path_o = let history = engine_state.history_config().unwrap();
crate::config_files::get_history_path("nushell", engine_state.config.history_file_format); let history_path =
assert!(history_path_o.is_some()); crate::config_files::get_history_path("nushell", history.file_format).unwrap();
let history_path = history_path_o.as_deref().unwrap();
let line_editor = reedline::Reedline::create(); let line_editor = reedline::Reedline::create();
let history_session_id = reedline::Reedline::create_history_session_id(); let history_session_id = reedline::Reedline::create_history_session_id();
let line_editor = let line_editor = update_line_editor_history(
update_line_editor_history(engine_state, history_path, line_editor, history_session_id); engine_state,
history_path,
history,
line_editor,
history_session_id,
);
assert_eq!( assert_eq!(
i64::from(line_editor.unwrap().get_history_session_id().unwrap()), i64::from(line_editor.unwrap().get_history_session_id().unwrap()),
engine_state.history_session_id engine_state.history_session_id

View File

@ -684,13 +684,14 @@ fn variables_completions() {
// Test completions for $nu // Test completions for $nu
let suggestions = completer.complete("$nu.", 4); let suggestions = completer.complete("$nu.", 4);
assert_eq!(14, suggestions.len()); assert_eq!(15, suggestions.len());
let expected: Vec<String> = vec![ let expected: Vec<String> = vec![
"config-path".into(), "config-path".into(),
"current-exe".into(), "current-exe".into(),
"default-config-dir".into(), "default-config-dir".into(),
"env-path".into(), "env-path".into(),
"history-enabled".into(),
"history-path".into(), "history-path".into(),
"home-path".into(), "home-path".into(),
"is-interactive".into(), "is-interactive".into(),
@ -709,9 +710,13 @@ fn variables_completions() {
// Test completions for $nu.h (filter) // Test completions for $nu.h (filter)
let suggestions = completer.complete("$nu.h", 5); let suggestions = completer.complete("$nu.h", 5);
assert_eq!(2, suggestions.len()); assert_eq!(3, suggestions.len());
let expected: Vec<String> = vec!["history-path".into(), "home-path".into()]; let expected: Vec<String> = vec![
"history-enabled".into(),
"history-path".into(),
"home-path".into(),
];
// Match results // Match results
match_suggestions(expected, suggestions); match_suggestions(expected, suggestions);

View File

@ -25,6 +25,25 @@ mod output;
mod reedline; mod reedline;
mod table; mod table;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct HistoryConfig {
pub max_size: i64,
pub sync_on_enter: bool,
pub file_format: HistoryFileFormat,
pub isolation: bool,
}
impl Default for HistoryConfig {
fn default() -> Self {
Self {
max_size: 100_000,
sync_on_enter: true,
file_format: HistoryFileFormat::PlainText,
isolation: false,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Config { pub struct Config {
pub external_completer: Option<usize>, pub external_completer: Option<usize>,
@ -46,10 +65,7 @@ pub struct Config {
pub partial_completions: bool, pub partial_completions: bool,
pub completion_algorithm: CompletionAlgorithm, pub completion_algorithm: CompletionAlgorithm,
pub edit_mode: EditBindings, pub edit_mode: EditBindings,
pub max_history_size: i64, pub history: HistoryConfig,
pub sync_history_on_enter: bool,
pub history_file_format: HistoryFileFormat,
pub history_isolation: bool,
pub keybindings: Vec<ParsedKeybinding>, pub keybindings: Vec<ParsedKeybinding>,
pub menus: Vec<ParsedMenu>, pub menus: Vec<ParsedMenu>,
pub hooks: Hooks, pub hooks: Hooks,
@ -104,10 +120,7 @@ impl Default for Config {
explore: HashMap::new(), explore: HashMap::new(),
max_history_size: 100_000, history: HistoryConfig::default(),
sync_history_on_enter: true,
history_file_format: HistoryFileFormat::PlainText,
history_isolation: false,
case_sensitive_completions: false, case_sensitive_completions: false,
quick_completions: true, quick_completions: true,
@ -232,22 +245,23 @@ impl Value {
} }
} }
"history" => { "history" => {
let history = &mut config.history;
if let Value::Record { val, .. } = value { if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value| { val.retain_mut(|key2, value| {
let span = value.span(); let span = value.span();
match key2 { match key2 {
"isolation" => { "isolation" => {
process_bool_config(value, &mut errors, &mut config.history_isolation); process_bool_config(value, &mut errors, &mut history.isolation);
} }
"sync_on_enter" => { "sync_on_enter" => {
process_bool_config(value, &mut errors, &mut config.sync_history_on_enter); process_bool_config(value, &mut errors, &mut history.sync_on_enter);
} }
"max_size" => { "max_size" => {
process_int_config(value, &mut errors, &mut config.max_history_size); process_int_config(value, &mut errors, &mut history.max_size);
} }
"file_format" => { "file_format" => {
process_string_enum( process_string_enum(
&mut config.history_file_format, &mut history.file_format,
&[key, key2], &[key, key2],
value, value,
&mut errors); &mut errors);
@ -264,10 +278,10 @@ impl Value {
// Reconstruct // Reconstruct
*value = Value::record( *value = Value::record(
record! { record! {
"sync_on_enter" => Value::bool(config.sync_history_on_enter, span), "sync_on_enter" => Value::bool(history.sync_on_enter, span),
"max_size" => Value::int(config.max_history_size, span), "max_size" => Value::int(history.max_size, span),
"file_format" => config.history_file_format.reconstruct_value(span), "file_format" => history.file_format.reconstruct_value(span),
"isolation" => Value::bool(config.history_isolation, span), "isolation" => Value::bool(history.isolation, span),
}, },
span, span,
); );

View File

@ -5,8 +5,8 @@ use super::{usage::build_usage, usage::Usage, StateDelta};
use super::{Command, EnvVars, OverlayFrame, ScopeFrame, Stack, Visibility, DEFAULT_OVERLAY_NAME}; use super::{Command, EnvVars, OverlayFrame, ScopeFrame, Stack, Visibility, DEFAULT_OVERLAY_NAME};
use crate::ast::Block; use crate::ast::Block;
use crate::{ use crate::{
BlockId, Config, DeclId, Example, FileId, Module, ModuleId, OverlayId, ShellError, Signature, BlockId, Config, DeclId, Example, FileId, HistoryConfig, Module, ModuleId, OverlayId,
Span, Type, VarId, Variable, VirtualPathId, ShellError, Signature, Span, Type, VarId, Variable, VirtualPathId,
}; };
use crate::{Category, Value}; use crate::{Category, Value};
use std::borrow::Borrow; use std::borrow::Borrow;
@ -96,6 +96,7 @@ pub struct EngineState {
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
pub plugin_signatures: Option<PathBuf>, pub plugin_signatures: Option<PathBuf>,
config_path: HashMap<String, PathBuf>, config_path: HashMap<String, PathBuf>,
pub history_enabled: bool,
pub history_session_id: i64, pub history_session_id: i64,
// If Nushell was started, e.g., with `nu spam.nu`, the file's parent is stored here // If Nushell was started, e.g., with `nu spam.nu`, the file's parent is stored here
pub(super) currently_parsed_cwd: Option<PathBuf>, pub(super) currently_parsed_cwd: Option<PathBuf>,
@ -151,6 +152,7 @@ impl EngineState {
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
plugin_signatures: None, plugin_signatures: None,
config_path: HashMap::new(), config_path: HashMap::new(),
history_enabled: true,
history_session_id: 0, history_session_id: 0,
currently_parsed_cwd: None, currently_parsed_cwd: None,
regex_cache: Arc::new(Mutex::new(LruCache::new( regex_cache: Arc::new(Mutex::new(LruCache::new(
@ -720,6 +722,15 @@ impl EngineState {
self.config.plugins.get(plugin) self.config.plugins.get(plugin)
} }
/// Returns the configuration settings for command history or `None` if history is disabled
pub fn history_config(&self) -> Option<HistoryConfig> {
if self.history_enabled {
Some(self.config.history)
} else {
None
}
}
pub fn get_var(&self, var_id: VarId) -> &Variable { pub fn get_var(&self, var_id: VarId) -> &Variable {
self.vars self.vars
.get(var_id) .get(var_id)

View File

@ -80,7 +80,7 @@ pub fn create_nu_constant(engine_state: &EngineState, span: Span) -> Result<Valu
"history-path", "history-path",
if let Some(mut path) = nu_path::config_dir() { if let Some(mut path) = nu_path::config_dir() {
path.push("nushell"); path.push("nushell");
match engine_state.config.history_file_format { match engine_state.config.history.file_format {
HistoryFileFormat::Sqlite => { HistoryFileFormat::Sqlite => {
path.push("history.sqlite3"); path.push("history.sqlite3");
} }
@ -187,6 +187,11 @@ pub fn create_nu_constant(engine_state: &EngineState, span: Span) -> Result<Valu
record.push("is-login", Value::bool(engine_state.is_login, span)); record.push("is-login", Value::bool(engine_state.is_login, span));
record.push(
"history-enabled",
Value::bool(engine_state.history_enabled, span),
);
record.push( record.push(
"current-exe", "current-exe",
if let Ok(current_exe) = std::env::current_exe() { if let Ok(current_exe) = std::env::current_exe() {

View File

@ -98,6 +98,7 @@ pub(crate) fn parse_commandline_args(
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
let plugin_file = call.get_flag_expr("plugin-config"); let plugin_file = call.get_flag_expr("plugin-config");
let no_config_file = call.get_named_arg("no-config-file"); let no_config_file = call.get_named_arg("no-config-file");
let no_history = call.get_named_arg("no-history");
let no_std_lib = call.get_named_arg("no-std-lib"); let no_std_lib = call.get_named_arg("no-std-lib");
let config_file = call.get_flag_expr("config"); let config_file = call.get_flag_expr("config");
let env_file = call.get_flag_expr("env-config"); let env_file = call.get_flag_expr("env-config");
@ -184,6 +185,7 @@ pub(crate) fn parse_commandline_args(
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
plugin_file, plugin_file,
no_config_file, no_config_file,
no_history,
no_std_lib, no_std_lib,
config_file, config_file,
env_file, env_file,
@ -223,6 +225,7 @@ pub(crate) struct NushellCliArgs {
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
pub(crate) plugin_file: Option<Spanned<String>>, pub(crate) plugin_file: Option<Spanned<String>>,
pub(crate) no_config_file: Option<Spanned<String>>, pub(crate) no_config_file: Option<Spanned<String>>,
pub(crate) no_history: Option<Spanned<String>>,
pub(crate) no_std_lib: Option<Spanned<String>>, pub(crate) no_std_lib: Option<Spanned<String>>,
pub(crate) config_file: Option<Spanned<String>>, pub(crate) config_file: Option<Spanned<String>>,
pub(crate) env_file: Option<Spanned<String>>, pub(crate) env_file: Option<Spanned<String>>,
@ -281,6 +284,11 @@ impl Command for Nu {
"start with no config file and no env file", "start with no config file and no env file",
Some('n'), Some('n'),
) )
.switch(
"no-history",
"disable reading and writing to command history",
None,
)
.switch("no-std-lib", "start with no standard library", None) .switch("no-std-lib", "start with no standard library", None)
.named( .named(
"threads", "threads",

View File

@ -128,6 +128,8 @@ fn main() -> Result<()> {
engine_state.is_login = parsed_nu_cli_args.login_shell.is_some(); engine_state.is_login = parsed_nu_cli_args.login_shell.is_some();
engine_state.history_enabled = parsed_nu_cli_args.no_history.is_none();
let use_color = engine_state.get_config().use_ansi_coloring; let use_color = engine_state.get_config().use_ansi_coloring;
if let Some(level) = parsed_nu_cli_args if let Some(level) = parsed_nu_cli_args
.log_level .log_level