Occasionally, I have a SQL file destined for psql's \i command whose name
contains a space.  Less often, I'll have a .csv destined for \copy with the
same problem.  psql's filename completion does not handle these well.  It
completes on the literal name, but the commands will only recognize quoted
names.  For example, given a file "foo bar", "\i f<TAB>" will complete to "\i
foo bar", which will not execute.  If I type "\i 'f<TAB>", completion will not
help at all.

The attached patch wraps rl_filename_completion_function() to dequote on input
and enquote on output.  Now, "\i f<TAB>" and "\i 'f<TAB>" will both complete
to "\i 'foo bar'", which executes as expected.  The new code handles embedded
whitespace, quotes, and backslashes.

tab-complete.c works in terms of whitespace-separated words.  As such, "\i
'foo b<TAB>" does not complete, because tab-complete.c has no notion of quotes
affecting token boundaries.  It thinks "'foo" is one token and "b" is another.
I'm sure we could fix this (Bash gets it right).  It seemed rather independent
code-wise, so I did not attempt that for this patch.

Thanks,
nm
diff --git a/src/bin/psql/stringutils.c b/src/bin/psql/stringutils.c
index 3b5ce1b..77387dc 100644
*** a/src/bin/psql/stringutils.c
--- b/src/bin/psql/stringutils.c
***************
*** 272,274 **** strip_quotes(char *source, char quote, char escape, int 
encoding)
--- 272,343 ----
  
        *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/stringuindex c7c5f38..c64fc58 100644
*** a/src/bin/psql/stringutils.h
--- b/src/bin/psql/stringutils.h
***************
*** 19,22 **** extern char *strtokx(const char *s,
--- 19,25 ----
                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-comindex a27ef69..d226106 100644
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 670,675 **** static char *complete_from_list(const char *text, int state);
--- 670,676 ----
  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 PGresult *exec_query(const char *query);
  
***************
*** 1619,1625 **** 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);
  
        /* Handle COPY|BINARY <sth> FROM|TO filename */
        else if ((pg_strcasecmp(prev4_wd, "COPY") == 0 ||
--- 1620,1629 ----
                          pg_strcasecmp(prev3_wd, "BINARY") == 0) &&
                         (pg_strcasecmp(prev_wd, "FROM") == 0 ||
                          pg_strcasecmp(prev_wd, "TO") == 0))
!       {
!               completion_charp = "";
!               matches = completion_matches(text, complete_from_files);
!       }
  
        /* Handle COPY|BINARY <sth> FROM|TO filename */
        else if ((pg_strcasecmp(prev4_wd, "COPY") == 0 ||
***************
*** 2899,2905 **** 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);
  
        /*
         * Finally, we look through the list of "things", such as TABLE, INDEX 
and
--- 2903,2912 ----
                         strcmp(prev_wd, "\\s") == 0 ||
                         strcmp(prev_wd, "\\w") == 0 || strcmp(prev_wd, 
"\\write") == 0
                )
!       {
!               completion_charp = "\\";
!               matches = completion_matches(text, complete_from_files);
!       }
  
        /*
         * Finally, we look through the list of "things", such as TABLE, INDEX 
and
***************
*** 3361,3366 **** complete_from_variables(char *text, const char *prefix, 
const char *suffix)
--- 3368,3420 ----
  }
  
  
+ /*
+  * 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 */
  
  
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to