From 1e54121b008b4e914fdf2d9ad2093c02f8953fd6 Mon Sep 17 00:00:00 2001 From: Masahiko Sawada Date: Mon, 6 Oct 2025 16:42:12 -0700 Subject: [PATCH 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 Reviewed-by: Masahiko Sawada 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 36ea6a4d5570..160d6331697a 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) @@ -1485,6 +1495,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); @@ -3325,11 +3336,17 @@ match_previous_words(int pattern_id, /* Complete COPY */ else if (Matches("COPY|\\copy", MatchAny)) COMPLETE_WITH("FROM", "TO"); - /* Complete COPY 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 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 TO */ else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny)) @@ -6249,6 +6266,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 @@ -6263,7 +6333,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 From 6063fbcd69be3c4937b6c1e71cd3792d30e9c09d Mon Sep 17 00:00:00 2001 From: Masahiko Sawada Date: Mon, 6 Oct 2025 16:52:42 -0700 Subject: [PATCH 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 Reviewed-by: Masahiko Sawada 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 160d6331697a..acc7925e4c10 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -3336,48 +3336,61 @@ match_previous_words(int pattern_id, /* Complete COPY */ else if (Matches("COPY|\\copy", MatchAny)) COMPLETE_WITH("FROM", "TO"); - /* Complete COPY|\copy FROM|TO with filename or STDIN/STDOUT */ + /* Complete COPY|\copy 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 TO */ - else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny)) + /* Complete COPY|\copy FROM|TO PROGRAM */ + else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM")) + COMPLETE_WITH_FILES("", HeadMatches("COPY")); /* COPY requires quoted + * filename */ + + /* Complete COPY TO [PROGRAM] */ + else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM")) || + Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny)) COMPLETE_WITH("WITH ("); - /* Complete COPY FROM */ - else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny)) + /* Complete COPY FROM [PROGRAM] */ + else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM")) || + Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny)) COMPLETE_WITH("WITH (", "WHERE"); - /* Complete COPY FROM filename WITH ( */ - else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "(")) + /* Complete COPY 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 TO filename WITH ( */ - else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "(")) + /* Complete COPY 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 FROM|TO filename WITH (FORMAT */ - else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "FORMAT")) + /* Complete COPY FROM|TO [PROGRAM] 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 FROM filename WITH (ON_ERROR */ - else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "(", "ON_ERROR")) + /* Complete COPY 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 FROM filename WITH (LOG_VERBOSITY */ - else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "(", "LOG_VERBOSITY")) + /* Complete COPY 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 FROM WITH () */ - else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", MatchAny)) + /* Complete COPY FROM [PROGRAM] WITH () */ + 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 */ From 3a19c3c183f9145d51d9b165d560f737b155723e Mon Sep 17 00:00:00 2001 From: Masahiko Sawada Date: Mon, 6 Oct 2025 15:03:31 -0700 Subject: [PATCH 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 Reviewed-by: Masahiko Sawada 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 acc7925e4c10..7636c6fbe226 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -3363,30 +3363,42 @@ match_previous_words(int pattern_id, Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny)) COMPLETE_WITH("WITH (", "WHERE"); - /* Complete COPY 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 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 FROM|TO [PROGRAM] 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 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 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 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 FROM|TO filename WITH (FORMAT */ + else if (TailMatches("FORMAT")) + COMPLETE_WITH("binary", "csv", "text"); + + /* Complete COPY FROM filename WITH (ON_ERROR */ + else if (TailMatches("ON_ERROR")) + COMPLETE_WITH("stop", "ignore"); + + /* Complete COPY 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 FROM [PROGRAM] WITH () */ else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", MatchAny) ||