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

View File

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

View File

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

View File

@ -25,6 +25,25 @@ mod output;
mod reedline;
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)]
pub struct Config {
pub external_completer: Option<usize>,
@ -46,10 +65,7 @@ pub struct Config {
pub partial_completions: bool,
pub completion_algorithm: CompletionAlgorithm,
pub edit_mode: EditBindings,
pub max_history_size: i64,
pub sync_history_on_enter: bool,
pub history_file_format: HistoryFileFormat,
pub history_isolation: bool,
pub history: HistoryConfig,
pub keybindings: Vec<ParsedKeybinding>,
pub menus: Vec<ParsedMenu>,
pub hooks: Hooks,
@ -104,10 +120,7 @@ impl Default for Config {
explore: HashMap::new(),
max_history_size: 100_000,
sync_history_on_enter: true,
history_file_format: HistoryFileFormat::PlainText,
history_isolation: false,
history: HistoryConfig::default(),
case_sensitive_completions: false,
quick_completions: true,
@ -172,7 +185,7 @@ impl Value {
// the `2`.
if let Value::Record { val, .. } = self {
val.retain_mut( |key, value| {
val.retain_mut(|key, value| {
let span = value.span();
match key {
// Grouped options
@ -232,22 +245,23 @@ impl Value {
}
}
"history" => {
let history = &mut config.history;
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value| {
let span = value.span();
match key2 {
"isolation" => {
process_bool_config(value, &mut errors, &mut config.history_isolation);
process_bool_config(value, &mut errors, &mut history.isolation);
}
"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" => {
process_int_config(value, &mut errors, &mut config.max_history_size);
process_int_config(value, &mut errors, &mut history.max_size);
}
"file_format" => {
process_string_enum(
&mut config.history_file_format,
&mut history.file_format,
&[key, key2],
value,
&mut errors);
@ -264,10 +278,10 @@ impl Value {
// Reconstruct
*value = Value::record(
record! {
"sync_on_enter" => Value::bool(config.sync_history_on_enter, span),
"max_size" => Value::int(config.max_history_size, span),
"file_format" => config.history_file_format.reconstruct_value(span),
"isolation" => Value::bool(config.history_isolation, span),
"sync_on_enter" => Value::bool(history.sync_on_enter, span),
"max_size" => Value::int(history.max_size, span),
"file_format" => history.file_format.reconstruct_value(span),
"isolation" => Value::bool(history.isolation, 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 crate::ast::Block;
use crate::{
BlockId, Config, DeclId, Example, FileId, Module, ModuleId, OverlayId, ShellError, Signature,
Span, Type, VarId, Variable, VirtualPathId,
BlockId, Config, DeclId, Example, FileId, HistoryConfig, Module, ModuleId, OverlayId,
ShellError, Signature, Span, Type, VarId, Variable, VirtualPathId,
};
use crate::{Category, Value};
use std::borrow::Borrow;
@ -96,6 +96,7 @@ pub struct EngineState {
#[cfg(feature = "plugin")]
pub plugin_signatures: Option<PathBuf>,
config_path: HashMap<String, PathBuf>,
pub history_enabled: bool,
pub history_session_id: i64,
// If Nushell was started, e.g., with `nu spam.nu`, the file's parent is stored here
pub(super) currently_parsed_cwd: Option<PathBuf>,
@ -151,6 +152,7 @@ impl EngineState {
#[cfg(feature = "plugin")]
plugin_signatures: None,
config_path: HashMap::new(),
history_enabled: true,
history_session_id: 0,
currently_parsed_cwd: None,
regex_cache: Arc::new(Mutex::new(LruCache::new(
@ -720,6 +722,15 @@ impl EngineState {
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 {
self.vars
.get(var_id)

View File

@ -80,7 +80,7 @@ pub fn create_nu_constant(engine_state: &EngineState, span: Span) -> Result<Valu
"history-path",
if let Some(mut path) = nu_path::config_dir() {
path.push("nushell");
match engine_state.config.history_file_format {
match engine_state.config.history.file_format {
HistoryFileFormat::Sqlite => {
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(
"history-enabled",
Value::bool(engine_state.history_enabled, span),
);
record.push(
"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")]
let plugin_file = call.get_flag_expr("plugin-config");
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 config_file = call.get_flag_expr("config");
let env_file = call.get_flag_expr("env-config");
@ -184,6 +185,7 @@ pub(crate) fn parse_commandline_args(
#[cfg(feature = "plugin")]
plugin_file,
no_config_file,
no_history,
no_std_lib,
config_file,
env_file,
@ -223,6 +225,7 @@ pub(crate) struct NushellCliArgs {
#[cfg(feature = "plugin")]
pub(crate) plugin_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) config_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",
Some('n'),
)
.switch(
"no-history",
"disable reading and writing to command history",
None,
)
.switch("no-std-lib", "start with no standard library", None)
.named(
"threads",

View File

@ -128,6 +128,8 @@ fn main() -> Result<()> {
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;
if let Some(level) = parsed_nu_cli_args
.log_level