-# 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';])],
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
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
$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`
# 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 = '';
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");
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";
#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@$><=;|&{() "
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 */
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
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
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;
/*
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
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",
"\\s|\\w|\\write|\\lo_import"))
{
completion_charp = "\\";
+ completion_force_quote = false;
matches = rl_completion_matches(text, complete_from_files);
}
* 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. */
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 */
}
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 */