psql: tab-completion support for COPY ... TO/FROM STDIN, STDOUT, and PROGRAM
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:
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 <nagata@sraoss.co.jp>
Attachments:
0003-Improve-tab-completion-for-COPY-option-lists.patchtext/x-diff; name=0003-Improve-tab-completion-for-COPY-option-lists.patchDownload
From 10e3ffa305e5a318b4a3ea8cbe8e36cea085e4d7 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:09 +0900
Subject: [PATCH 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 6d10c818ce0..6f8933a73a2 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3300,27 +3300,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
0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STDIN-.patchtext/x-diff; name=0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STDIN-.patchDownload
From ee5759c559d8132e2121cd209136c4d0452884e8 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:24 +0900
Subject: [PATCH 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 | 102 ++++++++++++++++++++++++++-------
1 file changed, 82 insertions(+), 20 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8a85a285281..6d10c818ce0 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1460,6 +1460,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 +3273,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 +6191,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
0001-Refactor-match_previous_words-to-remove-direct-use-o.patchtext/x-diff; name=0001-Refactor-match_previous_words-to-remove-direct-use-o.patchDownload
From aa19e7d2fe286beccf49c51656c5e2ec4c4d11a9 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:38:45 +0900
Subject: [PATCH 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
On Thu, 5 Jun 2025 10:08:35 +0900
Yugo Nagata <nagata@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 <nagata@sraoss.co.jp>
--
Yugo Nagata <nagata@sraoss.co.jp>
Attachments:
v2-0003-Improve-tab-completion-for-COPY-option-lists.patchtext/x-diff; name=v2-0003-Improve-tab-completion-for-COPY-option-lists.patchDownload
From b46da45111db1a079b0c4ede6632d3989c721f2a Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@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
v2-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patchtext/x-diff; name=v2-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patchDownload
From 1c15b6d8d65746fe1def6fbb581625e866f383d4 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@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
v2-0001-Refactor-match_previous_words-to-remove-direct-us.patchtext/x-diff; name=v2-0001-Refactor-match_previous_words-to-remove-direct-us.patchDownload
From aa19e7d2fe286beccf49c51656c5e2ec4c4d11a9 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@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
On Thu, 5 Jun 2025 16:52:00 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 5 Jun 2025 10:08:35 +0900
Yugo Nagata <nagata@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.
I've attached rebased patches.
Best regards,
Yugo Nagata
--
Yugo Nagata <nagata@sraoss.co.jp>
Attachments:
v3-0003-Improve-tab-completion-for-COPY-option-lists.patchtext/x-diff; name=v3-0003-Improve-tab-completion-for-COPY-option-lists.patchDownload
From 27ce6ff40386e0d43dc3dbf33ee0996365910655 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:09 +0900
Subject: [PATCH v3 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 e1605f8b73e..9f6ec4e61b2 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", "REJECT_LIMIT");
-
- /* 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", "REJECT_LIMIT");
+
+ /* 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
v3-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patchtext/x-diff; name=v3-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patchDownload
From 3aa523fe9ed1546e68da06d92c90f8329db2cae1 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:24 +0900
Subject: [PATCH v3 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 2716554aa0b..e1605f8b73e 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", "REJECT_LIMIT");
- /* 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 */
@@ -6174,9 +6202,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
v3-0001-Refactor-match_previous_words-to-remove-direct-us.patchtext/x-diff; name=v3-0001-Refactor-match_previous_words-to-remove-direct-us.patchDownload
From 5201ed1ebbc166943ac215d76074739aa98b81ec Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:38:45 +0900
Subject: [PATCH v3 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 2c0b4f28c14..2716554aa0b 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))
@@ -5348,9 +5350,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
@@ -5450,11 +5452,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
On Tue, 17 Jun 2025 00:08:32 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 5 Jun 2025 16:52:00 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 10:08:35 +0900
Yugo Nagata <nagata@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.
I've attached rebased patches.
Regards,
Yugo Nagata
--
Yugo Nagata <nagata@sraoss.co.jp>
Attachments:
v4-0003-Improve-tab-completion-for-COPY-option-lists.patchtext/x-diff; name=v4-0003-Improve-tab-completion-for-COPY-option-lists.patchDownload
From 9f39c69812212a51841e8f5b52d81df68b3872ed Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:09 +0900
Subject: [PATCH v4 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 | 52 ++++++++++++++++++----------------
1 file changed, 28 insertions(+), 24 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index c565c0218e1..1413ca527a9 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3353,30 +3353,34 @@ match_previous_words(int pattern_id,
Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny))
COMPLETE_WITH("WITH (", "WHERE");
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "("))
- COMPLETE_WITH(Copy_from_options);
-
- /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
- Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))
- COMPLETE_WITH(Copy_to_options);
-
- /* 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] filename WITH (ON_ERROR */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "ON_ERROR") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "ON_ERROR"))
- COMPLETE_WITH("stop", "ignore");
-
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH (LOG_VERBOSITY */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "LOG_VERBOSITY") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
- COMPLETE_WITH("silent", "default", "verbose");
+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */
+ else if ((HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
+ HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(")) &&
+ !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, ','))
+ {
+ if (HeadMatches("COPY|\\copy", MatchAny, "FROM"))
+ COMPLETE_WITH(Copy_from_options);
+ else
+ COMPLETE_WITH(Copy_to_options);
+ }
+
+ /* 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
v4-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patchtext/x-diff; name=v4-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patchDownload
From 31b0c0f115a9f28a51c1b7f598159de6dac240e8 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:24 +0900
Subject: [PATCH v4 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 | 119 ++++++++++++++++++++++++++-------
1 file changed, 96 insertions(+), 23 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 1b4855edab6..c565c0218e1 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)
@@ -1481,6 +1491,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);
@@ -3316,42 +3327,60 @@ 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 filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "("))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH ( */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
+ Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "("))
COMPLETE_WITH(Copy_from_options);
- /* Complete COPY <sth> TO filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "("))
+ /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
+ else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
+ Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))
COMPLETE_WITH(Copy_to_options);
- /* 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", MatchAny, "WITH", "(", "ON_ERROR"))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH (ON_ERROR */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "ON_ERROR") ||
+ Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "ON_ERROR"))
COMPLETE_WITH("stop", "ignore");
- /* Complete COPY <sth> FROM filename WITH (LOG_VERBOSITY */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH (LOG_VERBOSITY */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "LOG_VERBOSITY") ||
+ Matches("COPY|\\copy", MatchAny, "FROM", "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 */
@@ -6243,9 +6272,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
v4-0001-Refactor-match_previous_words-to-remove-direct-us.patchtext/x-diff; name=v4-0001-Refactor-match_previous_words-to-remove-direct-us.patchDownload
From f4a10aaee1686dde7cee5d4a5a15b629e4c90e42 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:38:45 +0900
Subject: [PATCH v4 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 37524364290..1b4855edab6 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
*
@@ -2179,7 +2189,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"))
@@ -2189,7 +2199,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 */
@@ -2200,7 +2210,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");
@@ -3308,17 +3318,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))
@@ -5417,9 +5419,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
@@ -5519,11 +5521,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
On Thu, 17 Jul 2025 10:57:36 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Tue, 17 Jun 2025 00:08:32 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 16:52:00 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 10:08:35 +0900
Yugo Nagata <nagata@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.
The previous patch was broken failed to complie since I missed following
the required format of if-conditions in match_previous_words().
I've attached update patches.
Regards,
Yugo Nagata
--
Yugo Nagata <nagata@sraoss.co.jp>
Attachments:
v5-0003-Improve-tab-completion-for-COPY-option-lists.patchtext/x-diff; name=v5-0003-Improve-tab-completion-for-COPY-option-lists.patchDownload
From 886f71294ba4ec6618b1707858a9eacef4d188ef Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:09 +0900
Subject: [PATCH v5 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 | 53 +++++++++++++++++++---------------
1 file changed, 29 insertions(+), 24 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index c565c0218e1..079ee1630e5 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3353,30 +3353,35 @@ match_previous_words(int pattern_id,
Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny))
COMPLETE_WITH("WITH (", "WHERE");
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "("))
- COMPLETE_WITH(Copy_from_options);
-
- /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
- Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))
- COMPLETE_WITH(Copy_to_options);
-
- /* 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] filename WITH (ON_ERROR */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "ON_ERROR") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "ON_ERROR"))
- COMPLETE_WITH("stop", "ignore");
-
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH (LOG_VERBOSITY */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "LOG_VERBOSITY") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
- COMPLETE_WITH("silent", "default", "verbose");
+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */
+ 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, ','))
+ {
+ if (HeadMatches("COPY|\\copy", MatchAny, "FROM"))
+ COMPLETE_WITH(Copy_from_options);
+ else
+ COMPLETE_WITH(Copy_to_options);
+ }
+
+ /* 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
v5-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patchtext/x-diff; name=v5-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patchDownload
From fe2def4bf5d0e56cd2d35c77b181d93b92c78374 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:24 +0900
Subject: [PATCH v5 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 | 119 ++++++++++++++++++++++++++-------
1 file changed, 96 insertions(+), 23 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 1b4855edab6..c565c0218e1 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)
@@ -1481,6 +1491,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);
@@ -3316,42 +3327,60 @@ 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 filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "("))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH ( */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
+ Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "("))
COMPLETE_WITH(Copy_from_options);
- /* Complete COPY <sth> TO filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "("))
+ /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
+ else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
+ Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))
COMPLETE_WITH(Copy_to_options);
- /* 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", MatchAny, "WITH", "(", "ON_ERROR"))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH (ON_ERROR */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "ON_ERROR") ||
+ Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "ON_ERROR"))
COMPLETE_WITH("stop", "ignore");
- /* Complete COPY <sth> FROM filename WITH (LOG_VERBOSITY */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH (LOG_VERBOSITY */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "LOG_VERBOSITY") ||
+ Matches("COPY|\\copy", MatchAny, "FROM", "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 */
@@ -6243,9 +6272,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
v5-0001-Refactor-match_previous_words-to-remove-direct-us.patchtext/x-diff; name=v5-0001-Refactor-match_previous_words-to-remove-direct-us.patchDownload
From db82aeb49deb4d6c0389ed715c930368ab05b3f1 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:38:45 +0900
Subject: [PATCH v5 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 37524364290..1b4855edab6 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
*
@@ -2179,7 +2189,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"))
@@ -2189,7 +2199,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 */
@@ -2200,7 +2210,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");
@@ -3308,17 +3318,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))
@@ -5417,9 +5419,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
@@ -5519,11 +5521,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
On Fri, 18 Jul 2025 at 12:49, Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 17 Jul 2025 10:57:36 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Tue, 17 Jun 2025 00:08:32 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 16:52:00 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 10:08:35 +0900
Yugo Nagata <nagata@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.
The previous patch was broken failed to complie since I missed following
the required format of if-conditions in match_previous_words().
I've attached update patches.Regards,
Yugo Nagata--
Yugo Nagata <nagata@sraoss.co.jp>
Hi! This tab completion support is indeed very useful. Thanks for
working on this.
I spotted potential improvement here: current v5 does not support COPY
completion for a pattern I do frequently use:
specifying COPY options without WITH clause e.g. "copy yy from
'/home/reshke/cpg/csv.csv' <tab>" Is not completed with BINARY, CSV,
DELIMITER etc.
--
Best regards,
Kirill Reshke
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.
Hi! This tab completion support is indeed very useful. Thanks for
working on this.
Thank you for your feedback!
I spotted potential improvement here: current v5 does not support COPY
completion for a pattern I do frequently use:
specifying COPY options without WITH clause e.g. "copy yy from
'/home/reshke/cpg/csv.csv' <tab>" Is not completed with BINARY, CSV,
DELIMITER etc.
This is the old syntax and is supported for backward compatibility
(commit 923413ac6d3), but I’m not sure it’s worth supporting in tab
completion. Perhaps it would be better to discuss this point in a
separate thread.
Regards,
Yugo Nagata
--
Yugo Nagata <nagata@sraoss.co.jp>
On Thu, 28 Aug 2025 at 12:22, Yugo Nagata <nagata@sraoss.co.jp> wrote:
Hi! This tab completion support is indeed very useful. Thanks for
working on this.Thank you for your feedback!
I spotted potential improvement here: current v5 does not support COPY
completion for a pattern I do frequently use:
specifying COPY options without WITH clause e.g. "copy yy from
'/home/reshke/cpg/csv.csv' <tab>" Is not completed with BINARY, CSV,
DELIMITER etc.This is the old syntax and is supported for backward compatibility
(commit 923413ac6d3), but I’m not sure it’s worth supporting in tab
completion. Perhaps it would be better to discuss this point in a
separate thread.
I see your point. However I do not think that old syntax is a show-stopper here.
current psql (on HEAD) has TAB support for archaic patterns like START
<tab> completed with TRANSACTION and ABORT W<tab> completed with WORK.
And I do think that the case I use in COPY (and propose to support
thus) is more frequent than these two. I came across this thread while
looking to support my usecase.
This does not make you obligated to support this in this patch series.
I can even send v5-0004 with this support in the thread if that's
okay.
--
Best regards,
Kirill Reshke
On Fri, Jul 18, 2025 at 12:49 AM Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 17 Jul 2025 10:57:36 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Tue, 17 Jun 2025 00:08:32 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 16:52:00 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 10:08:35 +0900
Yugo Nagata <nagata@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.
The previous patch was broken failed to complie since I missed following
the required format of if-conditions in match_previous_words().
I've attached update patches.
I agree with the basic direction of the patches. Here are some
comments on the first two patches:
v5-0001-Refactor-match_previous_words-to-remove-direct-us.patch:
---
+#define COMPLETE_WITH_GENERATOR(function) \
+ matches = rl_completion_matches(text, function)
I think it would be clearer if we use 'generator' or 'genfunc' instead
of 'function' as a macro argument.
v5-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patch:
---
+ /* 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 */
Why does it complete the query with files names even after 'PROGRAM'?
---
+static char *
+_complete_from_files(const char *text, int state)
{
I think the comments of complete_from_files() should be moved to this
new function. For instance, the comments starts with:
* 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.
But complete_from_files() function no longer calls
rl_filename_completion_function().
---
- /* 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))
I see this kind of conversion many places in the patch; convert one
condition with MatchAny into two conditions with
MatchAnyExcept("PROGRAM") and '"PROGRAM", MatchAny'. How about
simplifying it using MatchAnyN. For example,
else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN))
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
On Wed, 17 Sep 2025 12:53:10 -0700
Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Fri, Jul 18, 2025 at 12:49 AM Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 17 Jul 2025 10:57:36 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Tue, 17 Jun 2025 00:08:32 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 16:52:00 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 10:08:35 +0900
Yugo Nagata <nagata@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.
The previous patch was broken failed to complie since I missed following
the required format of if-conditions in match_previous_words().
I've attached update patches.I agree with the basic direction of the patches. Here are some
comments on the first two patches:
Thank you for reviewing it.
I've attached an updated patch.
v5-0001-Refactor-match_previous_words-to-remove-direct-us.patch:
--- +#define COMPLETE_WITH_GENERATOR(function) \ + matches = rl_completion_matches(text, function)I think it would be clearer if we use 'generator' or 'genfunc' instead
of 'function' as a macro argument.
I fixed it to use 'generator'.
v5-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patch:
--- + /* 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 */Why does it complete the query with files names even after 'PROGRAM'?
Users can specify the command by giving a filename with an absolute or
relative path, so I think it makes sense to allow filename completion
after PROGRAM.
--- +static char * +_complete_from_files(const char *text, int state) {I think the comments of complete_from_files() should be moved to this
new function. For instance, the comments starts with:* 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.But complete_from_files() function no longer calls
rl_filename_completion_function().
I moved the comments to the top of _complete_from_files() and added a new
comment for complete_from_files() to describe that it is a wrapper of the
former.
--- - /* 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))I see this kind of conversion many places in the patch; convert one
condition with MatchAny into two conditions with
MatchAnyExcept("PROGRAM") and '"PROGRAM", MatchAny'. How about
simplifying it using MatchAnyN. For example,else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN))
We could simplify this by using MatchAnyN, but doing so would cause "WITH ("
or "WHERE" to be suggested after "WITH (...)", even though that is not allowed
by the syntax. This could be misleading for users, so I wonder whether it is
worth adding a bit of complexity to prevent possible confusion.
Regards,
Yugo Nagata
--
Yugo Nagata <nagata@sraoss.co.jp>
Attachments:
v6-0003-Improve-tab-completion-for-COPY-option-lists.patchtext/x-diff; name=v6-0003-Improve-tab-completion-for-COPY-option-lists.patchDownload
From 8f99014ee3ca852c61406e9799a8752690dd1454 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:09 +0900
Subject: [PATCH v6 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 | 53 +++++++++++++++++++---------------
1 file changed, 29 insertions(+), 24 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index a8041401b8b..0cc2c6e516b 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3361,30 +3361,35 @@ match_previous_words(int pattern_id,
Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny))
COMPLETE_WITH("WITH (", "WHERE");
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "("))
- COMPLETE_WITH(Copy_from_options);
-
- /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
- Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))
- COMPLETE_WITH(Copy_to_options);
-
- /* 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] filename WITH (ON_ERROR */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "ON_ERROR") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "ON_ERROR"))
- COMPLETE_WITH("stop", "ignore");
-
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH (LOG_VERBOSITY */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "LOG_VERBOSITY") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
- COMPLETE_WITH("silent", "default", "verbose");
+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */
+ 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, ','))
+ {
+ if (HeadMatches("COPY|\\copy", MatchAny, "FROM"))
+ COMPLETE_WITH(Copy_from_options);
+ else
+ COMPLETE_WITH(Copy_to_options);
+ }
+
+ /* 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
v6-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patchtext/x-diff; name=v6-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patchDownload
From df49bba655d94b850fc8b2a1e946e8efcd874d37 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:24 +0900
Subject: [PATCH v6 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 | 125 ++++++++++++++++++++++++++-------
1 file changed, 101 insertions(+), 24 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 6176741d20b..a8041401b8b 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(generator) \
matches = rl_completion_matches(text, generator)
@@ -1484,6 +1494,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);
@@ -3324,42 +3335,60 @@ 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 filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "("))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH ( */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
+ Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "("))
COMPLETE_WITH(Copy_from_options);
- /* Complete COPY <sth> TO filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "("))
+ /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
+ else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
+ Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))
COMPLETE_WITH(Copy_to_options);
- /* 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", MatchAny, "WITH", "(", "ON_ERROR"))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH (ON_ERROR */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "ON_ERROR") ||
+ Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "ON_ERROR"))
COMPLETE_WITH("stop", "ignore");
- /* Complete COPY <sth> FROM filename WITH (LOG_VERBOSITY */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH (LOG_VERBOSITY */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "LOG_VERBOSITY") ||
+ Matches("COPY|\\copy", MatchAny, "FROM", "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 */
@@ -6241,6 +6270,54 @@ complete_from_variables(const char *text, const char *prefix, const char *suffix
}
+/*
+ * This function wraps _complete_from_files() so that both literal keywords
+ * and filenames can be included in the completion results.
+ *
+ * If completion_charpp is set to a null-terminated array of literal keywords,
+ * those keywords are added to 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;
+}
+
/*
* This function wraps rl_filename_completion_function() to strip quotes from
* the input before searching for matches and to quote any matches for which
@@ -6255,7 +6332,7 @@ complete_from_variables(const char *text, const char *prefix, const char *suffix
* quotes around the result. (The SQL COPY command requires that.)
*/
static char *
-complete_from_files(const char *text, int state)
+_complete_from_files(const char *text, int state)
{
#ifdef USE_FILENAME_QUOTING_FUNCTIONS
--
2.43.0
v6-0001-Refactor-match_previous_words-to-remove-direct-us.patchtext/x-diff; name=v6-0001-Refactor-match_previous_words-to-remove-direct-us.patchDownload
From 5ce0f42dd9865db8f484e733fb0e6794017f3188 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:38:45 +0900
Subject: [PATCH v6 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 6b20a4404b2..6176741d20b 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(generator) \
+ matches = rl_completion_matches(text, generator)
+
/*
* Assembly instructions for schema queries
*
@@ -2182,7 +2192,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"))
@@ -2192,7 +2202,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 */
@@ -2203,7 +2213,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");
@@ -3316,17 +3326,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))
@@ -5427,9 +5429,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
@@ -5529,11 +5531,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
On Thu, 18 Sep 2025 12:05:06 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Wed, 17 Sep 2025 12:53:10 -0700
Masahiko Sawada <sawada.mshk@gmail.com> wrote:On Fri, Jul 18, 2025 at 12:49 AM Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 17 Jul 2025 10:57:36 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Tue, 17 Jun 2025 00:08:32 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 16:52:00 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 10:08:35 +0900
Yugo Nagata <nagata@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.
The previous patch was broken failed to complie since I missed following
the required format of if-conditions in match_previous_words().
I've attached update patches.I agree with the basic direction of the patches. Here are some
comments on the first two patches:Thank you for reviewing it.
I've attached an updated patch.v5-0001-Refactor-match_previous_words-to-remove-direct-us.patch:
--- +#define COMPLETE_WITH_GENERATOR(function) \ + matches = rl_completion_matches(text, function)I think it would be clearer if we use 'generator' or 'genfunc' instead
of 'function' as a macro argument.I fixed it to use 'generator'.
v5-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patch:
--- + /* 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 */Why does it complete the query with files names even after 'PROGRAM'?
Users can specify the command by giving a filename with an absolute or
relative path, so I think it makes sense to allow filename completion
after PROGRAM.--- +static char * +_complete_from_files(const char *text, int state) {I think the comments of complete_from_files() should be moved to this
new function. For instance, the comments starts with:* 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.But complete_from_files() function no longer calls
rl_filename_completion_function().I moved the comments to the top of _complete_from_files() and added a new
comment for complete_from_files() to describe that it is a wrapper of the
former.--- - /* 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))I see this kind of conversion many places in the patch; convert one
condition with MatchAny into two conditions with
MatchAnyExcept("PROGRAM") and '"PROGRAM", MatchAny'. How about
simplifying it using MatchAnyN. For example,else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN))
We could simplify this by using MatchAnyN, but doing so would cause "WITH ("
or "WHERE" to be suggested after "WITH (...)", even though that is not allowed
by the syntax. This could be misleading for users, so I wonder whether it is
worth adding a bit of complexity to prevent possible confusion.
There was a mistake in the previous statement: "WHERE" appearing after "WITH (...)"
is actually correct. However, this also results in "WITH" being suggested after
"WHERE", which is not permitted by the syntax.
Regards,
Yugo Nagata
--
Yugo Nagata <nagata@sraoss.co.jp>
On Wed, Sep 17, 2025 at 8:27 PM Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 18 Sep 2025 12:05:06 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Wed, 17 Sep 2025 12:53:10 -0700
Masahiko Sawada <sawada.mshk@gmail.com> wrote:On Fri, Jul 18, 2025 at 12:49 AM Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 17 Jul 2025 10:57:36 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Tue, 17 Jun 2025 00:08:32 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 16:52:00 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:On Thu, 5 Jun 2025 10:08:35 +0900
Yugo Nagata <nagata@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.
The previous patch was broken failed to complie since I missed following
the required format of if-conditions in match_previous_words().
I've attached update patches.I agree with the basic direction of the patches. Here are some
comments on the first two patches:Thank you for reviewing it.
I've attached an updated patch.
Thank you for updating the patches!
v5-0001-Refactor-match_previous_words-to-remove-direct-us.patch:
--- +#define COMPLETE_WITH_GENERATOR(function) \ + matches = rl_completion_matches(text, function)I think it would be clearer if we use 'generator' or 'genfunc' instead
of 'function' as a macro argument.I fixed it to use 'generator'.
LGTM. I've pushed the 0001 patch.
v5-0002-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patch:
--- + /* 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 */Why does it complete the query with files names even after 'PROGRAM'?
Users can specify the command by giving a filename with an absolute or
relative path, so I think it makes sense to allow filename completion
after PROGRAM.
Agreed.
--- +static char * +_complete_from_files(const char *text, int state) {I think the comments of complete_from_files() should be moved to this
new function. For instance, the comments starts with:* 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.But complete_from_files() function no longer calls
rl_filename_completion_function().I moved the comments to the top of _complete_from_files() and added a new
comment for complete_from_files() to describe that it is a wrapper of the
former.--- - /* 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))I see this kind of conversion many places in the patch; convert one
condition with MatchAny into two conditions with
MatchAnyExcept("PROGRAM") and '"PROGRAM", MatchAny'. How about
simplifying it using MatchAnyN. For example,else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN))
We could simplify this by using MatchAnyN, but doing so would cause "WITH ("
or "WHERE" to be suggested after "WITH (...)", even though that is not allowed
by the syntax. This could be misleading for users, so I wonder whether it is
worth adding a bit of complexity to prevent possible confusion.There was a mistake in the previous statement: "WHERE" appearing after "WITH (...)"
is actually correct. However, this also results in "WITH" being suggested after
"WHERE", which is not permitted by the syntax.
True. How about other places? That is, where we check the completion
after "WITH (". For example:
- /* Complete COPY <sth> TO filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "("))
+ /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
+ else if (Matches("COPY|\\copy", MatchAny, "TO",
MatchAnyExcept("PROGRAM"), "WITH", "(") ||
+ Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM",
MatchAny, "WITH", "("))
Does it make sense to replace these two lines with the following one line?
else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, MatchAnyN,
"WITH", "("))
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
On Thu, 25 Sep 2025 14:31:18 -0700
Masahiko Sawada <sawada.mshk@gmail.com> wrote:
I fixed it to use 'generator'.
LGTM. I've pushed the 0001 patch.
Thank you!
--- - /* 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))I see this kind of conversion many places in the patch; convert one
condition with MatchAny into two conditions with
MatchAnyExcept("PROGRAM") and '"PROGRAM", MatchAny'. How about
simplifying it using MatchAnyN. For example,else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN))
We could simplify this by using MatchAnyN, but doing so would cause "WITH ("
or "WHERE" to be suggested after "WITH (...)", even though that is not allowed
by the syntax. This could be misleading for users, so I wonder whether it is
worth adding a bit of complexity to prevent possible confusion.There was a mistake in the previous statement: "WHERE" appearing after "WITH (...)"
is actually correct. However, this also results in "WITH" being suggested after
"WHERE", which is not permitted by the syntax.True. How about other places? That is, where we check the completion
after "WITH (". For example:- /* Complete COPY <sth> TO filename WITH ( */ - else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "(")) + /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */ + else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") || + Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))Does it make sense to replace these two lines with the following one line?
else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, MatchAnyN,
"WITH", "("))
That works for other places where options are suggested after "WITH (" and
"WHERE" is suggested after "WITH (*)".
I've attached updated patches using MatchAnyN following your suggestion.
The patch 0002 was also changed to use Matches, since MathAnyN cannot be used
with HeadMatches. I don't think this is a problem, because the COPY command cannot
be nested and "COPY or "\copy" would always appear at the beginning.
Regards,
Yugo Nagata
--
Yugo Nagata <nagata@sraoss.co.jp>
Attachments:
v7-0002-Improve-tab-completion-for-COPY-option-lists.patchtext/x-diff; name=v7-0002-Improve-tab-completion-for-COPY-option-lists.patchDownload
From bc53ffcf0c055f16e8f2d4a3f8f1a6607565380f Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:09 +0900
Subject: [PATCH v7 2/2] 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 | 40 ++++++++++++++++++++--------------
1 file changed, 24 insertions(+), 16 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index a918a65f0a8..2f98f09847c 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3361,25 +3361,33 @@ match_previous_words(int pattern_id,
Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny))
COMPLETE_WITH("WITH (", "WHERE");
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN, "WITH", "("))
- COMPLETE_WITH(Copy_from_options);
-
- /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, MatchAnyN, "WITH", "("))
- COMPLETE_WITH(Copy_to_options);
+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, MatchAnyN, "WITH", "("))
+ {
+ if (!Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, MatchAnyN, "WITH", "(*)"))
+ {
+ /* We're in an unfinished parenthesized option list. */
+ if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
+ {
+ if (HeadMatches("COPY|\\copy", MatchAny, "FROM"))
+ COMPLETE_WITH(Copy_from_options);
+ else
+ COMPLETE_WITH(Copy_to_options);
+ }
- /* Complete COPY <sth> FROM|TO [PROGRAM] <sth> WITH (FORMAT */
- else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, MatchAnyN, "WITH", "(", "FORMAT"))
- COMPLETE_WITH("binary", "csv", "text");
+ /* Complete COPY <sth> FROM|TO filename WITH (FORMAT */
+ else if (TailMatches("FORMAT"))
+ COMPLETE_WITH("binary", "csv", "text");
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH (ON_ERROR */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN, "WITH", "(", "ON_ERROR"))
- COMPLETE_WITH("stop", "ignore");
+ /* Complete COPY <sth> FROM filename WITH (ON_ERROR */
+ else if (TailMatches("ON_ERROR"))
+ COMPLETE_WITH("stop", "ignore");
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH (LOG_VERBOSITY */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN, "WITH", "(", "LOG_VERBOSITY"))
- COMPLETE_WITH("silent", "default", "verbose");
+ /* 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", MatchAny, MatchAnyN, "WITH", MatchAny))
--
2.43.0
v7-0001-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patchtext/x-diff; name=v7-0001-Add-tab-completion-support-for-COPY-.-TO-FROM-STD.patchDownload
From 9b61a23eb36de970b0982008256ce2ac20fc14f9 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Thu, 5 Jun 2025 09:39:24 +0900
Subject: [PATCH v7 1/2] 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 | 119 ++++++++++++++++++++++++++-------
1 file changed, 95 insertions(+), 24 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 6176741d20b..a918a65f0a8 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(generator) \
matches = rl_completion_matches(text, generator)
@@ -1484,6 +1494,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);
@@ -3324,42 +3335,54 @@ 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 filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "("))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH ( */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN, "WITH", "("))
COMPLETE_WITH(Copy_from_options);
- /* Complete COPY <sth> TO filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "("))
+ /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
+ else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, MatchAnyN, "WITH", "("))
COMPLETE_WITH(Copy_to_options);
- /* 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", MatchAny, MatchAnyN, "WITH", "(", "FORMAT"))
COMPLETE_WITH("binary", "csv", "text");
- /* Complete COPY <sth> FROM filename WITH (ON_ERROR */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "(", "ON_ERROR"))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH (ON_ERROR */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN, "WITH", "(", "ON_ERROR"))
COMPLETE_WITH("stop", "ignore");
- /* Complete COPY <sth> FROM filename WITH (LOG_VERBOSITY */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH (LOG_VERBOSITY */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN, "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", MatchAny, MatchAnyN, "WITH", MatchAny))
COMPLETE_WITH("WHERE");
/* CREATE ACCESS METHOD */
@@ -6241,6 +6264,54 @@ complete_from_variables(const char *text, const char *prefix, const char *suffix
}
+/*
+ * This function wraps _complete_from_files() so that both literal keywords
+ * and filenames can be included in the completion results.
+ *
+ * If completion_charpp is set to a null-terminated array of literal keywords,
+ * those keywords are added to 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;
+}
+
/*
* This function wraps rl_filename_completion_function() to strip quotes from
* the input before searching for matches and to quote any matches for which
@@ -6255,7 +6326,7 @@ complete_from_variables(const char *text, const char *prefix, const char *suffix
* quotes around the result. (The SQL COPY command requires that.)
*/
static char *
-complete_from_files(const char *text, int state)
+_complete_from_files(const char *text, int state)
{
#ifdef USE_FILENAME_QUOTING_FUNCTIONS
--
2.43.0
On Thu, Sep 25, 2025 at 6:16 PM Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 25 Sep 2025 14:31:18 -0700
Masahiko Sawada <sawada.mshk@gmail.com> wrote:I fixed it to use 'generator'.
LGTM. I've pushed the 0001 patch.
Thank you!
--- - /* 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))I see this kind of conversion many places in the patch; convert one
condition with MatchAny into two conditions with
MatchAnyExcept("PROGRAM") and '"PROGRAM", MatchAny'. How about
simplifying it using MatchAnyN. For example,else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN))
We could simplify this by using MatchAnyN, but doing so would cause "WITH ("
or "WHERE" to be suggested after "WITH (...)", even though that is not allowed
by the syntax. This could be misleading for users, so I wonder whether it is
worth adding a bit of complexity to prevent possible confusion.There was a mistake in the previous statement: "WHERE" appearing after "WITH (...)"
is actually correct. However, this also results in "WITH" being suggested after
"WHERE", which is not permitted by the syntax.True. How about other places? That is, where we check the completion
after "WITH (". For example:- /* Complete COPY <sth> TO filename WITH ( */ - else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "(")) + /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */ + else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") || + Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))Does it make sense to replace these two lines with the following one line?
else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, MatchAnyN,
"WITH", "("))That works for other places where options are suggested after "WITH (" and
"WHERE" is suggested after "WITH (*)".I've attached updated patches using MatchAnyN following your suggestion.
Thank you for updating the patch!
After reviewing both the v6 and v7 patches, I realize that your
original approach in v6 is actually better than what I suggested.
While it requires more lines of code, it provides more precise
completions. Additionally, since most of these extra lines are removed
by the next patch (v6-0003), the code size isn't really an issue.
Would it be possible to withdraw my previous comments and proceed with
the v6 approach? I apologize for the back-and-forth on this.
I have two review comments about the complete_from_files() function:
+ * This function wraps _complete_from_files() so that both literal keywords
+ * and filenames can be included in the completion results.
+ *
+ * If completion_charpp is set to a null-terminated array of literal keywords,
+ * those keywords are added to the completion results alongside filenames,
+ * as long as they case-insensitively match the current input.
How about rephrasing the comments to the following?
/*
* This function returns in order one of a fixed, NULL pointer terminated list
* of string that matches file names or optionally specified list of keywords.
*
* If completion_charpp is set to a null-terminated array of literal keywords,
* those keywords are added to the completion results alongside filenames if
* they case-insensitively match the current input.
*/
---
+ /* 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;
It works but it's odd a bit that we don't set files_done to true if
completion_charpp is not NULL. I think it becomes more readable if we
could set files_done to true if _complete_from_files() doesn't return
a string and proceed with the hard-wired keywords.
The attached patch that can be applied on top of v6-0002 patch,
implements my suggestions and includes pgindent fixes. Please review
these changes.
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
Attachments:
fix_v6_masahiko.patch.txttext/plain; charset=US-ASCII; name=fix_v6_masahiko.patch.txtDownload
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index a8041401b8b..c4e40042048 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3339,7 +3339,7 @@ match_previous_words(int pattern_id,
else if (Matches("COPY|\\copy", MatchAny, "FROM|TO"))
{
/* COPY requires quoted filename */
- bool force_quote = HeadMatches("COPY");
+ bool force_quote = HeadMatches("COPY");
if (TailMatches("FROM"))
COMPLETE_WITH_FILES_PLUS("", force_quote, "STDIN", "PROGRAM");
@@ -3349,7 +3349,8 @@ match_previous_words(int pattern_id,
/* 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_WITH_FILES("", HeadMatches("COPY")); /* COPY requires quoted
+ * filename */
/* Complete COPY <sth> TO [PROGRAM] <sth> */
else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM")) ||
@@ -6271,19 +6272,18 @@ complete_from_variables(const char *text, const char *prefix, const char *suffix
/*
- * This function wraps _complete_from_files() so that both literal keywords
- * and filenames can be included in the completion results.
+ * This function returns in order one of a fixed, NULL pointer terminated list
+ * of string that matches file names or optionally specified list of keywords.
*
* If completion_charpp is set to a null-terminated array of literal keywords,
- * those keywords are added to the completion results alongside filenames,
- * as long as they case-insensitively match the current input.
+ * those keywords are added to the completion results alongside filenames if
+ * 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;
+ static bool files_done;
const char *item;
/* Initialization */
@@ -6293,18 +6293,21 @@ complete_from_files(const char *text, int state)
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
+ if (!files_done)
+ {
+ char *result = _complete_from_files(text, state);
+
+ /* Return a filename that matches */
+ if (result)
+ return result;
+
+ /* There are no more matching files */
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.
+ * 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++]))
{
On Fri, Oct 3, 2025 at 2:06 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Thu, Sep 25, 2025 at 6:16 PM Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 25 Sep 2025 14:31:18 -0700
Masahiko Sawada <sawada.mshk@gmail.com> wrote:I fixed it to use 'generator'.
LGTM. I've pushed the 0001 patch.
Thank you!
--- - /* 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))I see this kind of conversion many places in the patch; convert one
condition with MatchAny into two conditions with
MatchAnyExcept("PROGRAM") and '"PROGRAM", MatchAny'. How about
simplifying it using MatchAnyN. For example,else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN))
We could simplify this by using MatchAnyN, but doing so would cause "WITH ("
or "WHERE" to be suggested after "WITH (...)", even though that is not allowed
by the syntax. This could be misleading for users, so I wonder whether it is
worth adding a bit of complexity to prevent possible confusion.There was a mistake in the previous statement: "WHERE" appearing after "WITH (...)"
is actually correct. However, this also results in "WITH" being suggested after
"WHERE", which is not permitted by the syntax.True. How about other places? That is, where we check the completion
after "WITH (". For example:- /* Complete COPY <sth> TO filename WITH ( */ - else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "(")) + /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */ + else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") || + Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))Does it make sense to replace these two lines with the following one line?
else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, MatchAnyN,
"WITH", "("))That works for other places where options are suggested after "WITH (" and
"WHERE" is suggested after "WITH (*)".I've attached updated patches using MatchAnyN following your suggestion.
Thank you for updating the patch!
After reviewing both the v6 and v7 patches, I realize that your
original approach in v6 is actually better than what I suggested.
While it requires more lines of code, it provides more precise
completions. Additionally, since most of these extra lines are removed
by the next patch (v6-0003), the code size isn't really an issue.
Would it be possible to withdraw my previous comments and proceed with
the v6 approach? I apologize for the back-and-forth on this.I have two review comments about the complete_from_files() function:
+ * This function wraps _complete_from_files() so that both literal keywords + * and filenames can be included in the completion results. + * + * If completion_charpp is set to a null-terminated array of literal keywords, + * those keywords are added to the completion results alongside filenames, + * as long as they case-insensitively match the current input.How about rephrasing the comments to the following?
/*
* This function returns in order one of a fixed, NULL pointer terminated list
* of string that matches file names or optionally specified list of keywords.
*
* If completion_charpp is set to a null-terminated array of literal keywords,
* those keywords are added to the completion results alongside filenames if
* they case-insensitively match the current input.
*/--- + /* 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;It works but it's odd a bit that we don't set files_done to true if
completion_charpp is not NULL. I think it becomes more readable if we
could set files_done to true if _complete_from_files() doesn't return
a string and proceed with the hard-wired keywords.The attached patch that can be applied on top of v6-0002 patch,
implements my suggestions and includes pgindent fixes. Please review
these changes.
I believe we can split the first patch into two patches: one adds
support for STDIN/STDOUT with COMPLETE_WIHT_FILES_PLUS() and another
one adds support for COPY syntaxes using the PROGRAM clause. I've
attached the reorganized patch set and made cosmetic changes to the
0003 patch (i.e., improving COPY option list). What do you think?
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
Attachments:
v8-0003-psql-Improve-tab-completion-for-COPY-option-lists.patchapplication/octet-stream; name=v8-0003-psql-Improve-tab-completion-for-COPY-option-lists.patchDownload
From 88a5f42ae92dca4c0c40b34fcb8362b4a8fbbe6c Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 6 Oct 2025 15:03:31 -0700
Subject: [PATCH v8 3/3] psql: Improve tab completion for COPY option lists.
Previously, only the first option in a parenthesized option list was
suggested by tab completion. This commit enhances tab completion for
both COPY TO and COPY FROM commands to suggest options after each comma.
Author: Yugo Nagata <nagata@sraoss.co.jp>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Discussion: https://postgr.es/m/20250605100835.b396f9d656df1018f65a4556@sraoss.co.jp
---
src/bin/psql/tab-complete.in.c | 60 ++++++++++++++++++++--------------
1 file changed, 36 insertions(+), 24 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 72ad8a16712..3cd1e7fbb8a 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3362,30 +3362,42 @@ match_previous_words(int pattern_id,
Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny))
COMPLETE_WITH("WITH (", "WHERE");
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "("))
- COMPLETE_WITH(Copy_from_options);
-
- /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
- Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))
- COMPLETE_WITH(Copy_to_options);
-
- /* 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] filename WITH (ON_ERROR */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "ON_ERROR") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "ON_ERROR"))
- COMPLETE_WITH("stop", "ignore");
-
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH (LOG_VERBOSITY */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "LOG_VERBOSITY") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
- COMPLETE_WITH("silent", "default", "verbose");
+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */
+ 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", "(*)"))
+ {
+ /*
+ * This fires if we're in an unfinished parenthesized option list.
+ * get_previous_words treats a completed parenthesized option list
+ * as one word, so the above tests are correct.
+ */
+
+ if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
+ {
+ if (HeadMatches("COPY|\\copy", MatchAny, "FROM"))
+ COMPLETE_WITH(Copy_from_options);
+ else
+ COMPLETE_WITH(Copy_to_options);
+ }
+
+ /* 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");
+ }
+
+ /* A completed parenthesized option list should be caught below */
+ }
/* Complete COPY <sth> FROM [PROGRAM] <sth> WITH (<options>) */
else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", MatchAny) ||
--
2.47.3
v8-0001-psql-Improve-tab-completion-for-COPY-.-STDIN-STDO.patchapplication/octet-stream; name=v8-0001-psql-Improve-tab-completion-for-COPY-.-STDIN-STDO.patchDownload
From ddc886912b7bbf4e756b5f5bdab21f0560afe05c Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 6 Oct 2025 16:42:12 -0700
Subject: [PATCH v8 1/3] psql: Improve tab completion for COPY ...
STDIN/STDOUT.
This commit enhances tab completion for both COPY FROM and COPY TO
commands to suggest STDIN and STDOUT, respectively.
To make suggesting both file names and keywords easier, it introduces
a new COMPLETE_WITH_FILES_PLUS() macro.
Author: Yugo Nagata <nagata@sraoss.co.jp>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Discussion: https://postgr.es/m/20250605100835.b396f9d656df1018f65a4556@sraoss.co.jp
---
src/bin/psql/tab-complete.in.c | 84 +++++++++++++++++++++++++++++++---
1 file changed, 77 insertions(+), 7 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 6176741d20b..1454f0da3f1 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(generator) \
matches = rl_completion_matches(text, generator)
@@ -1484,6 +1494,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);
@@ -3324,11 +3335,17 @@ 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|\copy <sth> FROM|TO with filename or STDIN/STDOUT */
+ 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");
+ else
+ COMPLETE_WITH_FILES_PLUS("", force_quote, "STDOUT");
+ }
/* Complete COPY <sth> TO <sth> */
else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny))
@@ -6241,6 +6258,59 @@ complete_from_variables(const char *text, const char *prefix, const char *suffix
}
+/*
+ * This function returns in order one of a fixed, NULL pointer terminated list
+ * of string that matches file names or optionally specified list of keywords.
+ *
+ * If completion_charpp is set to a null-terminated array of literal keywords,
+ * those keywords are added to the completion results alongside filenames if
+ * they case-insensitively match the current input.
+ */
+static char *
+complete_from_files(const char *text, int state)
+{
+ static int list_index;
+ static bool files_done;
+ const char *item;
+
+ /* Initialization */
+ if (state == 0)
+ {
+ list_index = 0;
+ files_done = false;
+ }
+
+ if (!files_done)
+ {
+ char *result = _complete_from_files(text, state);
+
+ /* Return a filename that matches */
+ if (result)
+ return result;
+
+ /* There are no more matching files */
+ files_done = true;
+ }
+
+ if (!completion_charpp)
+ return NULL;
+
+ /*
+ * 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;
+}
+
/*
* This function wraps rl_filename_completion_function() to strip quotes from
* the input before searching for matches and to quote any matches for which
@@ -6255,7 +6325,7 @@ complete_from_variables(const char *text, const char *prefix, const char *suffix
* quotes around the result. (The SQL COPY command requires that.)
*/
static char *
-complete_from_files(const char *text, int state)
+_complete_from_files(const char *text, int state)
{
#ifdef USE_FILENAME_QUOTING_FUNCTIONS
--
2.47.3
v8-0002-psql-Add-tab-completion-for-COPY-.-PROGRAM.patchapplication/octet-stream; name=v8-0002-psql-Add-tab-completion-for-COPY-.-PROGRAM.patchDownload
From 5aaa222ffa3d84d831144dc0cc30156c065842b9 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 6 Oct 2025 16:52:42 -0700
Subject: [PATCH v8 2/3] psql: Add tab completion for COPY ... PROGRAM.
This commit improve psql's tab completion for both COPY TO and COPY
FROM commands by supporting syntaxes using PROGRAM.
Author: Yugo Nagata <nagata@sraoss.co.jp>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Discussion: https://postgr.es/m/20250605100835.b396f9d656df1018f65a4556@sraoss.co.jp
---
src/bin/psql/tab-complete.in.c | 51 +++++++++++++++++++++-------------
1 file changed, 32 insertions(+), 19 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 1454f0da3f1..72ad8a16712 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3335,48 +3335,61 @@ match_previous_words(int pattern_id,
/* Complete COPY <sth> */
else if (Matches("COPY|\\copy", MatchAny))
COMPLETE_WITH("FROM", "TO");
- /* Complete COPY|\copy <sth> FROM|TO with filename or STDIN/STDOUT */
+ /* 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");
+ COMPLETE_WITH_FILES_PLUS("", force_quote, "STDIN", "PROGRAM");
else
- COMPLETE_WITH_FILES_PLUS("", force_quote, "STDOUT");
+ COMPLETE_WITH_FILES_PLUS("", force_quote, "STDOUT", "PROGRAM");
}
- /* Complete COPY <sth> TO <sth> */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny))
+ /* Complete COPY|\copy <sth> FROM|TO PROGRAM */
+ 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 filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "("))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH ( */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
+ Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "("))
COMPLETE_WITH(Copy_from_options);
- /* Complete COPY <sth> TO filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "("))
+ /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
+ else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
+ Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))
COMPLETE_WITH(Copy_to_options);
- /* 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", MatchAny, "WITH", "(", "ON_ERROR"))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH (ON_ERROR */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "ON_ERROR") ||
+ Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "ON_ERROR"))
COMPLETE_WITH("stop", "ignore");
- /* Complete COPY <sth> FROM filename WITH (LOG_VERBOSITY */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
+ /* Complete COPY <sth> FROM [PROGRAM] filename WITH (LOG_VERBOSITY */
+ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "LOG_VERBOSITY") ||
+ Matches("COPY|\\copy", MatchAny, "FROM", "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 */
--
2.47.3
On Mon, Oct 6, 2025 at 5:03 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Fri, Oct 3, 2025 at 2:06 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Thu, Sep 25, 2025 at 6:16 PM Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 25 Sep 2025 14:31:18 -0700
Masahiko Sawada <sawada.mshk@gmail.com> wrote:I fixed it to use 'generator'.
LGTM. I've pushed the 0001 patch.
Thank you!
--- - /* 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))I see this kind of conversion many places in the patch; convert one
condition with MatchAny into two conditions with
MatchAnyExcept("PROGRAM") and '"PROGRAM", MatchAny'. How about
simplifying it using MatchAnyN. For example,else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN))
We could simplify this by using MatchAnyN, but doing so would cause "WITH ("
or "WHERE" to be suggested after "WITH (...)", even though that is not allowed
by the syntax. This could be misleading for users, so I wonder whether it is
worth adding a bit of complexity to prevent possible confusion.There was a mistake in the previous statement: "WHERE" appearing after "WITH (...)"
is actually correct. However, this also results in "WITH" being suggested after
"WHERE", which is not permitted by the syntax.True. How about other places? That is, where we check the completion
after "WITH (". For example:- /* Complete COPY <sth> TO filename WITH ( */ - else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "(")) + /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */ + else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") || + Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))Does it make sense to replace these two lines with the following one line?
else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, MatchAnyN,
"WITH", "("))That works for other places where options are suggested after "WITH (" and
"WHERE" is suggested after "WITH (*)".I've attached updated patches using MatchAnyN following your suggestion.
Thank you for updating the patch!
After reviewing both the v6 and v7 patches, I realize that your
original approach in v6 is actually better than what I suggested.
While it requires more lines of code, it provides more precise
completions. Additionally, since most of these extra lines are removed
by the next patch (v6-0003), the code size isn't really an issue.
Would it be possible to withdraw my previous comments and proceed with
the v6 approach? I apologize for the back-and-forth on this.I have two review comments about the complete_from_files() function:
+ * This function wraps _complete_from_files() so that both literal keywords + * and filenames can be included in the completion results. + * + * If completion_charpp is set to a null-terminated array of literal keywords, + * those keywords are added to the completion results alongside filenames, + * as long as they case-insensitively match the current input.How about rephrasing the comments to the following?
/*
* This function returns in order one of a fixed, NULL pointer terminated list
* of string that matches file names or optionally specified list of keywords.
*
* If completion_charpp is set to a null-terminated array of literal keywords,
* those keywords are added to the completion results alongside filenames if
* they case-insensitively match the current input.
*/--- + /* 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;It works but it's odd a bit that we don't set files_done to true if
completion_charpp is not NULL. I think it becomes more readable if we
could set files_done to true if _complete_from_files() doesn't return
a string and proceed with the hard-wired keywords.The attached patch that can be applied on top of v6-0002 patch,
implements my suggestions and includes pgindent fixes. Please review
these changes.I believe we can split the first patch into two patches: one adds
support for STDIN/STDOUT with COMPLETE_WIHT_FILES_PLUS() and another
one adds support for COPY syntaxes using the PROGRAM clause. I've
attached the reorganized patch set and made cosmetic changes to the
0003 patch (i.e., improving COPY option list). What do you think?
Pushed the first two patches.
As for the remaining patch that adds tab completion for COPY option
lists, I think it would be a good idea to add tab completion for other
options too such as HEADER and FREEZE.
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
Hi Sawada-san,
I still cannot confirm that my previous email has been archived [1]https://www.postgresql.org/list/pgsql-hackers/since/202511130000/,
so let me resend it.
I apologize again for the repeated emails.
[1]: https://www.postgresql.org/list/pgsql-hackers/since/202511130000/
On Tue, 4 Nov 2025 10:57:14 -0800
Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Mon, Oct 6, 2025 at 5:03 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Fri, Oct 3, 2025 at 2:06 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Thu, Sep 25, 2025 at 6:16 PM Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 25 Sep 2025 14:31:18 -0700
Masahiko Sawada <sawada.mshk@gmail.com> wrote:I fixed it to use 'generator'.
LGTM. I've pushed the 0001 patch.
Thank you!
--- - /* 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))I see this kind of conversion many places in the patch; convert one
condition with MatchAny into two conditions with
MatchAnyExcept("PROGRAM") and '"PROGRAM", MatchAny'. How about
simplifying it using MatchAnyN. For example,else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN))
We could simplify this by using MatchAnyN, but doing so would cause "WITH ("
or "WHERE" to be suggested after "WITH (...)", even though that is not allowed
by the syntax. This could be misleading for users, so I wonder whether it is
worth adding a bit of complexity to prevent possible confusion.There was a mistake in the previous statement: "WHERE" appearing after "WITH (...)"
is actually correct. However, this also results in "WITH" being suggested after
"WHERE", which is not permitted by the syntax.True. How about other places? That is, where we check the completion
after "WITH (". For example:- /* Complete COPY <sth> TO filename WITH ( */ - else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "(")) + /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */ + else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") || + Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))Does it make sense to replace these two lines with the following one line?
else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, MatchAnyN,
"WITH", "("))That works for other places where options are suggested after "WITH (" and
"WHERE" is suggested after "WITH (*)".I've attached updated patches using MatchAnyN following your suggestion.
Thank you for updating the patch!
After reviewing both the v6 and v7 patches, I realize that your
original approach in v6 is actually better than what I suggested.
While it requires more lines of code, it provides more precise
completions. Additionally, since most of these extra lines are removed
by the next patch (v6-0003), the code size isn't really an issue.
Would it be possible to withdraw my previous comments and proceed with
the v6 approach? I apologize for the back-and-forth on this.I have two review comments about the complete_from_files() function:
+ * This function wraps _complete_from_files() so that both literal keywords + * and filenames can be included in the completion results. + * + * If completion_charpp is set to a null-terminated array of literal keywords, + * those keywords are added to the completion results alongside filenames, + * as long as they case-insensitively match the current input.How about rephrasing the comments to the following?
/*
* This function returns in order one of a fixed, NULL pointer terminated list
* of string that matches file names or optionally specified list of keywords.
*
* If completion_charpp is set to a null-terminated array of literal keywords,
* those keywords are added to the completion results alongside filenames if
* they case-insensitively match the current input.
*/--- + /* 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;It works but it's odd a bit that we don't set files_done to true if
completion_charpp is not NULL. I think it becomes more readable if we
could set files_done to true if _complete_from_files() doesn't return
a string and proceed with the hard-wired keywords.The attached patch that can be applied on top of v6-0002 patch,
implements my suggestions and includes pgindent fixes. Please review
these changes.I believe we can split the first patch into two patches: one adds
support for STDIN/STDOUT with COMPLETE_WIHT_FILES_PLUS() and another
one adds support for COPY syntaxes using the PROGRAM clause. I've
attached the reorganized patch set and made cosmetic changes to the
0003 patch (i.e., improving COPY option list). What do you think?Pushed the first two patches.
As for the remaining patch that adds tab completion for COPY option
lists, I think it would be a good idea to add tab completion for other
options too such as HEADER and FREEZE.
I'm sorry for the late reply.
Thank you for reviewing and committing the patches!
I've attached an updated patch that includes completions for the HEADER
and FREEZE option values.
Regards,
Yugo Nagata
--
Yugo Nagata <nagata@sraoss.co.jp>
Attachments:
v9-0001-psql-Improve-tab-completion-for-COPY-option-lists.patchtext/x-diff; name=v9-0001-psql-Improve-tab-completion-for-COPY-option-lists.patchDownload
From 5d444f70891a4ea5d0bb7b2d1af07750d2a38413 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 6 Oct 2025 15:03:31 -0700
Subject: [PATCH v9] psql: Improve tab completion for COPY option lists.
Previously, only the first option in a parenthesized option list was
suggested by tab completion. This commit enhances tab completion for
both COPY TO and COPY FROM commands to suggest options after each comma.
Also add completion for HEADER and FREEZE option value candidates.
Author: Yugo Nagata <nagata@sraoss.co.jp>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Discussion: https://postgr.es/m/20250605100835.b396f9d656df1018f65a4556@sraoss.co.jp
---
src/bin/psql/tab-complete.in.c | 68 ++++++++++++++++++++++------------
1 file changed, 44 insertions(+), 24 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 51806597037..c3b0cfc3391 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3364,30 +3364,50 @@ match_previous_words(int pattern_id,
Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny))
COMPLETE_WITH("WITH (", "WHERE");
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "("))
- COMPLETE_WITH(Copy_from_options);
-
- /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
- Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))
- COMPLETE_WITH(Copy_to_options);
-
- /* 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] filename WITH (ON_ERROR */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "ON_ERROR") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "ON_ERROR"))
- COMPLETE_WITH("stop", "ignore");
-
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH (LOG_VERBOSITY */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "LOG_VERBOSITY") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
- COMPLETE_WITH("silent", "default", "verbose");
+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */
+ 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", "(*)"))
+ {
+ /*
+ * This fires if we're in an unfinished parenthesized option list.
+ * get_previous_words treats a completed parenthesized option list
+ * as one word, so the above tests are correct.
+ */
+
+ if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
+ {
+ if (HeadMatches("COPY|\\copy", MatchAny, "FROM"))
+ COMPLETE_WITH(Copy_from_options);
+ else
+ COMPLETE_WITH(Copy_to_options);
+ }
+
+ /* Complete COPY <sth> FROM|TO filename WITH (FORMAT */
+ else if (TailMatches("FORMAT"))
+ COMPLETE_WITH("binary", "csv", "text");
+
+ /* Complete COPY <sth> FROM|TO filename WITH (FREEZE */
+ else if (TailMatches("FREEZE"))
+ COMPLETE_WITH("true", "false");
+
+ /* Complete COPY <sth> FROM|TO filename WITH (HEADER */
+ else if (TailMatches("HEADER"))
+ COMPLETE_WITH("true", "false", "MATCH");
+
+ /* 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");
+ }
+
+ /* A completed parenthesized option list should be caught below */
+ }
/* Complete COPY <sth> FROM [PROGRAM] <sth> WITH (<options>) */
else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", MatchAny) ||
--
2.43.0
On Thu, Nov 13, 2025 at 11:55 PM Yugo Nagata <nagata@sraoss.co.jp> wrote:
Hi Sawada-san,
I still cannot confirm that my previous email has been archived [1],
so let me resend it.
I apologize again for the repeated emails.[1] https://www.postgresql.org/list/pgsql-hackers/since/202511130000/
On Tue, 4 Nov 2025 10:57:14 -0800
Masahiko Sawada <sawada.mshk@gmail.com> wrote:On Mon, Oct 6, 2025 at 5:03 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Fri, Oct 3, 2025 at 2:06 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Thu, Sep 25, 2025 at 6:16 PM Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Thu, 25 Sep 2025 14:31:18 -0700
Masahiko Sawada <sawada.mshk@gmail.com> wrote:I fixed it to use 'generator'.
LGTM. I've pushed the 0001 patch.
Thank you!
--- - /* 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))I see this kind of conversion many places in the patch; convert one
condition with MatchAny into two conditions with
MatchAnyExcept("PROGRAM") and '"PROGRAM", MatchAny'. How about
simplifying it using MatchAnyN. For example,else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, MatchAnyN))
We could simplify this by using MatchAnyN, but doing so would cause "WITH ("
or "WHERE" to be suggested after "WITH (...)", even though that is not allowed
by the syntax. This could be misleading for users, so I wonder whether it is
worth adding a bit of complexity to prevent possible confusion.There was a mistake in the previous statement: "WHERE" appearing after "WITH (...)"
is actually correct. However, this also results in "WITH" being suggested after
"WHERE", which is not permitted by the syntax.True. How about other places? That is, where we check the completion
after "WITH (". For example:- /* Complete COPY <sth> TO filename WITH ( */ - else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "(")) + /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */ + else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") || + Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))Does it make sense to replace these two lines with the following one line?
else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, MatchAnyN,
"WITH", "("))That works for other places where options are suggested after "WITH (" and
"WHERE" is suggested after "WITH (*)".I've attached updated patches using MatchAnyN following your suggestion.
Thank you for updating the patch!
After reviewing both the v6 and v7 patches, I realize that your
original approach in v6 is actually better than what I suggested.
While it requires more lines of code, it provides more precise
completions. Additionally, since most of these extra lines are removed
by the next patch (v6-0003), the code size isn't really an issue.
Would it be possible to withdraw my previous comments and proceed with
the v6 approach? I apologize for the back-and-forth on this.I have two review comments about the complete_from_files() function:
+ * This function wraps _complete_from_files() so that both literal keywords + * and filenames can be included in the completion results. + * + * If completion_charpp is set to a null-terminated array of literal keywords, + * those keywords are added to the completion results alongside filenames, + * as long as they case-insensitively match the current input.How about rephrasing the comments to the following?
/*
* This function returns in order one of a fixed, NULL pointer terminated list
* of string that matches file names or optionally specified list of keywords.
*
* If completion_charpp is set to a null-terminated array of literal keywords,
* those keywords are added to the completion results alongside filenames if
* they case-insensitively match the current input.
*/--- + /* 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;It works but it's odd a bit that we don't set files_done to true if
completion_charpp is not NULL. I think it becomes more readable if we
could set files_done to true if _complete_from_files() doesn't return
a string and proceed with the hard-wired keywords.The attached patch that can be applied on top of v6-0002 patch,
implements my suggestions and includes pgindent fixes. Please review
these changes.I believe we can split the first patch into two patches: one adds
support for STDIN/STDOUT with COMPLETE_WIHT_FILES_PLUS() and another
one adds support for COPY syntaxes using the PROGRAM clause. I've
attached the reorganized patch set and made cosmetic changes to the
0003 patch (i.e., improving COPY option list). What do you think?Pushed the first two patches.
As for the remaining patch that adds tab completion for COPY option
lists, I think it would be a good idea to add tab completion for other
options too such as HEADER and FREEZE.I'm sorry for the late reply.
Thank you for reviewing and committing the patches!I've attached an updated patch that includes completions for the HEADER
and FREEZE option values.
Thank you for updating the patch! I have one question about the
following change:
+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */
+ 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", "(*)"))
+ {
Given get_previous_words() treats a completed parenthesized option
list as one word, why is the later if statement necessary? In the
situation where the condition of the later if statement becomes false,
the condition of the first 'else if' statement should have become
false, no?
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
On Mon, 24 Nov 2025 15:32:36 -0800
Masahiko Sawada <sawada.mshk@gmail.com> wrote:
I'm sorry for the late response.
Thank you for updating the patch! I have one question about the
following change:+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */ + 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", "(*)")) + {Given get_previous_words() treats a completed parenthesized option
list as one word, why is the later if statement necessary? In the
situation where the condition of the later if statement becomes false,
the condition of the first 'else if' statement should have become
false, no?
First of all, the condition of the outer 'else if' was wrong.
What I actually meant was:
+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */
+ else if (HeadMatches("COPY|\\copy", MatchAny, "FROM|TO",
MatchAnyExcept("PROGRAM"), "WITH", "(*") ||
+ HeadMatches("COPY|\\copy", MatchAny, "FROM|TO",
"PROGRAM", MatchAny, "WITH", "(*"))
That is, the last word should be "(*" instead of "(".
I've attached an updated patch reflecting this fix.
This patterm is mimic how option lists are handled in the other places.
For example:
3160 else if (HeadMatches("ANALYZE", "(*") &&
3161 !HeadMatches("ANALYZE", "(*)"))
This prevents option completion after a completed paranthesized option list.
For example, after "ANALYZE (VERVOSE ON) tbl (", no ANALYZE options are suggested,
instead column names of the table "tbl" are suggested.
Similarly, this change prevents completion of COPY options after
"COPY tbl FROM stdin WITH ( FORMAT text ) WHERE (", for example.
Regards,
Yugo Nagata
--
Yugo Nagata <nagata@sraoss.co.jp>
Attachments:
v10-0001-psql-Improve-tab-completion-for-COPY-option-list.patchtext/x-diff; name=v10-0001-psql-Improve-tab-completion-for-COPY-option-list.patchDownload
From fac5ac007c6a4d2de0c191d19a7f3661c1113295 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Mon, 6 Oct 2025 15:03:31 -0700
Subject: [PATCH v10] psql: Improve tab completion for COPY option lists.
Previously, only the first option in a parenthesized option list was
suggested by tab completion. This commit enhances tab completion for
both COPY TO and COPY FROM commands to suggest options after each comma.
Also add completion for HEADER and FREEZE option value candidates.
Author: Yugo Nagata <nagata@sraoss.co.jp>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Discussion: https://postgr.es/m/20250605100835.b396f9d656df1018f65a4556@sraoss.co.jp
---
src/bin/psql/tab-complete.in.c | 68 ++++++++++++++++++++++------------
1 file changed, 44 insertions(+), 24 deletions(-)
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index b1ff6f6cd94..ab2712216b5 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -3378,30 +3378,50 @@ match_previous_words(int pattern_id,
Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny))
COMPLETE_WITH("WITH (", "WHERE");
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "("))
- COMPLETE_WITH(Copy_from_options);
-
- /* Complete COPY <sth> TO [PROGRAM] filename WITH ( */
- else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM"), "WITH", "(") ||
- Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny, "WITH", "("))
- COMPLETE_WITH(Copy_to_options);
-
- /* 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] filename WITH (ON_ERROR */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "ON_ERROR") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "ON_ERROR"))
- COMPLETE_WITH("stop", "ignore");
-
- /* Complete COPY <sth> FROM [PROGRAM] filename WITH (LOG_VERBOSITY */
- else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", "(", "LOG_VERBOSITY") ||
- Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", "(", "LOG_VERBOSITY"))
- COMPLETE_WITH("silent", "default", "verbose");
+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */
+ 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", "(*)"))
+ {
+ /*
+ * This fires if we're in an unfinished parenthesized option list.
+ * get_previous_words treats a completed parenthesized option list
+ * as one word, so the above tests are correct.
+ */
+
+ if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
+ {
+ if (HeadMatches("COPY|\\copy", MatchAny, "FROM"))
+ COMPLETE_WITH(Copy_from_options);
+ else
+ COMPLETE_WITH(Copy_to_options);
+ }
+
+ /* Complete COPY <sth> FROM|TO filename WITH (FORMAT */
+ else if (TailMatches("FORMAT"))
+ COMPLETE_WITH("binary", "csv", "text");
+
+ /* Complete COPY <sth> FROM|TO filename WITH (FREEZE */
+ else if (TailMatches("FREEZE"))
+ COMPLETE_WITH("true", "false");
+
+ /* Complete COPY <sth> FROM|TO filename WITH (HEADER */
+ else if (TailMatches("HEADER"))
+ COMPLETE_WITH("true", "false", "MATCH");
+
+ /* 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");
+ }
+
+ /* A completed parenthesized option list should be caught below */
+ }
/* Complete COPY <sth> FROM [PROGRAM] <sth> WITH (<options>) */
else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", MatchAny) ||
--
2.43.0
On Tue, 23 Dec 2025 00:37:05 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Mon, 24 Nov 2025 15:32:36 -0800
Masahiko Sawada <sawada.mshk@gmail.com> wrote:I'm sorry for the late response.
Thank you for updating the patch! I have one question about the
following change:+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */ + 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", "(*)")) + {Given get_previous_words() treats a completed parenthesized option
list as one word, why is the later if statement necessary? In the
situation where the condition of the later if statement becomes false,
the condition of the first 'else if' statement should have become
false, no?First of all, the condition of the outer 'else if' was wrong.
What I actually meant was:+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */ + else if (HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(*") || + HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(*"))That is, the last word should be "(*" instead of "(".
I've attached an updated patch reflecting this fix.This patterm is mimic how option lists are handled in the other places.
For example:3160 else if (HeadMatches("ANALYZE", "(*") &&
3161 !HeadMatches("ANALYZE", "(*)"))This prevents option completion after a completed paranthesized option list.
For example, after "ANALYZE (VERVOSE ON) tbl (", no ANALYZE options are suggested,
instead column names of the table "tbl" are suggested.Similarly, this change prevents completion of COPY options after
"COPY tbl FROM stdin WITH ( FORMAT text ) WHERE (", for example.
By the way, while investigating your comments, I noticed an issue with
tab completion for VACUUM option values. I reported it in a separate
thread [1]/messages/by-id/20251223021509.19bba68ecbbc70c9f983c2b4@sraoss.co.jp, since it appears to be a bug introduced by another commit.
[1]: /messages/by-id/20251223021509.19bba68ecbbc70c9f983c2b4@sraoss.co.jp
--
Yugo Nagata <nagata@sraoss.co.jp>
On Mon, Dec 22, 2025 at 7:37 AM Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Mon, 24 Nov 2025 15:32:36 -0800
Masahiko Sawada <sawada.mshk@gmail.com> wrote:I'm sorry for the late response.
Thank you for updating the patch! I have one question about the
following change:+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */ + 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", "(*)")) + {Given get_previous_words() treats a completed parenthesized option
list as one word, why is the later if statement necessary? In the
situation where the condition of the later if statement becomes false,
the condition of the first 'else if' statement should have become
false, no?First of all, the condition of the outer 'else if' was wrong.
What I actually meant was:+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */ + else if (HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(*") || + HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(*"))That is, the last word should be "(*" instead of "(".
I've attached an updated patch reflecting this fix.This patterm is mimic how option lists are handled in the other places.
For example:3160 else if (HeadMatches("ANALYZE", "(*") &&
3161 !HeadMatches("ANALYZE", "(*)"))This prevents option completion after a completed paranthesized option list.
For example, after "ANALYZE (VERVOSE ON) tbl (", no ANALYZE options are suggested,
instead column names of the table "tbl" are suggested.Similarly, this change prevents completion of COPY options after
"COPY tbl FROM stdin WITH ( FORMAT text ) WHERE (", for example.
Thank you for updating the patch and the explanation!
I agree with these changes, so I've pushed the patch. Also, this
commit fest item is now marked as 'Committed'.
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
On Mon, 22 Dec 2025 14:31:51 -0800
Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Mon, Dec 22, 2025 at 7:37 AM Yugo Nagata <nagata@sraoss.co.jp> wrote:
On Mon, 24 Nov 2025 15:32:36 -0800
Masahiko Sawada <sawada.mshk@gmail.com> wrote:I'm sorry for the late response.
Thank you for updating the patch! I have one question about the
following change:+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */ + 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", "(*)")) + {Given get_previous_words() treats a completed parenthesized option
list as one word, why is the later if statement necessary? In the
situation where the condition of the later if statement becomes false,
the condition of the first 'else if' statement should have become
false, no?First of all, the condition of the outer 'else if' was wrong.
What I actually meant was:+ /* Complete COPY <sth> FROM|TO [PROGRAM] filename WITH ( */ + else if (HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(*") || + HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(*"))That is, the last word should be "(*" instead of "(".
I've attached an updated patch reflecting this fix.This patterm is mimic how option lists are handled in the other places.
For example:3160 else if (HeadMatches("ANALYZE", "(*") &&
3161 !HeadMatches("ANALYZE", "(*)"))This prevents option completion after a completed paranthesized option list.
For example, after "ANALYZE (VERVOSE ON) tbl (", no ANALYZE options are suggested,
instead column names of the table "tbl" are suggested.Similarly, this change prevents completion of COPY options after
"COPY tbl FROM stdin WITH ( FORMAT text ) WHERE (", for example.Thank you for updating the patch and the explanation!
I agree with these changes, so I've pushed the patch. Also, this
commit fest item is now marked as 'Committed'.
Thank you!
Regards,
Yugo Nagata
--
Yugo Nagata <nagata@sraoss.co.jp>