diff --git a/src/bin/psql/stringutils.c b/src/bin/psql/stringutils.c
index 3b5ce1ba4bf3c54b2388474d0afa691bdb8bac09..77387dcf3deaab0bac0b58751e8359279afe4134 100644
--- a/src/bin/psql/stringutils.c
+++ b/src/bin/psql/stringutils.c
@@ -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;
+}
diff --git a/src/bin/psql/stringutils.h b/src/bin/psql/stringutils.h
index c7c5f3877d92fbea189b0619eda316d55ac57042..c64fc584585f0957b1db94119d994f6b7ca4ba7b 100644
--- a/src/bin/psql/stringutils.h
+++ b/src/bin/psql/stringutils.h
@@ -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 */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 3854f7f421fc853a60ce538f6e3f2f670f454021..6f481bb24dd40ebc59c1895be0c754a3fa45ea93 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -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 <sth> 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 */