diff --git a/Cargo.lock b/Cargo.lock index ac4bdb3ced..6b7f9a1fe6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3317,6 +3317,7 @@ dependencies = [ "nu-path", "nu-plugin-engine", "nu-protocol", + "nu-utils", "rstest", "serde_json", ] @@ -3522,10 +3523,12 @@ name = "nu-utils" version = "0.99.2" dependencies = [ "crossterm_winapi", + "fancy-regex", "log", "lscolors", "nix 0.29.0", "num-format", + "once_cell", "serde", "strip-ansi-escapes", "sys-locale", @@ -3755,11 +3758,10 @@ name = "nuon" version = "0.99.2" dependencies = [ "chrono", - "fancy-regex", "nu-engine", "nu-parser", "nu-protocol", - "once_cell", + "nu-utils", ] [[package]] diff --git a/crates/nu-cli/src/util.rs b/crates/nu-cli/src/util.rs index 0285664fb9..67bc1ad8ef 100644 --- a/crates/nu-cli/src/util.rs +++ b/crates/nu-cli/src/util.rs @@ -1,6 +1,6 @@ use nu_cmd_base::hook::eval_hook; use nu_engine::{eval_block, eval_block_with_early_return}; -use nu_parser::{escape_quote_string, lex, parse, unescape_unquote_string, Token, TokenContents}; +use nu_parser::{lex, parse, unescape_unquote_string, Token, TokenContents}; use nu_protocol::{ cli_error::report_compile_error, debugger::WithoutDebug, @@ -10,7 +10,7 @@ use nu_protocol::{ }; #[cfg(windows)] use nu_utils::enable_vt_processing; -use nu_utils::perf; +use nu_utils::{escape_quote_string, perf}; use std::path::Path; // This will collect environment variables from std::env and adds them to a stack. diff --git a/crates/nu-parser/Cargo.toml b/crates/nu-parser/Cargo.toml index 0c331563fa..a64acbbd0b 100644 --- a/crates/nu-parser/Cargo.toml +++ b/crates/nu-parser/Cargo.toml @@ -19,6 +19,7 @@ nu-engine = { path = "../nu-engine", version = "0.99.2" } nu-path = { path = "../nu-path", version = "0.99.2" } nu-plugin-engine = { path = "../nu-plugin-engine", optional = true, version = "0.99.2" } nu-protocol = { path = "../nu-protocol", version = "0.99.2" } +nu-utils = { path = "../nu-utils", version = "0.99.2" } bytesize = { workspace = true } chrono = { default-features = false, features = ['std'], workspace = true } diff --git a/crates/nu-parser/src/deparse.rs b/crates/nu-parser/src/deparse.rs index 2a6339a5cb..a06bc5b6dc 100644 --- a/crates/nu-parser/src/deparse.rs +++ b/crates/nu-parser/src/deparse.rs @@ -1,3 +1,5 @@ +use nu_utils::escape_quote_string; + fn string_should_be_quoted(input: &str) -> bool { input.starts_with('$') || input @@ -5,21 +7,6 @@ fn string_should_be_quoted(input: &str) -> bool { .any(|c| c == ' ' || c == '(' || c == '\'' || c == '`' || c == '"' || c == '\\') } -pub fn escape_quote_string(input: &str) -> String { - let mut output = String::with_capacity(input.len() + 2); - output.push('"'); - - for c in input.chars() { - if c == '"' || c == '\\' { - output.push('\\'); - } - output.push(c); - } - - output.push('"'); - output -} - // Escape rules: // input argument is not a flag, does not start with $ and doesn't contain special characters, it is passed as it is (foo -> foo) // input argument is not a flag and either starts with $ or contains special characters, quotes are added, " and \ are escaped (two \words -> "two \\words") diff --git a/crates/nu-parser/src/lib.rs b/crates/nu-parser/src/lib.rs index c5d69cb270..8f698f22c9 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -11,7 +11,7 @@ mod parse_shape_specs; mod parser; mod type_check; -pub use deparse::{escape_for_script_arg, escape_quote_string}; +pub use deparse::escape_for_script_arg; pub use flatten::{ flatten_block, flatten_expression, flatten_pipeline, flatten_pipeline_element, FlatShape, }; diff --git a/crates/nu-test-support/src/macros.rs b/crates/nu-test-support/src/macros.rs index 1ae7ce593e..fdfdabd0b5 100644 --- a/crates/nu-test-support/src/macros.rs +++ b/crates/nu-test-support/src/macros.rs @@ -235,6 +235,7 @@ macro_rules! nu_with_plugins { use crate::{Outcome, NATIVE_PATH_ENV_VAR}; use nu_path::{AbsolutePath, AbsolutePathBuf, Path, PathBuf}; +use nu_utils::escape_quote_string; use std::{ ffi::OsStr, process::{Command, Stdio}, @@ -421,21 +422,6 @@ where Outcome::new(out, err.into_owned(), output.status) } -fn escape_quote_string(input: &str) -> String { - let mut output = String::with_capacity(input.len() + 2); - output.push('"'); - - for c in input.chars() { - if c == '"' || c == '\\' { - output.push('\\'); - } - output.push(c); - } - - output.push('"'); - output -} - fn with_exe(name: &str) -> String { #[cfg(windows)] { diff --git a/crates/nu-utils/Cargo.toml b/crates/nu-utils/Cargo.toml index 5f1f3a22ba..d4639805ea 100644 --- a/crates/nu-utils/Cargo.toml +++ b/crates/nu-utils/Cargo.toml @@ -17,9 +17,11 @@ bench = false bench = false [dependencies] +fancy-regex = { workspace = true } lscolors = { workspace = true, default-features = false, features = ["nu-ansi-term"] } log = { workspace = true } num-format = { workspace = true } +once_cell = { workspace = true } strip-ansi-escapes = { workspace = true } serde = { workspace = true } sys-locale = "0.3" diff --git a/crates/nu-utils/src/lib.rs b/crates/nu-utils/src/lib.rs index 0c021f1dc9..4b91d923db 100644 --- a/crates/nu-utils/src/lib.rs +++ b/crates/nu-utils/src/lib.rs @@ -4,6 +4,7 @@ mod deansi; pub mod emoji; pub mod filesystem; pub mod locale; +mod quoting; mod shared_cow; pub mod utils; @@ -18,6 +19,7 @@ pub use deansi::{ strip_ansi_likely, strip_ansi_string_likely, strip_ansi_string_unlikely, strip_ansi_unlikely, }; pub use emoji::contains_emoji; +pub use quoting::{escape_quote_string, needs_quoting}; pub use shared_cow::SharedCow; #[cfg(unix)] diff --git a/crates/nu-utils/src/quoting.rs b/crates/nu-utils/src/quoting.rs new file mode 100644 index 0000000000..f194095ee6 --- /dev/null +++ b/crates/nu-utils/src/quoting.rs @@ -0,0 +1,43 @@ +use fancy_regex::Regex; +use once_cell::sync::Lazy; + +// This hits, in order: +// • Any character of []:`{}#'";()|$, +// • Any digit (\d) +// • Any whitespace (\s) +// • Case-insensitive sign-insensitive float "keywords" inf, infinity and nan. +static NEEDS_QUOTING_REGEX: Lazy = Lazy::new(|| { + Regex::new(r#"[\[\]:`\{\}#'";\(\)\|\$,\d\s]|(?i)^[+\-]?(inf(inity)?|nan)$"#) + .expect("internal error: NEEDS_QUOTING_REGEX didn't compile") +}); + +pub fn needs_quoting(string: &str) -> bool { + if string.is_empty() { + return true; + } + // These are case-sensitive keywords + match string { + // `true`/`false`/`null` are active keywords in JSON and NUON + // `&&` is denied by the nu parser for diagnostics reasons + // (https://github.com/nushell/nushell/pull/7241) + "true" | "false" | "null" | "&&" => return true, + _ => (), + }; + // All other cases are handled here + NEEDS_QUOTING_REGEX.is_match(string).unwrap_or(false) +} + +pub fn escape_quote_string(string: &str) -> String { + let mut output = String::with_capacity(string.len() + 2); + output.push('"'); + + for c in string.chars() { + if c == '"' || c == '\\' { + output.push('\\'); + } + output.push(c); + } + + output.push('"'); + output +} diff --git a/crates/nuon/Cargo.toml b/crates/nuon/Cargo.toml index 795f72b9ef..edaa2d1072 100644 --- a/crates/nuon/Cargo.toml +++ b/crates/nuon/Cargo.toml @@ -13,8 +13,7 @@ version = "0.99.2" nu-parser = { path = "../nu-parser", version = "0.99.2" } nu-protocol = { path = "../nu-protocol", version = "0.99.2" } nu-engine = { path = "../nu-engine", version = "0.99.2" } -once_cell = { workspace = true } -fancy-regex = { workspace = true } +nu-utils = { path = "../nu-utils", version = "0.99.2" } [dev-dependencies] chrono = { workspace = true } diff --git a/crates/nuon/src/to.rs b/crates/nuon/src/to.rs index 57f208dcc0..c2af51d0bb 100644 --- a/crates/nuon/src/to.rs +++ b/crates/nuon/src/to.rs @@ -1,10 +1,8 @@ use core::fmt::Write; -use fancy_regex::Regex; -use once_cell::sync::Lazy; use nu_engine::get_columns; -use nu_parser::escape_quote_string; use nu_protocol::{Range, ShellError, Span, Value}; +use nu_utils::{escape_quote_string, needs_quoting}; use std::ops::Bound; @@ -129,11 +127,12 @@ fn value_to_string( let headers: Vec = headers .iter() .map(|string| { - if needs_quotes(string) { - format!("{idt}\"{string}\"") + let string = if needs_quoting(string) { + &escape_quote_string(string) } else { - format!("{idt}{string}") - } + string + }; + format!("{idt}{string}") }) .collect(); let headers_output = headers.join(&format!(",{sep}{nl}{idt_pt}")); @@ -199,19 +198,15 @@ fn value_to_string( Value::Record { val, .. } => { let mut collection = vec![]; for (col, val) in &**val { - collection.push(if needs_quotes(col) { - format!( - "{idt_po}\"{}\": {}", - col, - value_to_string_without_quotes(val, span, depth + 1, indent)? - ) + let col = if needs_quoting(col) { + &escape_quote_string(col) } else { - format!( - "{idt_po}{}: {}", - col, - value_to_string_without_quotes(val, span, depth + 1, indent)? - ) - }); + col + }; + collection.push(format!( + "{idt_po}{col}: {}", + value_to_string_without_quotes(val, span, depth + 1, indent)? + )); } Ok(format!( "{{{nl}{}{nl}{idt}}}", @@ -247,7 +242,7 @@ fn value_to_string_without_quotes( ) -> Result { match v { Value::String { val, .. } => Ok({ - if needs_quotes(val) { + if needs_quoting(val) { escape_quote_string(val) } else { val.clone() @@ -256,30 +251,3 @@ fn value_to_string_without_quotes( _ => value_to_string(v, span, depth, indent), } } - -// This hits, in order: -// • Any character of []:`{}#'";()|$, -// • Any digit (\d) -// • Any whitespace (\s) -// • Case-insensitive sign-insensitive float "keywords" inf, infinity and nan. -static NEEDS_QUOTES_REGEX: Lazy = Lazy::new(|| { - Regex::new(r#"[\[\]:`\{\}#'";\(\)\|\$,\d\s]|(?i)^[+\-]?(inf(inity)?|nan)$"#) - .expect("internal error: NEEDS_QUOTES_REGEX didn't compile") -}); - -fn needs_quotes(string: &str) -> bool { - if string.is_empty() { - return true; - } - // These are case-sensitive keywords - match string { - // `true`/`false`/`null` are active keywords in JSON and NUON - // `&&` is denied by the nu parser for diagnostics reasons - // (https://github.com/nushell/nushell/pull/7241) - // TODO: remove the extra check in the nuon codepath - "true" | "false" | "null" | "&&" => return true, - _ => (), - }; - // All other cases are handled here - NEEDS_QUOTES_REGEX.is_match(string).unwrap_or(false) -} diff --git a/src/command.rs b/src/command.rs index 2ba817ac6b..17a78e0342 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,11 +1,11 @@ use nu_engine::{command_prelude::*, get_full_help}; -use nu_parser::{escape_for_script_arg, escape_quote_string, parse}; +use nu_parser::{escape_for_script_arg, parse}; use nu_protocol::{ ast::{Expr, Expression}, engine::StateWorkingSet, report_parse_error, }; -use nu_utils::stdout_write_all_and_flush; +use nu_utils::{escape_quote_string, stdout_write_all_and_flush}; pub(crate) fn gather_commandline_args() -> (Vec, String, Vec) { // Would be nice if we had a way to parse this. The first flags we see will be going to nushell