Commit 511ae628 authored by Tom Lane's avatar Tom Lane

Make psql reject attempts to set special variables to invalid values.

Previously, if the user set a special variable such as ECHO to an
unrecognized value, psql would bleat but store the new value anyway, and
then fall back to a default setting for the behavior controlled by the
variable.  This was agreed to be a not particularly good idea.  With
this patch, invalid values result in an error message and no change in
state.

(But this applies only to variables that affect psql's behavior; purely
informational variables such as ENCODING can still be set to random
values.)

To do this, modify the API for psql's assign-hook functions so that they
can return an OK/not OK result, and give them the responsibility for
printing error messages when they reject a value.  Adjust the APIs for
ParseVariableBool and ParseVariableNum to support the new behavior
conveniently.

In passing, document the variable VERSION, which had somehow escaped that.
And improve the quite-inadequate commenting in psql/variables.c.

Daniel Vérité, reviewed by Rahila Syed, some further tweaking by me

Discussion: https://postgr.es/m/7356e741-fa59-4146-a8eb-cf95fd6b21fb@mm
parent 46aae594
...@@ -3078,10 +3078,8 @@ bar ...@@ -3078,10 +3078,8 @@ bar
by <application>psql</application>. They represent certain option by <application>psql</application>. They represent certain option
settings that can be changed at run time by altering the value of settings that can be changed at run time by altering the value of
the variable, or in some cases represent changeable state of the variable, or in some cases represent changeable state of
<application>psql</application>. Although <application>psql</application>.
you can use these variables for other purposes, this is not By convention, all specially treated variables' names
recommended, as the program behavior might grow really strange
really quickly. By convention, all specially treated variables' names
consist of all upper-case ASCII letters (and possibly digits and consist of all upper-case ASCII letters (and possibly digits and
underscores). To ensure maximum compatibility in the future, avoid underscores). To ensure maximum compatibility in the future, avoid
using such variable names for your own purposes. A list of all specially using such variable names for your own purposes. A list of all specially
...@@ -3170,12 +3168,11 @@ bar ...@@ -3170,12 +3168,11 @@ bar
start-up, use the switch <option>-a</option>. If set to start-up, use the switch <option>-a</option>. If set to
<literal>queries</literal>, <literal>queries</literal>,
<application>psql</application> prints each query to standard output <application>psql</application> prints each query to standard output
as it is sent to the server. The switch for this is as it is sent to the server. The switch to select this behavior is
<option>-e</option>. If set to <literal>errors</literal>, then only <option>-e</option>. If set to <literal>errors</literal>, then only
failed queries are displayed on standard error output. The switch failed queries are displayed on standard error output. The switch
for this is <option>-b</option>. If unset, or if set to for this behavior is <option>-b</option>. If unset, or if set to
<literal>none</literal> (or any other value than those above) then <literal>none</literal>, then no queries are displayed.
no queries are displayed.
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
...@@ -3201,6 +3198,9 @@ bar ...@@ -3201,6 +3198,9 @@ bar
<listitem> <listitem>
<para> <para>
The current client character set encoding. The current client character set encoding.
This is set every time you connect to a database (including
program start-up), and when you change the encoding
with <literal>\encoding</>, but it can be unset.
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
...@@ -3241,9 +3241,8 @@ bar ...@@ -3241,9 +3241,8 @@ bar
list. If set to a value of <literal>ignoredups</literal>, lines list. If set to a value of <literal>ignoredups</literal>, lines
matching the previous history line are not entered. A value of matching the previous history line are not entered. A value of
<literal>ignoreboth</literal> combines the two options. If <literal>ignoreboth</literal> combines the two options. If
unset, or if set to <literal>none</literal> (or any other value unset, or if set to <literal>none</literal> (the default), all lines
than those above), all lines read in interactive mode are read in interactive mode are saved on the history list.
saved on the history list.
</para> </para>
<note> <note>
<para> <para>
...@@ -3312,7 +3311,7 @@ bar ...@@ -3312,7 +3311,7 @@ bar
to an interactive session of <application>psql</application> to an interactive session of <application>psql</application>
will terminate the application. If set to a numeric value, will terminate the application. If set to a numeric value,
that many <acronym>EOF</> characters are ignored before the that many <acronym>EOF</> characters are ignored before the
application terminates. If the variable is set but has no application terminates. If the variable is set but not to a
numeric value, the default is 10. numeric value, the default is 10.
</para> </para>
<note> <note>
...@@ -3477,6 +3476,16 @@ bar ...@@ -3477,6 +3476,16 @@ bar
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><varname>VERSION</varname></term>
<listitem>
<para>
This variable is set at program start-up to
reflect <application>psql</>'s version. It can be unset or changed.
</para>
</listitem>
</varlistentry>
</variablelist> </variablelist>
</refsect3> </refsect3>
......
...@@ -248,32 +248,38 @@ exec_command(const char *cmd, ...@@ -248,32 +248,38 @@ exec_command(const char *cmd,
*opt2, *opt2,
*opt3, *opt3,
*opt4; *opt4;
enum trivalue reuse_previous; enum trivalue reuse_previous = TRI_DEFAULT;
opt1 = read_connect_arg(scan_state); opt1 = read_connect_arg(scan_state);
if (opt1 != NULL && strncmp(opt1, prefix, sizeof(prefix) - 1) == 0) if (opt1 != NULL && strncmp(opt1, prefix, sizeof(prefix) - 1) == 0)
{ {
reuse_previous = bool on_off;
ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix) ?
TRI_YES : TRI_NO;
success = ParseVariableBool(opt1 + sizeof(prefix) - 1,
"-reuse-previous",
&on_off);
if (success)
{
reuse_previous = on_off ? TRI_YES : TRI_NO;
free(opt1); free(opt1);
opt1 = read_connect_arg(scan_state); opt1 = read_connect_arg(scan_state);
} }
else }
reuse_previous = TRI_DEFAULT;
if (success) /* give up if reuse_previous was invalid */
{
opt2 = read_connect_arg(scan_state); opt2 = read_connect_arg(scan_state);
opt3 = read_connect_arg(scan_state); opt3 = read_connect_arg(scan_state);
opt4 = read_connect_arg(scan_state); opt4 = read_connect_arg(scan_state);
success = do_connect(reuse_previous, opt1, opt2, opt3, opt4); success = do_connect(reuse_previous, opt1, opt2, opt3, opt4);
free(opt1);
free(opt2); free(opt2);
free(opt3); free(opt3);
free(opt4); free(opt4);
} }
free(opt1);
}
/* \cd */ /* \cd */
else if (strcmp(cmd, "cd") == 0) else if (strcmp(cmd, "cd") == 0)
...@@ -1208,10 +1214,7 @@ exec_command(const char *cmd, ...@@ -1208,10 +1214,7 @@ exec_command(const char *cmd,
if (result && if (result &&
!SetVariable(pset.vars, opt, result)) !SetVariable(pset.vars, opt, result))
{
psql_error("\\%s: error while setting variable\n", cmd);
success = false; success = false;
}
if (result) if (result)
free(result); free(result);
...@@ -1325,10 +1328,8 @@ exec_command(const char *cmd, ...@@ -1325,10 +1328,8 @@ exec_command(const char *cmd,
} }
if (!SetVariable(pset.vars, opt0, newval)) if (!SetVariable(pset.vars, opt0, newval))
{
psql_error("\\%s: error while setting variable\n", cmd);
success = false; success = false;
}
free(newval); free(newval);
} }
free(opt0); free(opt0);
...@@ -1564,7 +1565,7 @@ exec_command(const char *cmd, ...@@ -1564,7 +1565,7 @@ exec_command(const char *cmd,
OT_NORMAL, NULL, false); OT_NORMAL, NULL, false);
if (opt) if (opt)
pset.timing = ParseVariableBool(opt, "\\timing"); success = ParseVariableBool(opt, "\\timing", &pset.timing);
else else
pset.timing = !pset.timing; pset.timing = !pset.timing;
if (!pset.quiet) if (!pset.quiet)
...@@ -1589,10 +1590,8 @@ exec_command(const char *cmd, ...@@ -1589,10 +1590,8 @@ exec_command(const char *cmd,
success = false; success = false;
} }
else if (!SetVariable(pset.vars, opt, NULL)) else if (!SetVariable(pset.vars, opt, NULL))
{
psql_error("\\%s: error while setting variable\n", cmd);
success = false; success = false;
}
free(opt); free(opt);
} }
...@@ -2593,7 +2592,6 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) ...@@ -2593,7 +2592,6 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
psql_error("\\pset: allowed formats are unaligned, aligned, wrapped, html, asciidoc, latex, latex-longtable, troff-ms\n"); psql_error("\\pset: allowed formats are unaligned, aligned, wrapped, html, asciidoc, latex, latex-longtable, troff-ms\n");
return false; return false;
} }
} }
/* set table line style */ /* set table line style */
...@@ -2612,7 +2610,6 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) ...@@ -2612,7 +2610,6 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
psql_error("\\pset: allowed line styles are ascii, old-ascii, unicode\n"); psql_error("\\pset: allowed line styles are ascii, old-ascii, unicode\n");
return false; return false;
} }
} }
/* set unicode border line style */ /* set unicode border line style */
...@@ -2665,7 +2662,6 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) ...@@ -2665,7 +2662,6 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
{ {
if (value) if (value)
popt->topt.border = atoi(value); popt->topt.border = atoi(value);
} }
/* set expanded/vertical mode */ /* set expanded/vertical mode */
...@@ -2676,7 +2672,17 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) ...@@ -2676,7 +2672,17 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
if (value && pg_strcasecmp(value, "auto") == 0) if (value && pg_strcasecmp(value, "auto") == 0)
popt->topt.expanded = 2; popt->topt.expanded = 2;
else if (value) else if (value)
popt->topt.expanded = ParseVariableBool(value, param); {
bool on_off;
if (ParseVariableBool(value, NULL, &on_off))
popt->topt.expanded = on_off ? 1 : 0;
else
{
PsqlVarEnumError(param, value, "on, off, auto");
return false;
}
}
else else
popt->topt.expanded = !popt->topt.expanded; popt->topt.expanded = !popt->topt.expanded;
} }
...@@ -2685,7 +2691,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) ...@@ -2685,7 +2691,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "numericlocale") == 0) else if (strcmp(param, "numericlocale") == 0)
{ {
if (value) if (value)
popt->topt.numericLocale = ParseVariableBool(value, param); return ParseVariableBool(value, param, &popt->topt.numericLocale);
else else
popt->topt.numericLocale = !popt->topt.numericLocale; popt->topt.numericLocale = !popt->topt.numericLocale;
} }
...@@ -2740,7 +2746,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) ...@@ -2740,7 +2746,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "t") == 0 || strcmp(param, "tuples_only") == 0) else if (strcmp(param, "t") == 0 || strcmp(param, "tuples_only") == 0)
{ {
if (value) if (value)
popt->topt.tuples_only = ParseVariableBool(value, param); return ParseVariableBool(value, param, &popt->topt.tuples_only);
else else
popt->topt.tuples_only = !popt->topt.tuples_only; popt->topt.tuples_only = !popt->topt.tuples_only;
} }
...@@ -2772,10 +2778,14 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) ...@@ -2772,10 +2778,14 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
popt->topt.pager = 2; popt->topt.pager = 2;
else if (value) else if (value)
{ {
if (ParseVariableBool(value, param)) bool on_off;
popt->topt.pager = 1;
else if (!ParseVariableBool(value, NULL, &on_off))
popt->topt.pager = 0; {
PsqlVarEnumError(param, value, "on, off, always");
return false;
}
popt->topt.pager = on_off ? 1 : 0;
} }
else if (popt->topt.pager == 1) else if (popt->topt.pager == 1)
popt->topt.pager = 0; popt->topt.pager = 0;
...@@ -2794,7 +2804,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) ...@@ -2794,7 +2804,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "footer") == 0) else if (strcmp(param, "footer") == 0)
{ {
if (value) if (value)
popt->topt.default_footer = ParseVariableBool(value, param); return ParseVariableBool(value, param, &popt->topt.default_footer);
else else
popt->topt.default_footer = !popt->topt.default_footer; popt->topt.default_footer = !popt->topt.default_footer;
} }
......
...@@ -841,7 +841,6 @@ StoreQueryTuple(const PGresult *result) ...@@ -841,7 +841,6 @@ StoreQueryTuple(const PGresult *result)
if (!SetVariable(pset.vars, varname, value)) if (!SetVariable(pset.vars, varname, value))
{ {
psql_error("could not set variable \"%s\"\n", varname);
free(varname); free(varname);
success = false; success = false;
break; break;
......
...@@ -541,7 +541,7 @@ finishInput(void) ...@@ -541,7 +541,7 @@ finishInput(void)
{ {
int hist_size; int hist_size;
hist_size = GetVariableNum(pset.vars, "HISTSIZE", 500, -1, true); hist_size = GetVariableNum(pset.vars, "HISTSIZE", 500, -1);
(void) saveHistory(psql_history, hist_size); (void) saveHistory(psql_history, hist_size);
free(psql_history); free(psql_history);
psql_history = NULL; psql_history = NULL;
......
...@@ -162,7 +162,7 @@ MainLoop(FILE *source) ...@@ -162,7 +162,7 @@ MainLoop(FILE *source)
/* This tries to mimic bash's IGNOREEOF feature. */ /* This tries to mimic bash's IGNOREEOF feature. */
count_eof++; count_eof++;
if (count_eof < GetVariableNum(pset.vars, "IGNOREEOF", 0, 10, false)) if (count_eof < GetVariableNum(pset.vars, "IGNOREEOF", 0, 10))
{ {
if (!pset.quiet) if (!pset.quiet)
printf(_("Use \"\\q\" to leave %s.\n"), pset.progname); printf(_("Use \"\\q\" to leave %s.\n"), pset.progname);
......
...@@ -588,12 +588,8 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts * options) ...@@ -588,12 +588,8 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts * options)
{ {
*equal_loc = '\0'; *equal_loc = '\0';
if (!SetVariable(pset.vars, value, equal_loc + 1)) if (!SetVariable(pset.vars, value, equal_loc + 1))
{
fprintf(stderr, _("%s: could not set variable \"%s\"\n"),
pset.progname, value);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
}
free(value); free(value);
break; break;
...@@ -786,43 +782,47 @@ showVersion(void) ...@@ -786,43 +782,47 @@ showVersion(void)
* This isn't an amazingly good place for them, but neither is anywhere else. * This isn't an amazingly good place for them, but neither is anywhere else.
*/ */
static void static bool
autocommit_hook(const char *newval) autocommit_hook(const char *newval)
{ {
pset.autocommit = ParseVariableBool(newval, "AUTOCOMMIT"); return ParseVariableBool(newval, "AUTOCOMMIT", &pset.autocommit);
} }
static void static bool
on_error_stop_hook(const char *newval) on_error_stop_hook(const char *newval)
{ {
pset.on_error_stop = ParseVariableBool(newval, "ON_ERROR_STOP"); return ParseVariableBool(newval, "ON_ERROR_STOP", &pset.on_error_stop);
} }
static void static bool
quiet_hook(const char *newval) quiet_hook(const char *newval)
{ {
pset.quiet = ParseVariableBool(newval, "QUIET"); return ParseVariableBool(newval, "QUIET", &pset.quiet);
} }
static void static bool
singleline_hook(const char *newval) singleline_hook(const char *newval)
{ {
pset.singleline = ParseVariableBool(newval, "SINGLELINE"); return ParseVariableBool(newval, "SINGLELINE", &pset.singleline);
} }
static void static bool
singlestep_hook(const char *newval) singlestep_hook(const char *newval)
{ {
pset.singlestep = ParseVariableBool(newval, "SINGLESTEP"); return ParseVariableBool(newval, "SINGLESTEP", &pset.singlestep);
} }
static void static bool
fetch_count_hook(const char *newval) fetch_count_hook(const char *newval)
{ {
pset.fetch_count = ParseVariableNum(newval, -1, -1, false); if (newval == NULL)
pset.fetch_count = -1; /* default value */
else if (!ParseVariableNum(newval, "FETCH_COUNT", &pset.fetch_count))
return false;
return true;
} }
static void static bool
echo_hook(const char *newval) echo_hook(const char *newval)
{ {
if (newval == NULL) if (newval == NULL)
...@@ -837,39 +837,57 @@ echo_hook(const char *newval) ...@@ -837,39 +837,57 @@ echo_hook(const char *newval)
pset.echo = PSQL_ECHO_NONE; pset.echo = PSQL_ECHO_NONE;
else else
{ {
psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n", PsqlVarEnumError("ECHO", newval, "none, errors, queries, all");
newval, "ECHO", "none"); return false;
pset.echo = PSQL_ECHO_NONE;
} }
return true;
} }
static void static bool
echo_hidden_hook(const char *newval) echo_hidden_hook(const char *newval)
{ {
if (newval == NULL) if (newval == NULL)
pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF; pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
else if (pg_strcasecmp(newval, "noexec") == 0) else if (pg_strcasecmp(newval, "noexec") == 0)
pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC; pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC;
else if (ParseVariableBool(newval, "ECHO_HIDDEN")) else
pset.echo_hidden = PSQL_ECHO_HIDDEN_ON; {
else /* ParseVariableBool printed msg if needed */ bool on_off;
pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
if (ParseVariableBool(newval, NULL, &on_off))
pset.echo_hidden = on_off ? PSQL_ECHO_HIDDEN_ON : PSQL_ECHO_HIDDEN_OFF;
else
{
PsqlVarEnumError("ECHO_HIDDEN", newval, "on, off, noexec");
return false;
}
}
return true;
} }
static void static bool
on_error_rollback_hook(const char *newval) on_error_rollback_hook(const char *newval)
{ {
if (newval == NULL) if (newval == NULL)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF; pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
else if (pg_strcasecmp(newval, "interactive") == 0) else if (pg_strcasecmp(newval, "interactive") == 0)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_INTERACTIVE; pset.on_error_rollback = PSQL_ERROR_ROLLBACK_INTERACTIVE;
else if (ParseVariableBool(newval, "ON_ERROR_ROLLBACK")) else
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_ON; {
else /* ParseVariableBool printed msg if needed */ bool on_off;
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
if (ParseVariableBool(newval, NULL, &on_off))
pset.on_error_rollback = on_off ? PSQL_ERROR_ROLLBACK_ON : PSQL_ERROR_ROLLBACK_OFF;
else
{
PsqlVarEnumError("ON_ERROR_ROLLBACK", newval, "on, off, interactive");
return false;
}
}
return true;
} }
static void static bool
comp_keyword_case_hook(const char *newval) comp_keyword_case_hook(const char *newval)
{ {
if (newval == NULL) if (newval == NULL)
...@@ -884,13 +902,14 @@ comp_keyword_case_hook(const char *newval) ...@@ -884,13 +902,14 @@ comp_keyword_case_hook(const char *newval)
pset.comp_case = PSQL_COMP_CASE_LOWER; pset.comp_case = PSQL_COMP_CASE_LOWER;
else else
{ {
psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n", PsqlVarEnumError("COMP_KEYWORD_CASE", newval,
newval, "COMP_KEYWORD_CASE", "preserve-upper"); "lower, upper, preserve-lower, preserve-upper");
pset.comp_case = PSQL_COMP_CASE_PRESERVE_UPPER; return false;
} }
return true;
} }
static void static bool
histcontrol_hook(const char *newval) histcontrol_hook(const char *newval)
{ {
if (newval == NULL) if (newval == NULL)
...@@ -905,31 +924,35 @@ histcontrol_hook(const char *newval) ...@@ -905,31 +924,35 @@ histcontrol_hook(const char *newval)
pset.histcontrol = hctl_none; pset.histcontrol = hctl_none;
else else
{ {
psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n", PsqlVarEnumError("HISTCONTROL", newval,
newval, "HISTCONTROL", "none"); "none, ignorespace, ignoredups, ignoreboth");
pset.histcontrol = hctl_none; return false;
} }
return true;
} }
static void static bool
prompt1_hook(const char *newval) prompt1_hook(const char *newval)
{ {
pset.prompt1 = newval ? newval : ""; pset.prompt1 = newval ? newval : "";
return true;
} }
static void static bool
prompt2_hook(const char *newval) prompt2_hook(const char *newval)
{ {
pset.prompt2 = newval ? newval : ""; pset.prompt2 = newval ? newval : "";
return true;
} }
static void static bool
prompt3_hook(const char *newval) prompt3_hook(const char *newval)
{ {
pset.prompt3 = newval ? newval : ""; pset.prompt3 = newval ? newval : "";
return true;
} }
static void static bool
verbosity_hook(const char *newval) verbosity_hook(const char *newval)
{ {
if (newval == NULL) if (newval == NULL)
...@@ -942,16 +965,16 @@ verbosity_hook(const char *newval) ...@@ -942,16 +965,16 @@ verbosity_hook(const char *newval)
pset.verbosity = PQERRORS_VERBOSE; pset.verbosity = PQERRORS_VERBOSE;
else else
{ {
psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n", PsqlVarEnumError("VERBOSITY", newval, "default, terse, verbose");
newval, "VERBOSITY", "default"); return false;
pset.verbosity = PQERRORS_DEFAULT;
} }
if (pset.db) if (pset.db)
PQsetErrorVerbosity(pset.db, pset.verbosity); PQsetErrorVerbosity(pset.db, pset.verbosity);
return true;
} }
static void static bool
show_context_hook(const char *newval) show_context_hook(const char *newval)
{ {
if (newval == NULL) if (newval == NULL)
...@@ -964,13 +987,13 @@ show_context_hook(const char *newval) ...@@ -964,13 +987,13 @@ show_context_hook(const char *newval)
pset.show_context = PQSHOW_CONTEXT_ALWAYS; pset.show_context = PQSHOW_CONTEXT_ALWAYS;
else else
{ {
psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n", PsqlVarEnumError("SHOW_CONTEXT", newval, "never, errors, always");
newval, "SHOW_CONTEXT", "errors"); return false;
pset.show_context = PQSHOW_CONTEXT_ERRORS;
} }
if (pset.db) if (pset.db)
PQsetErrorContextVisibility(pset.db, pset.show_context); PQsetErrorContextVisibility(pset.db, pset.show_context);
return true;
} }
......
...@@ -58,6 +58,11 @@ CreateVariableSpace(void) ...@@ -58,6 +58,11 @@ CreateVariableSpace(void)
return ptr; return ptr;
} }
/*
* Get string value of variable, or NULL if it's not defined.
*
* Note: result is valid until variable is next assigned to.
*/
const char * const char *
GetVariable(VariableSpace space, const char *name) GetVariable(VariableSpace space, const char *name)
{ {
...@@ -79,94 +84,121 @@ GetVariable(VariableSpace space, const char *name) ...@@ -79,94 +84,121 @@ GetVariable(VariableSpace space, const char *name)
} }
/* /*
* Try to interpret "value" as boolean value. * Try to interpret "value" as a boolean value, and if successful,
* store it in *result. Otherwise don't clobber *result.
* *
* Valid values are: true, false, yes, no, on, off, 1, 0; as well as unique * Valid values are: true, false, yes, no, on, off, 1, 0; as well as unique
* prefixes thereof. * prefixes thereof.
* *
* "name" is the name of the variable we're assigning to, to use in error * "name" is the name of the variable we're assigning to, to use in error
* report if any. Pass name == NULL to suppress the error report. * report if any. Pass name == NULL to suppress the error report.
*
* Return true when "value" is syntactically valid, false otherwise.
*/ */
bool bool
ParseVariableBool(const char *value, const char *name) ParseVariableBool(const char *value, const char *name, bool *result)
{ {
size_t len; size_t len;
bool valid = true;
if (value == NULL) if (value == NULL)
return false; /* not set -> assume "off" */ {
*result = false; /* not set -> assume "off" */
return valid;
}
len = strlen(value); len = strlen(value);
if (pg_strncasecmp(value, "true", len) == 0) if (len > 0 && pg_strncasecmp(value, "true", len) == 0)
return true; *result = true;
else if (pg_strncasecmp(value, "false", len) == 0) else if (len > 0 && pg_strncasecmp(value, "false", len) == 0)
return false; *result = false;
else if (pg_strncasecmp(value, "yes", len) == 0) else if (len > 0 && pg_strncasecmp(value, "yes", len) == 0)
return true; *result = true;
else if (pg_strncasecmp(value, "no", len) == 0) else if (len > 0 && pg_strncasecmp(value, "no", len) == 0)
return false; *result = false;
/* 'o' is not unique enough */ /* 'o' is not unique enough */
else if (pg_strncasecmp(value, "on", (len > 2 ? len : 2)) == 0) else if (pg_strncasecmp(value, "on", (len > 2 ? len : 2)) == 0)
return true; *result = true;
else if (pg_strncasecmp(value, "off", (len > 2 ? len : 2)) == 0) else if (pg_strncasecmp(value, "off", (len > 2 ? len : 2)) == 0)
return false; *result = false;
else if (pg_strcasecmp(value, "1") == 0) else if (pg_strcasecmp(value, "1") == 0)
return true; *result = true;
else if (pg_strcasecmp(value, "0") == 0) else if (pg_strcasecmp(value, "0") == 0)
return false; *result = false;
else else
{ {
/* NULL is treated as false, so a non-matching value is 'true' */ /* string is not recognized; don't clobber *result */
if (name) if (name)
psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n", psql_error("unrecognized value \"%s\" for \"%s\": boolean expected\n",
value, name, "on"); value, name);
return true; valid = false;
} }
return valid;
} }
/* /*
* Read numeric variable, or defaultval if it is not set, or faultval if its * Try to interpret "value" as an integer value, and if successful,
* value is not a valid numeric string. If allowtrail is false, this will * store it in *result. Otherwise don't clobber *result.
* include the case where there are trailing characters after the number. *
* "name" is the name of the variable we're assigning to, to use in error
* report if any. Pass name == NULL to suppress the error report.
*
* Return true when "value" is syntactically valid, false otherwise.
*/ */
int bool
ParseVariableNum(const char *val, ParseVariableNum(const char *value, const char *name, int *result)
int defaultval,
int faultval,
bool allowtrail)
{ {
int result; char *end;
long numval;
if (!val) if (value == NULL)
result = defaultval; return false;
else if (!val[0]) errno = 0;
result = faultval; numval = strtol(value, &end, 0);
if (errno == 0 && *end == '\0' && end != value && numval == (int) numval)
{
*result = (int) numval;
return true;
}
else else
{ {
char *end; /* string is not recognized; don't clobber *result */
if (name)
result = strtol(val, &end, 0); psql_error("invalid value \"%s\" for \"%s\": integer expected\n",
if (!allowtrail && *end) value, name);
result = faultval; return false;
} }
return result;
} }
/*
* Read integer value of the numeric variable named "name".
*
* Return defaultval if it is not set, or faultval if its value is not a
* valid integer. (No error message is issued.)
*/
int int
GetVariableNum(VariableSpace space, GetVariableNum(VariableSpace space,
const char *name, const char *name,
int defaultval, int defaultval,
int faultval, int faultval)
bool allowtrail)
{ {
const char *val; const char *val;
int result;
val = GetVariable(space, name); val = GetVariable(space, name);
return ParseVariableNum(val, defaultval, faultval, allowtrail); if (!val)
return defaultval;
if (ParseVariableNum(val, NULL, &result))
return result;
else
return faultval;
} }
/*
* Print values of all variables.
*/
void void
PrintVariables(VariableSpace space) PrintVariables(VariableSpace space)
{ {
...@@ -184,17 +216,28 @@ PrintVariables(VariableSpace space) ...@@ -184,17 +216,28 @@ PrintVariables(VariableSpace space)
} }
} }
/*
* Set the variable named "name" to value "value",
* or delete it if "value" is NULL.
*
* Returns true if successful, false if not; in the latter case a suitable
* error message has been printed, except for the unexpected case of
* space or name being NULL.
*/
bool bool
SetVariable(VariableSpace space, const char *name, const char *value) SetVariable(VariableSpace space, const char *name, const char *value)
{ {
struct _variable *current, struct _variable *current,
*previous; *previous;
if (!space) if (!space || !name)
return false; return false;
if (!valid_variable_name(name)) if (!valid_variable_name(name))
{
psql_error("invalid variable name: \"%s\"\n", name);
return false; return false;
}
if (!value) if (!value)
return DeleteVariable(space, name); return DeleteVariable(space, name);
...@@ -205,13 +248,30 @@ SetVariable(VariableSpace space, const char *name, const char *value) ...@@ -205,13 +248,30 @@ SetVariable(VariableSpace space, const char *name, const char *value)
{ {
if (strcmp(current->name, name) == 0) if (strcmp(current->name, name) == 0)
{ {
/* found entry, so update */ /*
if (current->value) * Found entry, so update, unless hook returns false. The hook
free(current->value); * may need the passed value to have the same lifespan as the
current->value = pg_strdup(value); * variable, so allocate it right away, even though we'll have to
* free it again if the hook returns false.
*/
char *new_value = pg_strdup(value);
bool confirmed;
if (current->assign_hook) if (current->assign_hook)
(*current->assign_hook) (current->value); confirmed = (*current->assign_hook) (new_value);
return true; else
confirmed = true;
if (confirmed)
{
if (current->value)
pg_free(current->value);
current->value = new_value;
}
else
pg_free(new_value); /* current->value is left unchanged */
return confirmed;
} }
} }
...@@ -226,19 +286,29 @@ SetVariable(VariableSpace space, const char *name, const char *value) ...@@ -226,19 +286,29 @@ SetVariable(VariableSpace space, const char *name, const char *value)
} }
/* /*
* This both sets a hook function, and calls it on the current value (if any) * Attach an assign hook function to the named variable.
*
* If the variable doesn't already exist, create it with value NULL,
* just so we have a place to store the hook function. (Externally,
* this isn't different from it not being defined.)
*
* The hook is immediately called on the variable's current value. This is
* meant to let it update any derived psql state. If the hook doesn't like
* the current value, it will print a message to that effect, but we'll ignore
* it. Generally we do not expect any such failure here, because this should
* get called before any user-supplied value is assigned.
*/ */
bool void
SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook hook) SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook hook)
{ {
struct _variable *current, struct _variable *current,
*previous; *previous;
if (!space) if (!space || !name)
return false; return;
if (!valid_variable_name(name)) if (!valid_variable_name(name))
return false; return;
for (previous = space, current = space->next; for (previous = space, current = space->next;
current; current;
...@@ -248,8 +318,8 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook ...@@ -248,8 +318,8 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
{ {
/* found entry, so update */ /* found entry, so update */
current->assign_hook = hook; current->assign_hook = hook;
(*hook) (current->value); (void) (*hook) (current->value);
return true; return;
} }
} }
...@@ -260,16 +330,24 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook ...@@ -260,16 +330,24 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
current->assign_hook = hook; current->assign_hook = hook;
current->next = NULL; current->next = NULL;
previous->next = current; previous->next = current;
(*hook) (NULL); (void) (*hook) (NULL);
return true;
} }
/*
* Convenience function to set a variable's value to "on".
*/
bool bool
SetVariableBool(VariableSpace space, const char *name) SetVariableBool(VariableSpace space, const char *name)
{ {
return SetVariable(space, name, "on"); return SetVariable(space, name, "on");
} }
/*
* Attempt to delete variable.
*
* If unsuccessful, print a message and return "false".
* Deleting a nonexistent variable is not an error.
*/
bool bool
DeleteVariable(VariableSpace space, const char *name) DeleteVariable(VariableSpace space, const char *name)
{ {
...@@ -277,7 +355,7 @@ DeleteVariable(VariableSpace space, const char *name) ...@@ -277,7 +355,7 @@ DeleteVariable(VariableSpace space, const char *name)
*previous; *previous;
if (!space) if (!space)
return false; return true;
for (previous = space, current = space->next; for (previous = space, current = space->next;
current; current;
...@@ -285,14 +363,21 @@ DeleteVariable(VariableSpace space, const char *name) ...@@ -285,14 +363,21 @@ DeleteVariable(VariableSpace space, const char *name)
{ {
if (strcmp(current->name, name) == 0) if (strcmp(current->name, name) == 0)
{ {
if (current->assign_hook)
{
/* Allow deletion only if hook is okay with NULL value */
if (!(*current->assign_hook) (NULL))
return false; /* message printed by hook */
if (current->value) if (current->value)
free(current->value); free(current->value);
current->value = NULL; current->value = NULL;
/* Physically delete only if no hook function to remember */ /* Don't delete entry, or we'd forget the hook function */
if (current->assign_hook) }
(*current->assign_hook) (NULL);
else else
{ {
/* We can delete the entry as well as its value */
if (current->value)
free(current->value);
previous->next = current->next; previous->next = current->next;
free(current->name); free(current->name);
free(current); free(current);
...@@ -303,3 +388,16 @@ DeleteVariable(VariableSpace space, const char *name) ...@@ -303,3 +388,16 @@ DeleteVariable(VariableSpace space, const char *name)
return true; return true;
} }
/*
* Emit error with suggestions for variables or commands
* accepting enum-style arguments.
* This function just exists to standardize the wording.
* suggestions should follow the format "fee, fi, fo, fum".
*/
void
PsqlVarEnumError(const char *name, const char *value, const char *suggestions)
{
psql_error("unrecognized value \"%s\" for \"%s\"\nAvailable values are: %s.\n",
value, name, suggestions);
}
...@@ -3,25 +3,39 @@ ...@@ -3,25 +3,39 @@
* *
* Copyright (c) 2000-2017, PostgreSQL Global Development Group * Copyright (c) 2000-2017, PostgreSQL Global Development Group
* *
* This implements a sort of variable repository. One could also think of it
* as a cheap version of an associative array. Each variable has a string
* name and a string value. The value can't be NULL, or more precisely
* that's not distinguishable from the variable being unset.
*
* src/bin/psql/variables.h * src/bin/psql/variables.h
*/ */
#ifndef VARIABLES_H #ifndef VARIABLES_H
#define VARIABLES_H #define VARIABLES_H
/* /*
* This implements a sort of variable repository. One could also think of it * Variables can be given "assign hook" functions. The assign hook can
* as a cheap version of an associative array. In each one of these * prevent invalid values from being assigned, and can update internal C
* datastructures you can store name/value pairs. There can also be an * variables to keep them in sync with the variable's current value.
* "assign hook" function that is called whenever the variable's value is *
* changed. * A hook function is called before any attempted assignment, with the
* proposed new value of the variable (or with NULL, if an \unset is being
* attempted). If it returns false, the assignment doesn't occur --- it
* should print an error message with psql_error() to tell the user why.
* *
* An "unset" operation causes the hook to be called with newval == NULL. * When a hook function is installed with SetVariableAssignHook(), it is
* called with the variable's current value (or with NULL, if it wasn't set
* yet). But its return value is ignored in this case. The hook should be
* set before any possibly-invalid value can be assigned.
*/
typedef bool (*VariableAssignHook) (const char *newval);
/*
* Data structure representing one variable.
* *
* Note: if value == NULL then the variable is logically unset, but we are * Note: if value == NULL then the variable is logically unset, but we are
* keeping the struct around so as not to forget about its hook function. * keeping the struct around so as not to forget about its hook function.
*/ */
typedef void (*VariableAssignHook) (const char *newval);
struct _variable struct _variable
{ {
char *name; char *name;
...@@ -30,27 +44,31 @@ struct _variable ...@@ -30,27 +44,31 @@ struct _variable
struct _variable *next; struct _variable *next;
}; };
/* Data structure representing a set of variables */
typedef struct _variable *VariableSpace; typedef struct _variable *VariableSpace;
VariableSpace CreateVariableSpace(void); VariableSpace CreateVariableSpace(void);
const char *GetVariable(VariableSpace space, const char *name); const char *GetVariable(VariableSpace space, const char *name);
bool ParseVariableBool(const char *value, const char *name); bool ParseVariableBool(const char *value, const char *name,
int ParseVariableNum(const char *val, bool *result);
int defaultval,
int faultval, bool ParseVariableNum(const char *value, const char *name,
bool allowtrail); int *result);
int GetVariableNum(VariableSpace space, int GetVariableNum(VariableSpace space,
const char *name, const char *name,
int defaultval, int defaultval,
int faultval, int faultval);
bool allowtrail);
void PrintVariables(VariableSpace space); void PrintVariables(VariableSpace space);
bool SetVariable(VariableSpace space, const char *name, const char *value); bool SetVariable(VariableSpace space, const char *name, const char *value);
bool SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook hook); void SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook hook);
bool SetVariableBool(VariableSpace space, const char *name); bool SetVariableBool(VariableSpace space, const char *name);
bool DeleteVariable(VariableSpace space, const char *name); bool DeleteVariable(VariableSpace space, const char *name);
void PsqlVarEnumError(const char *name, const char *value, const char *suggestions);
#endif /* VARIABLES_H */ #endif /* VARIABLES_H */
...@@ -2,6 +2,15 @@ ...@@ -2,6 +2,15 @@
-- Tests for psql features that aren't closely connected to any -- Tests for psql features that aren't closely connected to any
-- specific server features -- specific server features
-- --
-- \set
-- fail: invalid name
\set invalid/name foo
invalid variable name: "invalid/name"
-- fail: invalid value for special variable
\set AUTOCOMMIT foo
unrecognized value "foo" for "AUTOCOMMIT": boolean expected
\set FETCH_COUNT foo
invalid value "foo" for "FETCH_COUNT": integer expected
-- \gset -- \gset
select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_ select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_
\echo :pref01_test01 :pref01_test02 :pref01_test03 \echo :pref01_test01 :pref01_test02 :pref01_test03
...@@ -9,7 +18,7 @@ select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_ ...@@ -9,7 +18,7 @@ select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_
-- should fail: bad variable name -- should fail: bad variable name
select 10 as "bad name" select 10 as "bad name"
\gset \gset
could not set variable "bad name" invalid variable name: "bad name"
-- multiple backslash commands in one line -- multiple backslash commands in one line
select 1 as x, 2 as y \gset pref01_ \\ \echo :pref01_x select 1 as x, 2 as y \gset pref01_ \\ \echo :pref01_x
1 1
......
...@@ -3,6 +3,14 @@ ...@@ -3,6 +3,14 @@
-- specific server features -- specific server features
-- --
-- \set
-- fail: invalid name
\set invalid/name foo
-- fail: invalid value for special variable
\set AUTOCOMMIT foo
\set FETCH_COUNT foo
-- \gset -- \gset
select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_ select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment