diff --git a/src/parse_util.rs b/src/parse_util.rs index 19ec04997..02df38d6c 100644 --- a/src/parse_util.rs +++ b/src/parse_util.rs @@ -19,8 +19,6 @@ use crate::parse_constants::{ ERROR_NO_VAR_NAME, INVALID_BREAK_ERR_MSG, INVALID_CONTINUE_ERR_MSG, INVALID_PIPELINE_CMD_ERR_MSG, UNKNOWN_BUILTIN_ERR_MSG, }; -#[cfg(test)] -use crate::tests::prelude::*; use crate::tokenizer::{ comment_end, is_token_delimiter, quote_end, Tok, TokenType, Tokenizer, TOK_ACCEPT_UNFINISHED, TOK_SHOW_COMMENTS, @@ -1803,294 +1801,3 @@ const TIME_IN_PIPELINE_ERR_MSG: &str = /// Maximum length of a variable name to show in error reports before truncation const var_err_len: usize = 16; - -#[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" - ); - })(); -} diff --git a/src/tests/parse_util.rs b/src/tests/parse_util.rs index bfb122063..587dfadf3 100644 --- a/src/tests/parse_util.rs +++ b/src/tests/parse_util.rs @@ -1,12 +1,15 @@ 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_detect_errors, parse_util_process_extent, BOOL_AFTER_BACKGROUND_ERROR_MSG, + 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::*; @@ -100,3 +103,294 @@ fn test_parse_util_process_extent() { } 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" + ); + })(); +}