From f7dac82ed601135754d0c65536f79d07d83e9fae Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Fri, 23 Aug 2019 20:58:42 +0200 Subject: [PATCH] Escape separators (colon and equals) to improve completion Fish completes parts of words split by the separators, so things like `dd if=/dev/sd` work. This commit improves interactive completion if completion strings legitimately contain '=' or ':'. Consider this example where completion will suggest a:a:1 and other files in the cwd in addition to a:1 touch a:1; complete -C'ls a:' This behavior remains unchanged, but this commit allows to quote or escape separators, so that e.g. `ls "a:` and `ls a\:` successfully complete the filename. This also makes the completion insert those escapes automatically unless already quoted. So `ls a` will give `ls a\:1`. Both changes match bash's behavior. --- src/common.cpp | 8 ++++++-- src/common.h | 4 +++- src/complete.cpp | 13 ++++++++----- src/parse_util.cpp | 3 ++- src/reader.cpp | 5 +---- src/reader.h | 4 ++++ 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/common.cpp b/src/common.cpp index bc7a0dcff..7bd6cc92a 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -999,6 +999,7 @@ static void escape_string_script(const wchar_t *orig_in, size_t in_len, wcstring const bool escape_all = static_cast(flags & ESCAPE_ALL); const bool no_quoted = static_cast(flags & ESCAPE_NO_QUOTED); const bool no_tilde = static_cast(flags & ESCAPE_NO_TILDE); + const bool escape_separators = static_cast(flags & ESCAPE_SEPARATORS); const bool no_caret = feature_test(features_t::stderr_nocaret); const bool no_qmark = feature_test(features_t::qmark_noglob); @@ -1098,9 +1099,12 @@ static void escape_string_script(const wchar_t *orig_in, size_t in_len, wcstring case L';': case L'"': case L'%': - case L'~': { + case L'~': + case L':': + case L'=': { bool char_is_normal = (c == L'~' && no_tilde) || (c == L'^' && no_caret) || - (c == L'?' && no_qmark); + (c == L'?' && no_qmark) || + ((c == L':' || c == L'=') && !escape_separators); if (!char_is_normal) { need_escape = 1; if (escape_all) out += L'\\'; diff --git a/src/common.h b/src/common.h index 1065ac2db..da3135c06 100644 --- a/src/common.h +++ b/src/common.h @@ -134,7 +134,9 @@ enum { /// string. ESCAPE_NO_QUOTED = 1 << 1, /// Do not escape tildes. - ESCAPE_NO_TILDE = 1 << 2 + ESCAPE_NO_TILDE = 1 << 2, + /// Escape colon and equal sign. + ESCAPE_SEPARATORS = 1 << 3, }; typedef unsigned int escape_flags_t; diff --git a/src/complete.cpp b/src/complete.cpp index 3087ae889..c9c175da9 100644 --- a/src/complete.cpp +++ b/src/complete.cpp @@ -1098,6 +1098,7 @@ void completer_t::complete_param_expand(const wcstring &str, bool do_file, // Squelch file descriptions per issue #254. if (this->type() == COMPLETE_AUTOSUGGEST || do_file) flags |= expand_flag::no_descriptions; + // Expand words separated by '=' separately, unless '=' is escaped or quoted. // We have the following cases: // // --foo=bar => expand just bar @@ -1105,14 +1106,16 @@ void completer_t::complete_param_expand(const wcstring &str, bool do_file, // foo=bar => expand the whole thing, and also just bar // // We also support colon separator (#2178). If there's more than one, prefer the last one. - size_t sep_index = str.find_last_of(L"=:"); - bool complete_from_separator = (sep_index != wcstring::npos); + size_t sep_index = str.size(); + do { + sep_index = sep_index == 0 ? wcstring::npos : str.find_last_of(L"=:", sep_index - 1); + } while (sep_index != wcstring::npos && is_backslashed(str, sep_index)); + wchar_t quote = L'\0'; + parse_util_get_parameter_info(str, str.size(), "e, NULL, NULL); + bool complete_from_separator = (quote == L'\0') && (sep_index != wcstring::npos); bool complete_from_start = !complete_from_separator || !string_prefixes_string(L"-", str); if (complete_from_separator) { - // FIXME: This just cuts the token, - // so any quoting or braces gets lost. - // See #4954. const wcstring sep_string = wcstring(str, sep_index + 1); std::vector local_completions; if (expand_string(sep_string, &local_completions, flags, vars, parser, NULL) == diff --git a/src/parse_util.cpp b/src/parse_util.cpp index b4348f7d3..76ea30738 100644 --- a/src/parse_util.cpp +++ b/src/parse_util.cpp @@ -523,7 +523,8 @@ void parse_util_get_parameter_info(const wcstring &cmd, const size_t pos, wchar_ wcstring parse_util_escape_string_with_quote(const wcstring &cmd, wchar_t quote, bool no_tilde) { wcstring result; if (quote == L'\0') { - escape_flags_t flags = ESCAPE_ALL | ESCAPE_NO_QUOTED | (no_tilde ? ESCAPE_NO_TILDE : 0); + escape_flags_t flags = + ESCAPE_ALL | ESCAPE_NO_QUOTED | (no_tilde ? ESCAPE_NO_TILDE : 0) | ESCAPE_SEPARATORS; result = escape_string(cmd, flags); } else { // Here we are going to escape a string with quotes. diff --git a/src/reader.cpp b/src/reader.cpp index 7efe0d929..d080be6cd 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -488,7 +488,6 @@ class reader_data_t : public std::enable_shared_from_this { static volatile sig_atomic_t interrupted = 0; // Prototypes for a bunch of functions defined later on. -static bool is_backslashed(const wcstring &str, size_t pos); static wchar_t unescaped_quote(const wcstring &str, size_t pos); /// Mode on startup, which we restore on exit. @@ -2297,9 +2296,7 @@ static int can_read(int fd) { return select(fd + 1, &fds, 0, 0, &can_read_timeout) == 1; } -/// Test if the specified character in the specified string is backslashed. pos may be at the end of -/// the string, which indicates if there is a trailing backslash. -static bool is_backslashed(const wcstring &str, size_t pos) { +bool is_backslashed(const wcstring &str, size_t pos) { // note pos == str.size() is OK. if (pos > str.size()) return false; diff --git a/src/reader.h b/src/reader.h index 76d7a6137..cfa0c73ac 100644 --- a/src/reader.h +++ b/src/reader.h @@ -232,4 +232,8 @@ void reader_bg_job_warning(const parser_t &parser); /// been executed between invocations of code. uint64_t reader_run_count(); +/// Test if the specified character in the specified string is backslashed. pos may be at the end of +/// the string, which indicates if there is a trailing backslash. +bool is_backslashed(const wcstring &str, size_t pos); + #endif