> > Hmmm. ISTM that control-c must at least reset the stack, otherwise their > is not easy way to get out. What can be left to another patch is doing a > control-C for contents and then another one for the stack when there is no > content. >
And so it shall be. > - put Fabien's tap test in place verbatim >> > > Hmmm. That was really just a POC... I would suggest some more tests, eg: > > # elif error > "\\if false\n\\elif error\n\\endif\n" > > # ignore commands on error (stdout must be empty) > "\\if error\n\\echo NO\n\\else\n\\echo NO\n\\endif\n" > Those are already in the regression (around line 2763 of expected/psql.out), are you saying we should have them in TAP as well? Should we only do TAP tests? Anyway, here's the Ctrl-C behavior: # \if true new \if is true, executing commands # \echo msg msg # ^C escaped \if, executing commands # \if false new \if is false, ignoring commands until next \elif, \else, or \endif # \echo msg inside inactive branch, command ignored. # ^C escaped \if, executing commands # \echo msg msg # \endif encountered un-matched \endif # Ctrl-C exits do the same before/after state checks that \endif does, the lone difference being that it "escaped" the \if rather than "exited" the \if. Thanks to Daniel for pointing out where it should be handled, because I wasn't going to figure that out on my own. v7's only major difference from v6 is the Ctrl-C branch escaping.
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index ae58708..c0ba4c4 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2035,6 +2035,67 @@ hello 10 </listitem> </varlistentry> + <varlistentry> + <term><literal>\if</literal> <replaceable class="parameter">expr</replaceable></term> + <term><literal>\elif</literal> <replaceable class="parameter">expr</replaceable></term> + <term><literal>\else</literal></term> + <term><literal>\endif</literal></term> + <listitem> + <para> + This group of commands implements nestable conditional blocks, like + this: + </para> +<programlisting> +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 should never print' + \endif +\endif +</programlisting> + <para> + Conditional blocks must begin with a <command>\if</command> and end + with an <command>\endif</command>, and the pairs must be found in + the same source file. If an EOF is reached on the main file or an + <command>\include</command>-ed file before all local + <command>\if</command>-<command>\endif</command> are matched, then + psql will raise an error. + </para> + <para> + A conditional block can have any number of + <command>\elif</command> clauses, which may optionally be followed by a + single <command>\else</command> clause. + </para> + <para> + The <command>\if</command> and <command>\elif</command> commands + read the rest of the line and evaluate it as a boolean expression. + Currently, expressions are limited to a single unquoted string + which are evaluated like other on/off options, so the valid values + are any unambiguous case insensitive matches 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>. So + <literal>t</literal>, <literal>T</literal>, and <literal>tR</literal> + will all match <literal>true</literal>. + </para> + <para> + Lines within false branches are not evaluated in any way: queries are + not sent to the server, non-conditional commands are not evaluated but + bluntly ignored, nested if-expressions in such branches are also not + evaluated but are tallied to check for proper nesting. + </para> + </listitem> + </varlistentry> <varlistentry> <term><literal>\ir</literal> or <literal>\include_relative</literal> <replaceable class="parameter">filename</replaceable></term> diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile index c53733f..7a418c6 100644 --- a/src/bin/psql/Makefile +++ b/src/bin/psql/Makefile @@ -61,8 +61,16 @@ uninstall: clean distclean: rm -f psql$(X) $(OBJS) lex.backup + rm -rf tmp_check # files removed here are supposed to be in the distribution tarball, # so do not clean them in the clean/distclean rules maintainer-clean: distclean rm -f sql_help.h sql_help.c psqlscanslash.c + + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index f17f610..4a3e471 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -49,6 +49,7 @@ #include "psqlscanslash.h" #include "settings.h" #include "variables.h" +#include "fe_utils/psqlscan_int.h" /* * Editable database object types. @@ -132,7 +133,7 @@ HandleSlashCmds(PsqlScanState scan_state, status = PSQL_CMD_ERROR; } - if (status != PSQL_CMD_ERROR) + if (status != PSQL_CMD_ERROR && pset.active_branch) { /* eat any remaining arguments after a valid command */ /* note we suppress evaluation of backticks here */ @@ -194,6 +195,30 @@ read_connect_arg(PsqlScanState scan_state) return result; } +/* + * Read and interpret argument as a boolean expression. + * Return true if a boolean value was successfully parsed. + */ +static bool +read_boolean_expression(PsqlScanState scan_state, char *action, + bool *result) +{ + char *value = psql_scan_slash_option(scan_state, + OT_NORMAL, NULL, false); + bool success = ParseVariableBool(value, action, result); + free(value); + return success; +} + +/* + * Return true if the command given is a branching command. + */ +static bool +is_branching_command(const char *cmd) +{ + return (strcmp(cmd, "if") == 0 || strcmp(cmd, "elif") == 0 + || strcmp(cmd, "else") == 0 || strcmp(cmd, "endif") == 0); +} /* * Subroutine to actually try to execute a backslash command. @@ -207,6 +232,17 @@ exec_command(const char *cmd, * failed */ backslashResult status = PSQL_CMD_SKIP_LINE; + if (!pset.active_branch && !is_branching_command(cmd)) + { + if (pset.cur_cmd_interactive) + psql_error("inside inactive branch, command ignored.\n"); + + /* Continue with an empty buffer as if the command were never read */ + resetPQExpBuffer(query_buf); + psql_scan_reset(scan_state); + return status; + } + /* * \a -- toggle field alignment This makes little sense but we keep it * around. @@ -1006,6 +1042,251 @@ exec_command(const char *cmd, } } + /* + * \if <expr> is the beginning of an \if..\endif block. + * <expr> must be a valid boolean expression, or the whole block will be + * ignored. + * If this \if is itself a part of a branch that is false/ignored, it too + * will automatically be ignored. + */ + else if (strcmp(cmd, "if") == 0) + { + ifState new_if_state = IFSTATE_IGNORED; + /* + * only evaluate the expression for truth if the underlying + * branch is active + */ + if (pset.active_branch) + { + bool if_true = false; + success = read_boolean_expression(scan_state, "\\if <expr>", &if_true); + if (success) + { + if (if_true) + { + new_if_state = IFSTATE_TRUE; + if (pset.cur_cmd_interactive) + psql_error("new \\if is true, executing commands\n"); + pset.active_branch = true; + } + else + { + new_if_state = IFSTATE_FALSE; + if (pset.cur_cmd_interactive) + psql_error("new \\if is false, ignoring commands " + "until next \\elif, \\else, or \\endif\n"); + pset.active_branch = false; + } + } + else + { + /* + * show this error in both interactive and script, because the + * session is now in a state where all blocks will be ignored + * regardless of expression truth + */ + psql_error("new \\if is invalid, " + "ignoring commands until next \\endif\n"); + pset.active_branch = false; + } + } + else + { + if (pset.cur_cmd_interactive) + psql_error("new \\if is inside ignored block, " + "ignoring commands until next \\endif\n"); + pset.active_branch = false; + } + + psqlscan_branch_push(scan_state,new_if_state); + psql_scan_reset(scan_state); + } + + /* + * \elif <expr> is part of an \if..\endif block + * <expr> will only be evalated for boolean truth if no previous + * \if or \endif in the block has evaluated to true and the \if..\endif + * block is not itself being ignored. + * in the event that <expr> does not conform to a proper boolean expression, + * all following statements in the block will be ignored until \endif is + * encountered. + * + */ + else if (strcmp(cmd, "elif") == 0) + { + bool elif_true = false; + switch (psqlscan_branch_get_state(scan_state)) + { + case IFSTATE_IGNORED: + /* + * inactive branch, do nothing: + * either if-endif already had a true block, + * or whole parent block is false. + */ + if (pset.cur_cmd_interactive) + psql_error("\\elif is inside ignored block, " + "ignoring commands until next \\endif\n"); + pset.active_branch = false; + break; + case IFSTATE_TRUE: + /* + * just finished true section of active branch + * do not evaluate expression, just skip + */ + if (pset.cur_cmd_interactive) + psql_error("\\elif is after true block, " + "ignoring commands until next \\endif\n"); + psqlscan_branch_set_state(scan_state, IFSTATE_IGNORED); + pset.active_branch = false; + break; + case IFSTATE_FALSE: + /* + * have not yet found a true block in this if-endif, + * determine if this section is true or not. + * variable expansion must be temporarily turned back + * on to read the boolean expression. + */ + pset.active_branch = true; + success = read_boolean_expression(scan_state, "\\elif <expr>", + &elif_true); + if (success) + { + if (elif_true) + { + psqlscan_branch_set_state(scan_state, IFSTATE_TRUE); + if (pset.cur_cmd_interactive) + psql_error("\\elif is true, executing commands\n"); + pset.active_branch = false; + } + else + { + if (pset.cur_cmd_interactive) + psql_error("\\elif is false, ignoring commands " + "until next \\elif, \\else, or \\endif\n"); + pset.active_branch = false; + } + } + else + { + /* + * show this error in both interactive and script, because the + * session is now in a state where all blocks will be ignored + * regardless of expression truth + */ + psql_error("\\elif is invalid, " + "ignoring commands until next \\endif\n"); + psqlscan_branch_set_state(scan_state, IFSTATE_IGNORED); + pset.active_branch = false; + } + pset.active_branch = psqlscan_branch_active(scan_state); + break; + case IFSTATE_ELSE_TRUE: + case IFSTATE_ELSE_FALSE: + psql_error("encountered \\elif after \\else\n"); + success = false; + pset.active_branch = false; + break; + case IFSTATE_NONE: + /* no if to elif from */ + psql_error("encountered un-matched \\elif\n"); + success = false; + break; + default: + break; + } + psql_scan_reset(scan_state); + } + + /* + * \else is part of an \if..\endif block + * the statements within an \else branch will only be executed if + * all previous \if and \endif expressions evaluated to false + * and the block was not itself being ignored. + */ + else if (strcmp(cmd, "else") == 0) + { + switch (psqlscan_branch_get_state(scan_state)) + { + case IFSTATE_TRUE: + case IFSTATE_IGNORED: + /* + * either just finished true section of an active branch, + * or whole branch was inactive. either way, be on the + * lookout for any invalid \endif or \else commands + */ + if (pset.cur_cmd_interactive) + psql_error("\\else after true condition or in ignored block, " + "ignoring commands until next \\endif\n"); + pset.active_branch = false; + psqlscan_branch_set_state(scan_state, IFSTATE_ELSE_FALSE); + break; + case IFSTATE_FALSE: + /* just finished false section of an active branch */ + if (pset.cur_cmd_interactive) + psql_error("\\else after all previous conditions false, " + "executing commands\n"); + psqlscan_branch_set_state(scan_state, IFSTATE_ELSE_TRUE); + pset.active_branch = true; + break; + case IFSTATE_NONE: + /* no if to else from */ + psql_error("encountered un-matched \\else\n"); + success = false; + break; + case IFSTATE_ELSE_TRUE: + case IFSTATE_ELSE_FALSE: + psql_error("encountered \\else after \\else\n"); + success = false; + pset.active_branch = false; + break; + default: + break; + } + psql_scan_reset(scan_state); + } + + /* + * \endif - closing statment of an \if...\endif block + */ + else if (strcmp(cmd, "endif") == 0) + { + /* + * get rid of this ifstate element and look at the previous + * one, if any + */ + if (psqlscan_branch_pop(scan_state)) + { + bool was_active = pset.active_branch; + pset.active_branch = psqlscan_branch_active(scan_state); + + if (pset.cur_cmd_interactive) + { + if (psqlscan_branch_stack_empty(scan_state)) + /* no more branches */ + psql_error("exited \\if, executing commands\n"); + else if (!pset.active_branch) + /* was ignoring, still ignoring */ + psql_error("exited \\if to false parent branch, " + "ignoring commands until next \\endif\n"); + else if (was_active) + /* was true, still true */ + psql_error("exited \\if to true parent branch, " + "continuing executing commands\n"); + else + /* was false, is true again */ + psql_error("exited \\if to true parent branch, " + "resuming executing commands\n"); + } + } + else + { + psql_error("encountered un-matched \\endif\n"); + success = false; + } + + psql_scan_reset(scan_state); + } + /* \l is list databases */ else if (strcmp(cmd, "l") == 0 || strcmp(cmd, "list") == 0 || strcmp(cmd, "l+") == 0 || strcmp(cmd, "list+") == 0) diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index 6e3acdc..9f1a072 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -119,6 +119,11 @@ setQFout(const char *fname) * If "escape" is true, return the value suitably quoted and escaped, * as an identifier or string literal depending on "as_ident". * (Failure in escaping should lead to returning NULL.) + * + * Variables are not expanded if the current branch is inactive + * (part of an \if..\endif section which is false). \elif branches + * will need temporarily mark the branch active in order to + * properly evaluate conditionals. */ char * psql_get_variable(const char *varname, bool escape, bool as_ident) @@ -126,6 +131,10 @@ psql_get_variable(const char *varname, bool escape, bool as_ident) char *result; const char *value; + /* do not expand variables if the branch is inactive */ + if (!pset.active_branch) + return NULL; + value = GetVariable(pset.vars, varname); if (!value) return NULL; diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 3e3cab4..9f9e1a6 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -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, _("Conditionals\n")); + fprintf(output, _(" \\if <expr> begin a conditional block\n")); + fprintf(output, _(" \\elif <expr> else if in the current conditional block\n")); + fprintf(output, _(" \\else else in the current conditional block\n")); + fprintf(output, _(" \\endif end current 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")); diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c index 6e358e2..099cf8c 100644 --- a/src/bin/psql/mainloop.c +++ b/src/bin/psql/mainloop.c @@ -15,7 +15,7 @@ #include "settings.h" #include "mb/pg_wchar.h" - +#include "fe_utils/psqlscan_int.h" /* callback functions for our flex lexer */ const PsqlScanCallbacks psqlscan_callbacks = { @@ -23,6 +23,23 @@ const PsqlScanCallbacks psqlscan_callbacks = { psql_error }; +/* + * execute query if branch is active. + * warn interactive users about ignored queries. + */ +static +bool send_query(const char *query) +{ + /* execute query if branch is active */ + if (pset.active_branch) + return SendQuery(query); + + if (pset.cur_cmd_interactive) + psql_error("inside inactive branch, query ignored. " + "use \\endif to exit current branch.\n"); + + return true; +} /* * Main processing loop for reading lines of input @@ -122,7 +139,35 @@ MainLoop(FILE *source) cancel_pressed = false; if (pset.cur_cmd_interactive) + { putc('\n', stdout); + /* + * if interactive user is in a branch, then Ctrl-C will exit + * from the inner-most branch + */ + if (!psqlscan_branch_stack_empty(scan_state)) + { + bool was_active = pset.active_branch; + psqlscan_branch_pop(scan_state); + pset.active_branch = psqlscan_branch_active(scan_state); + + if (psqlscan_branch_stack_empty(scan_state)) + /* no more branches */ + psql_error("escaped \\if, executing commands\n"); + else if (!pset.active_branch) + /* was ignoring, still ignoring */ + psql_error("escaped \\if to false parent branch, " + "ignoring commands until next \\endif\n"); + else if (was_active) + /* was true, still true */ + psql_error("escaped \\if to true parent branch, " + "continuing executing commands\n"); + else + /* was false, is true again */ + psql_error("escaped \\if to true parent branch, " + "resuming executing commands\n"); + } + } else { successResult = EXIT_USER; @@ -296,8 +341,8 @@ MainLoop(FILE *source) line_saved_in_history = true; } - /* execute query */ - success = SendQuery(query_buf->data); + success = send_query(query_buf->data); + slashCmdStatus = success ? PSQL_CMD_SEND : PSQL_CMD_ERROR; pset.stmt_lineno = 1; @@ -358,7 +403,7 @@ MainLoop(FILE *source) if (slashCmdStatus == PSQL_CMD_SEND) { - success = SendQuery(query_buf->data); + success = send_query(query_buf->data); /* transfer query to previous_buf by pointer-swapping */ { @@ -367,6 +412,7 @@ MainLoop(FILE *source) previous_buf = query_buf; query_buf = swap_buf; } + resetPQExpBuffer(query_buf); /* flush any paren nesting info after forced send */ @@ -429,8 +475,7 @@ MainLoop(FILE *source) if (pset.cur_cmd_interactive) pg_send_history(history_buf); - /* execute query */ - success = SendQuery(query_buf->data); + success = send_query(query_buf->data); if (!success && die_on_error) successResult = EXIT_USER; @@ -451,6 +496,15 @@ MainLoop(FILE *source) destroyPQExpBuffer(previous_buf); destroyPQExpBuffer(history_buf); + /* check for unbalanced \if-\endifs unless user explicitly quit */ + if (slashCmdStatus != PSQL_CMD_TERMINATE + && !psqlscan_branch_stack_empty(scan_state)) + { + psql_error("found EOF before closing \\endif(s)\n"); + if (die_on_error) + successResult = EXIT_USER; + } + psql_scan_destroy(scan_state); pset.cur_cmd_source = prev_cmd_source; diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h index 195f5a1..c8aeab6 100644 --- a/src/bin/psql/settings.h +++ b/src/bin/psql/settings.h @@ -114,6 +114,9 @@ typedef struct _psqlSettings VariableSpace vars; /* "shell variable" repository */ + bool active_branch; /* true if session is not in an \if branch + * or the current branch is true */ + /* * The remaining fields are set by assign hooks associated with entries in * "vars". They should not be set directly except by those hook diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c index 88d686a..5c8760a 100644 --- a/src/bin/psql/startup.c +++ b/src/bin/psql/startup.c @@ -128,6 +128,8 @@ main(int argc, char *argv[]) setvbuf(stderr, NULL, _IONBF, 0); #endif + pset.active_branch = true; + pset.progname = get_progname(argv[0]); pset.db = NULL; diff --git a/src/bin/psql/t/001_if.pl b/src/bin/psql/t/001_if.pl new file mode 100644 index 0000000..68c9b15 --- /dev/null +++ b/src/bin/psql/t/001_if.pl @@ -0,0 +1,37 @@ +use strict; +use warnings; + +use Config; +use PostgresNode; +use TestLib; +use Test::More tests => 15; + +# +# test invalid \if respects ON_ERROR_STOP +# +my $node = get_new_node('master'); +$node->init; +$node->start; + +my $tests = [ + [ "\\if invalid_expression\n\\endif\n", '', 'boolean expected', 'syntax error' ], + # unmatched checks + [ "\\if true\\n", '', 'found EOF before closing.*endif', 'unmatched \if' ], + [ "\\elif true\\n", '', 'encountered un-matched.*elif', 'unmatched \elif' ], + [ "\\else\\n", '', 'encountered un-matched.*else', 'unmatched \else' ], + [ "\\endif\\n", '', 'encountered un-matched.*endif', 'unmatched \endif' ], +]; + +# 3 checks per tests +for my $test (@$tests) { + my ($script, $stdout_expect, $stderr_re, $name) = @$test; + my ($stdout, $stderr); + my $retcode = $node->psql('postgres', $script, + stdout => \$stdout, stderr => \$stderr, + on_error_stop => 1); + is($retcode,'3',"$name test respects ON_ERROR_STOP"); + is($stdout, $stdout_expect, "$name test STDOUT"); + like($stderr, qr/$stderr_re/, "$name test STDERR"); +} + +$node->teardown_node; diff --git a/src/fe_utils/psqlscan.l b/src/fe_utils/psqlscan.l index 1b29341..998630a 100644 --- a/src/fe_utils/psqlscan.l +++ b/src/fe_utils/psqlscan.l @@ -904,6 +904,8 @@ psql_scan_create(const PsqlScanCallbacks *callbacks) psql_scan_reset(state); + state->branch_stack = NULL; + return state; } @@ -919,6 +921,13 @@ psql_scan_destroy(PsqlScanState state) yylex_destroy(state->scanner); + while (state->branch_stack != NULL) + { + IfStackElem *p = state->branch_stack; + state->branch_stack = state->branch_stack->next; + free(p); + } + free(state); } @@ -1426,3 +1435,86 @@ psqlscan_escape_variable(PsqlScanState state, const char *txt, int len, psqlscan_emit(state, txt, len); } } + +/* + * psqlscan_branch_stack_empty + * + * True if there are no active \if-structures + */ +bool +psqlscan_branch_stack_empty(PsqlScanState state) +{ + return state->branch_stack == NULL; +} + +/* + * Fetch the current state of the top of the stack + */ +ifState +psqlscan_branch_get_state(PsqlScanState state) +{ + if (psqlscan_branch_stack_empty(state)) + return IFSTATE_NONE; + return state->branch_stack->if_state; +} + +/* + * psqlscan_branch_active + * + * True if the current \if-block (if any) is true and queries/commands + * should be executed. + */ +bool +psqlscan_branch_active(PsqlScanState state) +{ + ifState s = psqlscan_branch_get_state(state); + return s == IFSTATE_NONE || s == IFSTATE_TRUE || s == IFSTATE_ELSE_TRUE; +} + + +/* + * psqlscan_branch_push + * + * Create a new \if branch. + */ +bool +psqlscan_branch_push(PsqlScanState state, ifState new_state) +{ + IfStackElem *p = pg_malloc0(sizeof(IfStackElem)); + p->if_state = new_state; + p->next = state->branch_stack; + state->branch_stack = p; + return true; +} + +/* + * psqlscan_branch_set_state + * + * Change the state of the topmost branch. + * Returns false if there was branch state to set. + */ +bool +psqlscan_branch_set_state(PsqlScanState state, ifState new_state) +{ + if (psqlscan_branch_stack_empty(state)) + return false; + state->branch_stack->if_state = new_state; + return true; +} + +/* + * psqlscan_branch_pop + * + * Destroy the topmost branch because and \endif was encountered. + * Returns false if there was no branch to end. + */ +bool +psqlscan_branch_pop(PsqlScanState state) +{ + IfStackElem *p = state->branch_stack; + if (!p) + return false; + state->branch_stack = state->branch_stack->next; + free(p); + return true; +} diff --git a/src/include/fe_utils/psqlscan_int.h b/src/include/fe_utils/psqlscan_int.h index 0fddc7a..321d455 100644 --- a/src/include/fe_utils/psqlscan_int.h +++ b/src/include/fe_utils/psqlscan_int.h @@ -75,6 +75,30 @@ typedef struct StackElem struct StackElem *next; } StackElem; +typedef enum _ifState +{ + IFSTATE_NONE = 0, /* Not currently in an \if block */ + IFSTATE_TRUE, /* currently in an \if or \elif which is true + * and all parent branches (if any) are true */ + IFSTATE_FALSE, /* currently in an \if or \elif which is false + * but no true branch has yet been seen, + * and all parent branches (if any) are true */ + IFSTATE_IGNORED, /* currently in an \elif which follows a true \if + * or the whole \if is a child of a false parent */ + IFSTATE_ELSE_TRUE, /* currently in an \else which is true + * and all parent branches (if any) are true */ + IFSTATE_ELSE_FALSE /* currently in an \else which is false or ignored */ +} ifState; + +/* + * The state of nested ifs is stored in a stack. + */ +typedef struct IfStackElem +{ + ifState if_state; + struct IfStackElem *next; +} IfStackElem; + /* * All working state of the lexer must be stored in PsqlScanStateData * between calls. This allows us to have multiple open lexer operations, @@ -118,6 +142,11 @@ typedef struct PsqlScanStateData * Callback functions provided by the program making use of the lexer. */ const PsqlScanCallbacks *callbacks; + + /* + * \if branch state variables + */ + IfStackElem *branch_stack; } PsqlScanStateData; @@ -141,4 +170,21 @@ extern void psqlscan_escape_variable(PsqlScanState state, const char *txt, int len, bool as_ident); +/* + * branching commands + */ +extern bool psqlscan_branch_stack_empty(PsqlScanState state); + +extern bool psqlscan_branch_active(PsqlScanState state); + +extern ifState psqlscan_branch_get_state(PsqlScanState state); + +extern bool psqlscan_branch_push(PsqlScanState state, + ifState new_state); + +extern bool psqlscan_branch_set_state(PsqlScanState state, + ifState new_state); + +extern bool psqlscan_branch_pop(PsqlScanState state); + #endif /* PSQLSCAN_INT_H */ diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 026a4f0..d4f1849 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -2712,6 +2712,96 @@ deallocate q; \pset format aligned \pset expanded off \pset border 1 +-- 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 +-- test invalidation of whole if-endif when invalid boolean is given +\if invalid_boolean_expression +unrecognized value "invalid_boolean_expression" for "\if <expr>": boolean expected +new \if is invalid, ignoring commands until next \endif + \echo 'should not print #6-1' +\else + \echo 'should not print because whole if-then is wrecked #6-2' +\endif +-- test non-evaluation of variables in false blocks +\set var 'ab''cd' +-- select :var; +\if false + select :var ; +\else + select 1; + ?column? +---------- + 1 +(1 row) + +\endif +-- test un-matched endif +\endif +encountered un-matched \endif +-- test un-matched else +\else +encountered un-matched \else +-- test un-matched elif +\elif +encountered un-matched \elif +-- test double-else error +\if true +\else +\else +encountered \else after \else +\endif +-- test elif out-of-order +\if false +\else +\elif +encountered \elif after \else +\endif -- SHOW_CONTEXT \set SHOW_CONTEXT never do $$ diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index d823d11..ba41d9e 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -375,6 +375,91 @@ deallocate q; \pset expanded off \pset border 1 +-- 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 + +-- test invalidation of whole if-endif when invalid boolean is given +\if invalid_boolean_expression + \echo 'should not print #6-1' +\else + \echo 'should not print because whole if-then is wrecked #6-2' +\endif + +-- test non-evaluation of variables in false blocks +\set var 'ab''cd' +-- select :var; +\if false + select :var ; +\else + select 1; +\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 + -- SHOW_CONTEXT \set SHOW_CONTEXT never
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers