Skip to content

Commit

Permalink
Fix parsing of strings with special characters (#11030)
Browse files Browse the repository at this point in the history
<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->
If there were brackets in a string argument of a script it was always
interpreted as interpolation before the change. That lead to unexpected
outputs of such scripts. After this change arguments which are not
intended as interpolation (not starting with $) and containing brackets
will have implicitly added backticks for correct interpretation in the
scripts. This fixes #10908.

To fix other issues mentioned in #11035 I changed the deparsing logic.
Initially we added backticks for multi word variables and double quote
if there was \ or " in the string. My change would add double quotes any
time string starts with $ or contains any of character that might break
parsing. The characters I identified are white space, (, ', `, ",and \.
It's possible other characters should be added to this list.

I tested this solution with few simple scripts using both stand alone
arguments and flags and it seems to work but I would appreciate if
someone with more experience checked it with some more unusual cases I
missed.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->
Erroneous behaviour described  in the issue will no longer happen.

# 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
> ```
-->
Added tests for new formatting.

# 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.
-->
  • Loading branch information
MarikaChlebowska committed Jan 19, 2024
1 parent ff290a5 commit c8f30fa
Showing 1 changed file with 46 additions and 27 deletions.
73 changes: 46 additions & 27 deletions crates/nu-parser/src/deparse.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
fn string_should_be_quoted(input: &str) -> bool {
input.starts_with('$')
|| input
.chars()
.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('"');
Expand All @@ -14,37 +21,32 @@ pub fn escape_quote_string(input: &str) -> String {
}

// Escape rules:
// input argument contains ' '(like abc def), we will convert it to `abc def`.
// input argument contains --version='xx yy', we will convert it to --version=`'xx yy'`
// input argument contains " or \, we will try to escape input.
// 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")
// input argument is a flag without =, it's passed as it is (--foo -> --foo)
// input argument is a flag with =, the first two points apply to the value (--foo=bar -> --foo=bar; --foo=bar' -> --foo="bar'")
//
// special characters are white space, (, ', `, ",and \
pub fn escape_for_script_arg(input: &str) -> String {
// handle for flag, maybe we need to escape the value.
if input.starts_with("--") {
if let Some((arg_name, arg_val)) = input.split_once('=') {
// only want to escape arg_val.
let arg_val = if arg_val.contains(' ') {
format!("`{arg_val}`")
} else if arg_val.contains('"') || arg_val.contains('\\') {
let arg_val = if string_should_be_quoted(arg_val) {
escape_quote_string(arg_val)
} else if arg_val.is_empty() {
// return an empty string
"''".to_string()
} else {
arg_val.into()
};

return format!("{arg_name}={arg_val}");
} else {
return input.into();
}
}

if input.contains(' ') {
format!("`{input}`")
} else if input.contains('"') || input.contains('\\') {
if string_should_be_quoted(input) {
escape_quote_string(input)
} else if input.is_empty() {
// return an empty string
"''".to_string()
} else {
input.to_string()
input.into()
}
}

Expand All @@ -55,15 +57,28 @@ mod test {
#[test]
fn test_not_extra_quote() {
// check for input arg like this:
// nu b.nu 8
// nu b.nu word 8
assert_eq!(escape_for_script_arg("word"), "word".to_string());
assert_eq!(escape_for_script_arg("8"), "8".to_string());
}

#[test]
fn test_quote_special() {
// check for input arg like this:
// nu b.nu "two words" $nake "`123"
assert_eq!(
escape_for_script_arg("two words"),
r#""two words""#.to_string()
);
assert_eq!(escape_for_script_arg("$nake"), r#""$nake""#.to_string());
assert_eq!(escape_for_script_arg("`123"), r#""`123""#.to_string());
}

#[test]
fn test_arg_with_flag() {
// check for input arg like this:
// nu b.nu linux --version=v5.2
assert_eq!(escape_for_script_arg("linux"), "linux".to_string());
// nu b.nu --linux --version=v5.2
assert_eq!(escape_for_script_arg("--linux"), "--linux".to_string());
assert_eq!(
escape_for_script_arg("--version=v5.2"),
"--version=v5.2".to_string()
Expand All @@ -76,23 +91,27 @@ mod test {
}

#[test]
fn test_flag_arg_with_values_contains_space() {
fn test_flag_arg_with_values_contains_special() {
// check for input arg like this:
// nu b.nu test_ver --version='xx yy' --arch=ghi
// nu b.nu test_ver --version='xx yy' --separator="`"
assert_eq!(
escape_for_script_arg("--version='xx yy'"),
"--version=`'xx yy'`".to_string()
r#"--version="'xx yy'""#.to_string()
);
assert_eq!(
escape_for_script_arg("--arch=ghi"),
"--arch=ghi".to_string()
escape_for_script_arg("--separator=`"),
r#"--separator="`""#.to_string()
);
}

#[test]
fn test_escape() {
// check for input arg like this:
// nu b.nu '"'
assert_eq!(escape_for_script_arg(r#"""#), r#""\"""#.to_string());
// nu b.nu \ --arg='"'
assert_eq!(escape_for_script_arg(r#"\"#), r#""\\""#.to_string());

Check failure on line 111 in crates/nu-parser/src/deparse.rs

View workflow job for this annotation

GitHub Actions / fmt-clippy (ubuntu-20.04, default)

unnecessary hashes around raw string literal

Check failure on line 111 in crates/nu-parser/src/deparse.rs

View workflow job for this annotation

GitHub Actions / fmt-clippy (ubuntu-20.04, extra)

unnecessary hashes around raw string literal
assert_eq!(
escape_for_script_arg(r#"--arg=""#),
r#"--arg="\"""#.to_string()
);
}
}

0 comments on commit c8f30fa

Please sign in to comment.