diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f7c8bc16a7f..9ec2cfc9d60 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1172,10 +1172,14 @@ SELECT $1 \parse stmt1
         <tip>
         <para>
         Another way to obtain the same result as <literal>\copy
-        ... to</literal> is to use the <acronym>SQL</acronym> <literal>COPY
-        ... TO STDOUT</literal> command and terminate it
-        with <literal>\g <replaceable>filename</replaceable></literal>
-        or <literal>\g |<replaceable>program</replaceable></literal>.
+        ... to</literal> or <literal>from</literal> is to use the <acronym>SQL</acronym>
+        <literal>COPY ... TO STDOUT</literal> or <literal>FROM STDIN</literal>
+        command and terminate it with either
+        <literal>\g <replaceable>filename</replaceable></literal>
+        or <literal>\g |<replaceable>program</replaceable></literal> for output
+        and
+        <literal>\gi <replaceable>filename</replaceable></literal>
+        or <literal>\gi <replaceable>program</replaceable>|</literal> for input.
         Unlike <literal>\copy</literal>, this method allows the command to
         span multiple lines; also, variable interpolation and backquote
         expansion can be used.
@@ -2558,6 +2562,27 @@ CREATE INDEX
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-gi">
+        <term><literal>\gi <replaceable class="parameter">file</replaceable></literal></term>
+        <term><literal>\gi <replaceable class="parameter">command</replaceable>|</literal></term>
+        <listitem>
+        <para>
+        Sends the current query buffer to the server and uses the provided
+        <replaceable>file</replaceable> contents or <replaceable>command</replaceable>
+        output as input.
+        This should only apply to <acronym>SQL</acronym><command>COPY</command>
+        which seeks an input when used with <literal>FROM STDIN</literal>, and
+        will simply result in the command simple execution for other commands
+        which do not need an input stream.
+        </para>
+        <para>
+        This approach should be prefered to using <literal>\copy</literal>
+        as it achieves the same result but can span several lines and
+        is subject to variable interpolation and backquote expansion.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-meta-command-gset">
         <term><literal>\gset [ <replaceable class="parameter">prefix</replaceable> ]</literal></term>
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index a8a13c2b88b..fd10e458eb1 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 5249336bcf2..5d60204f2aa 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -12,6 +12,7 @@
 #include <math.h>
 #include <pwd.h>
 #include <signal.h>
+#include <sys/stat.h>
 #ifndef WIN32
 #include <unistd.h>				/* for write() */
 #else
@@ -41,35 +42,57 @@ 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("%s: cannot copy from/to a directory",
+					 fname ? fname : "<?>");
+
+	bad = result < 0 || S_ISDIR(st.st_mode);
+
+	if (bad)
+		fclose(file);
+
+	return bad;
+}
+
 /*
  * 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, const 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))
+		{
+			*fout = NULL;
+			return false;
+		}
 	}
 
 	if (*fout == NULL)
@@ -86,15 +109,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
@@ -103,21 +126,119 @@ SetupGOutput(FILE **gfile_fout, bool *is_pipe)
 	return true;
 }
 
+/*
+ * Close file with user feedback on errors.
+ */
+static bool
+CloseFile(FILE *stream, 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, 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 output stream for \g, if we opened it.
  */
 static void
-CloseGOutput(FILE *gfile_fout, bool is_pipe)
+CloseGOutput(FILE *gfile_fout)
 {
-	if (gfile_fout)
+	if (pset.gfname && gfile_fout)
 	{
-		if (is_pipe)
+		if (pset.g_pipe)
+			(void) ClosePipe(gfile_fout, pset.gfname);
+		else
+			(void) CloseFile(gfile_fout, pset.gfname);
+	}
+}
+
+/*
+ * 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)
+		{
+			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;
+			}
+		}
+		else
 		{
-			SetShellResultVariables(pclose(gfile_fout));
-			restore_sigpipe_trap();
+			input = fopen(pset.gi_fname, PG_BINARY_R);
+			if (!input)
+			{
+				pg_log_error("%s: %m", pset.gi_fname);
+				return false;
+			}
+			if (input && badFileStream(input, pset.gi_fname))
+				return false;
 		}
+
+		*input_stream = input;
+	}
+	else if (pset.copy_pstd)
+		*input_stream = pset.cur_cmd_source;
+	else
+		*input_stream = stdin;
+
+	return true;
+}
+
+/*
+ * Close input if opened by SetupGInput
+ */
+static void
+CloseGInput(FILE *input)
+{
+	/* FIXME should warn if no input, i.e. not a copy? */
+	if (pset.gi_fname && input)
+	{
+		if (pset.gi_pipe)
+			(void) ClosePipe(input, pset.gi_fname);
 		else
-			fclose(gfile_fout);
+			(void) CloseFile(input, pset.gi_fname);
 	}
 }
 
@@ -141,25 +262,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 +1050,6 @@ loop_exit:
  * 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 +1089,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 +1424,13 @@ sendquery_cleanup:
 		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)
 	{
@@ -1548,8 +1676,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);
@@ -1852,9 +1980,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)
 			{
@@ -1865,15 +1992,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
 				{
@@ -1881,10 +2008,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);
@@ -1905,7 +2047,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;
 
@@ -2094,7 +2236,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)
@@ -2124,8 +2266,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 */
+	CloseGOutput(gfile_fout);
+	CloseGInput(gfile_fin);
 
 	if (end_pipeline)
 	{
diff --git a/src/bin/psql/common.h b/src/bin/psql/common.h
index 7f1a23de1e8..38fb6bf66a9 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 92c955b637a..7ee221e8188 100644
--- a/src/bin/psql/copy.c
+++ b/src/bin/psql/copy.c
@@ -8,7 +8,6 @@
 #include "postgres_fe.h"
 
 #include <signal.h>
-#include <sys/stat.h>
 #ifndef WIN32
 #include <unistd.h>				/* 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 @@ copyin_cleanup:
 	 * 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 fe96e3e1de9..78b773407eb 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -177,6 +177,7 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\getresults [NUM_RES]  read NUM_RES pending results. All pending results are\n"
 		  "                         read if no argument is provided\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("  \\parse STMT_NAME       create a prepared statement\n");
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index fd82303f776..683d90e6fc5 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 249b6aa5169..158ccd13929 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -474,7 +474,7 @@ error:
 		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 739cb439708..5fff7352dc2 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -481,4 +481,40 @@ psql_like($node, "copy (values ('foo'),('bar')) to stdout \\g | $pipe_cmd",
 my $c4 = slurp_file($g_file);
 like($c4, qr/foo.*bar/s);
 
+# 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 98951aef82c..a1c5ef96086 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"))
 	{
