Re: BUG #16059: Tab-completion of filenames in COPY commands removes required quotes - Mailing list pgsql-hackers

From Tom Lane
Subject Re: BUG #16059: Tab-completion of filenames in COPY commands removes required quotes
Date
Msg-id 22096.1578965897@sss.pgh.pa.us
Whole thread Raw
In response to Re: BUG #16059: Tab-completion of filenames in COPY commands removes required quotes  (Tom Lane <tgl@sss.pgh.pa.us>)
Responses Re: BUG #16059: Tab-completion of filenames in COPY commands removesrequired quotes
List pgsql-hackers
I wrote:
> Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:
>> I have found a weird behavior with identifier quoting, which is not the 
>> subject of this patch, but it appears to be affected by it.

> I'll think about how to improve this.  Given that we're dequoting
> filenames explicitly anyway, maybe we don't need to tell readline that
> double-quote is a quoting character.  Another idea is that maybe
> we ought to refactor things so that identifier dequoting and requoting
> is handled explicitly, similarly to the way filenames work now.
> That would make the patch a lot bigger though.

On reflection, it seems like the best bet for the moment is to
remove double-quote from the rl_completer_quote_characters string,
which should restore all behavior around double-quoted strings to
what it was before.  (We have to keep single-quote in that string,
though, or quoted file names fail entirely.)

The only impact this has on filename completion is that we're no
longer bright enough to convert a double-quoted filename to
single-quoted for you, which would be a nice-to-have but it's
hardly core functionality.

At some point it'd be good to undo that and upgrade quoted-identifier
processing, but I don't really want to include such changes in this
patch.  I'm about burned out on tab completion for right now.

            regards, tom lane

diff --git a/config/programs.m4 b/config/programs.m4
index 90ff944..68ab823 100644
--- a/config/programs.m4
+++ b/config/programs.m4
@@ -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



diff --git a/configure b/configure
index 25cfbcb..a46ba40 100755
--- 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`
diff --git a/configure.in b/configure.in
index 1599fc5..8165f70 100644
--- a/configure.in
+++ b/configure.in
@@ -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
diff --git a/src/bin/psql/stringutils.c b/src/bin/psql/stringutils.c
index c74e58d..c521749 100644
--- a/src/bin/psql/stringutils.c
+++ b/src/bin/psql/stringutils.c
@@ -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');
diff --git a/src/bin/psql/stringutils.h b/src/bin/psql/stringutils.h
index 1fb1cfc..4be172e 100644
--- a/src/bin/psql/stringutils.h
+++ b/src/bin/psql/stringutils.h
@@ -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 */
diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index b35c4f8..c27f216 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -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";
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 2fd8886..2871159 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -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"
@@ -63,6 +64,15 @@
 #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,13 +2311,19 @@ 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);
+    }
+    else if (Matches("\\copy", MatchAny, "FROM|TO"))
+    {
+        completion_charp = "";
+        completion_force_quote = false;
         matches = rl_completion_matches(text, complete_from_files);
     }
-
     /* Handle COPY [BINARY] <sth> FROM|TO filename */
     else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny) ||
              Matches("COPY", "BINARY", MatchAny, "FROM|TO", MatchAny))
@@ -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 */
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 050c48b..983d94e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -488,6 +488,14 @@
 /* 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

diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index 909bded..979964c 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -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,

pgsql-hackers by date:

Previous
From: Tomas Vondra
Date:
Subject: Re: Amcheck: do rightlink verification with lock coupling
Next
From: Andrew Dunstan
Date:
Subject: Re: Unicode escapes with any backend encoding