Improve psql's tab completion for filenames.
authorTom Lane <tgl@sss.pgh.pa.us>
Thu, 23 Jan 2020 16:07:12 +0000 (11:07 -0500)
committerTom Lane <tgl@sss.pgh.pa.us>
Thu, 23 Jan 2020 16:07:12 +0000 (11:07 -0500)
The Readline library contains a fair amount of knowledge about how to
tab-complete filenames, but it turns out that that doesn't work too well
unless we follow its expectation that we use its filename quoting hooks
to quote and de-quote filenames.  We were trying to do such quote handling
within complete_from_files(), and that's still what we have to do if we're
using libedit, which lacks those hooks.  But for Readline, it works a lot
better if we tell Readline that single-quote is a quoting character and
then provide hooks that know the details of the quoting rules for SQL
and psql meta-commands.

Hence, resurrect the quoting hook functions that existed in the original
version of tab-complete.c (and were disabled by commit f6689a328 because
they "didn't work so well yet"), and whack on them until they do seem to
work well.

Notably, this fixes bug #16059 from Steven Winfield, who pointed out
that the previous coding would strip quote marks from filenames in SQL
COPY commands, even though they're syntactically necessary there.
Now, we not only don't do that, but we'll add a quote mark when you
tab-complete, even if you didn't type one.

Getting this to work across a range of libedit versions (and, to a
lesser extent, libreadline versions) was depressingly difficult.
It will be interesting to see whether the new regression test cases
pass everywhere in the buildfarm.

Some future patch might try to handle quoted SQL identifiers with
similar explicit quoting/dequoting logic, but that's for another day.

Patch by me, reviewed by Peter Eisentraut.

Discussion: https://postgr.es/m/16059-8836946734c02b84@postgresql.org

config/programs.m4
configure
configure.in
src/bin/psql/stringutils.c
src/bin/psql/stringutils.h
src/bin/psql/t/010_tab_completion.pl
src/bin/psql/tab-complete.c
src/include/pg_config.h.in
src/tools/msvc/Solution.pm

index 90ff9447bdd7f27c88a7b167aa3aecc2972f89fd..68ab82351ad4f60012253e2e1cdf901909df8cd7 100644 (file)
@@ -209,17 +209,20 @@ fi
 
 
 
-# PGAC_VAR_RL_COMPLETION_APPEND_CHARACTER
-# ---------------------------------------
+# PGAC_READLINE_VARIABLES
+# -----------------------
 # Readline versions < 2.1 don't have rl_completion_append_character
+# Libedit lacks rl_filename_quote_characters and rl_filename_quoting_function
 
-AC_DEFUN([PGAC_VAR_RL_COMPLETION_APPEND_CHARACTER],
+AC_DEFUN([PGAC_READLINE_VARIABLES],
 [AC_CACHE_CHECK([for rl_completion_append_character], pgac_cv_var_rl_completion_append_character,
 [AC_LINK_IFELSE([AC_LANG_PROGRAM([#include <stdio.h>
-#ifdef HAVE_READLINE_READLINE_H
-# include <readline/readline.h>
+#if defined(HAVE_READLINE_READLINE_H)
+#include <readline/readline.h>
+#elif defined(HAVE_EDITLINE_READLINE_H)
+#include <editline/readline.h>
 #elif defined(HAVE_READLINE_H)
-# include <readline.h>
+#include <readline.h>
 #endif
 ],
 [rl_completion_append_character = 'x';])],
@@ -228,7 +231,42 @@ AC_DEFUN([PGAC_VAR_RL_COMPLETION_APPEND_CHARACTER],
 if test x"$pgac_cv_var_rl_completion_append_character" = x"yes"; then
 AC_DEFINE(HAVE_RL_COMPLETION_APPEND_CHARACTER, 1,
           [Define to 1 if you have the global variable 'rl_completion_append_character'.])
-fi])# PGAC_VAR_RL_COMPLETION_APPEND_CHARACTER
+fi
+AC_CACHE_CHECK([for rl_filename_quote_characters], pgac_cv_var_rl_filename_quote_characters,
+[AC_LINK_IFELSE([AC_LANG_PROGRAM([#include <stdio.h>
+#if defined(HAVE_READLINE_READLINE_H)
+#include <readline/readline.h>
+#elif defined(HAVE_EDITLINE_READLINE_H)
+#include <editline/readline.h>
+#elif defined(HAVE_READLINE_H)
+#include <readline.h>
+#endif
+],
+[rl_filename_quote_characters = "x";])],
+[pgac_cv_var_rl_filename_quote_characters=yes],
+[pgac_cv_var_rl_filename_quote_characters=no])])
+if test x"$pgac_cv_var_rl_filename_quote_characters" = x"yes"; then
+AC_DEFINE(HAVE_RL_FILENAME_QUOTE_CHARACTERS, 1,
+          [Define to 1 if you have the global variable 'rl_filename_quote_characters'.])
+fi
+AC_CACHE_CHECK([for rl_filename_quoting_function], pgac_cv_var_rl_filename_quoting_function,
+[AC_LINK_IFELSE([AC_LANG_PROGRAM([#include <stdio.h>
+#if defined(HAVE_READLINE_READLINE_H)
+#include <readline/readline.h>
+#elif defined(HAVE_EDITLINE_READLINE_H)
+#include <editline/readline.h>
+#elif defined(HAVE_READLINE_H)
+#include <readline.h>
+#endif
+],
+[rl_filename_quoting_function = 0;])],
+[pgac_cv_var_rl_filename_quoting_function=yes],
+[pgac_cv_var_rl_filename_quoting_function=no])])
+if test x"$pgac_cv_var_rl_filename_quoting_function" = x"yes"; then
+AC_DEFINE(HAVE_RL_FILENAME_QUOTING_FUNCTION, 1,
+          [Define to 1 if you have the global variable 'rl_filename_quoting_function'.])
+fi
+])# PGAC_READLINE_VARIABLES
 
 
 
index 25cfbcb2cd02c013aec18c05f191f58ce967db16..a46ba4010481a829777466ae5fcf7dcc3426e3f0 100755 (executable)
--- a/configure
+++ b/configure
@@ -16316,10 +16316,12 @@ else
   cat confdefs.h - <<_ACEOF >conftest.$ac_ext
 /* end confdefs.h.  */
 #include <stdio.h>
-#ifdef HAVE_READLINE_READLINE_H
-# include <readline/readline.h>
+#if defined(HAVE_READLINE_READLINE_H)
+#include <readline/readline.h>
+#elif defined(HAVE_EDITLINE_READLINE_H)
+#include <editline/readline.h>
 #elif defined(HAVE_READLINE_H)
-# include <readline.h>
+#include <readline.h>
 #endif
 
 int
@@ -16345,6 +16347,85 @@ if test x"$pgac_cv_var_rl_completion_append_character" = x"yes"; then
 $as_echo "#define HAVE_RL_COMPLETION_APPEND_CHARACTER 1" >>confdefs.h
 
 fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for rl_filename_quote_characters" >&5
+$as_echo_n "checking for rl_filename_quote_characters... " >&6; }
+if ${pgac_cv_var_rl_filename_quote_characters+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <stdio.h>
+#if defined(HAVE_READLINE_READLINE_H)
+#include <readline/readline.h>
+#elif defined(HAVE_EDITLINE_READLINE_H)
+#include <editline/readline.h>
+#elif defined(HAVE_READLINE_H)
+#include <readline.h>
+#endif
+
+int
+main ()
+{
+rl_filename_quote_characters = "x";
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"; then :
+  pgac_cv_var_rl_filename_quote_characters=yes
+else
+  pgac_cv_var_rl_filename_quote_characters=no
+fi
+rm -f core conftest.err conftest.$ac_objext \
+    conftest$ac_exeext conftest.$ac_ext
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_var_rl_filename_quote_characters" >&5
+$as_echo "$pgac_cv_var_rl_filename_quote_characters" >&6; }
+if test x"$pgac_cv_var_rl_filename_quote_characters" = x"yes"; then
+
+$as_echo "#define HAVE_RL_FILENAME_QUOTE_CHARACTERS 1" >>confdefs.h
+
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for rl_filename_quoting_function" >&5
+$as_echo_n "checking for rl_filename_quoting_function... " >&6; }
+if ${pgac_cv_var_rl_filename_quoting_function+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <stdio.h>
+#if defined(HAVE_READLINE_READLINE_H)
+#include <readline/readline.h>
+#elif defined(HAVE_EDITLINE_READLINE_H)
+#include <editline/readline.h>
+#elif defined(HAVE_READLINE_H)
+#include <readline.h>
+#endif
+
+int
+main ()
+{
+rl_filename_quoting_function = 0;
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"; then :
+  pgac_cv_var_rl_filename_quoting_function=yes
+else
+  pgac_cv_var_rl_filename_quoting_function=no
+fi
+rm -f core conftest.err conftest.$ac_objext \
+    conftest$ac_exeext conftest.$ac_ext
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_var_rl_filename_quoting_function" >&5
+$as_echo "$pgac_cv_var_rl_filename_quoting_function" >&6; }
+if test x"$pgac_cv_var_rl_filename_quoting_function" = x"yes"; then
+
+$as_echo "#define HAVE_RL_FILENAME_QUOTING_FUNCTION 1" >>confdefs.h
+
+fi
+
   for ac_func in rl_completion_matches rl_filename_completion_function rl_reset_screen_size
 do :
   as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
index 1599fc514d80c3b0ce082e981487f265cc998a7c..8165f700398b15493456b518a94cc2b7d659add6 100644 (file)
@@ -1874,7 +1874,7 @@ fi
 LIBS="$LIBS_including_readline"
 
 if test "$with_readline" = yes; then
-  PGAC_VAR_RL_COMPLETION_APPEND_CHARACTER
+  PGAC_READLINE_VARIABLES
   AC_CHECK_FUNCS([rl_completion_matches rl_filename_completion_function rl_reset_screen_size])
   AC_CHECK_FUNCS([append_history history_truncate_file])
 fi
index c74e58d91258bc18fdd3afe9899ac75f2b3faaf5..c521749661c30eca40f422ff7488f0bd1f322f55 100644 (file)
@@ -282,6 +282,7 @@ strip_quotes(char *source, char quote, char escape, int encoding)
  * entails_quote - any of these present?  need outer quotes
  * quote -         doubled within string, affixed to both ends
  * escape -            doubled within string
+ * force_quote -   if true, quote the output even if it doesn't "need" it
  * encoding -      the active character-set encoding
  *
  * Do not use this as a substitute for PQescapeStringConn().  Use it for
@@ -289,12 +290,13 @@ strip_quotes(char *source, char quote, char escape, int encoding)
  */
 char *
 quote_if_needed(const char *source, const char *entails_quote,
-               char quote, char escape, int encoding)
+               char quote, char escape, bool force_quote,
+               int encoding)
 {
    const char *src;
    char       *ret;
    char       *dst;
-   bool        need_quotes = false;
+   bool        need_quotes = force_quote;
 
    Assert(source != NULL);
    Assert(quote != '\0');
index 1fb1cfc1430bfe31c47503805b54af73deca1129..4be172e031f9c62f7e17d6a4e450c90ac2b939cb 100644 (file)
@@ -22,6 +22,7 @@ extern char *strtokx(const char *s,
 extern void strip_quotes(char *source, char quote, char escape, int encoding);
 
 extern char *quote_if_needed(const char *source, const char *entails_quote,
-                            char quote, char escape, int encoding);
+                            char quote, char escape, bool force_quote,
+                            int encoding);
 
 #endif                         /* STRINGUTILS_H */
index b35c4f8dba0c1b82d3643dba31a106ad37b219f0..c27f216d39276af9ac7864b1b01958290c0a39a8 100644 (file)
@@ -60,6 +60,30 @@ delete $ENV{TERM};
 # Some versions of readline inspect LS_COLORS, so for luck unset that too.
 delete $ENV{LS_COLORS};
 
+# In a VPATH build, we'll be started in the source directory, but we want
+# to run in the build directory so that we can use relative paths to
+# access the tmp_check subdirectory; otherwise the output from filename
+# completion tests is too variable.
+if ($ENV{TESTDIR})
+{
+   chdir $ENV{TESTDIR} or die "could not chdir to \"$ENV{TESTDIR}\": $!";
+}
+
+# Create some junk files for filename completion testing.
+my $FH;
+open $FH, ">", "tmp_check/somefile"
+  or die("could not create file \"tmp_check/somefile\": $!");
+print $FH "some stuff\n";
+close $FH;
+open $FH, ">", "tmp_check/afile123"
+  or die("could not create file \"tmp_check/afile123\": $!");
+print $FH "more stuff\n";
+close $FH;
+open $FH, ">", "tmp_check/afile456"
+  or die("could not create file \"tmp_check/afile456\": $!");
+print $FH "other stuff\n";
+close $FH;
+
 # fire up an interactive psql session
 my $in  = '';
 my $out = '';
@@ -104,6 +128,15 @@ sub clear_query
    return;
 }
 
+# Clear current line to start over
+# (this will work in an incomplete string literal, but it's less desirable
+# than clear_query because we lose evidence in the history file)
+sub clear_line
+{
+   check_completion("\025\n", qr/postgres=# /, "control-U works");
+   return;
+}
+
 # check basic command completion: SEL<tab> produces SELECT<space>
 check_completion("SEL\t", qr/SELECT /, "complete SEL<tab> to SELECT");
 
@@ -142,6 +175,47 @@ check_completion("\\DRD\t", qr/drds /, "complete \\DRD<tab> to \\drds");
 
 clear_query();
 
+# check filename completion
+check_completion(
+   "\\lo_import tmp_check/some\t",
+   qr|tmp_check/somefile |,
+   "filename completion with one possibility");
+
+clear_query();
+
+# note: readline might print a bell before the completion
+check_completion(
+   "\\lo_import tmp_check/af\t",
+   qr|tmp_check/af\a?ile|,
+   "filename completion with multiple possibilities");
+
+clear_query();
+
+# COPY requires quoting
+# note: broken versions of libedit want to backslash the closing quote;
+# not much we can do about that
+check_completion(
+   "COPY foo FROM tmp_check/some\t",
+   qr|'tmp_check/somefile\\?' |,
+   "quoted filename completion with one possibility");
+
+clear_line();
+
+check_completion(
+   "COPY foo FROM tmp_check/af\t",
+   qr|'tmp_check/afile|,
+   "quoted filename completion with multiple possibilities");
+
+# some versions of readline/libedit require two tabs here, some only need one
+# also, some will offer the whole path name and some just the file name
+# the quotes might appear, too
+check_completion(
+   "\t\t",
+   qr|afile123'? +'?(tmp_check/)?afile456|,
+   "offer multiple file choices");
+
+clear_line();
+
 # send psql an explicit \q to shut it down, else pty won't close properly
 $timer->start(5);
 $in .= "\\q\n";
index 052d98b5c08850b900faefed3b110526d6d9fb71..086348824323cab55f8343701dba0b37a8af84c2 100644 (file)
@@ -41,6 +41,7 @@
 #ifdef USE_READLINE
 
 #include <ctype.h>
+#include <sys/stat.h>
 
 #include "catalog/pg_am_d.h"
 #include "catalog/pg_class_d.h"
 #define rl_completion_matches completion_matches
 #endif
 
+/*
+ * Currently we assume that rl_filename_dequoting_function exists if
+ * rl_filename_quoting_function does.  If that proves not to be the case,
+ * we'd need to test for the former, or possibly both, in configure.
+ */
+#ifdef HAVE_RL_FILENAME_QUOTING_FUNCTION
+#define USE_FILENAME_QUOTING_FUNCTIONS 1
+#endif
+
 /* word break characters */
 #define WORD_BREAKS        "\t\n@$><=;|&{() "
 
@@ -157,9 +167,11 @@ typedef struct SchemaQuery
 static int completion_max_records;
 
 /*
- * Communication variables set by COMPLETE_WITH_FOO macros and then used by
- * the completion callback functions.  Ugly but there is no better way.
+ * Communication variables set by psql_completion (mostly in COMPLETE_WITH_FOO
+ * macros) and then used by the completion callback functions.  Ugly but there
+ * is no better way.
  */
+static char completion_last_char;  /* last char of input word */
 static const char *completion_charp;   /* to pass a string */
 static const char *const *completion_charpp;   /* to pass a list of strings */
 static const char *completion_info_charp;  /* to pass a second string */
@@ -167,6 +179,7 @@ static const char *completion_info_charp2;  /* to pass a third string */
 static const VersionedQuery *completion_vquery; /* to pass a VersionedQuery */
 static const SchemaQuery *completion_squery;   /* to pass a SchemaQuery */
 static bool completion_case_sensitive; /* completion is case sensitive */
+static bool completion_force_quote; /* true to force-quote filenames */
 
 /*
  * A few macros to ease typing. You can use these to complete the given
@@ -1114,9 +1127,9 @@ static char **get_previous_words(int point, char **buffer, int *nwords);
 
 static char *get_guctype(const char *varname);
 
-#ifdef NOT_USED
-static char *quote_file_name(char *text, int match_type, char *quote_pointer);
-static char *dequote_file_name(char *text, char quote_char);
+#ifdef USE_FILENAME_QUOTING_FUNCTIONS
+static char *quote_file_name(char *fname, int match_type, char *quote_pointer);
+static char *dequote_file_name(char *fname, int quote_char);
 #endif
 
 
@@ -1129,8 +1142,37 @@ initialize_readline(void)
    rl_readline_name = (char *) pset.progname;
    rl_attempted_completion_function = psql_completion;
 
+#ifdef USE_FILENAME_QUOTING_FUNCTIONS
+   rl_filename_quoting_function = quote_file_name;
+   rl_filename_dequoting_function = dequote_file_name;
+#endif
+
    rl_basic_word_break_characters = WORD_BREAKS;
 
+   /*
+    * We should include '"' in rl_completer_quote_characters too, but that
+    * will require some upgrades to how we handle quoted identifiers, so
+    * that's for another day.
+    */
+   rl_completer_quote_characters = "'";
+
+   /*
+    * Set rl_filename_quote_characters to "all possible characters",
+    * otherwise Readline will skip filename quoting if it thinks a filename
+    * doesn't need quoting.  Readline actually interprets this as bytes, so
+    * there are no encoding considerations here.
+    */
+#ifdef HAVE_RL_FILENAME_QUOTE_CHARACTERS
+   {
+       unsigned char *fqc = (unsigned char *) pg_malloc(256);
+
+       for (int i = 0; i < 255; i++)
+           fqc[i] = (unsigned char) (i + 1);
+       fqc[255] = '\0';
+       rl_filename_quote_characters = (const char *) fqc;
+   }
+#endif
+
    completion_max_records = 1000;
 
    /*
@@ -1455,6 +1497,10 @@ psql_completion(const char *text, int start, int end)
    char       *text_copy = pnstrdup(rl_line_buffer + start, end - start);
    text = text_copy;
 
+   /* Remember last char of the given input word. */
+   completion_last_char = (end > start) ? text[end - start - 1] : '\0';
+
+   /* We usually want the append character to be a space. */
 #ifdef HAVE_RL_COMPLETION_APPEND_CHARACTER
    rl_completion_append_character = ' ';
 #endif
@@ -2265,20 +2311,26 @@ psql_completion(const char *text, int start, int end)
             Matches("COPY", "BINARY", MatchAny))
        COMPLETE_WITH("FROM", "TO");
    /* If we have COPY [BINARY] <sth> FROM|TO, complete with filename */
-   else if (Matches("COPY|\\copy", MatchAny, "FROM|TO") ||
+   else if (Matches("COPY", MatchAny, "FROM|TO") ||
             Matches("COPY", "BINARY", MatchAny, "FROM|TO"))
    {
        completion_charp = "";
+       completion_force_quote = true;  /* COPY requires quoted filename */
        matches = rl_completion_matches(text, complete_from_files);
    }
-
-   /* Handle COPY [BINARY] <sth> FROM|TO filename */
+   else if (Matches("\\copy", MatchAny, "FROM|TO"))
+   {
+       completion_charp = "";
+       completion_force_quote = false;
+       matches = rl_completion_matches(text, complete_from_files);
+   }
+   /* Offer options after COPY [BINARY] <sth> FROM|TO filename */
    else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny) ||
             Matches("COPY", "BINARY", MatchAny, "FROM|TO", MatchAny))
        COMPLETE_WITH("BINARY", "DELIMITER", "NULL", "CSV",
                      "ENCODING");
 
-   /* Handle COPY [BINARY] <sth> FROM|TO filename CSV */
+   /* Offer options after COPY [BINARY] <sth> FROM|TO filename CSV */
    else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "CSV") ||
             Matches("COPY", "BINARY", MatchAny, "FROM|TO", MatchAny, "CSV"))
        COMPLETE_WITH("HEADER", "QUOTE", "ESCAPE", "FORCE QUOTE",
@@ -3821,6 +3873,7 @@ psql_completion(const char *text, int start, int end)
                           "\\s|\\w|\\write|\\lo_import"))
    {
        completion_charp = "\\";
+       completion_force_quote = false;
        matches = rl_completion_matches(text, complete_from_files);
    }
 
@@ -4387,14 +4440,58 @@ complete_from_variables(const 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.
+ *
+ * Caller must set 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.
+ *
+ * Caller must also set completion_force_quote to indicate whether to force
+ * quotes around the result.  (The SQL COPY command requires that.)
  */
 static char *
 complete_from_files(const char *text, int state)
 {
+#ifdef USE_FILENAME_QUOTING_FUNCTIONS
+
+   /*
+    * If we're using a version of Readline that supports filename quoting
+    * hooks, rely on those, and invoke rl_filename_completion_function()
+    * without messing with its arguments.  Readline does stuff internally
+    * that does not work well at all if we try to handle dequoting here.
+    * Instead, Readline will call quote_file_name() and dequote_file_name()
+    * (see below) at appropriate times.
+    *
+    * ... or at least, mostly it will.  There are some paths involving
+    * unmatched file names in which Readline never calls quote_file_name(),
+    * and if left to its own devices it will incorrectly append a quote
+    * anyway.  Set rl_completion_suppress_quote to prevent that.  If we do
+    * get to quote_file_name(), we'll clear this again.  (Yes, this seems
+    * like it's working around Readline bugs.)
+    *
+    * (For now, we assume that rl_completion_suppress_quote exists if the
+    * filename quoting hooks do.)
+    */
+   rl_completion_suppress_quote = 1;
+
+   /* If user typed a quote, force quoting (never remove user's quote) */
+   if (*text == '\'')
+       completion_force_quote = true;
+
+   return rl_filename_completion_function(text, state);
+#else
+
+   /*
+    * Otherwise, we have to do the best we can.
+    */
    static const char *unquoted_text;
    char       *unquoted_match;
    char       *ret = NULL;
 
+   /* If user typed a quote, force quoting (never remove user's quote) */
+   if (*text == '\'')
+       completion_force_quote = true;
+
    if (state == 0)
    {
        /* Initialization: stash the unquoted input. */
@@ -4411,22 +4508,40 @@ complete_from_files(const char *text, int state)
    unquoted_match = rl_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.
-        */
+       struct stat statbuf;
+       bool        is_dir = (stat(unquoted_match, &statbuf) == 0 &&
+                             S_ISDIR(statbuf.st_mode) != 0);
+
+       /* Re-quote the result, if needed. */
        ret = quote_if_needed(unquoted_match, " \t\r\n\"`",
-                             '\'', *completion_charp, pset.encoding);
+                             '\'', *completion_charp,
+                             completion_force_quote,
+                             pset.encoding);
        if (ret)
            free(unquoted_match);
        else
            ret = unquoted_match;
+
+       /*
+        * If it's a directory, replace trailing quote with a slash; this is
+        * usually more convenient.  (If we didn't quote, leave this to
+        * libedit.)
+        */
+       if (*ret == '\'' && is_dir)
+       {
+           char       *retend = ret + strlen(ret) - 1;
+
+           Assert(*retend == '\'');
+           *retend = '/';
+           /* Try to prevent libedit from adding a space, too */
+#ifdef HAVE_RL_COMPLETION_APPEND_CHARACTER
+           rl_completion_append_character = '\0';
+#endif
+       }
    }
 
    return ret;
+#endif                         /* USE_FILENAME_QUOTING_FUNCTIONS */
 }
 
 
@@ -4678,46 +4793,108 @@ get_guctype(const char *varname)
    return guctype;
 }
 
-#ifdef NOT_USED
+#ifdef USE_FILENAME_QUOTING_FUNCTIONS
 
 /*
- * Surround a string with single quotes. This works for both SQL and
- * psql internal. Currently disabled because it is reported not to
- * cooperate with certain versions of readline.
+ * Quote a filename according to SQL rules, returning a malloc'd string.
+ * completion_charp must point to escape character or '\0', and
+ * completion_force_quote must be set correctly, as per comments for
+ * complete_from_files().
  */
 static char *
-quote_file_name(char *text, int match_type, char *quote_pointer)
+quote_file_name(char *fname, int match_type, char *quote_pointer)
 {
    char       *s;
-   size_t      length;
+   struct stat statbuf;
+
+   /* Quote if needed. */
+   s = quote_if_needed(fname, " \t\r\n\"`",
+                       '\'', *completion_charp,
+                       completion_force_quote,
+                       pset.encoding);
+   if (!s)
+       s = pg_strdup(fname);
+
+   /*
+    * However, some of the time we have to strip the trailing quote from what
+    * we send back.  Never strip the trailing quote if the user already typed
+    * one; otherwise, suppress the trailing quote if we have multiple/no
+    * matches (because we don't want to add a quote if the input is seemingly
+    * unfinished), or if the input was already quoted (because Readline will
+    * do arguably-buggy things otherwise), or if the file does not exist, or
+    * if it's a directory.
+    */
+   if (*s == '\'' &&
+       completion_last_char != '\'' &&
+       (match_type != SINGLE_MATCH ||
+        (quote_pointer && *quote_pointer == '\'') ||
+        stat(fname, &statbuf) != 0 ||
+        S_ISDIR(statbuf.st_mode)))
+   {
+       char       *send = s + strlen(s) - 1;
+
+       Assert(*send == '\'');
+       *send = '\0';
+   }
 
-   (void) quote_pointer;       /* not used */
+   /*
+    * And now we can let Readline do its thing with possibly adding a quote
+    * on its own accord.  (This covers some additional cases beyond those
+    * dealt with above.)
+    */
+   rl_completion_suppress_quote = 0;
+
+   /*
+    * If user typed a leading quote character other than single quote (i.e.,
+    * double quote), zap it, so that we replace it with the correct single
+    * quote.
+    */
+   if (quote_pointer && *quote_pointer != '\'')
+       *quote_pointer = '\0';
 
-   length = strlen(text) +(match_type == SINGLE_MATCH ? 3 : 2);
-   s = pg_malloc(length);
-   s[0] = '\'';
-   strcpy(s + 1, text);
-   if (match_type == SINGLE_MATCH)
-       s[length - 2] = '\'';
-   s[length - 1] = '\0';
    return s;
 }
 
+/*
+ * Dequote a filename, if it's quoted.
+ * completion_charp must point to escape character or '\0', as per
+ * comments for complete_from_files().
+ */
 static char *
-dequote_file_name(char *text, char quote_char)
+dequote_file_name(char *fname, int quote_char)
 {
-   char       *s;
-   size_t      length;
+   char       *unquoted_fname;
+
+   /*
+    * If quote_char is set, it's not included in "fname".  We have to add it
+    * or strtokx will not interpret the string correctly (notably, it won't
+    * recognize escapes).
+    */
+   if (quote_char == '\'')
+   {
+       char       *workspace = (char *) pg_malloc(strlen(fname) + 2);
 
-   if (!quote_char)
-       return pg_strdup(text);
+       workspace[0] = quote_char;
+       strcpy(workspace + 1, fname);
+       unquoted_fname = strtokx(workspace, "", NULL, "'", *completion_charp,
+                                false, true, pset.encoding);
+       free(workspace);
+   }
+   else
+       unquoted_fname = strtokx(fname, "", NULL, "'", *completion_charp,
+                                false, true, pset.encoding);
 
-   length = strlen(text);
-   s = pg_malloc(length - 2 + 1);
-   strlcpy(s, text +1, length - 2 + 1);
+   /* expect a NULL return for the empty string only */
+   if (!unquoted_fname)
+   {
+       Assert(*fname == '\0');
+       unquoted_fname = fname;
+   }
 
-   return s;
+   /* readline expects a malloc'd result that it is to free */
+   return pg_strdup(unquoted_fname);
 }
-#endif                         /* NOT_USED */
+
+#endif                         /* USE_FILENAME_QUOTING_FUNCTIONS */
 
 #endif                         /* USE_READLINE */
index 050c48b108b860369848717852063aeb03e4a7c2..983d94e3fe7c7a66947ddcb0e9dc1811392ef5d4 100644 (file)
 /* Define to 1 if you have the `rl_filename_completion_function' function. */
 #undef HAVE_RL_FILENAME_COMPLETION_FUNCTION
 
+/* Define to 1 if you have the global variable 'rl_filename_quote_characters'.
+   */
+#undef HAVE_RL_FILENAME_QUOTE_CHARACTERS
+
+/* Define to 1 if you have the global variable 'rl_filename_quoting_function'.
+   */
+#undef HAVE_RL_FILENAME_QUOTING_FUNCTION
+
 /* Define to 1 if you have the `rl_reset_screen_size' function. */
 #undef HAVE_RL_RESET_SCREEN_SIZE
 
index be02bd4524c3b350a3650300a1b779cd2951da54..cc4e238318158a2c3c66aa6232094067ad213ded 100644 (file)
@@ -333,6 +333,8 @@ sub GenerateFiles
        HAVE_RL_COMPLETION_APPEND_CHARACTER      => undef,
        HAVE_RL_COMPLETION_MATCHES               => undef,
        HAVE_RL_FILENAME_COMPLETION_FUNCTION     => undef,
+       HAVE_RL_FILENAME_QUOTE_CHARACTERS        => undef,
+       HAVE_RL_FILENAME_QUOTING_FUNCTION        => undef,
        HAVE_RL_RESET_SCREEN_SIZE                => undef,
        HAVE_SECURITY_PAM_APPL_H                 => undef,
        HAVE_SETPROCTITLE                        => undef,