From 5ff47e6d9eddb2ed398154370493b5a404e20dcf Mon Sep 17 00:00:00 2001 From: Commitfest Bot Date: Thu, 1 May 2025 17:05:23 +0000 Subject: [PATCH] [PATCH]: ./psql-copy-var-3.patch --- doc/src/sgml/ref/psql-ref.sgml | 35 ++++- src/bin/psql/command.c | 59 ++++++++- src/bin/psql/common.c | 227 +++++++++++++++++++++++++-------- src/bin/psql/common.h | 6 +- src/bin/psql/copy.c | 130 +++++-------------- src/bin/psql/help.c | 1 + src/bin/psql/settings.h | 14 +- src/bin/psql/startup.c | 12 +- src/bin/psql/t/001_basic.pl | 36 ++++++ src/bin/psql/tab-complete.in.c | 5 +- 10 files changed, 359 insertions(+), 166 deletions(-) diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index f7c8bc16a7fc..8f2506517db6 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1172,10 +1172,13 @@ SELECT $1 \parse stmt1 Another way to obtain the same result as \copy - ... to is to use the SQL COPY - ... TO STDOUT command and terminate it - with \g filename - or \g |program. + ... to or from is to use the SQL + COPY ... TO STDOUT or FROM STDIN + command and terminate it with either + \g filename + or \g |program for output + and \gi filename + or \gi program| for input. Unlike \copy, this method allows the command to span multiple lines; also, variable interpolation and backquote expansion can be used. @@ -1188,7 +1191,7 @@ SELECT $1 \parse stmt1 COPY command with a file or program data source or destination, because all data must pass through the client/server connection. For large amounts of data the SQL - command might be preferable. + command might be preferable if data are available on the server. @@ -2558,6 +2561,28 @@ CREATE INDEX + + \gi file + \gi command| + + + Sends the current query buffer to the server and uses the provided + file contents or command + output as input. + This should only apply to SQL + COPY + which seeks an input when used with FROM STDIN, and + will simply result in the command simple execution for other commands + which do not need an input stream. + + + This approach should be prefered to using \copy + as it achieves the same result but can span several lines and + is subject to variable interpolation and backquote expansion. + + + + \gset [ prefix ] diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 81a5ba844ba0..f5804663fa67 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -107,6 +107,7 @@ static backslashResult exec_command_getenv(PsqlScanState scan_state, bool active const char *cmd); static backslashResult exec_command_gexec(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_getresults(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_gi(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_gset(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_help(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_html(PsqlScanState scan_state, bool active_branch); @@ -380,6 +381,8 @@ exec_command(const char *cmd, status = exec_command_getresults(scan_state, active_branch); else if (strcmp(cmd, "gexec") == 0) status = exec_command_gexec(scan_state, active_branch); + else if (strcmp(cmd, "gi") == 0) + status = exec_command_gi(scan_state, active_branch); else if (strcmp(cmd, "gset") == 0) status = exec_command_gset(scan_state, active_branch); else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0) @@ -1750,7 +1753,8 @@ exec_command_g(PsqlScanState scan_state, bool active_branch, const char *cmd) else { expand_tilde(&fname); - pset.gfname = pg_strdup(fname); + pset.g_pipe = fname[0] == '|'; + pset.gfname = pg_strdup(fname + (pset.g_pipe ? 1 : 0)); } if (strcmp(cmd, "gx") == 0) { @@ -1957,6 +1961,56 @@ exec_command_gexec(PsqlScanState scan_state, bool active_branch) return status; } +/* + * \gi filename/shell-command + * + * Send the current query with a query input from the filename or pipe + * command. + */ +static backslashResult +exec_command_gi(PsqlScanState scan_state, bool active_branch) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) + { + char *fname; + int last; + + fname = psql_scan_slash_option(scan_state, OT_FILEPIPE, NULL, false); + + if (fname == NULL) + { + pg_log_error("\\gi expects a filename or pipe command"); + clean_extended_state(); + free(fname); + return PSQL_CMD_ERROR; + } + + /* check and truncate final pipe character */ + last = strlen(fname) - 1; + pset.gi_pipe = last >= 0 && fname[last] == '|'; + if (pset.gi_pipe) + fname[last] = '\0'; + + if (PQpipelineStatus(pset.db) != PQ_PIPELINE_OFF) + { + pg_log_error("\\gi not allowed in pipeline mode"); + clean_extended_state(); + free(fname); + return PSQL_CMD_ERROR; + } + + expand_tilde(&fname); + pset.gi_fname = pg_strdup(fname); + + status = PSQL_CMD_SEND; + free(fname); + } + + return status; +} + /* * \gset [prefix] -- send query and store result into variables */ @@ -2440,9 +2494,10 @@ exec_command_out(PsqlScanState scan_state, bool active_branch) { char *fname = psql_scan_slash_option(scan_state, OT_FILEPIPE, NULL, true); + bool is_pipe = *fname == '|'; expand_tilde(&fname); - success = setQFout(fname); + success = setQFout(fname + (is_pipe ? 1 : 0), is_pipe); free(fname); } else diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index 3e4e444f3fd9..7bba6e0f7fba 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -12,6 +12,7 @@ #include #include #include +#include #ifndef WIN32 #include /* for write() */ #else @@ -41,35 +42,53 @@ static int ExecQueryAndProcessResults(const char *query, static bool command_no_begin(const char *query); +/* make sure the file stream is not a directory */ +static bool +badFileStream(FILE *file, const char *fname) +{ + struct stat st; + int result; + bool bad; + + if ((result = fstat(fileno(file), &st)) < 0) + pg_log_error("could not stat file \"%s\": %m", + fname ? fname : ""); + + if (result == 0 && S_ISDIR(st.st_mode)) + pg_log_error("cannot copy from/to directory \"%s\"", + fname ? fname : ""); + + return result < 0 || S_ISDIR(st.st_mode); +} + /* * openQueryOutputFile --- attempt to open a query output file * - * fname == NULL selects stdout, else an initial '|' selects a pipe, - * else plain file. - * - * Returns output file pointer into *fout, and is-a-pipe flag into *is_pipe. + * Returns output file pointer into *fout. * Caller is responsible for adjusting SIGPIPE state if it's a pipe. * * On error, reports suitable error message and returns false. */ bool -openQueryOutputFile(const char *fname, FILE **fout, bool *is_pipe) +openQueryOutputFile(const char *fname, bool is_pipe, FILE **fout) { if (!fname || fname[0] == '\0') - { *fout = stdout; - *is_pipe = false; - } - else if (*fname == '|') + else if (is_pipe) { fflush(NULL); - *fout = popen(fname + 1, "w"); - *is_pipe = true; + *fout = popen(fname, PG_BINARY_W); } else { - *fout = fopen(fname, "w"); - *is_pipe = false; + *fout = fopen(fname, PG_BINARY_W); + + if (*fout && badFileStream(*fout, fname)) + { + fclose(*fout); + *fout = NULL; + return false; + } } if (*fout == NULL) @@ -86,15 +105,15 @@ openQueryOutputFile(const char *fname, FILE **fout, bool *is_pipe) * open it and update the caller's gfile_fout and is_pipe state variables. * Return true if OK, false if an error occurred. */ -static bool -SetupGOutput(FILE **gfile_fout, bool *is_pipe) +bool +SetupGOutput(FILE **gfile_fout) { /* If there is a \g file or program, and it's not already open, open it */ if (pset.gfname != NULL && *gfile_fout == NULL) { - if (openQueryOutputFile(pset.gfname, gfile_fout, is_pipe)) + if (openQueryOutputFile(pset.gfname, pset.g_pipe, gfile_fout)) { - if (*is_pipe) + if (pset.g_pipe) disable_sigpipe_trap(); } else @@ -104,21 +123,107 @@ SetupGOutput(FILE **gfile_fout, bool *is_pipe) } /* - * Close the output stream for \g, if we opened it. + * Close file with user feedback on errors. + */ +static bool +CloseFile(FILE *stream, const char *fname) +{ + if (fclose(stream) != 0) + { + pg_log_error("%s: %m", fname); + return false; + } + return true; +} + +/* + * Close pipe with user feedback on errors. + */ +static bool +ClosePipe(FILE *stream, const char *fname) +{ + int pclose_rc = pclose(stream); + + if (pclose_rc != 0) + { + if (pclose_rc < 0) + pg_log_error("could not close pipe to/from external command: %m"); + else + { + char *reason = wait_result_to_str(pclose_rc); + + pg_log_error("%s: %s", fname ? fname: "", reason ? reason : ""); + free(reason); + } + return false; + } + + SetShellResultVariables(pclose_rc); + restore_sigpipe_trap(); + return true; +} + +/* + * Close the input (\gi) or output (\g) stream, if we opened it. */ static void -CloseGOutput(FILE *gfile_fout, bool is_pipe) +CloseStream(FILE *stream, const char *fname, bool is_pipe) { - if (gfile_fout) + if (fname && stream) { if (is_pipe) + (void) ClosePipe(stream, fname); + else + (void) CloseFile(stream, fname); + } +} + +/* + * Open or use input stream, only under COPY_IN (COPY or \copy) + */ +bool +SetupGInput(FILE **input_stream) +{ + if (pset.gi_fname != NULL && *input_stream == NULL) + { + FILE *input = NULL; + + if (pset.gi_pipe) { - SetShellResultVariables(pclose(gfile_fout)); - restore_sigpipe_trap(); + fflush(NULL); + errno = 0; + input = popen(pset.gi_fname, PG_BINARY_R); + if (!input) + { + pg_log_error("could not execute command \"%s\": %m", + pset.gi_fname); + return false; + } + disable_sigpipe_trap(); } else - fclose(gfile_fout); + { + input = fopen(pset.gi_fname, PG_BINARY_R); + if (!input) + { + pg_log_error("could not open file \"%s\": %m", pset.gi_fname); + return false; + } + if (input && badFileStream(input, pset.gi_fname)) + { + fclose(input); + return false; + } + } + + *input_stream = input; } + else if (pset.copy_pstd) + *input_stream = pset.cur_cmd_source; + else + *input_stream = stdin; + + return true; } /* @@ -141,25 +246,31 @@ pipelineReset(void) * On failure, returns false without changing pset state. */ bool -setQFout(const char *fname) +setQFout(const char *fname, bool is_pipe) { FILE *fout; - bool is_pipe; /* First make sure we can open the new output file/pipe */ - if (!openQueryOutputFile(fname, &fout, &is_pipe)) + if (!openQueryOutputFile(fname, is_pipe, &fout)) return false; /* Close old file/pipe */ if (pset.queryFout && pset.queryFout != stdout && pset.queryFout != stderr) { if (pset.queryFoutPipe) - SetShellResultVariables(pclose(pset.queryFout)); + ClosePipe(pset.queryFout, pset.queryFName); else - fclose(pset.queryFout); + CloseFile(pset.queryFout, pset.queryFName); + + if (pset.queryFName) + { + free(pset.queryFName); + pset.queryFName = NULL; + } } pset.queryFout = fout; + pset.queryFName = fname ? pg_strdup(fname) : NULL; pset.queryFoutPipe = is_pipe; /* Adjust SIGPIPE handling appropriately: ignore signal if is_pipe */ @@ -923,10 +1034,6 @@ ExecQueryTuples(const PGresult *result) * connection out of its COPY state, then call PQresultStatus() * once and report any error. Return whether all was ok. * - * For COPY OUT, direct the output to copystream, or discard if that's NULL. - * For COPY IN, use pset.copyStream as data source if it's set, - * otherwise cur_cmd_source. - * * Update *resultp if further processing is necessary; set to NULL otherwise. * Return a result when queryFout can safely output a result status: on COPY * IN, or on COPY OUT if written to something other than pset.queryFout. @@ -966,8 +1073,6 @@ HandleCopyResult(PGresult **resultp, FILE *copystream) else { /* COPY IN */ - /* Ignore the copystream argument passed to the function */ - copystream = pset.copyStream ? pset.copyStream : pset.cur_cmd_source; success = handleCopyIn(pset.db, copystream, PQbinaryTuples(*resultp), @@ -1303,6 +1408,13 @@ SendQuery(const char *query) pset.gfname = NULL; } + /* idem \gi */ + if (pset.gi_fname) + { + free(pset.gi_fname); + pset.gi_fname = NULL; + } + /* restore print settings if \g changed them */ if (pset.gsavepopt) { @@ -1565,8 +1677,8 @@ ExecQueryAndProcessResults(const char *query, instr_time before, after; PGresult *result; - FILE *gfile_fout = NULL; - bool gfile_is_pipe = false; + FILE *gfile_fout = NULL, + *gfile_fin = NULL; if (timing) INSTR_TIME_SET_CURRENT(before); @@ -1869,9 +1981,8 @@ ExecQueryAndProcessResults(const char *query, /* * For COPY OUT, direct the output to the default place (probably - * a pager pipe) for \watch, or to pset.copyStream for \copy, - * otherwise to pset.gfname if that's set, otherwise to - * pset.queryFout. + * a pager pipe) for \watch, or use to pset.gfname if that's set, + * otherwise to pset.queryFout. */ if (result_status == PGRES_COPY_OUT) { @@ -1882,15 +1993,15 @@ ExecQueryAndProcessResults(const char *query, } else if (pset.copyStream) { - /* invoked by \copy */ + /* \copy ... to ... */ copy_stream = pset.copyStream; } else if (pset.gfname) { - /* COPY followed by \g filename or \g |program */ - success &= SetupGOutput(&gfile_fout, &gfile_is_pipe); - if (gfile_fout) - copy_stream = gfile_fout; + /* COPY with \g filename or \g |program */ + if (!gfile_fout) + success &= SetupGOutput(&gfile_fout); + copy_stream = gfile_fout; } else { @@ -1898,10 +2009,25 @@ ExecQueryAndProcessResults(const char *query, copy_stream = pset.queryFout; } } + else if (result_status == PGRES_COPY_IN) + { + if (pset.copyStream) + { + /* \copy ... from ... */ + copy_stream = pset.copyStream; + } + else + { + /* COPY with or without \gi ... */ + if (!gfile_fin) + success &= SetupGInput(&gfile_fin); + copy_stream = gfile_fin; + } + } /* - * Even if the output stream could not be opened, we call - * HandleCopyResult() with a NULL output stream to collect and + * Even if the input or output stream could not be opened, we call + * HandleCopyResult() with a NULL stream to collect and * discard the COPY data. */ success &= HandleCopyResult(&result, copy_stream); @@ -1922,7 +2048,7 @@ ExecQueryAndProcessResults(const char *query, my_popt.topt.prior_records = 0; /* open \g file if needed */ - success &= SetupGOutput(&gfile_fout, &gfile_is_pipe); + success &= SetupGOutput(&gfile_fout); if (gfile_fout) tuples_fout = gfile_fout; @@ -2111,7 +2237,7 @@ ExecQueryAndProcessResults(const char *query, FILE *tuples_fout = printQueryFout; if (PQresultStatus(result) == PGRES_TUPLES_OK) - success &= SetupGOutput(&gfile_fout, &gfile_is_pipe); + success &= SetupGOutput(&gfile_fout); if (gfile_fout) tuples_fout = gfile_fout; if (success) @@ -2141,8 +2267,9 @@ ExecQueryAndProcessResults(const char *query, } } - /* close \g file if we opened it */ - CloseGOutput(gfile_fout, gfile_is_pipe); + /* close \g and \gi files if we opened one */ + CloseStream(gfile_fout, pset.gfname, pset.g_pipe); + CloseStream(gfile_fin, pset.gi_fname, pset.gi_pipe); if (end_pipeline) { diff --git a/src/bin/psql/common.h b/src/bin/psql/common.h index 7f1a23de1e82..38fb6bf66a97 100644 --- a/src/bin/psql/common.h +++ b/src/bin/psql/common.h @@ -15,8 +15,10 @@ #include "fe_utils/psqlscan.h" #include "libpq-fe.h" -extern bool openQueryOutputFile(const char *fname, FILE **fout, bool *is_pipe); -extern bool setQFout(const char *fname); +extern bool openQueryOutputFile(const char *fname, bool is_pipe, FILE **fout); +extern bool SetupGOutput(FILE **output); +extern bool SetupGInput(FILE **input); +extern bool setQFout(const char *fname, bool is_pipe); extern char *psql_get_variable(const char *varname, PsqlScanQuoteType quote, void *passthrough); diff --git a/src/bin/psql/copy.c b/src/bin/psql/copy.c index 92c955b637a4..7ee221e8188c 100644 --- a/src/bin/psql/copy.c +++ b/src/bin/psql/copy.c @@ -8,7 +8,6 @@ #include "postgres_fe.h" #include -#include #ifndef WIN32 #include /* for isatty */ #else @@ -18,6 +17,7 @@ #include "common.h" #include "common/logging.h" #include "copy.h" +#include "common.h" #include "libpq-fe.h" #include "pqexpbuffer.h" #include "prompt.h" @@ -268,7 +268,6 @@ bool do_copy(const char *args) { PQExpBufferData query; - FILE *copystream; struct copy_options *options; bool success; @@ -282,81 +281,43 @@ do_copy(const char *args) if (options->file && !options->program) canonicalize_path_enc(options->file, pset.encoding); + /* \copy with pstdout/pstdin vs stdout/stdin */ + pset.copy_pstd = options->psql_inout; + + /* + * Translate \copy destination as \g or source as \gi. + * + * The stream is opened to return early if there is some error, + * which means that any error at this level does **not** break + * the current transaction. + * If okay, the stream is closed in SendQuery. + */ if (options->from) { if (options->file) { - if (options->program) - { - fflush(NULL); - errno = 0; - copystream = popen(options->file, PG_BINARY_R); - } - else - copystream = fopen(options->file, PG_BINARY_R); + pset.gi_fname = pg_strdup(options->file); + pset.gi_pipe = options->program; } - else if (!options->psql_inout) - copystream = pset.cur_cmd_source; - else - copystream = stdin; + SetupGInput(&pset.copyStream); + if (!pset.copyStream) + return false; } else { if (options->file) { - if (options->program) - { - fflush(NULL); - disable_sigpipe_trap(); - errno = 0; - copystream = popen(options->file, PG_BINARY_W); - } - else - copystream = fopen(options->file, PG_BINARY_W); + pset.gfname = pg_strdup(options->file); + pset.g_pipe = options->program; } - else if (!options->psql_inout) - copystream = pset.queryFout; - else - copystream = stdout; - } - - if (!copystream) - { - if (options->program) - pg_log_error("could not execute command \"%s\": %m", - options->file); - else - pg_log_error("%s: %m", - options->file); - free_copy_options(options); - return false; - } - - if (!options->program) - { - struct stat st; - int result; - - /* make sure the specified file is not a directory */ - if ((result = fstat(fileno(copystream), &st)) < 0) - pg_log_error("could not stat file \"%s\": %m", - options->file); - - if (result == 0 && S_ISDIR(st.st_mode)) - pg_log_error("%s: cannot copy from/to a directory", - options->file); - - if (result < 0 || S_ISDIR(st.st_mode)) - { - fclose(copystream); - free_copy_options(options); + SetupGOutput(&pset.copyStream); + if (!pset.copyStream) return false; - } } /* build the command we will send to the backend */ initPQExpBuffer(&query); - printfPQExpBuffer(&query, "COPY "); + appendPQExpBuffer(&query, "COPY "); appendPQExpBufferStr(&query, options->before_tofrom); if (options->from) appendPQExpBufferStr(&query, " FROM STDIN "); @@ -365,44 +326,11 @@ do_copy(const char *args) if (options->after_tofrom) appendPQExpBufferStr(&query, options->after_tofrom); - /* run it like a user command, but with copystream as data source/sink */ - pset.copyStream = copystream; + /* run it like a user command */ success = SendQuery(query.data); + pset.copyStream = NULL; termPQExpBuffer(&query); - - if (options->file != NULL) - { - if (options->program) - { - int pclose_rc = pclose(copystream); - - if (pclose_rc != 0) - { - if (pclose_rc < 0) - pg_log_error("could not close pipe to external command: %m"); - else - { - char *reason = wait_result_to_str(pclose_rc); - - pg_log_error("%s: %s", options->file, - reason ? reason : ""); - free(reason); - } - success = false; - } - SetShellResultVariables(pclose_rc); - restore_sigpipe_trap(); - } - else - { - if (fclose(copystream) != 0) - { - pg_log_error("%s: %m", options->file); - success = false; - } - } - } free_copy_options(options); return success; } @@ -514,6 +442,13 @@ handleCopyIn(PGconn *conn, FILE *copystream, bool isbinary, PGresult **res) char buf[COPYBUFSIZ]; bool showprompt; + /* No input stream on COPY ... \gi 'non-such-file' */ + if (copystream == NULL) + { + OK = false; + goto copyin_cleanup; + } + /* * Establish longjmp destination for exiting from wait-for-input. (This is * only effective while sigint_interrupt_enabled is TRUE.) @@ -703,7 +638,8 @@ handleCopyIn(PGconn *conn, FILE *copystream, bool isbinary, PGresult **res) * with feof(), some fread() implementations won't read more data if it's * set. This also clears the error flag, but we already checked that. */ - clearerr(copystream); + if (copystream) + clearerr(copystream); /* * Check command status and return to normal libpq state. diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 403b51325a72..c8ea744091d7 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -168,6 +168,7 @@ slashUsage(unsigned short int pager) " \\g with no arguments is equivalent to a semicolon\n"); HELP0(" \\gdesc describe result of query, without executing it\n"); HELP0(" \\gexec execute query, then execute each value in its result\n"); + HELP0(" \\gi FILE execute query, reading from file or pipe| if needed\n"); HELP0(" \\gset [PREFIX] execute query and store result in psql variables\n"); HELP0(" \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n"); HELP0(" \\q quit psql\n"); diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h index fd82303f776c..683d90e6fc5d 100644 --- a/src/bin/psql/settings.h +++ b/src/bin/psql/settings.h @@ -102,18 +102,24 @@ typedef struct _psqlSettings { PGconn *db; /* connection to backend */ int encoding; /* client_encoding */ + FILE *queryFout; /* where to send the query results */ + char *queryFName; /* name of above Fout stream */ bool queryFoutPipe; /* queryFout is from a popen() */ - FILE *copyStream; /* Stream to read/write for \copy command */ - PGresult *last_error_result; /* most recent error result, if any */ printQueryOpt popt; /* The active print format settings */ - - char *gfname; /* one-shot file output argument for \g */ printQueryOpt *gsavepopt; /* if not null, saved print format settings */ + char *gfname; /* one-shot output argument for \g */ + bool g_pipe; /* whether \g is to a pipe or file */ + char *gi_fname; /* one-shot input argument for \gi */ + bool gi_pipe; /* whether \gi is from a pipe or file */ + + bool copy_pstd; /* \copy pstdout/pstdin vs stdout/stdin */ + FILE *copyStream; /* current \copy to/from file/pipe */ + char *gset_prefix; /* one-shot prefix argument for \gset */ bool gdesc_flag; /* one-shot request to describe query result */ bool gexec_flag; /* one-shot request to execute query result */ diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c index 249b6aa51690..158ccd13929a 100644 --- a/src/bin/psql/startup.c +++ b/src/bin/psql/startup.c @@ -474,7 +474,7 @@ main(int argc, char *argv[]) PQfinish(pset.db); if (pset.dead_conn) PQfinish(pset.dead_conn); - setQFout(NULL); + setQFout(NULL, false); return successResult; } @@ -591,9 +591,13 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options) options->no_readline = true; break; case 'o': - if (!setQFout(optarg)) - exit(EXIT_FAILURE); - break; + { + bool is_pipe = *optarg == '|'; + + if (!setQFout(optarg + (is_pipe ? 1 : 0), is_pipe)) + exit(EXIT_FAILURE); + break; + } case 'p': options->port = pg_strdup(optarg); break; diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl index 4050f9a5e3e1..e7310ad65c47 100644 --- a/src/bin/psql/t/001_basic.pl +++ b/src/bin/psql/t/001_basic.pl @@ -524,4 +524,40 @@ sub psql_fails_like qr/server closed the connection unexpectedly/, 'protocol sync loss in pipeline: COPY, SELECT and sync'); +# Test \gi file +my $gi_file = "$tempdir/gi_file.in"; +append_to_file($gi_file, "Susie\nCalvin\nHobbes\n"); +psql_like( + $node, + "CREATE TABLE gi_data(stuff TEXT);\n" . + "COPY gi_data(stuff) FROM STDIN \\gi '$gi_file'\n" . + "SELECT stuff FROM gi_data ORDER BY 1;\n", + qr/Calvin.*Hobbes.*Susie/s, + "COPY ... \\gi file"); + +psql_like( + $node, + "COPY gi_data FROM STDIN \\gi '$perlbin -e \"print qq{Rosalyn\\n}\"|'\n" . + "SELECT * FROM gi_data WHERE stuff ILIKE '%sal%';", + qr/Rosalyn/, + "COPY ... \\gi command"); + +psql_like( + $node, + "SELECT 'hello' \\gi '$gi_file'\n", + qr/hello/, + "SELECT ... \\gi file is simply executed"); + +psql_fails_like( + $node, + "SELECT 'missing file' \\gi\n", + qr/\\gi expects a filename or pipe command/, + "missing file parameter to \\gi"); + +psql_fails_like( + $node, + "COPY gi_data(stuff) FROM STDIN \\gi '$tempdir/no-such-file'\n", + qr/No such file or directory/, + "COPY ... \\gi no-such-file"); + done_testing(); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index c916b9299a80..dd1c04346e13 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1887,7 +1887,8 @@ psql_completion(const char *text, int start, int end) "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", - "\\g", "\\gdesc", "\\getenv", "\\getresults", "\\gexec", "\\gset", "\\gx", + "\\g", "\\gdesc", "\\getenv", "\\getresults", "\\gexec", "\\gi", + "\\gset", "\\gx", "\\help", "\\html", "\\if", "\\include", "\\include_relative", "\\ir", "\\list", "\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink", @@ -5440,7 +5441,7 @@ match_previous_words(int pattern_id, COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_routines); else if (TailMatchesCS("\\sv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); - else if (TailMatchesCS("\\cd|\\e|\\edit|\\g|\\gx|\\i|\\include|" + else if (TailMatchesCS("\\cd|\\e|\\edit|\\g|\\gx|\\gi|\\i|\\include|" "\\ir|\\include_relative|\\o|\\out|" "\\s|\\w|\\write|\\lo_import")) {