make the ansi command const (#11682)

# Description

This PR changes the `ansi` command to be a `const` command. 

- ~~It's breaking because I found that I had to change the way `ansi` is
used in scripts a little bit.
https://github.com/nushell/nu_scripts/pull/751~~

- I had to change one of the examples because apparently `const` can't
be tested yet.

- ~~I'm not sure this is right at all
https://github.com/nushell/nushell/pull/11682/files#diff-ba932369a40eb40d6e1985eac1c784af403dab4500a7f0568e593900bf6cd740R654-R655.
I just didn't want to duplicate a ton of code. Maybe if I duplicated the
code it wouldn't be a breaking change because it would have a run and
run_const?~~

- I had to add `opt_const` to CallExt.

/cc @kubouch Can you take a look at this? I'm a little iffy if I'm doing
this right, or even if we should do this at all.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking 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` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use std testing; testing run-tests --path
crates/nu-std"` to run the tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# 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:
Darren Schroeder 2024-01-30 16:09:43 -06:00 committed by GitHub
parent cf9813cbf8
commit 0a355db5c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 182 additions and 120 deletions

View File

@ -1,13 +1,16 @@
use nu_ansi_term::*; use nu_ansi_term::*;
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::record;
use nu_protocol::{ use nu_protocol::{
ast::Call, engine::Command, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, ast::Call,
PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, engine::Command,
engine::StateWorkingSet,
engine::{EngineState, Stack},
record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData,
ShellError, Signature, Span, SyntaxShape, Type, Value,
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{atomic::AtomicBool, Arc};
#[derive(Clone)] #[derive(Clone)]
pub struct AnsiCommand; pub struct AnsiCommand;
@ -529,6 +532,10 @@ impl Command for AnsiCommand {
.category(Category::Platform) .category(Category::Platform)
} }
fn is_const(&self) -> bool {
true
}
fn usage(&self) -> &str { fn usage(&self) -> &str {
"Output ANSI codes to change color and style of text." "Output ANSI codes to change color and style of text."
} }
@ -632,9 +639,9 @@ Operating system commands:
bg: '#ff0000' bg: '#ff0000'
attr: b attr: b
} }
$"(ansi --escape $bold_blue_on_red)Hello Nu World(ansi reset)""#, $"(ansi --escape $bold_blue_on_red)Hello, Nu World!(ansi reset)""#,
result: Some(Value::test_string( result: Some(Value::test_string(
"\u{1b}[1;48;2;255;0;0;38;2;0;0;255mHello Nu World\u{1b}[0m", "\u{1b}[1;48;2;255;0;0;38;2;0;0;255mHello, Nu World!\u{1b}[0m",
)), )),
}, },
] ]
@ -655,9 +662,10 @@ Operating system commands:
let escape: bool = call.has_flag(engine_state, stack, "escape")?; let escape: bool = call.has_flag(engine_state, stack, "escape")?;
let osc: bool = call.has_flag(engine_state, stack, "osc")?; let osc: bool = call.has_flag(engine_state, stack, "osc")?;
let use_ansi_coloring = engine_state.get_config().use_ansi_coloring; let use_ansi_coloring = engine_state.get_config().use_ansi_coloring;
let ctrlc = engine_state.ctrlc.clone();
if list { if list {
return generate_ansi_code_list(engine_state, call.head, use_ansi_coloring); return generate_ansi_code_list(ctrlc, call.head, use_ansi_coloring);
} }
// The code can now be one of the ansi abbreviations like green_bold // The code can now be one of the ansi abbreviations like green_bold
@ -673,118 +681,152 @@ Operating system commands:
} }
}; };
let param_is_string = matches!(code, Value::String { .. }); let output = heavy_lifting(code, escape, osc, call)?;
if escape && osc {
return Err(ShellError::IncompatibleParameters {
left_message: "escape".into(),
left_span: call
.get_named_arg("escape")
.expect("Unexpected missing argument")
.span,
right_message: "osc".into(),
right_span: call
.get_named_arg("osc")
.expect("Unexpected missing argument")
.span,
});
}
let code_string = if param_is_string {
code.as_string().expect("error getting code as string")
} else {
"".to_string()
};
let param_is_valid_string = param_is_string && !code_string.is_empty();
if (escape || osc) && (param_is_valid_string) {
let code_vec: Vec<char> = code_string.chars().collect();
if code_vec[0] == '\\' {
let span = match call.get_flag_expr("escape") {
Some(expr) => expr.span,
None => call.head,
};
return Err(ShellError::TypeMismatch {
err_message: "no need for escape characters".into(),
span,
});
}
}
let output = if escape && param_is_valid_string {
format!("\x1b[{code_string}")
} else if osc && param_is_valid_string {
// Operating system command aka osc ESC ] <- note the right brace, not left brace for osc
// OCS's need to end with either:
// bel '\x07' char
// string terminator aka st '\\' char
format!("\x1b]{code_string}")
} else if param_is_valid_string {
// parse hex colors like #00FF00
if code_string.starts_with('#') {
match nu_color_config::color_from_hex(&code_string) {
Ok(color) => match color {
Some(c) => c.prefix().to_string(),
None => Color::White.prefix().to_string(),
},
Err(err) => {
return Err(ShellError::GenericError {
error: "error parsing hex color".into(),
msg: format!("{err}"),
span: Some(code.span()),
help: None,
inner: vec![],
});
}
}
} else {
match str_to_ansi(&code_string) {
Some(c) => c,
None => {
return Err(ShellError::TypeMismatch {
err_message: String::from("Unknown ansi code"),
span: call
.positional_nth(0)
.expect("Unexpected missing argument")
.span,
})
}
}
}
} else {
// This is a record that should look like
// { fg: "#ff0000" bg: "#00ff00" attr: bli }
let record = code.as_record()?;
// create a NuStyle to parse the information into
let mut nu_style = nu_color_config::NuStyle {
fg: None,
bg: None,
attr: None,
};
// Iterate and populate NuStyle with real values
for (k, v) in record {
match k.as_str() {
"fg" => nu_style.fg = Some(v.as_string()?),
"bg" => nu_style.bg = Some(v.as_string()?),
"attr" => nu_style.attr = Some(v.as_string()?),
_ => {
return Err(ShellError::IncompatibleParametersSingle {
msg: format!("unknown ANSI format key: expected one of ['fg', 'bg', 'attr'], found '{k}'"),
span: code.span(),
})
}
}
}
// Now create a nu_ansi_term::Style from the NuStyle
let style = nu_color_config::parse_nustyle(nu_style);
// Return the prefix string. The prefix is the Ansi String. The suffix would be 0m, reset/stop coloring.
style.prefix().to_string()
};
Ok(Value::string(output, call.head).into_pipeline_data()) Ok(Value::string(output, call.head).into_pipeline_data())
} }
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let list: bool = call.has_flag_const(working_set, "list")?;
let escape: bool = call.has_flag_const(working_set, "escape")?;
let osc: bool = call.has_flag_const(working_set, "osc")?;
let use_ansi_coloring = working_set.get_config().use_ansi_coloring;
let ctrlc = working_set.permanent().ctrlc.clone();
if list {
return generate_ansi_code_list(ctrlc, call.head, use_ansi_coloring);
}
// The code can now be one of the ansi abbreviations like green_bold
// or it can be a record like this: { fg: "#ff0000" bg: "#00ff00" attr: bli }
// this record is defined in nu-color-config crate
let code: Value = match call.opt_const(working_set, 0)? {
Some(c) => c,
None => {
return Err(ShellError::MissingParameter {
param_name: "code".into(),
span: call.head,
})
}
};
let output = heavy_lifting(code, escape, osc, call)?;
Ok(Value::string(output, call.head).into_pipeline_data())
}
}
fn heavy_lifting(code: Value, escape: bool, osc: bool, call: &Call) -> Result<String, ShellError> {
let param_is_string = matches!(code, Value::String { .. });
if escape && osc {
return Err(ShellError::IncompatibleParameters {
left_message: "escape".into(),
left_span: call
.get_named_arg("escape")
.expect("Unexpected missing argument")
.span,
right_message: "osc".into(),
right_span: call
.get_named_arg("osc")
.expect("Unexpected missing argument")
.span,
});
}
let code_string = if param_is_string {
code.as_string().expect("error getting code as string")
} else {
"".to_string()
};
let param_is_valid_string = param_is_string && !code_string.is_empty();
if (escape || osc) && (param_is_valid_string) {
let code_vec: Vec<char> = code_string.chars().collect();
if code_vec[0] == '\\' {
let span = match call.get_flag_expr("escape") {
Some(expr) => expr.span,
None => call.head,
};
return Err(ShellError::TypeMismatch {
err_message: "no need for escape characters".into(),
span,
});
}
}
let output = if escape && param_is_valid_string {
format!("\x1b[{code_string}")
} else if osc && param_is_valid_string {
// Operating system command aka osc ESC ] <- note the right brace, not left brace for osc
// OCS's need to end with either:
// bel '\x07' char
// string terminator aka st '\\' char
format!("\x1b]{code_string}")
} else if param_is_valid_string {
// parse hex colors like #00FF00
if code_string.starts_with('#') {
match nu_color_config::color_from_hex(&code_string) {
Ok(color) => match color {
Some(c) => c.prefix().to_string(),
None => Color::White.prefix().to_string(),
},
Err(err) => {
return Err(ShellError::GenericError {
error: "error parsing hex color".into(),
msg: format!("{err}"),
span: Some(code.span()),
help: None,
inner: vec![],
});
}
}
} else {
match str_to_ansi(&code_string) {
Some(c) => c,
None => {
return Err(ShellError::TypeMismatch {
err_message: String::from("Unknown ansi code"),
span: call
.positional_nth(0)
.expect("Unexpected missing argument")
.span,
})
}
}
}
} else {
// This is a record that should look like
// { fg: "#ff0000" bg: "#00ff00" attr: bli }
let record = code.as_record()?;
// create a NuStyle to parse the information into
let mut nu_style = nu_color_config::NuStyle {
fg: None,
bg: None,
attr: None,
};
// Iterate and populate NuStyle with real values
for (k, v) in record {
match k.as_str() {
"fg" => nu_style.fg = Some(v.as_string()?),
"bg" => nu_style.bg = Some(v.as_string()?),
"attr" => nu_style.attr = Some(v.as_string()?),
_ => {
return Err(ShellError::IncompatibleParametersSingle {
msg: format!("unknown ANSI format key: expected one of ['fg', 'bg', 'attr'], found '{k}'"),
span: code.span(),
})
}
}
}
// Now create a nu_ansi_term::Style from the NuStyle
let style = nu_color_config::parse_nustyle(nu_style);
// Return the prefix string. The prefix is the Ansi String. The suffix would be 0m, reset/stop coloring.
style.prefix().to_string()
};
Ok(output)
} }
pub fn str_to_ansi(s: &str) -> Option<String> { pub fn str_to_ansi(s: &str) -> Option<String> {
@ -792,7 +834,7 @@ pub fn str_to_ansi(s: &str) -> Option<String> {
} }
fn generate_ansi_code_list( fn generate_ansi_code_list(
engine_state: &EngineState, ctrlc: Option<Arc<AtomicBool>>,
call_span: Span, call_span: Span,
use_ansi_coloring: bool, use_ansi_coloring: bool,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
@ -827,7 +869,7 @@ fn generate_ansi_code_list(
Value::record(record, call_span) Value::record(record, call_span)
}) })
.into_pipeline_data(engine_state.ctrlc.clone())); .into_pipeline_data(ctrlc));
} }
fn build_ansi_hashmap(v: &[AnsiCode]) -> HashMap<&str, &str> { fn build_ansi_hashmap(v: &[AnsiCode]) -> HashMap<&str, &str> {

View File

@ -283,7 +283,7 @@ fn errors_if_moving_to_itself() {
"umv mydir mydir/mydir_2/" "umv mydir mydir/mydir_2/"
); );
assert!(actual.err.contains("cannot move")); assert!(actual.err.contains("cannot move"));
assert!(actual.err.contains("to a subdirectory of")); assert!(actual.err.contains("to a subdirectory"));
}); });
} }

View File

@ -1,6 +1,7 @@
use nu_protocol::{ use nu_protocol::{
ast::Call, ast::Call,
engine::{EngineState, Stack}, engine::{EngineState, Stack, StateWorkingSet},
eval_const::eval_constant,
FromValue, ShellError, Value, FromValue, ShellError, Value,
}; };
@ -36,6 +37,12 @@ pub trait CallExt {
pos: usize, pos: usize,
) -> Result<Option<T>, ShellError>; ) -> Result<Option<T>, ShellError>;
fn opt_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
pos: usize,
) -> Result<Option<T>, ShellError>;
fn req<T: FromValue>( fn req<T: FromValue>(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
@ -126,6 +133,19 @@ impl CallExt for Call {
} }
} }
fn opt_const<T: FromValue>(
&self,
working_set: &StateWorkingSet,
pos: usize,
) -> Result<Option<T>, ShellError> {
if let Some(expr) = self.positional_nth(pos) {
let result = eval_constant(working_set, expr)?;
FromValue::from_value(result).map(Some)
} else {
Ok(None)
}
}
fn req<T: FromValue>( fn req<T: FromValue>(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,