Escape separators (colon and equals) to improve completion

Fish completes parts of words split by the separators, so things like
`dd if=/dev/sd<TAB>` work.
This commit improves interactive completion if completion strings legitimately
contain '=' or ':'.  Consider this example where completion will suggest
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:<TAB>` and `ls a\:<TAB>` successfully complete
the filename.

This also makes the completion insert those escapes automatically unless
already quoted.
So `ls a<TAB>` will give `ls a\:1`.

Both changes match bash's behavior.
This commit is contained in:
Johannes Altmanninger 2019-08-23 20:58:42 +02:00 committed by ridiculousfish
parent 54ed2ad440
commit f7dac82ed6
6 changed files with 24 additions and 13 deletions

View file

@ -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<bool>(flags & ESCAPE_ALL); const bool escape_all = static_cast<bool>(flags & ESCAPE_ALL);
const bool no_quoted = static_cast<bool>(flags & ESCAPE_NO_QUOTED); const bool no_quoted = static_cast<bool>(flags & ESCAPE_NO_QUOTED);
const bool no_tilde = static_cast<bool>(flags & ESCAPE_NO_TILDE); const bool no_tilde = static_cast<bool>(flags & ESCAPE_NO_TILDE);
const bool escape_separators = static_cast<bool>(flags & ESCAPE_SEPARATORS);
const bool no_caret = feature_test(features_t::stderr_nocaret); const bool no_caret = feature_test(features_t::stderr_nocaret);
const bool no_qmark = feature_test(features_t::qmark_noglob); 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'~': { case L'~':
case L':':
case L'=': {
bool char_is_normal = (c == L'~' && no_tilde) || (c == L'^' && no_caret) || 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) { if (!char_is_normal) {
need_escape = 1; need_escape = 1;
if (escape_all) out += L'\\'; if (escape_all) out += L'\\';

View file

@ -134,7 +134,9 @@ enum {
/// string. /// string.
ESCAPE_NO_QUOTED = 1 << 1, ESCAPE_NO_QUOTED = 1 << 1,
/// Do not escape tildes. /// 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; typedef unsigned int escape_flags_t;

View file

@ -1098,6 +1098,7 @@ void completer_t::complete_param_expand(const wcstring &str, bool do_file,
// Squelch file descriptions per issue #254. // Squelch file descriptions per issue #254.
if (this->type() == COMPLETE_AUTOSUGGEST || do_file) flags |= expand_flag::no_descriptions; 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: // We have the following cases:
// //
// --foo=bar => expand just bar // --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 // 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. // 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"=:"); size_t sep_index = str.size();
bool complete_from_separator = (sep_index != wcstring::npos); 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(), &quote, 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); bool complete_from_start = !complete_from_separator || !string_prefixes_string(L"-", str);
if (complete_from_separator) { 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); const wcstring sep_string = wcstring(str, sep_index + 1);
std::vector<completion_t> local_completions; std::vector<completion_t> local_completions;
if (expand_string(sep_string, &local_completions, flags, vars, parser, NULL) == if (expand_string(sep_string, &local_completions, flags, vars, parser, NULL) ==

View file

@ -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 parse_util_escape_string_with_quote(const wcstring &cmd, wchar_t quote, bool no_tilde) {
wcstring result; wcstring result;
if (quote == L'\0') { 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); result = escape_string(cmd, flags);
} else { } else {
// Here we are going to escape a string with quotes. // Here we are going to escape a string with quotes.

View file

@ -488,7 +488,6 @@ class reader_data_t : public std::enable_shared_from_this<reader_data_t> {
static volatile sig_atomic_t interrupted = 0; static volatile sig_atomic_t interrupted = 0;
// Prototypes for a bunch of functions defined later on. // 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); static wchar_t unescaped_quote(const wcstring &str, size_t pos);
/// Mode on startup, which we restore on exit. /// 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; 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 bool is_backslashed(const wcstring &str, size_t pos) {
/// the string, which indicates if there is a trailing backslash.
static bool is_backslashed(const wcstring &str, size_t pos) {
// note pos == str.size() is OK. // note pos == str.size() is OK.
if (pos > str.size()) return false; if (pos > str.size()) return false;

View file

@ -232,4 +232,8 @@ void reader_bg_job_warning(const parser_t &parser);
/// been executed between invocations of code. /// been executed between invocations of code.
uint64_t reader_run_count(); 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 #endif