diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 6b20a4404b21..d0eebf58f19d 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -443,6 +443,26 @@ do { \ matches = rl_completion_matches(text, complete_from_schema_query); \ } while (0) +#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) + /* * Assembly instructions for schema queries * @@ -1474,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); @@ -2182,7 +2203,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 +2213,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 +2224,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"); @@ -3314,50 +3335,65 @@ 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 COPY|\copy FROM|TO with filename or STDIN/STDOUT/PROGRAM */ + else if (Matches("COPY|\\copy", MatchAny, "FROM|TO")) { - completion_charp = ""; - completion_force_quote = true; /* COPY requires quoted filename */ - matches = rl_completion_matches(text, complete_from_files); - } - else if (Matches("\\copy", MatchAny, "FROM|TO")) - { - completion_charp = ""; - completion_force_quote = false; - matches = rl_completion_matches(text, complete_from_files); + /* 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 TO */ - else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny)) + /* Complete COPY|\copy FROM|TO PROGRAM command */ + 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_WITH(Copy_from_options); - - /* Complete COPY TO filename WITH ( */ - else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny, "WITH", "(")) - COMPLETE_WITH(Copy_to_options); + /* 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", "(*)")) + { + /* 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 FROM|TO filename WITH (FORMAT */ - else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "FORMAT")) - COMPLETE_WITH("binary", "csv", "text"); + /* 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 (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "(", "ON_ERROR")) - COMPLETE_WITH("stop", "ignore"); + /* 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 (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", "(", "LOG_VERBOSITY")) - COMPLETE_WITH("silent", "default", "verbose"); + /* Complete COPY FROM filename WITH (LOG_VERBOSITY */ + else if (TailMatches("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 */ @@ -5427,9 +5463,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 +5565,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 */ @@ -6255,9 +6287,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