Allow psql's \g and \gx commands to transiently change \pset options.

We invented \gx to allow the "\pset expanded" flag to be forced on
for the duration of one command output, but that turns out to not
be nearly enough to satisfy the demand for variant output formats.
Hence, make it possible to change any pset option(s) for the duration
of a single command output, by writing "option=value ..." inside
parentheses, for example
	\g (format=csv csv_fieldsep='\t') somefile

\gx can now be understood as a shorthand for including expanded=on
inside the parentheses.

Patch by me, expanding on a proposal by Pavel Stehule

Discussion: https://postgr.es/m/CAFj8pRBx9OnBPRJVtfA5ycUpySge-XootAXAsv_4rrkHxJ8eRg@mail.gmail.com
This commit is contained in:
Tom Lane 2020-04-07 17:46:29 -04:00
parent 0f5ca02f53
commit b63c293bcb
8 changed files with 315 additions and 47 deletions

View File

@ -2105,12 +2105,34 @@ Tue Oct 26 21:40:57 CEST 1999
<varlistentry>
<term><literal>\g [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
<term><literal>\g [ |<replaceable class="parameter">command</replaceable> ]</literal></term>
<term><literal>\g [ (<replaceable class="parameter">option</replaceable>=<replaceable class="parameter">value</replaceable> [...]) ] [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
<term><literal>\g [ (<replaceable class="parameter">option</replaceable>=<replaceable class="parameter">value</replaceable> [...]) ] [ |<replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
Sends the current query buffer to the server for execution.
If an argument is given, the query's output is written to the named
</para>
<para>
If parentheses appear after <literal>\g</literal>, they surround a
space-separated list
of <replaceable class="parameter">option</replaceable><literal>=</literal><replaceable class="parameter">value</replaceable>
formatting-option clauses, which are interpreted in the same way
as <literal>\pset</literal>
<replaceable class="parameter">option</replaceable>
<replaceable class="parameter">value</replaceable> commands, but take
effect only for the duration of this query. In this list, spaces are
not allowed around <literal>=</literal> signs, but are required
between option clauses.
If <literal>=</literal><replaceable class="parameter">value</replaceable>
is omitted, the
named <replaceable class="parameter">option</replaceable> is changed
in the same way as for
<literal>\pset</literal> <replaceable class="parameter">option</replaceable>
with no explicit <replaceable class="parameter">value</replaceable>.
</para>
<para>
If a <replaceable class="parameter">filename</replaceable>
or <literal>|</literal><replaceable class="parameter">command</replaceable>
argument is given, the query's output is written to the named
file or piped to the given shell command, instead of displaying it as
usual. The file or command is written to only if the query
successfully returns zero or more tuples, not if the query fails or
@ -2119,13 +2141,15 @@ Tue Oct 26 21:40:57 CEST 1999
<para>
If the current query buffer is empty, the most recently sent query is
re-executed instead. Except for that behavior, <literal>\g</literal>
without an argument is essentially equivalent to a semicolon.
A <literal>\g</literal> with argument is a <quote>one-shot</quote>
alternative to the <command>\o</command> command.
without any arguments is essentially equivalent to a semicolon.
With arguments, <literal>\g</literal> provides
a <quote>one-shot</quote> alternative to the <command>\o</command>
command, and additionally allows one-shot adjustments of the
output formatting options normally set by <literal>\pset</literal>.
</para>
<para>
If the argument begins with <literal>|</literal>, then the entire remainder
of the line is taken to be
When the last argument begins with <literal>|</literal>, the entire
remainder of the line is taken to be
the <replaceable class="parameter">command</replaceable> to execute,
and neither variable interpolation nor backquote expansion are
performed in it. The rest of the line is simply passed literally to
@ -2246,12 +2270,14 @@ hello 10
<varlistentry>
<term><literal>\gx [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
<term><literal>\gx [ |<replaceable class="parameter">command</replaceable> ]</literal></term>
<term><literal>\gx [ (<replaceable class="parameter">option</replaceable>=<replaceable class="parameter">value</replaceable> [...]) ] [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
<term><literal>\gx [ (<replaceable class="parameter">option</replaceable>=<replaceable class="parameter">value</replaceable> [...]) ] [ |<replaceable class="parameter">command</replaceable> ]</literal></term>
<listitem>
<para>
<literal>\gx</literal> is equivalent to <literal>\g</literal>, but
forces expanded output mode for this query. See <literal>\x</literal>.
<literal>\gx</literal> is equivalent to <literal>\g</literal>, except
that it forces expanded output mode for this query, as
if <literal>expanded=on</literal> were included in the list of
<literal>\pset</literal> options. See also <literal>\x</literal>.
</para>
</listitem>
</varlistentry>
@ -4879,9 +4905,31 @@ second | three
-[ RECORD 4 ]-
first | 4
second | four
</programlisting></para>
</programlisting>
</para>
<para>
<para>
Also, these output format options can be set for just one query by using
<literal>\g</literal>:
<programlisting>
peter@localhost testdb=&gt; <userinput>SELECT * FROM my_table</userinput>
peter@localhost testdb-&gt; <userinput>\g (format=aligned tuples_only=off expanded=on)</userinput>
-[ RECORD 1 ]-
first | 1
second | one
-[ RECORD 2 ]-
first | 2
second | two
-[ RECORD 3 ]-
first | 3
second | three
-[ RECORD 4 ]-
first | 4
second | four
</programlisting>
</para>
<para>
When suitable, query results can be shown in a crosstab representation
with the <command>\crosstabview</command> command:
<programlisting>

View File

@ -86,6 +86,10 @@ static backslashResult exec_command_errverbose(PsqlScanState scan_state, bool ac
static backslashResult exec_command_f(PsqlScanState scan_state, bool active_branch);
static backslashResult exec_command_g(PsqlScanState scan_state, bool active_branch,
const char *cmd);
static backslashResult process_command_g_options(char *first_option,
PsqlScanState scan_state,
bool active_branch,
const char *cmd);
static backslashResult exec_command_gdesc(PsqlScanState scan_state, bool active_branch);
static backslashResult exec_command_gexec(PsqlScanState scan_state, bool active_branch);
static backslashResult exec_command_gset(PsqlScanState scan_state, bool active_branch);
@ -160,8 +164,8 @@ static void minimal_error_message(PGresult *res);
static void printSSLInfo(void);
static void printGSSInfo(void);
static bool printPsetInfo(const char *param, struct printQueryOpt *popt);
static char *pset_value_string(const char *param, struct printQueryOpt *popt);
static bool printPsetInfo(const char *param, printQueryOpt *popt);
static char *pset_value_string(const char *param, printQueryOpt *popt);
#ifdef WIN32
static void checkWin32Codepage(void);
@ -1280,19 +1284,40 @@ exec_command_f(PsqlScanState scan_state, bool active_branch)
}
/*
* \g [filename] -- send query, optionally with output to file/pipe
* \gx [filename] -- same as \g, with expanded mode forced
* \g [(pset-option[=pset-value] ...)] [filename/shell-command]
* \gx [(pset-option[=pset-value] ...)] [filename/shell-command]
*
* Send the current query. If pset options are specified, they are made
* active just for this query. If a filename or pipe command is given,
* the query output goes there. \gx implicitly sets "expanded=on" along
* with any other pset options that are specified.
*/
static backslashResult
exec_command_g(PsqlScanState scan_state, bool active_branch, const char *cmd)
{
backslashResult status = PSQL_CMD_SKIP_LINE;
char *fname;
if (active_branch)
/*
* Because the option processing for this is fairly complicated, we do it
* and then decide whether the branch is active.
*/
fname = psql_scan_slash_option(scan_state,
OT_FILEPIPE, NULL, false);
if (fname && fname[0] == '(')
{
char *fname = psql_scan_slash_option(scan_state,
OT_FILEPIPE, NULL, false);
/* Consume pset options through trailing ')' ... */
status = process_command_g_options(fname + 1, scan_state,
active_branch, cmd);
free(fname);
/* ... and again attempt to scan the filename. */
fname = psql_scan_slash_option(scan_state,
OT_FILEPIPE, NULL, false);
}
if (status == PSQL_CMD_SKIP_LINE && active_branch)
{
if (!fname)
pset.gfname = NULL;
else
@ -1300,17 +1325,98 @@ exec_command_g(PsqlScanState scan_state, bool active_branch, const char *cmd)
expand_tilde(&fname);
pset.gfname = pg_strdup(fname);
}
free(fname);
if (strcmp(cmd, "gx") == 0)
pset.g_expanded = true;
{
/* save settings if not done already, then force expanded=on */
if (pset.gsavepopt == NULL)
pset.gsavepopt = savePsetInfo(&pset.popt);
pset.popt.topt.expanded = 1;
}
status = PSQL_CMD_SEND;
}
else
ignore_slash_filepipe(scan_state);
free(fname);
return status;
}
/*
* Process parenthesized pset options for \g
*
* Note: okay to modify first_option, but not to free it; caller does that
*/
static backslashResult
process_command_g_options(char *first_option, PsqlScanState scan_state,
bool active_branch, const char *cmd)
{
bool success = true;
bool found_r_paren = false;
do
{
char *option;
size_t optlen;
/* If not first time through, collect a new option */
if (first_option)
option = first_option;
else
{
option = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, false);
if (!option)
{
if (active_branch)
{
pg_log_error("\\%s: missing right parenthesis", cmd);
success = false;
}
break;
}
}
/* Check for terminating right paren, and remove it from string */
optlen = strlen(option);
if (optlen > 0 && option[optlen - 1] == ')')
{
option[--optlen] = '\0';
found_r_paren = true;
}
/* If there was anything besides parentheses, parse/execute it */
if (optlen > 0)
{
/* We can have either "name" or "name=value" */
char *valptr = strchr(option, '=');
if (valptr)
*valptr++ = '\0';
if (active_branch)
{
/* save settings if not done already, then apply option */
if (pset.gsavepopt == NULL)
pset.gsavepopt = savePsetInfo(&pset.popt);
success &= do_pset(option, valptr, &pset.popt, true);
}
}
/* Clean up after this option. We should not free first_option. */
if (first_option)
first_option = NULL;
else
free(option);
} while (!found_r_paren);
/* If we failed after already changing some options, undo side-effects */
if (!success && active_branch && pset.gsavepopt)
{
restorePsetInfo(&pset.popt, pset.gsavepopt);
pset.gsavepopt = NULL;
}
return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR;
}
/*
* \gdesc -- describe query result
*/
@ -3785,6 +3891,17 @@ _unicode_linestyle2string(int linestyle)
/*
* do_pset
*
* Performs the assignment "param = value", where value could be NULL;
* for some params that has an effect such as inversion, for others
* it does nothing.
*
* Adjusts the state of the formatting options at *popt. (In practice that
* is always pset.popt, but maybe someday it could be different.)
*
* If successful and quiet is false, then invokes printPsetInfo() to report
* the change.
*
* Returns true if successful, else false (eg for invalid param or value).
*/
bool
do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
@ -4109,9 +4226,11 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
return true;
}
/*
* printPsetInfo: print the state of the "param" formatting parameter in popt.
*/
static bool
printPsetInfo(const char *param, struct printQueryOpt *popt)
printPsetInfo(const char *param, printQueryOpt *popt)
{
Assert(param != NULL);
@ -4292,6 +4411,77 @@ printPsetInfo(const char *param, struct printQueryOpt *popt)
return true;
}
/*
* savePsetInfo: make a malloc'd copy of the data in *popt.
*
* Possibly this should be somewhere else, but it's a bit specific to psql.
*/
printQueryOpt *
savePsetInfo(const printQueryOpt *popt)
{
printQueryOpt *save;
save = (printQueryOpt *) pg_malloc(sizeof(printQueryOpt));
/* Flat-copy all the scalar fields, then duplicate sub-structures. */
memcpy(save, popt, sizeof(printQueryOpt));
/* topt.line_style points to const data that need not be duplicated */
if (popt->topt.fieldSep.separator)
save->topt.fieldSep.separator = pg_strdup(popt->topt.fieldSep.separator);
if (popt->topt.recordSep.separator)
save->topt.recordSep.separator = pg_strdup(popt->topt.recordSep.separator);
if (popt->topt.tableAttr)
save->topt.tableAttr = pg_strdup(popt->topt.tableAttr);
if (popt->nullPrint)
save->nullPrint = pg_strdup(popt->nullPrint);
if (popt->title)
save->title = pg_strdup(popt->title);
/*
* footers and translate_columns are never set in psql's print settings,
* so we needn't write code to duplicate them.
*/
Assert(popt->footers == NULL);
Assert(popt->translate_columns == NULL);
return save;
}
/*
* restorePsetInfo: restore *popt from the previously-saved copy *save,
* then free *save.
*/
void
restorePsetInfo(printQueryOpt *popt, printQueryOpt *save)
{
/* Free all the old data we're about to overwrite the pointers to. */
/* topt.line_style points to const data that need not be duplicated */
if (popt->topt.fieldSep.separator)
free(popt->topt.fieldSep.separator);
if (popt->topt.recordSep.separator)
free(popt->topt.recordSep.separator);
if (popt->topt.tableAttr)
free(popt->topt.tableAttr);
if (popt->nullPrint)
free(popt->nullPrint);
if (popt->title)
free(popt->title);
/*
* footers and translate_columns are never set in psql's print settings,
* so we needn't write code to duplicate them.
*/
Assert(popt->footers == NULL);
Assert(popt->translate_columns == NULL);
/* Now we may flat-copy all the fields, including pointers. */
memcpy(popt, save, sizeof(printQueryOpt));
/* Lastly, free "save" ... but its sub-structures now belong to popt. */
free(save);
}
static const char *
pset_bool_string(bool val)
@ -4339,7 +4529,7 @@ pset_quoted_string(const char *str)
* output that produces the correct setting when fed back into \pset.
*/
static char *
pset_value_string(const char *param, struct printQueryOpt *popt)
pset_value_string(const char *param, printQueryOpt *popt)
{
Assert(param != NULL);

View File

@ -36,6 +36,10 @@ extern bool do_pset(const char *param,
printQueryOpt *popt,
bool quiet);
extern printQueryOpt *savePsetInfo(const printQueryOpt *popt);
extern void restorePsetInfo(printQueryOpt *popt, printQueryOpt *save);
extern void connection_warnings(bool in_startup);
extern void SyncVariables(void);

View File

@ -707,13 +707,8 @@ PrintNotifications(void)
static bool
PrintQueryTuples(const PGresult *results)
{
printQueryOpt my_popt = pset.popt;
bool result = true;
/* one-shot expanded output requested via \gx */
if (pset.g_expanded)
my_popt.topt.expanded = 1;
/* write output to \g argument, if any */
if (pset.gfname)
{
@ -725,7 +720,7 @@ PrintQueryTuples(const PGresult *results)
if (is_pipe)
disable_sigpipe_trap();
printQuery(results, &my_popt, fout, false, pset.logfile);
printQuery(results, &pset.popt, fout, false, pset.logfile);
if (ferror(fout))
{
pg_log_error("could not print result table: %m");
@ -742,7 +737,7 @@ PrintQueryTuples(const PGresult *results)
}
else
{
printQuery(results, &my_popt, pset.queryFout, false, pset.logfile);
printQuery(results, &pset.popt, pset.queryFout, false, pset.logfile);
if (ferror(pset.queryFout))
{
pg_log_error("could not print result table: %m");
@ -1418,8 +1413,12 @@ sendquery_cleanup:
pset.gfname = NULL;
}
/* reset \gx's expanded-mode flag */
pset.g_expanded = false;
/* restore print settings if \g changed them */
if (pset.gsavepopt)
{
restorePsetInfo(&pset.popt, pset.gsavepopt);
pset.gsavepopt = NULL;
}
/* reset \gset trigger */
if (pset.gset_prefix)
@ -1646,10 +1645,6 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
"FETCH FORWARD %d FROM _psql_cursor",
fetch_count);
/* one-shot expanded output requested via \gx */
if (pset.g_expanded)
my_popt.topt.expanded = 1;
/* prepare to write output to \g argument, if any */
if (pset.gfname)
{

View File

@ -68,7 +68,7 @@ usage(unsigned short int pager)
* Keep this line count in sync with the number of lines printed below!
* Use "psql --help=options | wc" to count correctly.
*/
output = PageOutput(62, pager ? &(pset.popt.topt) : NULL);
output = PageOutput(63, pager ? &(pset.popt.topt) : NULL);
fprintf(output, _("psql is the PostgreSQL interactive terminal.\n\n"));
fprintf(output, _("Usage:\n"));
@ -169,17 +169,18 @@ slashUsage(unsigned short int pager)
* Use "psql --help=commands | wc" to count correctly. It's okay to count
* the USE_READLINE line even in builds without that.
*/
output = PageOutput(128, pager ? &(pset.popt.topt) : NULL);
output = PageOutput(133, pager ? &(pset.popt.topt) : NULL);
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
fprintf(output, _(" \\crosstabview [COLUMNS] execute query and display results in crosstab\n"));
fprintf(output, _(" \\errverbose show most recent error message at maximum verbosity\n"));
fprintf(output, _(" \\g [FILE] or ; execute query (and send results to file or |pipe)\n"));
fprintf(output, _(" \\g [(OPTIONS)] [FILE] execute query (and send results to file or |pipe);\n"));
fprintf(output, _(" \\g with no arguments is equivalent to a semicolon\n"));
fprintf(output, _(" \\gdesc describe result of query, without executing it\n"));
fprintf(output, _(" \\gexec execute query, then execute each value in its result\n"));
fprintf(output, _(" \\gset [PREFIX] execute query and store results in psql variables\n"));
fprintf(output, _(" \\gx [FILE] as \\g, but forces expanded output mode\n"));
fprintf(output, _(" \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n"));
fprintf(output, _(" \\q quit psql\n"));
fprintf(output, _(" \\watch [SEC] execute query every SEC seconds\n"));
fprintf(output, "\n");

View File

@ -88,10 +88,11 @@ typedef struct _psqlSettings
PGresult *last_error_result; /* most recent error result, if any */
printQueryOpt popt;
printQueryOpt popt; /* The active print format settings */
char *gfname; /* one-shot file output argument for \g */
bool g_expanded; /* one-shot expanded output requested via \gx */
printQueryOpt *gsavepopt; /* if not null, saved print format settings */
char *gset_prefix; /* one-shot prefix argument for \gset */
bool gdesc_flag; /* one-shot request to describe query results */
bool gexec_flag; /* one-shot request to execute query results */

View File

@ -76,6 +76,28 @@ four | 4
(1 row)
\unset FETCH_COUNT
-- \g/\gx with pset options
SELECT 1 as one, 2 as two \g (format=csv csv_fieldsep='\t')
one two
1 2
\g
one | two
-----+-----
1 | 2
(1 row)
SELECT 1 as one, 2 as two \gx (title='foo bar')
foo bar
-[ RECORD 1 ]
one | 1
two | 2
\g
one | two
-----+-----
1 | 2
(1 row)
-- \gset
select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_
\echo :pref01_test01 :pref01_test02 :pref01_test03

View File

@ -38,6 +38,13 @@ SELECT 3 as three, 4 as four \gx
\unset FETCH_COUNT
-- \g/\gx with pset options
SELECT 1 as one, 2 as two \g (format=csv csv_fieldsep='\t')
\g
SELECT 1 as one, 2 as two \gx (title='foo bar')
\g
-- \gset
select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_