psql: Support multiple -c and -f options, and allow mixing them.

To support this, we must reconcile some historical anomalies in the
behavior of -c.  In particular, as a backward-incompatibility, -c no
longer implies --no-psqlrc.

Pavel Stehule (code) and Catalin Iacob (documentation).  Review by
Michael Paquier and myself.  Proposed behavior per Tom Lane.
This commit is contained in:
Robert Haas 2015-12-08 14:04:08 -05:00
parent 385f337c9f
commit d5563d7df9
5 changed files with 202 additions and 132 deletions

View File

@ -38,9 +38,10 @@ PostgreSQL documentation
<productname>PostgreSQL</productname>. It enables you to type in <productname>PostgreSQL</productname>. It enables you to type in
queries interactively, issue them to queries interactively, issue them to
<productname>PostgreSQL</productname>, and see the query results. <productname>PostgreSQL</productname>, and see the query results.
Alternatively, input can be from a file. In addition, it provides a Alternatively, input can be from a file or from command line
number of meta-commands and various shell-like features to arguments. In addition, it provides a number of meta-commands and various
facilitate writing scripts and automating a wide variety of tasks. shell-like features to facilitate writing scripts and automating a wide
variety of tasks.
</para> </para>
</refsect1> </refsect1>
@ -89,11 +90,10 @@ PostgreSQL documentation
<term><option>--command=<replaceable class="parameter">command</replaceable></></term> <term><option>--command=<replaceable class="parameter">command</replaceable></></term>
<listitem> <listitem>
<para> <para>
Specifies that <application>psql</application> is to execute one Specifies that <application>psql</application> is to execute the given
command string, <replaceable class="parameter">command</replaceable>, command string, <replaceable class="parameter">command</replaceable>.
and then exit. This is useful in shell scripts. Start-up files This option can be repeated and combined in any order with
(<filename>psqlrc</filename> and <filename>~/.psqlrc</filename>) are the <option>-f</option> option.
ignored with this option.
</para> </para>
<para> <para>
<replaceable class="parameter">command</replaceable> must be either <replaceable class="parameter">command</replaceable> must be either
@ -102,25 +102,34 @@ PostgreSQL documentation
or a single backslash command. Thus you cannot mix or a single backslash command. Thus you cannot mix
<acronym>SQL</acronym> and <application>psql</application> <acronym>SQL</acronym> and <application>psql</application>
meta-commands with this option. To achieve that, you could meta-commands with this option. To achieve that, you could
pipe the string into <application>psql</application>, for example: use repeated <option>-c</option> options or pipe the string
into <application>psql</application>, for example:
<literal>psql -c '\x' -c 'SELECT * FROM foo;'</literal> or
<literal>echo '\x \\ SELECT * FROM foo;' | psql</literal>. <literal>echo '\x \\ SELECT * FROM foo;' | psql</literal>.
(<literal>\\</> is the separator meta-command.) (<literal>\\</> is the separator meta-command.)
</para> </para>
<para> <para>
If the command string contains multiple SQL commands, they are Each command string passed to <option>-c</option> is sent to the server
processed in a single transaction, unless there are explicit as a single query. Because of this, the server executes it as a single
<command>BEGIN</>/<command>COMMIT</> commands included in the transaction, even if a command string contains
string to divide it into multiple transactions. This is multiple <acronym>SQL</acronym> commands, unless there are
different from the behavior when the same string is fed to explicit <command>BEGIN</>/<command>COMMIT</> commands included in the
<application>psql</application>'s standard input. Also, only string to divide it into multiple transactions. Also, the server only
the result of the last SQL command is returned. returns the result of the last <acronym>SQL</acronym> command to the
client. This is different from the behavior when the same string with
multiple <acronym>SQL</acronym> commands is fed
to <application>psql</application>'s standard input because
then <application>psql</application> sends each <acronym>SQL</acronym>
command separately.
</para> </para>
<para> <para>
Because of these legacy behaviors, putting more than one command in Putting more than one command in the <option>-c</option> string often
the <option>-c</option> string often has unexpected results. It's has unexpected results. This is a result of the fact that the whole
better to feed multiple commands to <application>psql</application>'s string is sent to the server as a single query.
standard input, either using <application>echo</application> as It's better to use repeated <option>-c</option> commands or feed
illustrated above, or via a shell here-document, for example: multiple commands to <application>psql</application>'s standard input,
either using <application>echo</application> as illustrated above, or
via a shell here-document, for example:
<programlisting> <programlisting>
psql &lt;&lt;EOF psql &lt;&lt;EOF
\x \x
@ -185,9 +194,11 @@ EOF
<para> <para>
Use the file <replaceable class="parameter">filename</replaceable> Use the file <replaceable class="parameter">filename</replaceable>
as the source of commands instead of reading commands interactively. as the source of commands instead of reading commands interactively.
After the file is processed, <application>psql</application> This option can be repeated and combined in any order with
terminates. This is in many ways equivalent to the meta-command the <option>-c</option> option. After the commands in
<command>\i</command>. every <option>-c</option> command string and <option>-f</option> file
are processed, <application>psql</application> terminates. This option
is in many ways equivalent to the meta-command <command>\i</command>.
</para> </para>
<para> <para>
@ -539,20 +550,21 @@ EOF
<term><option>--single-transaction</option></term> <term><option>--single-transaction</option></term>
<listitem> <listitem>
<para> <para>
When <application>psql</application> executes a script, adding When <application>psql</application> executes commands from a script
this option wraps <command>BEGIN</>/<command>COMMIT</> around the and/or a <option>-c</option> option, adding this option
script to execute it as a single transaction. This ensures that wraps <command>BEGIN</>/<command>COMMIT</> around all of those
either all the commands complete successfully, or no changes are commands as a whole to execute them as a single transaction. This
applied. ensures that either all the commands complete successfully, or no
changes are applied.
</para> </para>
<para> <para>
If the script itself uses <command>BEGIN</>, <command>COMMIT</>, If the commands themselves
contain <command>BEGIN</>, <command>COMMIT</>,
or <command>ROLLBACK</>, this option will not have the desired or <command>ROLLBACK</>, this option will not have the desired
effects. effects. Also, if an individual command cannot be executed inside a
Also, if the script contains any command that cannot be executed transaction block, specifying this option will cause the whole
inside a transaction block, specifying this option will cause that transaction to fail.
command (and hence the whole transaction) to fail.
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
@ -3725,7 +3737,7 @@ PSQL_EDITOR_LINENUMBER_ARG='--line '
<term><filename>psqlrc</filename> and <filename>~/.psqlrc</filename></term> <term><filename>psqlrc</filename> and <filename>~/.psqlrc</filename></term>
<listitem> <listitem>
<para> <para>
Unless it is passed an <option>-X</option> or <option>-c</option> option, Unless it is passed an <option>-X</option> option,
<application>psql</application> attempts to read and execute commands <application>psql</application> attempts to read and execute commands
from the system-wide startup file (<filename>psqlrc</filename>) and then from the system-wide startup file (<filename>psqlrc</filename>) and then
the user's personal startup file (<filename>~/.psqlrc</filename>), after the user's personal startup file (<filename>~/.psqlrc</filename>), after
@ -3819,6 +3831,12 @@ PSQL_EDITOR_LINENUMBER_ARG='--line '
</para> </para>
</listitem> </listitem>
<listitem>
<para>
Before <productname>PostgreSQL</productname> 9.6, <option>-c</option>
implied <option>-X</option>; this is no longer the case.
</para>
</listitem>
</itemizedlist> </itemizedlist>
</refsect1> </refsect1>

View File

@ -916,7 +916,7 @@ exec_command(const char *cmd,
include_relative = (strcmp(cmd, "ir") == 0 include_relative = (strcmp(cmd, "ir") == 0
|| strcmp(cmd, "include_relative") == 0); || strcmp(cmd, "include_relative") == 0);
expand_tilde(&fname); expand_tilde(&fname);
success = (process_file(fname, false, include_relative) == EXIT_SUCCESS); success = (process_file(fname, include_relative) == EXIT_SUCCESS);
free(fname); free(fname);
} }
} }
@ -2288,13 +2288,12 @@ do_edit(const char *filename_arg, PQExpBuffer query_buf,
* the file from where the currently processed file (if any) is located. * the file from where the currently processed file (if any) is located.
*/ */
int int
process_file(char *filename, bool single_txn, bool use_relative_path) process_file(char *filename, bool use_relative_path)
{ {
FILE *fd; FILE *fd;
int result; int result;
char *oldfilename; char *oldfilename;
char relpath[MAXPGPATH]; char relpath[MAXPGPATH];
PGresult *res;
if (!filename) if (!filename)
{ {
@ -2339,37 +2338,8 @@ process_file(char *filename, bool single_txn, bool use_relative_path)
oldfilename = pset.inputfile; oldfilename = pset.inputfile;
pset.inputfile = filename; pset.inputfile = filename;
if (single_txn)
{
if ((res = PSQLexec("BEGIN")) == NULL)
{
if (pset.on_error_stop)
{
result = EXIT_USER;
goto error;
}
}
else
PQclear(res);
}
result = MainLoop(fd); result = MainLoop(fd);
if (single_txn)
{
if ((res = PSQLexec("COMMIT")) == NULL)
{
if (pset.on_error_stop)
{
result = EXIT_USER;
goto error;
}
}
else
PQclear(res);
}
error:
if (fd != stdin) if (fd != stdin)
fclose(fd); fclose(fd);

View File

@ -27,7 +27,7 @@ typedef enum _backslashResult
extern backslashResult HandleSlashCmds(PsqlScanState scan_state, extern backslashResult HandleSlashCmds(PsqlScanState scan_state,
PQExpBuffer query_buf); PQExpBuffer query_buf);
extern int process_file(char *filename, bool single_txn, bool use_relative_path); extern int process_file(char *filename, bool use_relative_path);
extern bool do_pset(const char *param, extern bool do_pset(const char *param,
const char *value, const char *value,

View File

@ -50,13 +50,24 @@ PsqlSettings pset;
*/ */
enum _actions enum _actions
{ {
ACT_NOTHING = 0,
ACT_SINGLE_SLASH,
ACT_LIST_DB,
ACT_SINGLE_QUERY, ACT_SINGLE_QUERY,
ACT_SINGLE_SLASH,
ACT_FILE ACT_FILE
}; };
typedef struct SimpleActionListCell
{
struct SimpleActionListCell *next;
int action;
char *val;
} SimpleActionListCell;
typedef struct SimpleActionList
{
SimpleActionListCell *head;
SimpleActionListCell *tail;
} SimpleActionList;
struct adhoc_opts struct adhoc_opts
{ {
char *dbname; char *dbname;
@ -64,11 +75,11 @@ struct adhoc_opts
char *port; char *port;
char *username; char *username;
char *logfilename; char *logfilename;
enum _actions action;
char *action_string;
bool no_readline; bool no_readline;
bool no_psqlrc; bool no_psqlrc;
bool single_txn; bool single_txn;
bool list_dbs;
SimpleActionList actions;
}; };
static void parse_psql_options(int argc, char *argv[], static void parse_psql_options(int argc, char *argv[],
@ -76,6 +87,8 @@ static void parse_psql_options(int argc, char *argv[],
static void process_psqlrc(char *argv0); static void process_psqlrc(char *argv0);
static void process_psqlrc_file(char *filename); static void process_psqlrc_file(char *filename);
static void showVersion(void); static void showVersion(void);
static void simple_action_list_append(SimpleActionList *list,
int action, const char *val);
static void EstablishVariableSpace(void); static void EstablishVariableSpace(void);
#define NOPAGER 0 #define NOPAGER 0
@ -159,6 +172,9 @@ main(int argc, char *argv[])
SetVariable(pset.vars, "PROMPT2", DEFAULT_PROMPT2); SetVariable(pset.vars, "PROMPT2", DEFAULT_PROMPT2);
SetVariable(pset.vars, "PROMPT3", DEFAULT_PROMPT3); SetVariable(pset.vars, "PROMPT3", DEFAULT_PROMPT3);
options.actions.head = NULL;
options.actions.tail = NULL;
parse_psql_options(argc, argv, &options); parse_psql_options(argc, argv, &options);
/* /*
@ -166,14 +182,11 @@ main(int argc, char *argv[])
* as if the user had specified "-f -". This lets single-transaction mode * as if the user had specified "-f -". This lets single-transaction mode
* work in this case. * work in this case.
*/ */
if (options.action == ACT_NOTHING && pset.notty) if (options.actions.head == NULL && pset.notty)
{ simple_action_list_append(&options.actions, ACT_FILE, NULL);
options.action = ACT_FILE;
options.action_string = NULL;
}
/* Bail out if -1 was specified but will be ignored. */ /* Bail out if -1 was specified but will be ignored. */
if (options.single_txn && options.action != ACT_FILE && options.action == ACT_NOTHING) if (options.single_txn && options.actions.head == NULL)
{ {
fprintf(stderr, _("%s: -1 can only be used in non-interactive mode\n"), pset.progname); fprintf(stderr, _("%s: -1 can only be used in non-interactive mode\n"), pset.progname);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
@ -217,8 +230,7 @@ main(int argc, char *argv[])
keywords[3] = "password"; keywords[3] = "password";
values[3] = password; values[3] = password;
keywords[4] = "dbname"; keywords[4] = "dbname";
values[4] = (options.action == ACT_LIST_DB && values[4] = (options.list_dbs && options.dbname == NULL) ?
options.dbname == NULL) ?
"postgres" : options.dbname; "postgres" : options.dbname;
keywords[5] = "fallback_application_name"; keywords[5] = "fallback_application_name";
values[5] = pset.progname; values[5] = pset.progname;
@ -259,7 +271,7 @@ main(int argc, char *argv[])
SyncVariables(); SyncVariables();
if (options.action == ACT_LIST_DB) if (options.list_dbs)
{ {
int success; int success;
@ -279,52 +291,91 @@ main(int argc, char *argv[])
pset.progname, options.logfilename, strerror(errno)); pset.progname, options.logfilename, strerror(errno));
} }
/* if (!options.no_psqlrc)
* Now find something to do process_psqlrc(argv[0]);
*/
/* /*
* process file given by -f * If any actions were given by caller, process them in the order in
* which they were specified.
*/ */
if (options.action == ACT_FILE) if (options.actions.head != NULL)
{ {
if (!options.no_psqlrc) PGresult *res;
process_psqlrc(argv[0]); SimpleActionListCell *cell;
successResult = process_file(options.action_string, options.single_txn, false); successResult = EXIT_SUCCESS; /* silence compiler */
}
/* if (options.single_txn)
* process slash command if one was given to -c {
*/ if ((res = PSQLexec("BEGIN")) == NULL)
else if (options.action == ACT_SINGLE_SLASH) {
{ if (pset.on_error_stop)
PsqlScanState scan_state; {
successResult = EXIT_USER;
goto error;
}
}
else
PQclear(res);
}
if (pset.echo == PSQL_ECHO_ALL) for (cell = options.actions.head; cell; cell = cell->next)
puts(options.action_string); {
if (cell->action == ACT_SINGLE_QUERY)
{
if (pset.echo == PSQL_ECHO_ALL)
puts(cell->val);
scan_state = psql_scan_create(); successResult = SendQuery(cell->val)
psql_scan_setup(scan_state, ? EXIT_SUCCESS : EXIT_FAILURE;
options.action_string, }
strlen(options.action_string)); else if (cell->action == ACT_SINGLE_SLASH)
{
PsqlScanState scan_state;
successResult = HandleSlashCmds(scan_state, NULL) != PSQL_CMD_ERROR if (pset.echo == PSQL_ECHO_ALL)
? EXIT_SUCCESS : EXIT_FAILURE; puts(cell->val);
psql_scan_destroy(scan_state); scan_state = psql_scan_create();
} psql_scan_setup(scan_state,
cell->val,
strlen(cell->val));
/* successResult = HandleSlashCmds(scan_state, NULL) != PSQL_CMD_ERROR
* If the query given to -c was a normal one, send it ? EXIT_SUCCESS : EXIT_FAILURE;
*/
else if (options.action == ACT_SINGLE_QUERY)
{
if (pset.echo == PSQL_ECHO_ALL)
puts(options.action_string);
successResult = SendQuery(options.action_string) psql_scan_destroy(scan_state);
? EXIT_SUCCESS : EXIT_FAILURE; }
else if (cell->action == ACT_FILE)
{
successResult = process_file(cell->val, false);
}
else
{
/* should never come here */
Assert(0);
}
if (successResult != EXIT_SUCCESS && pset.on_error_stop)
break;
}
if (options.single_txn)
{
if ((res = PSQLexec("COMMIT")) == NULL)
{
if (pset.on_error_stop)
{
successResult = EXIT_USER;
goto error;
}
}
else
PQclear(res);
}
error:
;
} }
/* /*
@ -332,9 +383,6 @@ main(int argc, char *argv[])
*/ */
else else
{ {
if (!options.no_psqlrc)
process_psqlrc(argv[0]);
connection_warnings(true); connection_warnings(true);
if (!pset.quiet) if (!pset.quiet)
printf(_("Type \"help\" for help.\n\n")); printf(_("Type \"help\" for help.\n\n"));
@ -419,14 +467,14 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts * options)
SetVariable(pset.vars, "ECHO", "errors"); SetVariable(pset.vars, "ECHO", "errors");
break; break;
case 'c': case 'c':
options->action_string = pg_strdup(optarg);
if (optarg[0] == '\\') if (optarg[0] == '\\')
{ simple_action_list_append(&options->actions,
options->action = ACT_SINGLE_SLASH; ACT_SINGLE_SLASH,
options->action_string++; pstrdup(optarg + 1));
}
else else
options->action = ACT_SINGLE_QUERY; simple_action_list_append(&options->actions,
ACT_SINGLE_QUERY,
pstrdup(optarg));
break; break;
case 'd': case 'd':
options->dbname = pg_strdup(optarg); options->dbname = pg_strdup(optarg);
@ -438,8 +486,9 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts * options)
SetVariableBool(pset.vars, "ECHO_HIDDEN"); SetVariableBool(pset.vars, "ECHO_HIDDEN");
break; break;
case 'f': case 'f':
options->action = ACT_FILE; simple_action_list_append(&options->actions,
options->action_string = pg_strdup(optarg); ACT_FILE,
pg_strdup(optarg));
break; break;
case 'F': case 'F':
pset.popt.topt.fieldSep.separator = pg_strdup(optarg); pset.popt.topt.fieldSep.separator = pg_strdup(optarg);
@ -452,7 +501,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts * options)
pset.popt.topt.format = PRINT_HTML; pset.popt.topt.format = PRINT_HTML;
break; break;
case 'l': case 'l':
options->action = ACT_LIST_DB; options->list_dbs = true;
break; break;
case 'L': case 'L':
options->logfilename = pg_strdup(optarg); options->logfilename = pg_strdup(optarg);
@ -675,11 +724,11 @@ process_psqlrc_file(char *filename)
/* check for minor version first, then major, then no version */ /* check for minor version first, then major, then no version */
if (access(psqlrc_minor, R_OK) == 0) if (access(psqlrc_minor, R_OK) == 0)
(void) process_file(psqlrc_minor, false, false); (void) process_file(psqlrc_minor, false);
else if (access(psqlrc_major, R_OK) == 0) else if (access(psqlrc_major, R_OK) == 0)
(void) process_file(psqlrc_major, false, false); (void) process_file(psqlrc_major, false);
else if (access(filename, R_OK) == 0) else if (access(filename, R_OK) == 0)
(void) process_file(filename, false, false); (void) process_file(filename, false);
free(psqlrc_minor); free(psqlrc_minor);
free(psqlrc_major); free(psqlrc_major);
@ -893,6 +942,39 @@ show_context_hook(const char *newval)
} }
/*
* Support for list of actions. SimpleStringList cannot be used due possible
* combination different actions with the requirement to save the order.
*/
static void
simple_action_list_append(SimpleActionList *list, int action, const char *val)
{
SimpleActionListCell *cell;
size_t vallen = 0;
if (val)
vallen = strlen(val);
cell = (SimpleActionListCell *)
pg_malloc(offsetof(SimpleActionListCell, val) + vallen + 1);
cell->next = NULL;
cell->action = action;
if (val)
{
cell->val = pg_malloc(vallen + 1);
strcpy(cell->val, val);
}
else
cell->val = NULL;
if (list->tail)
list->tail->next = cell;
else
list->head = cell;
list->tail = cell;
}
static void static void
EstablishVariableSpace(void) EstablishVariableSpace(void)
{ {

View File

@ -400,7 +400,7 @@ sub poll_query_until
while ($attempts < $max_attempts) while ($attempts < $max_attempts)
{ {
my $cmd = my $cmd =
[ 'psql', '-At', '-c', $query, '-d', $self->connstr($dbname) ]; [ 'psql', '-XAt', '-c', $query, '-d', $self->connstr($dbname) ];
my $result = IPC::Run::run $cmd, '>', \$stdout, '2>', \$stderr; my $result = IPC::Run::run $cmd, '>', \$stdout, '2>', \$stderr;
chomp($stdout); chomp($stdout);