diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 8f7d8758ca02..b38da49d500a 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 ec65ab79fecb..85b99c29dc12 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",
@@ -5446,7 +5447,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"))
{