From 41e3c94cac0e68257126b2d264dc5e877e892490 Mon Sep 17 00:00:00 2001 From: Alvaro Herrera Date: Tue, 28 Feb 2012 01:06:29 -0300 Subject: [PATCH] psql: when tab-completing, use quotes on file names that need them psql backslash commands that deal with file or directory names require quotes around those that have spaces, single quotes, or backslashes. However, tab-completing such names does not provide said quotes, and is thus almost useless with them. This patch fixes the problem by having a wrapper function around rl_filename_completion_function that dequotes on input and quotes on output. This eases dealing with such names. Author: Noah Misch --- src/bin/psql/stringutils.c | 69 +++++++++++++++++++++++++++++++++++++ src/bin/psql/stringutils.h | 3 ++ src/bin/psql/tab-complete.c | 58 +++++++++++++++++++++++++++++-- 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/src/bin/psql/stringutils.c b/src/bin/psql/stringutils.c index 3b5ce1ba4b..77387dcf3d 100644 --- a/src/bin/psql/stringutils.c +++ b/src/bin/psql/stringutils.c @@ -272,3 +272,72 @@ strip_quotes(char *source, char quote, char escape, int encoding) *dst = '\0'; } + + +/* + * quote_if_needed + * + * Opposite of strip_quotes(). If "source" denotes itself literally without + * quoting or escaping, returns NULL. Otherwise, returns a malloc'd copy with + * quoting and escaping applied: + * + * source - string to parse + * entails_quote - any of these present? need outer quotes + * quote - doubled within string, affixed to both ends + * escape - doubled within string + * encoding - the active character-set encoding + * + * Do not use this as a substitute for PQescapeStringConn(). Use it for + * strings to be parsed by strtokx() or psql_scan_slash_option(). + */ +char * +quote_if_needed(const char *source, const char *entails_quote, + char quote, char escape, int encoding) +{ + const char *src; + char *ret; + char *dst; + bool need_quotes = false; + + psql_assert(source); + psql_assert(quote); + + src = source; + dst = ret = pg_malloc(2 * strlen(src) + 3); /* excess */ + + *dst++ = quote; + + while (*src) + { + char c = *src; + int i; + + if (c == quote) + { + need_quotes = true; + *dst++ = quote; + } + else if (c == escape) + { + need_quotes = true; + *dst++ = escape; + } + else if (strchr(entails_quote, c)) + need_quotes = true; + + i = PQmblen(src, encoding); + while (i--) + *dst++ = *src++; + } + + *dst++ = quote; + *dst = '\0'; + + if (!need_quotes) + { + free(ret); + ret = NULL; + } + + return ret; +} diff --git a/src/bin/psql/stringutils.h b/src/bin/psql/stringutils.h index c7c5f3877d..c64fc58458 100644 --- a/src/bin/psql/stringutils.h +++ b/src/bin/psql/stringutils.h @@ -19,4 +19,7 @@ extern char *strtokx(const char *s, bool del_quotes, int encoding); +extern char *quote_if_needed(const char *source, const char *entails_quote, + char quote, char escape, int encoding); + #endif /* STRINGUTILS_H */ diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 3854f7f421..6f481bb24d 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -680,6 +680,7 @@ static char *complete_from_list(const char *text, int state); static char *complete_from_const(const char *text, int state); static char **complete_from_variables(char *text, const char *prefix, const char *suffix); +static char *complete_from_files(const char *text, int state); static char *pg_strdup_same_case(const char *s, const char *ref); static PGresult *exec_query(const char *query); @@ -1630,7 +1631,10 @@ psql_completion(char *text, int start, int end) pg_strcasecmp(prev3_wd, "BINARY") == 0) && (pg_strcasecmp(prev_wd, "FROM") == 0 || pg_strcasecmp(prev_wd, "TO") == 0)) - matches = completion_matches(text, filename_completion_function); + { + completion_charp = ""; + matches = completion_matches(text, complete_from_files); + } /* Handle COPY|BINARY FROM|TO filename */ else if ((pg_strcasecmp(prev4_wd, "COPY") == 0 || @@ -2953,7 +2957,10 @@ psql_completion(char *text, int start, int end) strcmp(prev_wd, "\\s") == 0 || strcmp(prev_wd, "\\w") == 0 || strcmp(prev_wd, "\\write") == 0 ) - matches = completion_matches(text, filename_completion_function); + { + completion_charp = "\\"; + matches = completion_matches(text, complete_from_files); + } /* * Finally, we look through the list of "things", such as TABLE, INDEX and @@ -3426,6 +3433,53 @@ complete_from_variables(char *text, const char *prefix, const char *suffix) } +/* + * This function wraps rl_filename_completion_function() to strip quotes from + * the input before searching for matches and to quote any matches for which + * the consuming command will require it. + */ +static char * +complete_from_files(const char *text, int state) +{ + static const char *unquoted_text; + char *unquoted_match; + char *ret = NULL; + + if (state == 0) + { + /* Initialization: stash the unquoted input. */ + unquoted_text = strtokx(text, "", NULL, "'", *completion_charp, + false, true, pset.encoding); + /* expect a NULL return for the empty string only */ + if (!unquoted_text) + { + psql_assert(!*text); + unquoted_text = text; + } + } + + unquoted_match = filename_completion_function(unquoted_text, state); + if (unquoted_match) + { + /* + * Caller sets completion_charp to a zero- or one-character string + * containing the escape character. This is necessary since \copy has + * no escape character, but every other backslash command recognizes + * "\" as an escape character. Since we have only two callers, don't + * bother providing a macro to simplify this. + */ + ret = quote_if_needed(unquoted_match, " \t\r\n\"`", + '\'', *completion_charp, pset.encoding); + if (ret) + free(unquoted_match); + else + ret = unquoted_match; + } + + return ret; +} + + /* HELPER FUNCTIONS */ -- 2.40.0