fix: fix commandline when called with no arguments (#8207)

# Description

This fixes the `commandline` command when it's run with no arguments, so
it outputs the command being run. New line characters are included.

This allows for:

- [A way to get current command inside pre_execution hook · Issue #6264
· nushell/nushell](https://github.com/nushell/nushell/issues/6264)
- The possibility of *Atuin* to work work *Nushell*. *Atuin* hooks need
to know the current repl input before it is run.

# User-Facing Changes

# Tests + Formatting

Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect` to check that you're using the standard code
style
- `cargo test --workspace` to check that all tests pass

# After Submitting

If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
This commit is contained in:
Steven Xu 2023-03-17 09:45:35 +11:00 committed by GitHub
parent 0903a891e4
commit b2a557d4ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 244 additions and 57 deletions

1
Cargo.lock generated
View file

@ -2746,6 +2746,7 @@ dependencies = [
"nu-test-support", "nu-test-support",
"nu-utils", "nu-utils",
"shadow-rs", "shadow-rs",
"unicode-segmentation",
] ]
[[package]] [[package]]

View file

@ -14,7 +14,7 @@ use nu_parser::{lex, parse, trim_quotes_str};
use nu_protocol::{ use nu_protocol::{
ast::PathMember, ast::PathMember,
config::NuCursorShape, config::NuCursorShape,
engine::{EngineState, ReplOperation, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
format_duration, BlockId, HistoryFileFormat, PipelineData, PositionalArg, ShellError, Span, format_duration, BlockId, HistoryFileFormat, PipelineData, PositionalArg, ShellError, Span,
Spanned, Type, Value, VarId, Spanned, Type, Value, VarId,
}; };
@ -459,18 +459,30 @@ pub fn evaluate_repl(
.into_diagnostic()?; // todo: don't stop repl if error here? .into_diagnostic()?; // todo: don't stop repl if error here?
} }
engine_state
.repl_buffer_state
.lock()
.expect("repl buffer state mutex")
.replace(line_editor.current_buffer_contents().to_string());
// Right before we start running the code the user gave us, // Right before we start running the code the user gave us,
// fire the "pre_execution" hook // fire the "pre_execution" hook
if let Some(hook) = config.hooks.pre_execution.clone() { if let Some(hook) = config.hooks.pre_execution.clone() {
// Set the REPL buffer to the current command for the "pre_execution" hook
let mut repl_buffer = engine_state
.repl_buffer_state
.lock()
.expect("repl buffer state mutex");
let next_repl_buffer = repl_buffer.to_string();
*repl_buffer = s.to_string();
drop(repl_buffer);
if let Err(err) = eval_hook(engine_state, stack, None, vec![], &hook) { if let Err(err) = eval_hook(engine_state, stack, None, vec![], &hook) {
report_error_new(engine_state, &err); report_error_new(engine_state, &err);
} }
// Restore the REPL buffer state for the next command. It could've been edited
// by `commandline`.
let mut repl_buffer = engine_state
.repl_buffer_state
.lock()
.expect("repl buffer state mutex");
*repl_buffer = next_repl_buffer;
drop(repl_buffer);
} }
if shell_integration { if shell_integration {
@ -628,23 +640,23 @@ pub fn evaluate_repl(
run_ansi_sequence(RESET_APPLICATION_MODE)?; run_ansi_sequence(RESET_APPLICATION_MODE)?;
} }
let mut ops = engine_state let mut repl_buffer = engine_state
.repl_operation_queue .repl_buffer_state
.lock() .lock()
.expect("repl op queue mutex"); .expect("repl buffer state mutex");
while let Some(op) = ops.pop_front() { let mut repl_cursor_pos = engine_state
match op { .repl_cursor_pos
ReplOperation::Append(s) => line_editor.run_edit_commands(&[ .lock()
EditCommand::MoveToEnd, .expect("repl cursor pos mutex");
EditCommand::InsertString(s), line_editor.run_edit_commands(&[
]), EditCommand::Clear,
ReplOperation::Insert(s) => { EditCommand::InsertString(repl_buffer.to_string()),
line_editor.run_edit_commands(&[EditCommand::InsertString(s)]) EditCommand::MoveToPosition(*repl_cursor_pos),
} ]);
ReplOperation::Replace(s) => line_editor *repl_buffer = "".to_string();
.run_edit_commands(&[EditCommand::Clear, EditCommand::InsertString(s)]), drop(repl_buffer);
} *repl_cursor_pos = 0;
} drop(repl_cursor_pos);
} }
Ok(Signal::CtrlC) => { Ok(Signal::CtrlC) => {
// `Reedline` clears the line content. New prompt is shown // `Reedline` clears the line content. New prompt is shown

View file

@ -24,6 +24,7 @@ fancy-regex = "0.11.0"
itertools = "0.10.0" itertools = "0.10.0"
log = "0.4.14" log = "0.4.14"
shadow-rs = { version = "0.21.0", default-features = false } shadow-rs = { version = "0.21.0", default-features = false }
unicode-segmentation = "1.10.0"
[build-dependencies] [build-dependencies]
shadow-rs = { version = "0.21.0", default-features = false } shadow-rs = { version = "0.21.0", default-features = false }

View file

@ -1,10 +1,10 @@
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::ReplOperation;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::Category; use nu_protocol::Category;
use nu_protocol::IntoPipelineData; use nu_protocol::IntoPipelineData;
use nu_protocol::{PipelineData, ShellError, Signature, SyntaxShape, Type, Value}; use nu_protocol::{PipelineData, ShellError, Signature, SyntaxShape, Type, Value};
use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone)] #[derive(Clone)]
pub struct Commandline; pub struct Commandline;
@ -17,6 +17,11 @@ impl Command for Commandline {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("commandline") Signature::build("commandline")
.input_output_types(vec![(Type::Nothing, Type::Nothing)]) .input_output_types(vec![(Type::Nothing, Type::Nothing)])
.switch(
"cursor",
"Set or get the current cursor position",
Some('c'),
)
.switch( .switch(
"append", "append",
"appends the string to the end of the buffer", "appends the string to the end of the buffer",
@ -56,30 +61,77 @@ impl Command for Commandline {
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
if let Some(cmd) = call.opt::<Value>(engine_state, stack, 0)? { if let Some(cmd) = call.opt::<Value>(engine_state, stack, 0)? {
let mut ops = engine_state let mut buffer = engine_state
.repl_operation_queue .repl_buffer_state
.lock() .lock()
.expect("repl op queue mutex"); .expect("repl buffer state mutex");
ops.push_back(if call.has_flag("append") { let mut cursor_pos = engine_state
ReplOperation::Append(cmd.as_string()?) .repl_cursor_pos
.lock()
.expect("repl cursor pos mutex");
if call.has_flag("cursor") {
let cmd_str = cmd.as_string()?;
match cmd_str.parse::<i64>() {
Ok(n) => {
*cursor_pos = if n <= 0 {
0usize
} else {
buffer
.grapheme_indices(true)
.map(|(i, _c)| i)
.nth(n as usize)
.unwrap_or(buffer.len())
}
}
Err(_) => {
return Err(ShellError::CantConvert {
to_type: "int".to_string(),
from_type: "string".to_string(),
span: cmd.span()?,
help: Some(format!(
r#"string "{cmd_str}" does not represent a valid integer"#
)),
})
}
}
} else if call.has_flag("append") {
buffer.push_str(&cmd.as_string()?);
} else if call.has_flag("insert") { } else if call.has_flag("insert") {
ReplOperation::Insert(cmd.as_string()?) let cmd_str = cmd.as_string()?;
buffer.insert_str(*cursor_pos, &cmd_str);
*cursor_pos += cmd_str.len();
} else { } else {
ReplOperation::Replace(cmd.as_string()?) *buffer = cmd.as_string()?;
}); *cursor_pos = buffer.len();
Ok(Value::Nothing { span: call.head }.into_pipeline_data())
} else if let Some(ref cmd) = *engine_state
.repl_buffer_state
.lock()
.expect("repl buffer state mutex")
{
Ok(Value::String {
val: cmd.clone(),
span: call.head,
} }
.into_pipeline_data())
} else {
Ok(Value::Nothing { span: call.head }.into_pipeline_data()) Ok(Value::Nothing { span: call.head }.into_pipeline_data())
} else {
let buffer = engine_state
.repl_buffer_state
.lock()
.expect("repl buffer state mutex");
if call.has_flag("cursor") {
let cursor_pos = engine_state
.repl_cursor_pos
.lock()
.expect("repl cursor pos mutex");
let char_pos = buffer
.grapheme_indices(true)
.position(|(i, _c)| i == *cursor_pos)
.unwrap_or(buffer.len());
Ok(Value::String {
val: char_pos.to_string(),
span: call.head,
}
.into_pipeline_data())
} else {
Ok(Value::String {
val: buffer.to_string(),
span: call.head,
}
.into_pipeline_data())
}
} }
} }
} }

View file

@ -13,7 +13,7 @@ use std::num::NonZeroUsize;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::{ use std::{
collections::{HashMap, HashSet, VecDeque}, collections::{HashMap, HashSet},
sync::{ sync::{
atomic::{AtomicBool, AtomicU32}, atomic::{AtomicBool, AtomicU32},
Arc, Mutex, Arc, Mutex,
@ -22,15 +22,6 @@ use std::{
static PWD_ENV: &str = "PWD"; static PWD_ENV: &str = "PWD";
// TODO: move to different file? where?
/// An operation to be performed with the current buffer of the interactive shell.
#[derive(Debug, Clone)]
pub enum ReplOperation {
Append(String),
Insert(String),
Replace(String),
}
/// Organizes usage messages for various primitives /// Organizes usage messages for various primitives
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Usage { pub struct Usage {
@ -134,8 +125,9 @@ pub struct EngineState {
pub previous_env_vars: HashMap<String, Value>, pub previous_env_vars: HashMap<String, Value>,
pub config: Config, pub config: Config,
pub pipeline_externals_state: Arc<(AtomicU32, AtomicU32)>, pub pipeline_externals_state: Arc<(AtomicU32, AtomicU32)>,
pub repl_buffer_state: Arc<Mutex<Option<String>>>, pub repl_buffer_state: Arc<Mutex<String>>,
pub repl_operation_queue: Arc<Mutex<VecDeque<ReplOperation>>>, // A byte position, as `EditCommand::MoveToPosition` is also a byte position
pub repl_cursor_pos: Arc<Mutex<usize>>,
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
pub plugin_signatures: Option<PathBuf>, pub plugin_signatures: Option<PathBuf>,
#[cfg(not(windows))] #[cfg(not(windows))]
@ -186,8 +178,8 @@ impl EngineState {
previous_env_vars: HashMap::new(), previous_env_vars: HashMap::new(),
config: Config::default(), config: Config::default(),
pipeline_externals_state: Arc::new((AtomicU32::new(0), AtomicU32::new(0))), pipeline_externals_state: Arc::new((AtomicU32::new(0), AtomicU32::new(0))),
repl_buffer_state: Arc::new(Mutex::new(None)), repl_buffer_state: Arc::new(Mutex::new("".to_string())),
repl_operation_queue: Arc::new(Mutex::new(VecDeque::new())), repl_cursor_pos: Arc::new(Mutex::new(0)),
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
plugin_signatures: None, plugin_signatures: None,
#[cfg(not(windows))] #[cfg(not(windows))]

View file

@ -209,6 +209,12 @@ pub fn nu_repl() {
// Check for pre_execution hook // Check for pre_execution hook
let config = engine_state.get_config(); let config = engine_state.get_config();
*engine_state
.repl_buffer_state
.lock()
.expect("repl buffer state mutex") = line.to_string();
if let Some(hook) = config.hooks.pre_execution.clone() { if let Some(hook) = config.hooks.pre_execution.clone() {
if let Err(err) = eval_hook(&mut engine_state, &mut stack, None, vec![], &hook) { if let Err(err) = eval_hook(&mut engine_state, &mut stack, None, vec![], &hook) {
outcome_err(&engine_state, &err); outcome_err(&engine_state, &err);

View file

@ -1,5 +1,6 @@
mod test_bits; mod test_bits;
mod test_cell_path; mod test_cell_path;
mod test_commandline;
mod test_conditionals; mod test_conditionals;
mod test_config_path; mod test_config_path;
mod test_converters; mod test_converters;

View file

@ -0,0 +1,109 @@
use crate::tests::{fail_test, run_test, TestResult};
#[test]
fn commandline_test_get_empty() -> TestResult {
run_test("commandline", "")
}
#[test]
fn commandline_test_append() -> TestResult {
run_test(
"commandline --replace '0👩👩2'\n\
commandline --cursor '2'\n\
commandline --append 'ab'\n\
commandline\n\
commandline --cursor",
"0👩👩2ab\n\
2",
)
}
#[test]
fn commandline_test_insert() -> TestResult {
run_test(
"commandline --replace '0👩👩2'\n\
commandline --cursor '2'\n\
commandline --insert 'ab'\n\
commandline\n\
commandline --cursor",
"0👩👩ab2\n\
4",
)
}
#[test]
fn commandline_test_replace() -> TestResult {
run_test(
"commandline --replace '0👩👩2'\n\
commandline --replace 'ab'\n\
commandline\n\
commandline --cursor",
"ab\n\
2",
)
}
#[test]
fn commandline_test_cursor() -> TestResult {
run_test(
"commandline --replace '0👩👩2'\n\
commandline --cursor '1' \n\
commandline --insert 'x'\n\
commandline",
"0x👩👩2",
)?;
run_test(
"commandline --replace '0👩👩2'\n\
commandline --cursor '2' \n\
commandline --insert 'x'\n\
commandline",
"0👩👩x2",
)
}
#[test]
fn commandline_test_cursor_show_pos() -> TestResult {
run_test(
"commandline --replace '0👩👩2'\n\
commandline --cursor '1' \n\
commandline --cursor",
"1",
)?;
run_test(
"commandline --replace '0👩👩2'\n\
commandline --cursor '2' \n\
commandline --cursor",
"2",
)
}
#[test]
fn commandline_test_cursor_too_small() -> TestResult {
run_test(
"commandline --replace '123456'\n\
commandline --cursor '-1' \n\
commandline --insert '0'\n\
commandline",
"0123456",
)
}
#[test]
fn commandline_test_cursor_too_large() -> TestResult {
run_test(
"commandline --replace '123456'\n\
commandline --cursor '10' \n\
commandline --insert '0'\n\
commandline",
"1234560",
)
}
#[test]
fn commandline_test_cursor_invalid() -> TestResult {
fail_test(
"commandline --replace '123456'\n\
commandline --cursor 'abc'",
r#"string "abc" does not represent a valid integer"#,
)
}

View file

@ -325,6 +325,19 @@ fn pre_execution_block_preserve_env_var() {
assert_eq!(actual_repl.out, "spam"); assert_eq!(actual_repl.out, "spam");
} }
#[test]
fn pre_execution_commandline() {
let inp = &[
&pre_execution_hook_code(r#"{ let-env repl_commandline = (commandline) }"#),
"echo foo!; $env.repl_commandline",
];
let actual_repl = nu!(cwd: "tests/hooks", nu_repl_code(inp));
assert_eq!(actual_repl.err, "");
assert_eq!(actual_repl.out, "foo!echo foo!; $env.repl_commandline");
}
#[test] #[test]
fn env_change_shadow_command() { fn env_change_shadow_command() {
let inp = &[ let inp = &[