On Thu, 5 Jun 2025 10:08:35 +0900
Yugo Nagata <nag...@sraoss.co.jp> wrote:

> Hi,
> 
> Currently, tab completion for COPY only suggests filenames after TO or
> FROM, even though STDIN, STDOUT, and PROGRAM are also valid syntax options.
> 
> I'd like to propose improving the tab completion behavior as described in
> the subject, so that these keywords are suggested appropriately, and filenames
> are offered as potential command names after the PROGRAM keyword.
> 
> I've attached this proposal as a patch series with the following three parts:

I'm sorry but the previous patches were accidentally broken and didn't work.
I've attached fixed patches. 
 
> 
> 0001: Refactor match_previous_words() to remove direct use of 
> rl_completion_matches()
> 
> This is a preparatory cleanup. Most completions in match_previous_words() 
> already use 
> COMPLETE_WITH* macros, which wrap rl_completion_matches(). However, some 
> direct calls
> still remain.
> 
> This patch replaces the remaining direct calls with COMPLETE_WITH_FILES or 
> COMPLETE_WITH_GENERATOR, improving consistency and readability.
> 
> 0002: Add tab completion support for COPY ... TO/FROM STDIN, STDOUT, and 
> PROGRAM
> 
> This is the main patch. It extends tab completion to suggest STDIN, STDOUT, 
> and PROGRAM
> after TO or FROM. After PROGRAM, filenames are suggested as possible command 
> names.
> 
> To support this, a new macro COMPLETE_WITH_FILES_PLUS is introduced. This 
> allows
> combining literal keywords with filename suggestions in the completion list.
> 
> 0003: Improve tab completion for COPY option lists
> 
> Currently, only the first option in a parenthesized list is suggested during 
> completion,
> and nothing is suggested after a comma.
> 
> This patch enables suggestions after each comma, improving usability when 
> specifying
> multiple options.
> 
> Although not directly related to the main proposal, I believe this is a 
> helpful enhancement
> to COPY tab completion and included it here for completeness.
> 
> I’d appreciate your review and feedback on this series.
> 
> 
> Best regards,
> Yugo Nagata
> 
> -- 
> Yugo Nagata <nag...@sraoss.co.jp>


-- 
Yugo Nagata <nag...@sraoss.co.jp>
>From b46da45111db1a079b0c4ede6632d3989c721f2a Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nag...@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:09 +0900
Subject: [PATCH v2 3/3] Improve tab completion for COPY option lists

Previously, only the first option in a parenthesized list was suggested
during tab completion. Subsequent options after a comma were not completed.
This commit enhances the behavior to suggest valid options after each comma.
---
 src/bin/psql/tab-complete.in.c | 47 +++++++++++++++++++---------------
 1 file changed, 26 insertions(+), 21 deletions(-)

diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index e11c88bfdb5..a98ec019f62 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3310,27 +3310,32 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("WITH (", "WHERE");
 
 	/* Complete COPY <sth> FROM|TO [PROGRAM] <sth> WITH ( */
-	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
-			 Matches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "("))
-		COMPLETE_WITH("FORMAT", "FREEZE", "DELIMITER", "NULL",
-					  "HEADER", "QUOTE", "ESCAPE", "FORCE_QUOTE",
-					  "FORCE_NOT_NULL", "FORCE_NULL", "ENCODING", "DEFAULT",
-					  "ON_ERROR", "LOG_VERBOSITY");
-
-	/* Complete COPY <sth> FROM|TO [PROGRAM] <sth> WITH (FORMAT */
-	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(", "FORMAT") ||
-			 Matches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(", "FORMAT"))
-		COMPLETE_WITH("binary", "csv", "text");
-
-	/* Complete COPY <sth> FROM [PROGRAM] <sth> WITH (ON_ERROR */
-	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(", "ON_ERROR") ||
-			 Matches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(", "ON_ERROR"))
-		COMPLETE_WITH("stop", "ignore");
-
-	/* Complete COPY <sth> FROM [PROGRAM] <sth> WITH (LOG_VERBOSITY */
-	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(", "LOG_VERBOSITY") ||
-			 Matches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
-		COMPLETE_WITH("silent", "default", "verbose");
+	else if (HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(*") ||
+			 HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(*"))
+	{
+		if (!HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(*)") &&
+			!HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(*)"))
+		{
+			/* We're in an unfinished parenthesized option list. */
+			if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
+				COMPLETE_WITH("FORMAT", "FREEZE", "DELIMITER", "NULL",
+							  "HEADER", "QUOTE", "ESCAPE", "FORCE_QUOTE",
+							  "FORCE_NOT_NULL", "FORCE_NULL", "ENCODING", "DEFAULT",
+							  "ON_ERROR", "LOG_VERBOSITY");
+
+			/* Complete COPY <sth> FROM|TO filename WITH (FORMAT */
+			else if (TailMatches("FORMAT"))
+				COMPLETE_WITH("binary", "csv", "text");
+
+			/* Complete COPY <sth> FROM filename WITH (ON_ERROR */
+			else if (TailMatches("ON_ERROR"))
+				COMPLETE_WITH("stop", "ignore");
+
+			/* Complete COPY <sth> FROM filename WITH (LOG_VERBOSITY */
+			else if (TailMatches("LOG_VERBOSITY"))
+				COMPLETE_WITH("silent", "default", "verbose");
+		}
+	}
 
 	/* Complete COPY <sth> FROM [PROGRAM] <sth> WITH (<options>) */
 	else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", MatchAny) ||
-- 
2.43.0

>From 1c15b6d8d65746fe1def6fbb581625e866f383d4 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nag...@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:24 +0900
Subject: [PATCH v2 2/3] Add tab completion support for COPY ... TO/FROM STDIN,
 STDOUT, and PROGRAM

Previously, tab completion for COPY only suggested filenames after TO or
FROM, even though STDIN, STDOUT, and PROGRAM are also valid options.
This commit extends the completion to include these keywords. After PROGRAM,
filename suggestions are shown as potential command names.

To support this, a new macro COMPLETE_WITH_FILES_PLUS is introduced, allowing
both literal keywords and filenames to be included in the completion results.
---
 src/bin/psql/tab-complete.in.c | 114 +++++++++++++++++++++++++++------
 1 file changed, 93 insertions(+), 21 deletions(-)

diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8a85a285281..e11c88bfdb5 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -443,13 +443,23 @@ do { \
 	matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)
 
-#define COMPLETE_WITH_FILES(escape, force_quote) \
+#define COMPLETE_WITH_FILES_LIST(escape, force_quote, list) \
 do { \
 	completion_charp = escape; \
+	completion_charpp = list; \
 	completion_force_quote = force_quote; \
 	matches = rl_completion_matches(text, complete_from_files); \
 } while (0)
 
+#define COMPLETE_WITH_FILES(escape, force_quote) \
+	COMPLETE_WITH_FILES_LIST(escape, force_quote, NULL)
+
+#define COMPLETE_WITH_FILES_PLUS(escape, force_quote, ...) \
+do { \
+	static const char *const list[] = { __VA_ARGS__, NULL }; \
+	COMPLETE_WITH_FILES_LIST(escape, force_quote, list); \
+} while (0)
+
 #define COMPLETE_WITH_GENERATOR(function) \
 	matches = rl_completion_matches(text, function)
 
@@ -1460,6 +1470,7 @@ static void append_variable_names(char ***varnames, int *nvars,
 static char **complete_from_variables(const char *text,
 									  const char *prefix, const char *suffix, bool need_value);
 static char *complete_from_files(const char *text, int state);
+static char *_complete_from_files(const char *text, int state);
 
 static char *pg_strdup_keyword_case(const char *s, const char *ref);
 static char *escape_string(const char *text);
@@ -3272,41 +3283,58 @@ match_previous_words(int pattern_id,
 	/* Complete COPY <sth> */
 	else if (Matches("COPY|\\copy", MatchAny))
 		COMPLETE_WITH("FROM", "TO");
-	/* Complete COPY <sth> FROM|TO with filename */
-	else if (Matches("COPY", MatchAny, "FROM|TO"))
-		COMPLETE_WITH_FILES("", true);	/* COPY requires quoted filename */
-	else if (Matches("\\copy", MatchAny, "FROM|TO"))
-		COMPLETE_WITH_FILES("", false);
-
-	/* Complete COPY <sth> TO <sth> */
-	else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny))
+	/* Complete COPY|\copy <sth> FROM|TO with filename or STDIN/STDOUT/PROGRAM */
+	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO"))
+	{
+		/* COPY requires quoted filename */
+		bool force_quote = HeadMatches("COPY");
+
+		if (TailMatches("FROM"))
+			COMPLETE_WITH_FILES_PLUS("", force_quote, "STDIN", "PROGRAM");
+		else
+			COMPLETE_WITH_FILES_PLUS("", force_quote, "STDOUT", "PROGRAM");
+	}
+
+	/* Complete COPY|\copy <sth> FROM|TO PROGRAM command */
+	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM"))
+		COMPLETE_WITH_FILES("", HeadMatches("COPY"));	/* COPY requires quoted filename */
+
+	/* Complete COPY <sth> TO [PROGRAM] <sth> */
+	else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM")) ||
+			 Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny))
 		COMPLETE_WITH("WITH (");
 
-	/* Complete COPY <sth> FROM <sth> */
-	else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny))
+	/* Complete COPY <sth> FROM [PROGRAM] <sth> */
+	else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM")) ||
+			 Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny))
 		COMPLETE_WITH("WITH (", "WHERE");
 
-	/* Complete COPY <sth> FROM|TO filename WITH ( */
-	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "("))
+	/* Complete COPY <sth> FROM|TO [PROGRAM] <sth> WITH ( */
+	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
+			 Matches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "("))
 		COMPLETE_WITH("FORMAT", "FREEZE", "DELIMITER", "NULL",
 					  "HEADER", "QUOTE", "ESCAPE", "FORCE_QUOTE",
 					  "FORCE_NOT_NULL", "FORCE_NULL", "ENCODING", "DEFAULT",
 					  "ON_ERROR", "LOG_VERBOSITY");
 
-	/* Complete COPY <sth> FROM|TO filename WITH (FORMAT */
-	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "FORMAT"))
+	/* Complete COPY <sth> FROM|TO [PROGRAM] <sth> WITH (FORMAT */
+	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(", "FORMAT") ||
+			 Matches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(", "FORMAT"))
 		COMPLETE_WITH("binary", "csv", "text");
 
-	/* Complete COPY <sth> FROM filename WITH (ON_ERROR */
-	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "ON_ERROR"))
+	/* Complete COPY <sth> FROM [PROGRAM] <sth> WITH (ON_ERROR */
+	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(", "ON_ERROR") ||
+			 Matches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(", "ON_ERROR"))
 		COMPLETE_WITH("stop", "ignore");
 
-	/* Complete COPY <sth> FROM filename WITH (LOG_VERBOSITY */
-	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
+	/* Complete COPY <sth> FROM [PROGRAM] <sth> WITH (LOG_VERBOSITY */
+	else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(", "LOG_VERBOSITY") ||
+			 Matches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
 		COMPLETE_WITH("silent", "default", "verbose");
 
-	/* Complete COPY <sth> FROM <sth> WITH (<options>) */
-	else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", MatchAny))
+	/* Complete COPY <sth> FROM [PROGRAM] <sth> WITH (<options>) */
+	else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", MatchAny) ||
+			 Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", MatchAny))
 		COMPLETE_WITH("WHERE");
 
 	/* CREATE ACCESS METHOD */
@@ -6173,9 +6201,53 @@ complete_from_variables(const char *text, const char *prefix, const char *suffix
  *
  * Caller must also set completion_force_quote to indicate whether to force
  * quotes around the result.  (The SQL COPY command requires that.)
+ *
+ * If completion_charpp is set to a null-terminated array of literal keywords,
+ * these keywords will be included in the completion results alongside filenames,
+ * as long as they case-insensitively match the current input.
  */
 static char *
 complete_from_files(const char *text, int state)
+{
+	char 	   *result;
+	static int	list_index;
+	static bool	files_done;
+	const char *item;
+
+	/* Initialization */
+	if (state == 0)
+	{
+		list_index = 0;
+		files_done = false;
+	}
+
+	/* Return a filename that matches */
+	if (!files_done && (result = _complete_from_files(text, state)))
+		return result;
+	else if (!completion_charpp)
+		return NULL;
+	else
+		files_done = true;
+
+	/*
+	 * If there are no more matching files, check for hard-wired keywords.
+	 * These will only be returned if they match the input-so-far,
+	 * ignoring case.
+	 */
+	while ((item = completion_charpp[list_index++]))
+	{
+		if (pg_strncasecmp(text, item, strlen(text)) == 0)
+		{
+			completion_force_quote = false;
+			return pg_strdup_keyword_case(item, text);
+		}
+	}
+
+	return NULL;
+}
+
+static char *
+_complete_from_files(const char *text, int state)
 {
 #ifdef USE_FILENAME_QUOTING_FUNCTIONS
 
-- 
2.43.0

>From aa19e7d2fe286beccf49c51656c5e2ec4c4d11a9 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nag...@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:38:45 +0900
Subject: [PATCH v2 1/3] Refactor match_previous_words() to remove direct use
 of rl_completion_matches()

Most tab completions in match_previous_words() use COMPLETE_WITH* macros,
which wrap rl_completion_matches(). However, some direct calls to
rl_completion_matches() still remained.

This commit replaces the remaining direct calls with the new macro,
COMPLETE_WITH_FILES or COMPLETE_WITH_GENERATOR, for improved consistency and
readability.
---
 src/bin/psql/tab-complete.in.c | 38 ++++++++++++++++------------------
 1 file changed, 18 insertions(+), 20 deletions(-)

diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index ec65ab79fec..8a85a285281 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -443,6 +443,16 @@ do { \
 	matches = rl_completion_matches(text, complete_from_schema_query); \
 } while (0)
 
+#define COMPLETE_WITH_FILES(escape, force_quote) \
+do { \
+	completion_charp = escape; \
+	completion_force_quote = force_quote; \
+	matches = rl_completion_matches(text, complete_from_files); \
+} while (0)
+
+#define COMPLETE_WITH_GENERATOR(function) \
+	matches = rl_completion_matches(text, function)
+
 /*
  * Assembly instructions for schema queries
  *
@@ -2158,7 +2168,7 @@ match_previous_words(int pattern_id,
 			/* for INDEX and TABLE/SEQUENCE, respectively */
 						  "UNIQUE", "UNLOGGED");
 		else
-			matches = rl_completion_matches(text, create_command_generator);
+			COMPLETE_WITH_GENERATOR(create_command_generator);
 	}
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
@@ -2168,7 +2178,7 @@ match_previous_words(int pattern_id,
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
 	else if (Matches("DROP"))
-		matches = rl_completion_matches(text, drop_command_generator);
+		COMPLETE_WITH_GENERATOR(drop_command_generator);
 
 /* ALTER */
 
@@ -2179,7 +2189,7 @@ match_previous_words(int pattern_id,
 
 	/* ALTER something */
 	else if (Matches("ALTER"))
-		matches = rl_completion_matches(text, alter_command_generator);
+		COMPLETE_WITH_GENERATOR(alter_command_generator);
 	/* ALTER TABLE,INDEX,MATERIALIZED VIEW ALL IN TABLESPACE xxx */
 	else if (TailMatches("ALL", "IN", "TABLESPACE", MatchAny))
 		COMPLETE_WITH("SET TABLESPACE", "OWNED BY");
@@ -3264,17 +3274,9 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("FROM", "TO");
 	/* Complete COPY <sth> FROM|TO with filename */
 	else if (Matches("COPY", MatchAny, "FROM|TO"))
-	{
-		completion_charp = "";
-		completion_force_quote = true;	/* COPY requires quoted filename */
-		matches = rl_completion_matches(text, complete_from_files);
-	}
+		COMPLETE_WITH_FILES("", true);	/* COPY requires quoted filename */
 	else if (Matches("\\copy", MatchAny, "FROM|TO"))
-	{
-		completion_charp = "";
-		completion_force_quote = false;
-		matches = rl_completion_matches(text, complete_from_files);
-	}
+		COMPLETE_WITH_FILES("", false);
 
 	/* Complete COPY <sth> TO <sth> */
 	else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny))
@@ -5347,9 +5349,9 @@ match_previous_words(int pattern_id,
 	else if (TailMatchesCS("\\h|\\help", MatchAny))
 	{
 		if (TailMatches("DROP"))
-			matches = rl_completion_matches(text, drop_command_generator);
+			COMPLETE_WITH_GENERATOR(drop_command_generator);
 		else if (TailMatches("ALTER"))
-			matches = rl_completion_matches(text, alter_command_generator);
+			COMPLETE_WITH_GENERATOR(alter_command_generator);
 
 		/*
 		 * CREATE is recognized by tail match elsewhere, so doesn't need to be
@@ -5449,11 +5451,7 @@ match_previous_words(int pattern_id,
 	else if (TailMatchesCS("\\cd|\\e|\\edit|\\g|\\gx|\\i|\\include|"
 						   "\\ir|\\include_relative|\\o|\\out|"
 						   "\\s|\\w|\\write|\\lo_import"))
-	{
-		completion_charp = "\\";
-		completion_force_quote = false;
-		matches = rl_completion_matches(text, complete_from_files);
-	}
+		COMPLETE_WITH_FILES("\\", false);
 
 	/* gen_tabcomplete.pl ends special processing here */
 	/* END GEN_TABCOMPLETE */
-- 
2.43.0

Reply via email to