diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 38a9ac101..d1639966c 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -150,6 +150,7 @@ pub fn create_default_context() -> EngineState { bind_command! { Ansi, AnsiGradient, + AnsiStrip, Clear, Kill, Sleep, diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs index d40fd0a87..ae6b85699 100644 --- a/crates/nu-command/src/example_test.rs +++ b/crates/nu-command/src/example_test.rs @@ -7,7 +7,7 @@ use nu_protocol::{ use crate::To; -use super::{Date, From, Into, Math, Path, Random, Split, Str, StrCollect, Url}; +use super::{Ansi, Date, From, Into, Math, Path, Random, Split, Str, StrCollect, Url}; pub fn test_examples(cmd: impl Command + 'static) { let examples = cmd.examples(); @@ -28,7 +28,7 @@ pub fn test_examples(cmd: impl Command + 'static) { working_set.add_decl(Box::new(Path)); working_set.add_decl(Box::new(Date)); working_set.add_decl(Box::new(Url)); - working_set.add_decl(Box::new(StrCollect)); + working_set.add_decl(Box::new(Ansi)); use super::Echo; working_set.add_decl(Box::new(Echo)); diff --git a/crates/nu-command/src/platform/ansi/mod.rs b/crates/nu-command/src/platform/ansi/mod.rs index d485c47fa..d62dea52f 100644 --- a/crates/nu-command/src/platform/ansi/mod.rs +++ b/crates/nu-command/src/platform/ansi/mod.rs @@ -1,5 +1,7 @@ mod command; mod gradient; +mod strip; pub use command::AnsiCommand as Ansi; pub use gradient::SubCommand as AnsiGradient; +pub use strip::SubCommand as AnsiStrip; diff --git a/crates/nu-command/src/platform/ansi/strip.rs b/crates/nu-command/src/platform/ansi/strip.rs new file mode 100644 index 000000000..987f4e460 --- /dev/null +++ b/crates/nu-command/src/platform/ansi/strip.rs @@ -0,0 +1,123 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, ast::CellPath, engine::Command, engine::EngineState, engine::Stack, Category, + Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use strip_ansi_escapes::strip; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "ansi strip" + } + + fn signature(&self) -> Signature { + Signature::build("ansi strip") + .rest( + "column path", + SyntaxShape::CellPath, + "optionally, remove ansi sequences by column paths", + ) + .category(Category::Platform) + } + + fn usage(&self) -> &str { + "strip ansi escape sequences from string" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "strip ansi escape sequences from string", + example: r#"echo [ (ansi green) (ansi cursor_on) "hello" ] | str collect | ansi strip"#, + result: Some(Value::test_string("hello")), + }] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + let head = call.head; + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, &head) + } else { + let mut ret = v; + + for path in &column_paths { + let r = ret + .update_cell_path(&path.members, Box::new(move |old| action(old, &head))); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action(input: &Value, command_span: &Span) -> Value { + match input { + Value::String { val, span } => { + let stripped_string = { + if let Ok(bytes) = strip(&val) { + String::from_utf8_lossy(&bytes).to_string() + } else { + val.to_string() + } + }; + + Value::string(stripped_string, *span) + } + other => { + let got = format!("value is {}, not string", other.get_type().to_string()); + + Value::Error { + error: ShellError::TypeMismatch(got, other.span().unwrap_or(*command_span)), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{action, SubCommand}; + use nu_protocol::{Span, Value}; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn test_stripping() { + let input_string = + Value::test_string("\u{1b}[3;93;41mHello\u{1b}[0m \u{1b}[1;32mNu \u{1b}[1;35mWorld"); + let expected = Value::test_string("Hello Nu World"); + + let actual = action(&input_string, &Span::unknown()); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/platform/mod.rs b/crates/nu-command/src/platform/mod.rs index 63ae7fdd2..65836f079 100644 --- a/crates/nu-command/src/platform/mod.rs +++ b/crates/nu-command/src/platform/mod.rs @@ -3,7 +3,7 @@ mod clear; mod kill; mod sleep; -pub use ansi::{Ansi, AnsiGradient}; +pub use ansi::{Ansi, AnsiGradient, AnsiStrip}; pub use clear::Clear; pub use kill::Kill; pub use sleep::Sleep;