psql: when tab-completing, use quotes on file names that need them
authorAlvaro Herrera
Tue, 28 Feb 2012 04:06:29 +0000 (01:06 -0300)
committerAlvaro Herrera
Tue, 28 Feb 2012 04:06:29 +0000 (01:06 -0300)
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
src/bin/psql/stringutils.h
src/bin/psql/tab-complete.c

index 3b5ce1ba4bf3c54b2388474d0afa691bdb8bac09..77387dcf3deaab0bac0b58751e8359279afe4134 100644 (file)
@@ -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;
+}
index c7c5f3877d92fbea189b0619eda316d55ac57042..c64fc584585f0957b1db94119d994f6b7ca4ba7b 100644 (file)
@@ -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 */
index 3854f7f421fc853a60ce538f6e3f2f670f454021..6f481bb24dd40ebc59c1895be0c754a3fa45ea93 100644 (file)
@@ -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 */