diff --git a/src/bin/psql/stringutils.c b/src/bin/psql/stringutils.c index 3b5ce1ba4bf..77387dcf3de 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 c7c5f3877d9..c64fc584585 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 3854f7f421f..6f481bb24dd 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 */