mirror of
https://github.com/fish-shell/fish-shell.git
synced 2024-12-19 21:43:41 +08:00
b00899179f
On a command with multiline quoted string like begin echo "line1 line2" end we actually indent line2 which seeems misleading because the indentation changes the behavior when typed into a script. This has become more prominent since commits -a37629f86
(fish_clipboard_copy: indent multiline commands, 2024-04-13) -611a0572b
(builtins type/functions: indent interactively-defined functions, 2024-04-12) -222673f33
(edit_command_buffer: send indented commandline to editor, 2024-04-12) which add indentation to an exported commandline. Never indent quoted strings, to make sure the rendering matches the semantics. Note that we do need to indent the opening quote which is fine because it's on the same line. While at it, indent command substitutions recursively. That feature should also be added to fish_indent's formatting mode (which is the default). Fortunately the formatting mode already works fine with quoted strings; it does not indent them. Not sure how that's done and whether indentation can use the same logic.
440 lines
13 KiB
Rust
440 lines
13 KiB
Rust
use pcre2::utf32::Regex;
|
|
|
|
use crate::common::EscapeFlags;
|
|
use crate::parse_constants::{
|
|
ERROR_BAD_VAR_CHAR1, ERROR_BRACKETED_VARIABLE1, ERROR_BRACKETED_VARIABLE_QUOTED1,
|
|
ERROR_NOT_ARGV_AT, ERROR_NOT_ARGV_COUNT, ERROR_NOT_ARGV_STAR, ERROR_NOT_PID, ERROR_NOT_STATUS,
|
|
ERROR_NO_VAR_NAME,
|
|
};
|
|
use crate::parse_util::{
|
|
parse_util_cmdsubst_extent, parse_util_compute_indents, parse_util_detect_errors,
|
|
parse_util_escape_string_with_quote, parse_util_process_extent, parse_util_slice_length,
|
|
BOOL_AFTER_BACKGROUND_ERROR_MSG,
|
|
};
|
|
use crate::tests::prelude::*;
|
|
use crate::wchar::prelude::*;
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn test_error_messages() {
|
|
let _cleanup = test_init();
|
|
// Given a format string, returns a list of non-empty strings separated by format specifiers. The
|
|
// format specifiers themselves are omitted.
|
|
fn separate_by_format_specifiers(format: &wstr) -> Vec<&wstr> {
|
|
let format_specifier_regex = Regex::new(L!(r"%l?[cds]").as_char_slice()).unwrap();
|
|
let mut result = vec![];
|
|
let mut offset = 0;
|
|
for mtch in format_specifier_regex.find_iter(format.as_char_slice()) {
|
|
let mtch = mtch.unwrap();
|
|
let component = &format[offset..mtch.start()];
|
|
result.push(component);
|
|
offset = mtch.end();
|
|
}
|
|
result.push(&format[offset..]);
|
|
// Avoid mismatch from localized quotes.
|
|
for component in &mut result {
|
|
*component = component.trim_matches('\'');
|
|
}
|
|
result
|
|
}
|
|
|
|
// Given a format string 'format', return true if the string may have been produced by that format
|
|
// string. We do this by splitting the format string around the format specifiers, and then ensuring
|
|
// that each of the remaining chunks is found (in order) in the string.
|
|
fn string_matches_format(s: &wstr, format: &wstr) -> bool {
|
|
let components = separate_by_format_specifiers(format);
|
|
assert!(!components.is_empty());
|
|
let mut idx = 0;
|
|
for component in components {
|
|
let Some(relpos) = s[idx..].find(component) else {
|
|
return false;
|
|
};
|
|
idx += relpos + component.len();
|
|
assert!(idx <= s.len());
|
|
}
|
|
true
|
|
}
|
|
|
|
macro_rules! validate {
|
|
($src:expr, $error_text_format:expr) => {
|
|
let mut errors = vec![];
|
|
let res = parse_util_detect_errors(L!($src), Some(&mut errors), false);
|
|
let fmt = wgettext!($error_text_format);
|
|
assert!(res.is_err());
|
|
assert!(
|
|
string_matches_format(&errors[0].text, fmt),
|
|
"command '{}' is expected to match error pattern '{}' but is '{}'",
|
|
$src,
|
|
$error_text_format,
|
|
&errors[0].text
|
|
);
|
|
};
|
|
}
|
|
|
|
validate!("echo $^", ERROR_BAD_VAR_CHAR1);
|
|
validate!("echo foo${a}bar", ERROR_BRACKETED_VARIABLE1);
|
|
validate!("echo foo\"${a}\"bar", ERROR_BRACKETED_VARIABLE_QUOTED1);
|
|
validate!("echo foo\"${\"bar", ERROR_BAD_VAR_CHAR1);
|
|
validate!("echo $?", ERROR_NOT_STATUS);
|
|
validate!("echo $$", ERROR_NOT_PID);
|
|
validate!("echo $#", ERROR_NOT_ARGV_COUNT);
|
|
validate!("echo $@", ERROR_NOT_ARGV_AT);
|
|
validate!("echo $*", ERROR_NOT_ARGV_STAR);
|
|
validate!("echo $", ERROR_NO_VAR_NAME);
|
|
validate!("echo foo\"$\"bar", ERROR_NO_VAR_NAME);
|
|
validate!("echo \"foo\"$\"bar\"", ERROR_NO_VAR_NAME);
|
|
validate!("echo foo $ bar", ERROR_NO_VAR_NAME);
|
|
validate!("echo 1 & && echo 2", BOOL_AFTER_BACKGROUND_ERROR_MSG);
|
|
validate!(
|
|
"echo 1 && echo 2 & && echo 3",
|
|
BOOL_AFTER_BACKGROUND_ERROR_MSG
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_util_process_extent() {
|
|
macro_rules! validate {
|
|
($commandline:literal, $cursor:expr, $expected_range:expr) => {
|
|
assert_eq!(
|
|
parse_util_process_extent(L!($commandline), $cursor, None),
|
|
$expected_range
|
|
);
|
|
};
|
|
}
|
|
validate!("for file in (path base\necho", 22, 13..22);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn test_parse_util_cmdsubst_extent() {
|
|
let _cleanup = test_init();
|
|
const a: &wstr = L!("echo (echo (echo hi");
|
|
assert_eq!(parse_util_cmdsubst_extent(a, 0), 0..a.len());
|
|
assert_eq!(parse_util_cmdsubst_extent(a, 1), 0..a.len());
|
|
assert_eq!(parse_util_cmdsubst_extent(a, 2), 0..a.len());
|
|
assert_eq!(parse_util_cmdsubst_extent(a, 3), 0..a.len());
|
|
assert_eq!(
|
|
parse_util_cmdsubst_extent(a, 8),
|
|
"echo (".chars().count()..a.len()
|
|
);
|
|
assert_eq!(
|
|
parse_util_cmdsubst_extent(a, 17),
|
|
"echo (echo (".chars().count()..a.len()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn test_parse_util_slice_length() {
|
|
let _cleanup = test_init();
|
|
assert_eq!(parse_util_slice_length(L!("[2]")), Some(3));
|
|
assert_eq!(parse_util_slice_length(L!("[12]")), Some(4));
|
|
assert_eq!(parse_util_slice_length(L!("[\"foo\"]")), Some(7));
|
|
assert_eq!(parse_util_slice_length(L!("[\"foo\"")), None);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn test_escape_quotes() {
|
|
let _cleanup = test_init();
|
|
macro_rules! validate {
|
|
($cmd:expr, $quote:expr, $no_tilde:expr, $expected:expr) => {
|
|
assert_eq!(
|
|
parse_util_escape_string_with_quote(
|
|
L!($cmd),
|
|
$quote,
|
|
if $no_tilde {
|
|
EscapeFlags::NO_TILDE
|
|
} else {
|
|
EscapeFlags::empty()
|
|
}
|
|
),
|
|
L!($expected)
|
|
);
|
|
};
|
|
}
|
|
macro_rules! validate_no_quoted {
|
|
($cmd:expr, $quote:expr, $no_tilde:expr, $expected:expr) => {
|
|
assert_eq!(
|
|
parse_util_escape_string_with_quote(
|
|
L!($cmd),
|
|
$quote,
|
|
EscapeFlags::NO_QUOTED
|
|
| if $no_tilde {
|
|
EscapeFlags::NO_TILDE
|
|
} else {
|
|
EscapeFlags::empty()
|
|
}
|
|
),
|
|
L!($expected)
|
|
);
|
|
};
|
|
}
|
|
|
|
validate!("abc~def", None, false, "'abc~def'");
|
|
validate!("abc~def", None, true, "abc~def");
|
|
validate!("~abc", None, false, "'~abc'");
|
|
validate!("~abc", None, true, "~abc");
|
|
|
|
// These are "raw string literals"
|
|
validate_no_quoted!("abc", None, false, "abc");
|
|
validate_no_quoted!("abc~def", None, false, "abc\\~def");
|
|
validate_no_quoted!("abc~def", None, true, "abc~def");
|
|
validate_no_quoted!("abc\\~def", None, false, "abc\\\\\\~def");
|
|
validate_no_quoted!("abc\\~def", None, true, "abc\\\\~def");
|
|
validate_no_quoted!("~abc", None, false, "\\~abc");
|
|
validate_no_quoted!("~abc", None, true, "~abc");
|
|
validate_no_quoted!("~abc|def", None, false, "\\~abc\\|def");
|
|
validate_no_quoted!("|abc~def", None, false, "\\|abc\\~def");
|
|
validate_no_quoted!("|abc~def", None, true, "\\|abc~def");
|
|
validate_no_quoted!("foo\nbar", None, false, "foo\\nbar");
|
|
|
|
// Note tildes are not expanded inside quotes, so no_tilde is ignored with a quote.
|
|
validate_no_quoted!("abc", Some('\''), false, "abc");
|
|
validate_no_quoted!("abc\\def", Some('\''), false, "abc\\\\def");
|
|
validate_no_quoted!("abc'def", Some('\''), false, "abc\\'def");
|
|
validate_no_quoted!("~abc'def", Some('\''), false, "~abc\\'def");
|
|
validate_no_quoted!("~abc'def", Some('\''), true, "~abc\\'def");
|
|
validate_no_quoted!("foo\nba'r", Some('\''), false, "foo'\\n'ba\\'r");
|
|
validate_no_quoted!("foo\\\\bar", Some('\''), false, "foo\\\\\\\\bar");
|
|
|
|
validate_no_quoted!("abc", Some('"'), false, "abc");
|
|
validate_no_quoted!("abc\\def", Some('"'), false, "abc\\\\def");
|
|
validate_no_quoted!("~abc'def", Some('"'), false, "~abc'def");
|
|
validate_no_quoted!("~abc'def", Some('"'), true, "~abc'def");
|
|
validate_no_quoted!("foo\nba'r", Some('"'), false, "foo\"\\n\"ba'r");
|
|
validate_no_quoted!("foo\\\\bar", Some('"'), false, "foo\\\\\\\\bar");
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn test_indents() {
|
|
let _cleanup = test_init();
|
|
// A struct which is either text or a new indent.
|
|
struct Segment {
|
|
// The indent to set
|
|
indent: i32,
|
|
text: &'static str,
|
|
}
|
|
fn do_validate(segments: &[Segment]) {
|
|
// Compute the indents.
|
|
let mut expected_indents = vec![];
|
|
let mut text = WString::new();
|
|
for segment in segments {
|
|
text.push_str(segment.text);
|
|
for _ in segment.text.chars() {
|
|
expected_indents.push(segment.indent);
|
|
}
|
|
}
|
|
let indents = parse_util_compute_indents(&text);
|
|
assert_eq!(indents, expected_indents);
|
|
}
|
|
macro_rules! validate {
|
|
( $( $(,)? $indent:literal, $text:literal )* $(,)? ) => {
|
|
let segments = vec![
|
|
$(
|
|
Segment{ indent: $indent, text: $text },
|
|
)*
|
|
];
|
|
do_validate(&segments);
|
|
};
|
|
}
|
|
|
|
#[rustfmt::skip]
|
|
#[allow(clippy::redundant_closure_call)]
|
|
(|| {
|
|
validate!(
|
|
0, "if", 1, " foo",
|
|
0, "\nend"
|
|
);
|
|
validate!(
|
|
0, "if", 1, " foo",
|
|
1, "\nfoo",
|
|
0, "\nend"
|
|
);
|
|
|
|
validate!(
|
|
0, "if", 1, " foo",
|
|
1, "\nif", 2, " bar",
|
|
1, "\nend",
|
|
0, "\nend"
|
|
);
|
|
|
|
validate!(
|
|
0, "if", 1, " foo",
|
|
1, "\nif", 2, " bar",
|
|
2, "\n",
|
|
1, "\nend\n"
|
|
);
|
|
|
|
validate!(
|
|
0, "if", 1, " foo",
|
|
1, "\nif", 2, " bar",
|
|
2, "\n"
|
|
);
|
|
|
|
validate!(
|
|
0, "begin",
|
|
1, "\nfoo",
|
|
1, "\n"
|
|
);
|
|
|
|
validate!(
|
|
0, "begin",
|
|
1, "\n;",
|
|
0, "end",
|
|
0, "\nfoo", 0, "\n"
|
|
);
|
|
|
|
validate!(
|
|
0, "begin",
|
|
1, "\n;",
|
|
0, "end",
|
|
0, "\nfoo", 0, "\n"
|
|
);
|
|
|
|
validate!(
|
|
0, "if", 1, " foo",
|
|
1, "\nif", 2, " bar",
|
|
2, "\nbaz",
|
|
1, "\nend", 1, "\n"
|
|
);
|
|
|
|
validate!(
|
|
0, "switch foo",
|
|
1, "\n"
|
|
);
|
|
|
|
validate!(
|
|
0, "switch foo",
|
|
1, "\ncase bar",
|
|
1, "\ncase baz",
|
|
2, "\nquux",
|
|
2, "\nquux"
|
|
);
|
|
|
|
validate!(
|
|
0,
|
|
"switch foo",
|
|
1,
|
|
"\ncas" // parse error indentation handling
|
|
);
|
|
|
|
validate!(
|
|
0, "while",
|
|
1, " false",
|
|
1, "\n# comment", // comment indentation handling
|
|
1, "\ncommand",
|
|
1, "\n# comment 2"
|
|
);
|
|
|
|
validate!(
|
|
0, "begin",
|
|
1, "\n", // "begin" is special because this newline belongs to the block header
|
|
1, "\n"
|
|
);
|
|
|
|
// Continuation lines.
|
|
validate!(
|
|
0, "echo 'continuation line' \\",
|
|
1, "\ncont",
|
|
0, "\n"
|
|
);
|
|
validate!(
|
|
0, "echo 'empty continuation line' \\",
|
|
1, "\n"
|
|
);
|
|
validate!(
|
|
0, "begin # continuation line in block",
|
|
1, "\necho \\",
|
|
2, "\ncont"
|
|
);
|
|
validate!(
|
|
0, "begin # empty continuation line in block",
|
|
1, "\necho \\",
|
|
2, "\n",
|
|
0, "\nend"
|
|
);
|
|
validate!(
|
|
0, "echo 'multiple continuation lines' \\",
|
|
1, "\nline1 \\",
|
|
1, "\n# comment",
|
|
1, "\n# more comment",
|
|
1, "\nline2 \\",
|
|
1, "\n"
|
|
);
|
|
validate!(
|
|
0, "echo # inline comment ending in \\",
|
|
0, "\nline"
|
|
);
|
|
validate!(
|
|
0, "# line comment ending in \\",
|
|
0, "\nline"
|
|
);
|
|
validate!(
|
|
0, "echo 'multiple empty continuation lines' \\",
|
|
1, "\n\\",
|
|
1, "\n",
|
|
0, "\n"
|
|
);
|
|
validate!(
|
|
0, "echo 'multiple statements with continuation lines' \\",
|
|
1, "\nline 1",
|
|
0, "\necho \\",
|
|
1, "\n"
|
|
);
|
|
// This is an edge case, probably okay to change the behavior here.
|
|
validate!(
|
|
0, "begin",
|
|
1, " \\",
|
|
2, "\necho 'continuation line in block header' \\",
|
|
2, "\n",
|
|
1, "\n",
|
|
0, "\nend"
|
|
);
|
|
validate!(
|
|
0, "if", 1, " true",
|
|
1, "\n begin",
|
|
2, "\n echo",
|
|
1, "\n end",
|
|
0, "\nend",
|
|
);
|
|
|
|
// Quotes and command substitutions.
|
|
validate!(
|
|
0, "if", 1, " foo \"",
|
|
0, "\nquoted",
|
|
);
|
|
validate!(
|
|
0, "if", 1, " foo \"",
|
|
0, "\n",
|
|
);
|
|
validate!(
|
|
0, "echo (",
|
|
1, "\n", // )
|
|
);
|
|
validate!(
|
|
0, "echo \"$(",
|
|
1, "\n" // )
|
|
);
|
|
validate!(
|
|
0, "echo (", // )
|
|
1, "\necho \"",
|
|
0, "\n"
|
|
);
|
|
validate!(
|
|
0, "echo (", // )
|
|
1, "\necho (", // )
|
|
2, "\necho"
|
|
);
|
|
validate!(
|
|
0, "if", 1, " true",
|
|
1, "\n echo \"line1",
|
|
0, "\nline2 ", 1, "$(",
|
|
2, "\n echo line3",
|
|
0, "\n) line4",
|
|
0, "\nline5\"",
|
|
);
|
|
})();
|
|
}
|