diff --git a/src/common.rs b/src/common.rs index da88f8433..39cafef49 100644 --- a/src/common.rs +++ b/src/common.rs @@ -6,7 +6,6 @@ }; use crate::global_safety::AtomicRef; use crate::global_safety::RelaxedAtomicBool; -use crate::parse_util::escape_string_with_quote; use crate::prelude::*; use crate::terminal::Outputter; use crate::termsize::Termsize; @@ -236,6 +235,64 @@ fn escape_string_script(input: &wstr, flags: EscapeFlags) -> WString { out } +/// Attempts to escape the string 'cmd' using the given quote type, as determined by the quote +/// character. The quote can be a single quote or double quote, or L'\0' to indicate no quoting (and +/// thus escaping should be with backslashes). Optionally do not escape tildes. +pub fn escape_string_with_quote( + cmd: &wstr, + quote: Option, + escape_flags: EscapeFlags, +) -> WString { + let Some(quote) = quote else { + return escape_string(cmd, EscapeStringStyle::Script(escape_flags)); + }; + // Here we are going to escape a string with quotes. + // A few characters cannot be represented inside quotes, e.g. newlines. In that case, + // terminate the quote and then re-enter it. + let mut result = WString::new(); + result.reserve(cmd.len()); + for c in cmd.chars() { + match c { + '\n' => { + for c in [quote, '\\', 'n', quote] { + result.push(c); + } + } + '\t' => { + for c in [quote, '\\', 't', quote] { + result.push(c); + } + } + '\x08' => { + for c in [quote, '\\', 'b', quote] { + result.push(c); + } + } + '\r' => { + for c in [quote, '\\', 'r', quote] { + result.push(c); + } + } + '\\' => { + result.push_str("\\\\"); + } + '$' => { + if quote == '"' { + result.push('\\'); + } + result.push('$'); + } + _ => { + if c == quote { + result.push('\\'); + } + result.push(c); + } + } + } + result +} + /// Test whether the char is a valid hex digit as used by the `escape_string_*()` functions. /// Note this only considers uppercase characters. fn is_upper_hex_digit(c: char) -> bool { @@ -1253,11 +1310,11 @@ macro_rules! env_stack_set_from_env { #[cfg(test)] mod tests { - use super::{ ESCAPE_TEST_CHAR, EscapeFlags, EscapeStringStyle, UnescapeStringStyle, bytes2wcstring, - escape_string, unescape_string, wcs2bytes, + escape_string, escape_string_with_quote, unescape_string, wcs2bytes, }; + use crate::tests::prelude::*; use fish_util::get_seeded_rng; use fish_widestring::{ ENCODE_DIRECT_BASE, ENCODE_DIRECT_END, L, WString, decode_with_replacement, wstr, @@ -1397,6 +1454,79 @@ fn test_escape_no_printables() { ); } + #[test] + #[serial] + fn test_escape_quotes() { + let _cleanup = test_init(); + macro_rules! validate { + ($cmd:expr, $quote:expr, $no_tilde:expr, $expected:expr) => { + assert_eq!( + 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!( + 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"); + } + /// The number of tests to run. const ESCAPE_TEST_COUNT: usize = 20_000; /// The average length of strings to unescape. diff --git a/src/parse_util.rs b/src/parse_util.rs index c4eb1af40..c7720ae11 100644 --- a/src/parse_util.rs +++ b/src/parse_util.rs @@ -4,7 +4,7 @@ is_same_node, }; use crate::builtins::shared::builtin_exists; -use crate::common::{escape_string, unescape_string, valid_var_name, valid_var_name_char}; +use crate::common::{unescape_string, valid_var_name, valid_var_name_char}; use crate::expand::{ BRACE_BEGIN, BRACE_END, BRACE_SEP, ExpandFlags, ExpandResultCode, INTERNAL_SEPARATOR, VARIABLE_EXPAND, VARIABLE_EXPAND_EMPTY, VARIABLE_EXPAND_SINGLE, expand_one, @@ -25,9 +25,7 @@ is_token_delimiter, quote_end, }; use crate::wildcard::{ANY_CHAR, ANY_STRING, ANY_STRING_RECURSIVE}; -use fish_common::{ - EscapeFlags, EscapeStringStyle, UnescapeFlags, UnescapeStringStyle, help_section, -}; +use fish_common::{UnescapeFlags, UnescapeStringStyle, help_section}; use fish_feature_flags::{FeatureFlag, feature_test}; use fish_wcstringutil::{count_newlines, truncate}; use std::ops::Range; @@ -693,64 +691,6 @@ fn error_for_character(c: char) -> WString { } } -/// Attempts to escape the string 'cmd' using the given quote type, as determined by the quote -/// character. The quote can be a single quote or double quote, or L'\0' to indicate no quoting (and -/// thus escaping should be with backslashes). Optionally do not escape tildes. -pub fn escape_string_with_quote( - cmd: &wstr, - quote: Option, - escape_flags: EscapeFlags, -) -> WString { - let Some(quote) = quote else { - return escape_string(cmd, EscapeStringStyle::Script(escape_flags)); - }; - // Here we are going to escape a string with quotes. - // A few characters cannot be represented inside quotes, e.g. newlines. In that case, - // terminate the quote and then re-enter it. - let mut result = WString::new(); - result.reserve(cmd.len()); - for c in cmd.chars() { - match c { - '\n' => { - for c in [quote, '\\', 'n', quote] { - result.push(c); - } - } - '\t' => { - for c in [quote, '\\', 't', quote] { - result.push(c); - } - } - '\x08' => { - for c in [quote, '\\', 'b', quote] { - result.push(c); - } - } - '\r' => { - for c in [quote, '\\', 'r', quote] { - result.push(c); - } - } - '\\' => { - result.push_str("\\\\"); - } - '$' => { - if quote == '"' { - result.push('\\'); - } - result.push('$'); - } - _ => { - if c == quote { - result.push('\\'); - } - result.push(c); - } - } - } - result -} - /// Given a string, parse it as fish code and then return the indents. The return value has the same /// size as the string. pub fn compute_indents(src: &wstr) -> Vec { @@ -1964,8 +1904,8 @@ pub fn expand_variable_error( #[cfg(test)] mod tests { use super::{ - BOOL_AFTER_BACKGROUND_ERROR_MSG, compute_indents, detect_parse_errors, - escape_string_with_quote, get_cmdsubst_extent, get_process_extent, slice_length, + BOOL_AFTER_BACKGROUND_ERROR_MSG, compute_indents, detect_parse_errors, get_cmdsubst_extent, + get_process_extent, slice_length, }; use crate::parse_constants::{ ERROR_BAD_VAR_CHAR1, ERROR_BRACKETED_VARIABLE_QUOTED1, ERROR_BRACKETED_VARIABLE1, @@ -1974,7 +1914,6 @@ mod tests { }; use crate::prelude::*; use crate::tests::prelude::*; - use fish_common::EscapeFlags; use pcre2::utf32::Regex; #[test] @@ -2095,79 +2034,6 @@ fn test_slice_length() { assert_eq!(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!( - 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!( - 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() { diff --git a/src/reader/reader.rs b/src/reader/reader.rs index 23ace4e7c..543c88d0d 100644 --- a/src/reader/reader.rs +++ b/src/reader/reader.rs @@ -20,96 +20,95 @@ use super::history_search::{ReaderHistorySearch, SearchMode, smartcase_flags}; use super::iothreads::{self, Debouncers}; use super::word_motion::{MoveWordDir, MoveWordStateMachine, MoveWordStyle}; -use crate::abbrs::abbrs_match; -use crate::ast::{self, Kind, is_same_node}; -use crate::builtins::shared::ErrorCode; -use crate::builtins::shared::STATUS_CMD_ERROR; -use crate::builtins::shared::STATUS_CMD_OK; -use crate::common::{bytes2wcstring, escape, escape_string, get_program_name, shell_modes}; -use crate::complete::{ - CompleteFlags, Completion, CompletionList, CompletionRequestOptions, complete, complete_load, - sort_and_prioritize, +use crate::{ + abbrs::{self, abbrs_match}, + ast::{self, Kind, is_same_node}, + builtins::shared::{ErrorCode, STATUS_CMD_ERROR, STATUS_CMD_OK}, + common::{ + bytes2wcstring, escape, escape_string, escape_string_with_quote, get_program_name, + shell_modes, + }, + complete::{ + CompleteFlags, Completion, CompletionList, CompletionRequestOptions, complete, + complete_load, sort_and_prioritize, + }, + editable_line::{Edit, EditableLine, line_at_cursor, range_of_line_at_cursor}, + env::{EnvMode, EnvStack, Environment, Statuses}, + env_dispatch::{MIDNIGHT_COMMANDER_SID, handle_emoji_width}, + event, + exec::exec_subshell, + expand::{ExpandFlags, ExpandResultCode, expand_one, expand_string, expand_tilde}, + fd_readable_set::poll_fd_readable, + fds::{make_fd_blocking, wopen_cloexec}, + flog::{flog, flogf}, + function, + global_safety::RelaxedAtomicBool, + highlight::{ + HighlightRole, HighlightSpec, autosuggest_validate_from_history, highlight_shell, + parse_text_face_for_highlight, + }, + history::{ + History, HistorySearch, PersistenceMode, SearchDirection, SearchFlags, SearchType, + history_session_id, in_private_mode, + }, + input_common::{ + BackgroundColorQuery, CharEvent, CharInputStyle, CursorPositionQuery, + CursorPositionQueryReason, ImplicitEvent, InputData, InputEventQueue, + InputEventQueuer as _, LONG_READ_TIMEOUT, QueryResponse, QueryResultEvent, ReadlineCmd, + RecurrentQuery, TerminalQuery, stop_query, + }, + io::IoChain, + key::ViewportPosition, + kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate}, + nix::isatty, + operation_context::{OperationContext, get_bg_context}, + pager::{PageRendering, Pager, SelectionMotion}, + panic::AT_EXIT, + parse_constants::{ParseIssue, ParseTreeFlags, SourceRange}, + parse_util::{ + MaybeParentheses, SPACES_PER_INDENT, compute_indents, contains_wildcards, + detect_parse_errors, escape_wildcards, get_cmdsubst_extent, get_line_from_offset, + get_offset, get_offset_from_line, get_process_extent, get_process_first_token_offset, + get_token_extent, lineno, locate_cmdsubst_range, + }, + parser::{BlockType, EvalRes, Parser, ParserEnvSetMode}, + portable_atomic::AtomicU64, + prelude::*, + proc::{ + HAVE_PROC_STAT, hup_jobs, is_interactive_session, job_reap, jobs_requiring_warning_on_exit, + print_exit_warning_for_jobs, proc_update_jiffies, + }, + reader::word_motion::bigword_class, + screen::{CharOffset, Screen, is_dumb, screen_force_clear_to_end}, + should_flog, + signal::{ + signal_check_cancel, signal_clear_cancel, signal_reset_handlers, signal_set_handlers, + signal_set_handlers_once, + }, + terminal::{ + BufferedOutputter, Outputter, + TerminalCommand::{ + self, ClearScreen, DecrstAlternateScreenBuffer, DecsetAlternateScreenBuffer, + DecsetShowCursor, Osc0WindowTitle, Osc1TabTitle, Osc133CommandFinished, + Osc133CommandStart, QueryBackgroundColor, QueryCursorPosition, + QueryKittyKeyboardProgressiveEnhancements, QueryPrimaryDeviceAttribute, QueryXtgettcap, + QueryXtversion, + }, + }, + termsize::{safe_termsize_invalidate_tty, termsize_last, termsize_update}, + text_face::{TextFace, parse_text_face}, + threads::{assert_is_background_thread, assert_is_main_thread}, + tokenizer::{ + TOK_ACCEPT_UNFINISHED, TOK_SHOW_COMMENTS, TokenType, Tokenizer, quote_end, tok_command, + variable_assignment_equals_pos, + }, + tty_handoff::{ + SCROLL_CONTENT_UP_TERMINFO_CODE, TtyHandoff, XTGETTCAP_QUERY_OS_NAME, + get_tty_protocols_active, initialize_tty_protocols, safe_deactivate_tty_protocols, + }, + wildcard::wildcard_has, + wutil::{fstat, perror_nix, wstat}, }; -use crate::editable_line::{Edit, EditableLine, line_at_cursor, range_of_line_at_cursor}; -use crate::env::EnvStack; -use crate::env::{EnvMode, Environment, Statuses}; -use crate::env_dispatch::MIDNIGHT_COMMANDER_SID; -use crate::env_dispatch::handle_emoji_width; -use crate::exec::exec_subshell; -use crate::expand::expand_one; -use crate::expand::{ExpandFlags, ExpandResultCode, expand_string, expand_tilde}; -use crate::fd_readable_set::poll_fd_readable; -use crate::fds::{make_fd_blocking, wopen_cloexec}; -use crate::flog::{flog, flogf}; -use crate::global_safety::RelaxedAtomicBool; -use crate::highlight::{ - HighlightRole, HighlightSpec, autosuggest_validate_from_history, highlight_shell, - parse_text_face_for_highlight, -}; -use crate::history::{ - History, HistorySearch, PersistenceMode, SearchDirection, SearchFlags, SearchType, - history_session_id, in_private_mode, -}; -use crate::input_common::BackgroundColorQuery; -use crate::input_common::CursorPositionQueryReason; -use crate::input_common::InputEventQueue; -use crate::input_common::InputEventQueuer as _; -use crate::input_common::QueryResponse; -use crate::input_common::{ - CharEvent, CharInputStyle, CursorPositionQuery, ImplicitEvent, InputData, LONG_READ_TIMEOUT, - QueryResultEvent, ReadlineCmd, RecurrentQuery, TerminalQuery, stop_query, -}; -use crate::io::IoChain; -use crate::key::ViewportPosition; -use crate::kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate}; -use crate::nix::isatty; -use crate::operation_context::{OperationContext, get_bg_context}; -use crate::pager::{PageRendering, Pager, SelectionMotion}; -use crate::panic::AT_EXIT; -use crate::parse_constants::SourceRange; -use crate::parse_constants::{ParseIssue, ParseTreeFlags}; -use crate::parse_util::{ - MaybeParentheses, SPACES_PER_INDENT, compute_indents, contains_wildcards, detect_parse_errors, - escape_string_with_quote, escape_wildcards, get_cmdsubst_extent, get_line_from_offset, - get_offset, get_offset_from_line, get_process_extent, get_process_first_token_offset, - get_token_extent, lineno, locate_cmdsubst_range, -}; -use crate::parser::{BlockType, EvalRes, Parser, ParserEnvSetMode}; -use crate::portable_atomic::AtomicU64; -use crate::prelude::*; -use crate::proc::{ - HAVE_PROC_STAT, hup_jobs, is_interactive_session, job_reap, jobs_requiring_warning_on_exit, - print_exit_warning_for_jobs, proc_update_jiffies, -}; -use crate::reader::word_motion::bigword_class; -use crate::screen::{CharOffset, Screen, is_dumb, screen_force_clear_to_end}; -use crate::should_flog; -use crate::signal::{ - signal_check_cancel, signal_clear_cancel, signal_reset_handlers, signal_set_handlers, - signal_set_handlers_once, -}; -use crate::terminal::TerminalCommand::{ - self, ClearScreen, DecrstAlternateScreenBuffer, DecsetAlternateScreenBuffer, DecsetShowCursor, - Osc0WindowTitle, Osc1TabTitle, Osc133CommandFinished, Osc133CommandStart, QueryBackgroundColor, - QueryCursorPosition, QueryKittyKeyboardProgressiveEnhancements, QueryPrimaryDeviceAttribute, - QueryXtgettcap, QueryXtversion, -}; -use crate::terminal::{BufferedOutputter, Outputter}; -use crate::termsize::{safe_termsize_invalidate_tty, termsize_last, termsize_update}; -use crate::text_face::{TextFace, parse_text_face}; -use crate::threads::{assert_is_background_thread, assert_is_main_thread}; -use crate::tokenizer::{ - TOK_ACCEPT_UNFINISHED, TOK_SHOW_COMMENTS, TokenType, Tokenizer, quote_end, tok_command, - variable_assignment_equals_pos, -}; -use crate::tty_handoff::SCROLL_CONTENT_UP_TERMINFO_CODE; -use crate::tty_handoff::XTGETTCAP_QUERY_OS_NAME; -use crate::tty_handoff::{ - TtyHandoff, get_tty_protocols_active, initialize_tty_protocols, safe_deactivate_tty_protocols, -}; -use crate::wildcard::wildcard_has; -use crate::wutil::{fstat, perror_nix, wstat}; -use crate::{abbrs, event, function}; use assert_matches::assert_matches; use errno::{Errno, errno}; use fish_common::{