Support \if ... \elif ... \else ... \endif in psql scripting.

This patch adds nestable conditional blocks to psql.  The control
structure feature per se is complete, but the boolean expressions
understood by \if and \elif are pretty primitive; basically, after
variable substitution and backtick expansion, the result has to be
"true" or "false" or one of the other standard spellings of a boolean
value.  But that's enough for many purposes, since you can always
do the heavy lifting on the server side; and we can extend it later.

Along the way, pay down some of the technical debt that had built up
around psql/command.c:
* Refactor exec_command() into a function per command, instead of
being a 1500-line monstrosity.  This makes the file noticeably longer
because of repetitive function header/trailer overhead, but it seems
much more readable.
* Teach psql_get_variable() and psqlscanslash.l to suppress variable
substitution and backtick expansion on the basis of the conditional
stack state, thereby allowing removal of the OT_NO_EVAL kluge.
* Fix the no-doubt-once-expedient hack of sometimes silently substituting
mainloop.c's previous_buf for query_buf when calling HandleSlashCmds.
(It's a bit remarkable that commands like \r worked at all with that.)
Recall of a previous query is now done explicitly in the slash commands
where that should happen.

Corey Huinker, reviewed by Fabien Coelho, further hacking by me

Discussion: https://postgr.es/m/CADkLM=c94OSRTnat=LX0ivNq4pxDNeoomFfYvBKM5N_xfmLtAA@mail.gmail.com
This commit is contained in:
Tom Lane 2017-03-30 12:59:11 -04:00
parent ffae6733db
commit e984ef5861
17 changed files with 2181 additions and 279 deletions

View File

@ -2063,6 +2063,95 @@ hello 10
</varlistentry>
<varlistentry>
<term><literal>\if</literal> <replaceable class="parameter">expression</replaceable></term>
<term><literal>\elif</literal> <replaceable class="parameter">expression</replaceable></term>
<term><literal>\else</literal></term>
<term><literal>\endif</literal></term>
<listitem>
<para>
This group of commands implements nestable conditional blocks.
A conditional block must begin with an <command>\if</command> and end
with an <command>\endif</command>. In between there may be any number
of <command>\elif</command> clauses, which may optionally be followed
by a single <command>\else</command> clause. Ordinary queries and
other types of backslash commands may (and usually do) appear between
the commands forming a conditional block.
</para>
<para>
The <command>\if</command> and <command>\elif</command> commands read
their argument(s) and evaluate them as a boolean expression. If the
expression yields <literal>true</literal> then processing continues
normally; otherwise, lines are skipped until a
matching <command>\elif</command>, <command>\else</command>,
or <command>\endif</command> is reached. Once
an <command>\if</command> or <command>\elif</command> test has
succeeded, the arguments of later <command>\elif</command> commands in
the same block are not evaluated but are treated as false. Lines
following an <command>\else</command> are processed only if no earlier
matching <command>\if</command> or <command>\elif</command> succeeded.
</para>
<para>
The <replaceable class="parameter">expression</replaceable> argument
of an <command>\if</command> or <command>\elif</command> command
is subject to variable interpolation and backquote expansion, just
like any other backslash command argument. After that it is evaluated
like the value of an on/off option variable. So a valid value
is any unambiguous case-insensitive match for one of:
<literal>true</literal>, <literal>false</literal>, <literal>1</literal>,
<literal>0</literal>, <literal>on</literal>, <literal>off</literal>,
<literal>yes</literal>, <literal>no</literal>. For example,
<literal>t</literal>, <literal>T</literal>, and <literal>tR</literal>
will all be considered to be <literal>true</literal>.
</para>
<para>
Expressions that do not properly evaluate to true or false will
generate a warning and be treated as false.
</para>
<para>
Lines being skipped are parsed normally to identify queries and
backslash commands, but queries are not sent to the server, and
backslash commands other than conditionals
(<command>\if</command>, <command>\elif</command>,
<command>\else</command>, <command>\endif</command>) are
ignored. Conditional commands are checked only for valid nesting.
Variable references in skipped lines are not expanded, and backquote
expansion is not performed either.
</para>
<para>
All the backslash commands of a given conditional block must appear in
the same source file. If EOF is reached on the main input file or an
<command>\include</command>-ed file before all local
<command>\if</command>-blocks have been closed,
then <application>psql</> will raise an error.
</para>
<para>
Here is an example:
</para>
<programlisting>
-- check for the existence of two separate records in the database and store
-- the results in separate psql variables
SELECT
EXISTS(SELECT 1 FROM customer WHERE customer_id = 123) as is_customer,
EXISTS(SELECT 1 FROM employee WHERE employee_id = 456) as is_employee
\gset
\if :is_customer
SELECT * FROM customer WHERE customer_id = 123;
\elif :is_employee
\echo 'is not a customer but is an employee'
SELECT * FROM employee WHERE employee_id = 456;
\else
\if yes
\echo 'not a customer or employee'
\else
\echo 'this will never print'
\endif
\endif
</programlisting>
</listitem>
</varlistentry>
<varlistentry>
<term><literal>\l[+]</literal> or <literal>\list[+] [ <link linkend="APP-PSQL-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
<listitem>
@ -3715,7 +3804,8 @@ testdb=&gt; <userinput>INSERT INTO my_table VALUES (:'content');</userinput>
<listitem>
<para>
In prompt 1 normally <literal>=</literal>,
but <literal>^</literal> if in single-line mode,
but <literal>@</literal> if the session is in an inactive branch of a
conditional block, or <literal>^</literal> if in single-line mode,
or <literal>!</literal> if the session is disconnected from the
database (which can happen if <command>\connect</command> fails).
In prompt 2 <literal>%R</literal> is replaced by a character that

View File

@ -21,10 +21,10 @@ REFDOCDIR= $(top_srcdir)/doc/src/sgml/ref
override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) $(CPPFLAGS)
LDFLAGS += -L$(top_builddir)/src/fe_utils -lpgfeutils -lpq
OBJS= command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
startup.o prompt.o variables.o large_obj.o describe.o \
crosstabview.o tab-complete.o \
sql_help.o psqlscanslash.o \
OBJS= command.o common.o conditional.o copy.o crosstabview.o \
describe.o help.o input.o large_obj.o mainloop.o \
prompt.o psqlscanslash.o sql_help.o startup.o stringutils.o \
tab-complete.o variables.o \
$(WIN32RES)

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
#include "fe_utils/print.h"
#include "fe_utils/psqlscan.h"
#include "conditional.h"
typedef enum _backslashResult
@ -25,7 +26,9 @@ typedef enum _backslashResult
extern backslashResult HandleSlashCmds(PsqlScanState scan_state,
PQExpBuffer query_buf);
ConditionalStack cstack,
PQExpBuffer query_buf,
PQExpBuffer previous_buf);
extern int process_file(char *filename, bool use_relative_path);

View File

@ -121,7 +121,8 @@ setQFout(const char *fname)
* (Failure in escaping should lead to returning NULL.)
*
* "passthrough" is the pointer previously given to psql_scan_set_passthrough.
* psql currently doesn't use this.
* In psql, passthrough points to a ConditionalStack, which we check to
* determine whether variable expansion is allowed.
*/
char *
psql_get_variable(const char *varname, bool escape, bool as_ident,
@ -130,6 +131,10 @@ psql_get_variable(const char *varname, bool escape, bool as_ident,
char *result;
const char *value;
/* In an inactive \if branch, suppress all variable substitutions */
if (passthrough && !conditional_active((ConditionalStack) passthrough))
return NULL;
value = GetVariable(pset.vars, varname);
if (!value)
return NULL;

153
src/bin/psql/conditional.c Normal file
View File

@ -0,0 +1,153 @@
/*
* psql - the PostgreSQL interactive terminal
*
* Copyright (c) 2000-2017, PostgreSQL Global Development Group
*
* src/bin/psql/conditional.c
*/
#include "postgres_fe.h"
#include "conditional.h"
/*
* create stack
*/
ConditionalStack
conditional_stack_create(void)
{
ConditionalStack cstack = pg_malloc(sizeof(ConditionalStackData));
cstack->head = NULL;
return cstack;
}
/*
* destroy stack
*/
void
conditional_stack_destroy(ConditionalStack cstack)
{
while (conditional_stack_pop(cstack))
continue;
free(cstack);
}
/*
* Create a new conditional branch.
*/
void
conditional_stack_push(ConditionalStack cstack, ifState new_state)
{
IfStackElem *p = (IfStackElem *) pg_malloc(sizeof(IfStackElem));
p->if_state = new_state;
p->query_len = -1;
p->paren_depth = -1;
p->next = cstack->head;
cstack->head = p;
}
/*
* Destroy the topmost conditional branch.
* Returns false if there was no branch to end.
*/
bool
conditional_stack_pop(ConditionalStack cstack)
{
IfStackElem *p = cstack->head;
if (!p)
return false;
cstack->head = cstack->head->next;
free(p);
return true;
}
/*
* Fetch the current state of the top of the stack.
*/
ifState
conditional_stack_peek(ConditionalStack cstack)
{
if (conditional_stack_empty(cstack))
return IFSTATE_NONE;
return cstack->head->if_state;
}
/*
* Change the state of the topmost branch.
* Returns false if there was no branch state to set.
*/
bool
conditional_stack_poke(ConditionalStack cstack, ifState new_state)
{
if (conditional_stack_empty(cstack))
return false;
cstack->head->if_state = new_state;
return true;
}
/*
* True if there are no active \if-blocks.
*/
bool
conditional_stack_empty(ConditionalStack cstack)
{
return cstack->head == NULL;
}
/*
* True if we should execute commands normally; that is, the current
* conditional branch is active, or there is no open \if block.
*/
bool
conditional_active(ConditionalStack cstack)
{
ifState s = conditional_stack_peek(cstack);
return s == IFSTATE_NONE || s == IFSTATE_TRUE || s == IFSTATE_ELSE_TRUE;
}
/*
* Save current query buffer length in topmost stack entry.
*/
void
conditional_stack_set_query_len(ConditionalStack cstack, int len)
{
Assert(!conditional_stack_empty(cstack));
cstack->head->query_len = len;
}
/*
* Fetch last-recorded query buffer length from topmost stack entry.
* Will return -1 if no stack or it was never saved.
*/
int
conditional_stack_get_query_len(ConditionalStack cstack)
{
if (conditional_stack_empty(cstack))
return -1;
return cstack->head->query_len;
}
/*
* Save current parenthesis nesting depth in topmost stack entry.
*/
void
conditional_stack_set_paren_depth(ConditionalStack cstack, int depth)
{
Assert(!conditional_stack_empty(cstack));
cstack->head->paren_depth = depth;
}
/*
* Fetch last-recorded parenthesis nesting depth from topmost stack entry.
* Will return -1 if no stack or it was never saved.
*/
int
conditional_stack_get_paren_depth(ConditionalStack cstack)
{
if (conditional_stack_empty(cstack))
return -1;
return cstack->head->paren_depth;
}

View File

@ -0,0 +1,83 @@
/*
* psql - the PostgreSQL interactive terminal
*
* Copyright (c) 2000-2017, PostgreSQL Global Development Group
*
* src/bin/psql/conditional.h
*/
#ifndef CONDITIONAL_H
#define CONDITIONAL_H
/*
* Possible states of a single level of \if block.
*/
typedef enum ifState
{
IFSTATE_NONE = 0, /* not currently in an \if block */
IFSTATE_TRUE, /* currently in an \if or \elif that is true
* and all parent branches (if any) are true */
IFSTATE_FALSE, /* currently in an \if or \elif that is false
* but no true branch has yet been seen, and
* all parent branches (if any) are true */
IFSTATE_IGNORED, /* currently in an \elif that follows a true
* branch, or the whole \if is a child of a
* false parent branch */
IFSTATE_ELSE_TRUE, /* currently in an \else that is true and all
* parent branches (if any) are true */
IFSTATE_ELSE_FALSE /* currently in an \else that is false or
* ignored */
} ifState;
/*
* The state of nested \ifs is stored in a stack.
*
* query_len is used to determine what accumulated text to throw away at the
* end of an inactive branch. (We could, perhaps, teach the lexer to not add
* stuff to the query buffer in the first place when inside an inactive branch;
* but that would be very invasive.) We also need to save and restore the
* lexer's parenthesis nesting depth when throwing away text. (We don't need
* to save and restore any of its other state, such as comment nesting depth,
* because a backslash command could never appear inside a comment or SQL
* literal.)
*/
typedef struct IfStackElem
{
ifState if_state; /* current state, see enum above */
int query_len; /* length of query_buf at last branch start */
int paren_depth; /* parenthesis depth at last branch start */
struct IfStackElem *next; /* next surrounding \if, if any */
} IfStackElem;
typedef struct ConditionalStackData
{
IfStackElem *head;
} ConditionalStackData;
typedef struct ConditionalStackData *ConditionalStack;
extern ConditionalStack conditional_stack_create(void);
extern void conditional_stack_destroy(ConditionalStack cstack);
extern void conditional_stack_push(ConditionalStack cstack, ifState new_state);
extern bool conditional_stack_pop(ConditionalStack cstack);
extern ifState conditional_stack_peek(ConditionalStack cstack);
extern bool conditional_stack_poke(ConditionalStack cstack, ifState new_state);
extern bool conditional_stack_empty(ConditionalStack cstack);
extern bool conditional_active(ConditionalStack cstack);
extern void conditional_stack_set_query_len(ConditionalStack cstack, int len);
extern int conditional_stack_get_query_len(ConditionalStack cstack);
extern void conditional_stack_set_paren_depth(ConditionalStack cstack, int depth);
extern int conditional_stack_get_paren_depth(ConditionalStack cstack);
#endif /* CONDITIONAL_H */

View File

@ -552,7 +552,7 @@ handleCopyIn(PGconn *conn, FILE *copystream, bool isbinary, PGresult **res)
/* interactive input probably silly, but give one prompt anyway */
if (showprompt)
{
const char *prompt = get_prompt(PROMPT_COPY);
const char *prompt = get_prompt(PROMPT_COPY, NULL);
fputs(prompt, stdout);
fflush(stdout);
@ -590,7 +590,7 @@ handleCopyIn(PGconn *conn, FILE *copystream, bool isbinary, PGresult **res)
if (showprompt)
{
const char *prompt = get_prompt(PROMPT_COPY);
const char *prompt = get_prompt(PROMPT_COPY, NULL);
fputs(prompt, stdout);
fflush(stdout);

View File

@ -167,7 +167,7 @@ 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(113, pager ? &(pset.popt.topt) : NULL);
output = PageOutput(122, pager ? &(pset.popt.topt) : NULL);
fprintf(output, _("General\n"));
fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n"));
@ -210,6 +210,13 @@ slashUsage(unsigned short int pager)
fprintf(output, _(" \\qecho [STRING] write string to query output stream (see \\o)\n"));
fprintf(output, "\n");
fprintf(output, _("Conditional\n"));
fprintf(output, _(" \\if EXPR begin conditional block\n"));
fprintf(output, _(" \\elif EXPR alternative within current conditional block\n"));
fprintf(output, _(" \\else final alternative within current conditional block\n"));
fprintf(output, _(" \\endif end conditional block\n"));
fprintf(output, "\n");
fprintf(output, _("Informational\n"));
fprintf(output, _(" (options: S = show system objects, + = additional detail)\n"));
fprintf(output, _(" \\d[S+] list tables, views, and sequences\n"));

View File

@ -35,6 +35,7 @@ int
MainLoop(FILE *source)
{
PsqlScanState scan_state; /* lexer working state */
ConditionalStack cond_stack; /* \if status stack */
volatile PQExpBuffer query_buf; /* buffer for query being accumulated */
volatile PQExpBuffer previous_buf; /* if there isn't anything in the new
* buffer yet, use this one for \e,
@ -50,16 +51,15 @@ MainLoop(FILE *source)
volatile promptStatus_t prompt_status = PROMPT_READY;
volatile int count_eof = 0;
volatile bool die_on_error = false;
/* Save the prior command source */
FILE *prev_cmd_source;
bool prev_cmd_interactive;
uint64 prev_lineno;
/* Save old settings */
/* Save the prior command source */
prev_cmd_source = pset.cur_cmd_source;
prev_cmd_interactive = pset.cur_cmd_interactive;
prev_lineno = pset.lineno;
/* pset.stmt_lineno does not need to be saved and restored */
/* Establish new source */
pset.cur_cmd_source = source;
@ -69,6 +69,8 @@ MainLoop(FILE *source)
/* Create working state */
scan_state = psql_scan_create(&psqlscan_callbacks);
cond_stack = conditional_stack_create();
psql_scan_set_passthrough(scan_state, (void *) cond_stack);
query_buf = createPQExpBuffer();
previous_buf = createPQExpBuffer();
@ -122,7 +124,19 @@ MainLoop(FILE *source)
cancel_pressed = false;
if (pset.cur_cmd_interactive)
{
putc('\n', stdout);
/*
* if interactive user is in an \if block, then Ctrl-C will
* exit from the innermost \if.
*/
if (!conditional_stack_empty(cond_stack))
{
psql_error("\\if: escaped\n");
conditional_stack_pop(cond_stack);
}
}
else
{
successResult = EXIT_USER;
@ -140,7 +154,8 @@ MainLoop(FILE *source)
/* May need to reset prompt, eg after \r command */
if (query_buf->len == 0)
prompt_status = PROMPT_READY;
line = gets_interactive(get_prompt(prompt_status), query_buf);
line = gets_interactive(get_prompt(prompt_status, cond_stack),
query_buf);
}
else
{
@ -286,8 +301,10 @@ MainLoop(FILE *source)
(scan_result == PSCAN_EOL && pset.singleline))
{
/*
* Save query in history. We use history_buf to accumulate
* multi-line queries into a single history entry.
* Save line in history. We use history_buf to accumulate
* multi-line queries into a single history entry. Note that
* history accumulation works on input lines, so it doesn't
* matter whether the query will be ignored due to \if.
*/
if (pset.cur_cmd_interactive && !line_saved_in_history)
{
@ -296,22 +313,36 @@ MainLoop(FILE *source)
line_saved_in_history = true;
}
/* execute query */
success = SendQuery(query_buf->data);
slashCmdStatus = success ? PSQL_CMD_SEND : PSQL_CMD_ERROR;
pset.stmt_lineno = 1;
/* transfer query to previous_buf by pointer-swapping */
/* execute query unless we're in an inactive \if branch */
if (conditional_active(cond_stack))
{
PQExpBuffer swap_buf = previous_buf;
success = SendQuery(query_buf->data);
slashCmdStatus = success ? PSQL_CMD_SEND : PSQL_CMD_ERROR;
pset.stmt_lineno = 1;
previous_buf = query_buf;
query_buf = swap_buf;
/* transfer query to previous_buf by pointer-swapping */
{
PQExpBuffer swap_buf = previous_buf;
previous_buf = query_buf;
query_buf = swap_buf;
}
resetPQExpBuffer(query_buf);
added_nl_pos = -1;
/* we need not do psql_scan_reset() here */
}
else
{
/* if interactive, warn about non-executed query */
if (pset.cur_cmd_interactive)
psql_error("query ignored; use \\endif or Ctrl-C to exit current \\if block\n");
/* fake an OK result for purposes of loop checks */
success = true;
slashCmdStatus = PSQL_CMD_SEND;
pset.stmt_lineno = 1;
/* note that query_buf doesn't change state */
}
resetPQExpBuffer(query_buf);
added_nl_pos = -1;
/* we need not do psql_scan_reset() here */
}
else if (scan_result == PSCAN_BACKSLASH)
{
@ -343,21 +374,24 @@ MainLoop(FILE *source)
/* execute backslash command */
slashCmdStatus = HandleSlashCmds(scan_state,
query_buf->len > 0 ?
query_buf : previous_buf);
cond_stack,
query_buf,
previous_buf);
success = slashCmdStatus != PSQL_CMD_ERROR;
pset.stmt_lineno = 1;
if ((slashCmdStatus == PSQL_CMD_SEND || slashCmdStatus == PSQL_CMD_NEWEDIT) &&
query_buf->len == 0)
{
/* copy previous buffer to current for handling */
appendPQExpBufferStr(query_buf, previous_buf->data);
}
/*
* Resetting stmt_lineno after a backslash command isn't
* always appropriate, but it's what we've done historically
* and there have been few complaints.
*/
pset.stmt_lineno = 1;
if (slashCmdStatus == PSQL_CMD_SEND)
{
/* should not see this in inactive branch */
Assert(conditional_active(cond_stack));
success = SendQuery(query_buf->data);
/* transfer query to previous_buf by pointer-swapping */
@ -374,6 +408,8 @@ MainLoop(FILE *source)
}
else if (slashCmdStatus == PSQL_CMD_NEWEDIT)
{
/* should not see this in inactive branch */
Assert(conditional_active(cond_stack));
/* rescan query_buf as new input */
psql_scan_finish(scan_state);
free(line);
@ -429,8 +465,17 @@ MainLoop(FILE *source)
if (pset.cur_cmd_interactive)
pg_send_history(history_buf);
/* execute query */
success = SendQuery(query_buf->data);
/* execute query unless we're in an inactive \if branch */
if (conditional_active(cond_stack))
{
success = SendQuery(query_buf->data);
}
else
{
if (pset.cur_cmd_interactive)
psql_error("query ignored; use \\endif or Ctrl-C to exit current \\if block\n");
success = true;
}
if (!success && die_on_error)
successResult = EXIT_USER;
@ -438,6 +483,19 @@ MainLoop(FILE *source)
successResult = EXIT_BADCONN;
}
/*
* Check for unbalanced \if-\endifs unless user explicitly quit, or the
* script is erroring out
*/
if (slashCmdStatus != PSQL_CMD_TERMINATE &&
successResult != EXIT_USER &&
!conditional_stack_empty(cond_stack))
{
psql_error("reached EOF without finding closing \\endif(s)\n");
if (die_on_error && !pset.cur_cmd_interactive)
successResult = EXIT_USER;
}
/*
* Let's just make real sure the SIGINT handler won't try to use
* sigint_interrupt_jmp after we exit this routine. If there is an outer
@ -452,6 +510,7 @@ MainLoop(FILE *source)
destroyPQExpBuffer(history_buf);
psql_scan_destroy(scan_state);
conditional_stack_destroy(cond_stack);
pset.cur_cmd_source = prev_cmd_source;
pset.cur_cmd_interactive = prev_cmd_interactive;

View File

@ -66,7 +66,7 @@
*/
char *
get_prompt(promptStatus_t status)
get_prompt(promptStatus_t status, ConditionalStack cstack)
{
#define MAX_PROMPT_SIZE 256
static char destination[MAX_PROMPT_SIZE + 1];
@ -188,7 +188,9 @@ get_prompt(promptStatus_t status)
switch (status)
{
case PROMPT_READY:
if (!pset.db)
if (cstack != NULL && !conditional_active(cstack))
buf[0] = '@';
else if (!pset.db)
buf[0] = '!';
else if (!pset.singleline)
buf[0] = '=';

View File

@ -10,7 +10,8 @@
/* enum promptStatus_t is now defined by psqlscan.h */
#include "fe_utils/psqlscan.h"
#include "conditional.h"
char *get_prompt(promptStatus_t status);
char *get_prompt(promptStatus_t status, ConditionalStack cstack);
#endif /* PROMPT_H */

View File

@ -18,8 +18,7 @@ enum slash_option_type
OT_SQLID, /* treat as SQL identifier */
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE, /* just snarf the rest of the line */
OT_NO_EVAL /* no expansion of backticks or variables */
OT_WHOLE_LINE /* just snarf the rest of the line */
};
@ -32,6 +31,10 @@ extern char *psql_scan_slash_option(PsqlScanState state,
extern void psql_scan_slash_command_end(PsqlScanState state);
extern int psql_scan_get_paren_depth(PsqlScanState state);
extern void psql_scan_set_paren_depth(PsqlScanState state, int depth);
extern void dequote_downcase_identifier(char *str, bool downcase, int encoding);
#endif /* PSQLSCANSLASH_H */

View File

@ -19,6 +19,7 @@
#include "postgres_fe.h"
#include "psqlscanslash.h"
#include "conditional.h"
#include "libpq-fe.h"
}
@ -230,8 +231,7 @@ other .
:{variable_char}+ {
/* Possible psql variable substitution */
if (option_type == OT_NO_EVAL ||
cur_state->callbacks->get_variable == NULL)
if (cur_state->callbacks->get_variable == NULL)
ECHO;
else
{
@ -268,25 +268,15 @@ other .
}
:'{variable_char}+' {
if (option_type == OT_NO_EVAL)
ECHO;
else
{
psqlscan_escape_variable(cur_state, yytext, yyleng, false);
*option_quote = ':';
}
psqlscan_escape_variable(cur_state, yytext, yyleng, false);
*option_quote = ':';
unquoted_option_chars = 0;
}
:\"{variable_char}+\" {
if (option_type == OT_NO_EVAL)
ECHO;
else
{
psqlscan_escape_variable(cur_state, yytext, yyleng, true);
*option_quote = ':';
}
psqlscan_escape_variable(cur_state, yytext, yyleng, true);
*option_quote = ':';
unquoted_option_chars = 0;
}
@ -353,8 +343,9 @@ other .
*/
"`" {
/* In NO_EVAL mode, don't evaluate the command */
if (option_type != OT_NO_EVAL)
/* In an inactive \if branch, don't evaluate the command */
if (cur_state->cb_passthrough == NULL ||
conditional_active((ConditionalStack) cur_state->cb_passthrough))
evaluate_backtick(cur_state);
BEGIN(xslasharg);
}
@ -641,6 +632,25 @@ psql_scan_slash_command_end(PsqlScanState state)
psql_scan_reselect_sql_lexer(state);
}
/*
* Fetch current paren nesting depth
*/
int
psql_scan_get_paren_depth(PsqlScanState state)
{
return state->paren_depth;
}
/*
* Set paren nesting depth
*/
void
psql_scan_set_paren_depth(PsqlScanState state, int depth)
{
Assert(depth >= 0);
state->paren_depth = depth;
}
/*
* De-quote and optionally downcase a SQL identifier.
*

View File

@ -331,6 +331,7 @@ main(int argc, char *argv[])
else if (cell->action == ACT_SINGLE_SLASH)
{
PsqlScanState scan_state;
ConditionalStack cond_stack;
if (pset.echo == PSQL_ECHO_ALL)
puts(cell->val);
@ -339,11 +340,17 @@ main(int argc, char *argv[])
psql_scan_setup(scan_state,
cell->val, strlen(cell->val),
pset.encoding, standard_strings());
cond_stack = conditional_stack_create();
psql_scan_set_passthrough(scan_state, (void *) cond_stack);
successResult = HandleSlashCmds(scan_state, NULL) != PSQL_CMD_ERROR
successResult = HandleSlashCmds(scan_state,
cond_stack,
NULL,
NULL) != PSQL_CMD_ERROR
? EXIT_SUCCESS : EXIT_FAILURE;
psql_scan_destroy(scan_state);
conditional_stack_destroy(cond_stack);
}
else if (cell->action == ACT_FILE)
{

View File

@ -2735,6 +2735,175 @@ deallocate q;
\pset format aligned
\pset expanded off
\pset border 1
-- tests for \if ... \endif
\if true
select 'okay';
?column?
----------
okay
(1 row)
select 'still okay';
?column?
------------
still okay
(1 row)
\else
not okay;
still not okay
\endif
-- at this point query buffer should still have last valid line
\g
?column?
------------
still okay
(1 row)
-- \if should work okay on part of a query
select
\if true
42
\else
(bogus
\endif
forty_two;
forty_two
-----------
42
(1 row)
select \if false \\ (bogus \else \\ 42 \endif \\ forty_two;
forty_two
-----------
42
(1 row)
-- test a large nested if using a variety of true-equivalents
\if true
\if 1
\if yes
\if on
\echo 'all true'
all true
\else
\echo 'should not print #1-1'
\endif
\else
\echo 'should not print #1-2'
\endif
\else
\echo 'should not print #1-3'
\endif
\else
\echo 'should not print #1-4'
\endif
-- test a variety of false-equivalents in an if/elif/else structure
\if false
\echo 'should not print #2-1'
\elif 0
\echo 'should not print #2-2'
\elif no
\echo 'should not print #2-3'
\elif off
\echo 'should not print #2-4'
\else
\echo 'all false'
all false
\endif
-- test simple true-then-else
\if true
\echo 'first thing true'
first thing true
\else
\echo 'should not print #3-1'
\endif
-- test simple false-true-else
\if false
\echo 'should not print #4-1'
\elif true
\echo 'second thing true'
second thing true
\else
\echo 'should not print #5-1'
\endif
-- invalid boolean expressions are false
\if invalid boolean expression
unrecognized value "invalid boolean expression" for "\if expression": boolean expected
\echo 'will not print #6-1'
\else
\echo 'will print anyway #6-2'
will print anyway #6-2
\endif
-- test un-matched endif
\endif
\endif: no matching \if
-- test un-matched else
\else
\else: no matching \if
-- test un-matched elif
\elif
\elif: no matching \if
-- test double-else error
\if true
\else
\else
\else: cannot occur after \else
\endif
-- test elif out-of-order
\if false
\else
\elif
\elif: cannot occur after \else
\endif
-- test if-endif matching in a false branch
\if false
\if false
\echo 'should not print #7-1'
\else
\echo 'should not print #7-2'
\endif
\echo 'should not print #7-3'
\else
\echo 'should print #7-4'
should print #7-4
\endif
-- show that vars and backticks are not expanded when ignoring extra args
\set foo bar
\echo :foo :'foo' :"foo"
bar 'bar' "bar"
\pset fieldsep | `nosuchcommand` :foo :'foo' :"foo"
\pset: extra argument "nosuchcommand" ignored
\pset: extra argument ":foo" ignored
\pset: extra argument ":'foo'" ignored
\pset: extra argument ":"foo"" ignored
-- show that vars and backticks are not expanded and commands are ignored
-- when in a false if-branch
\set try_to_quit '\\q'
\if false
:try_to_quit
\echo `nosuchcommand` :foo :'foo' :"foo"
\pset fieldsep | `nosuchcommand` :foo :'foo' :"foo"
\a \C arg1 \c arg1 arg2 arg3 arg4 \cd arg1 \conninfo
\copy arg1 arg2 arg3 arg4 arg5 arg6
\copyright \dt arg1 \e arg1 arg2
\ef whole_line
\ev whole_line
\echo arg1 arg2 arg3 arg4 arg5 \echo arg1 \encoding arg1 \errverbose
\g arg1 \gx arg1 \gexec \h \html \i arg1 \ir arg1 \l arg1 \lo arg1 arg2
\o arg1 \p \password arg1 \prompt arg1 arg2 \pset arg1 arg2 \q
\reset \s arg1 \set arg1 arg2 arg3 arg4 arg5 arg6 arg7 \setenv arg1 arg2
\sf whole_line
\sv whole_line
\t arg1 \T arg1 \timing arg1 \unset arg1 \w arg1 \watch arg1 \x arg1
-- \else here is eaten as part of OT_FILEPIPE argument
\w |/no/such/file \else
-- \endif here is eaten as part of whole-line argument
\! whole_line \endif
\else
\echo 'should print #8-1'
should print #8-1
\endif
-- SHOW_CONTEXT
\set SHOW_CONTEXT never
do $$

View File

@ -382,6 +382,150 @@ deallocate q;
\pset expanded off
\pset border 1
-- tests for \if ... \endif
\if true
select 'okay';
select 'still okay';
\else
not okay;
still not okay
\endif
-- at this point query buffer should still have last valid line
\g
-- \if should work okay on part of a query
select
\if true
42
\else
(bogus
\endif
forty_two;
select \if false \\ (bogus \else \\ 42 \endif \\ forty_two;
-- test a large nested if using a variety of true-equivalents
\if true
\if 1
\if yes
\if on
\echo 'all true'
\else
\echo 'should not print #1-1'
\endif
\else
\echo 'should not print #1-2'
\endif
\else
\echo 'should not print #1-3'
\endif
\else
\echo 'should not print #1-4'
\endif
-- test a variety of false-equivalents in an if/elif/else structure
\if false
\echo 'should not print #2-1'
\elif 0
\echo 'should not print #2-2'
\elif no
\echo 'should not print #2-3'
\elif off
\echo 'should not print #2-4'
\else
\echo 'all false'
\endif
-- test simple true-then-else
\if true
\echo 'first thing true'
\else
\echo 'should not print #3-1'
\endif
-- test simple false-true-else
\if false
\echo 'should not print #4-1'
\elif true
\echo 'second thing true'
\else
\echo 'should not print #5-1'
\endif
-- invalid boolean expressions are false
\if invalid boolean expression
\echo 'will not print #6-1'
\else
\echo 'will print anyway #6-2'
\endif
-- test un-matched endif
\endif
-- test un-matched else
\else
-- test un-matched elif
\elif
-- test double-else error
\if true
\else
\else
\endif
-- test elif out-of-order
\if false
\else
\elif
\endif
-- test if-endif matching in a false branch
\if false
\if false
\echo 'should not print #7-1'
\else
\echo 'should not print #7-2'
\endif
\echo 'should not print #7-3'
\else
\echo 'should print #7-4'
\endif
-- show that vars and backticks are not expanded when ignoring extra args
\set foo bar
\echo :foo :'foo' :"foo"
\pset fieldsep | `nosuchcommand` :foo :'foo' :"foo"
-- show that vars and backticks are not expanded and commands are ignored
-- when in a false if-branch
\set try_to_quit '\\q'
\if false
:try_to_quit
\echo `nosuchcommand` :foo :'foo' :"foo"
\pset fieldsep | `nosuchcommand` :foo :'foo' :"foo"
\a \C arg1 \c arg1 arg2 arg3 arg4 \cd arg1 \conninfo
\copy arg1 arg2 arg3 arg4 arg5 arg6
\copyright \dt arg1 \e arg1 arg2
\ef whole_line
\ev whole_line
\echo arg1 arg2 arg3 arg4 arg5 \echo arg1 \encoding arg1 \errverbose
\g arg1 \gx arg1 \gexec \h \html \i arg1 \ir arg1 \l arg1 \lo arg1 arg2
\o arg1 \p \password arg1 \prompt arg1 arg2 \pset arg1 arg2 \q
\reset \s arg1 \set arg1 arg2 arg3 arg4 arg5 arg6 arg7 \setenv arg1 arg2
\sf whole_line
\sv whole_line
\t arg1 \T arg1 \timing arg1 \unset arg1 \w arg1 \watch arg1 \x arg1
-- \else here is eaten as part of OT_FILEPIPE argument
\w |/no/such/file \else
-- \endif here is eaten as part of whole-line argument
\! whole_line \endif
\else
\echo 'should print #8-1'
\endif
-- SHOW_CONTEXT
\set SHOW_CONTEXT never