On Mon, Feb 6, 2017 at 3:43 PM, Corey Huinker <corey.huin...@gmail.com>
wrote:

> I did some more tests. I found a subtlety that I missed before: when
>> running under ON_ERROR_STOP, messages are not fully consistent:
>>
>>  sh> cat test.sql
>>  \set ON_ERROR_STOP on
>>  \if error
>>    \echo NO
>>  \endif
>>  \echo NO
>>
>>  sh> ./psql < test.sql
>>  SET
>>  # ok
>>  unrecognized value "error" for "\if <expr>": boolean expected
>>  # ok
>
>
> That's straight from ParseVariableBool, and we can keep that or suppress
> it. Whatever we do, we should do it with the notion that more complex
> expressions will eventually be allowed, but they'll still have to resolve
> to something that's a text boolean.
>
>
>>
>>  new \if is invalid, ignoring commands until next \endif
>>  # hmmm... but it does not, it is really stopping immediately...
>
>  found EOF before closing \endif(s)
>>  # no, it has just stopped before EOF because of the error...
>>
>
> Yeah, chattiness caught up to us here. Both of these messages can be
> suppressed, I think.
>
>
>>
>> Also I'm not quite sure why psql decided that it is in interactive mode
>> above, its stdin is a file, but why not.
>>
>> The issue is made more explicit with -f:
>>
>>  sh> ./psql -f test.sql
>>  SET
>>  psql:test.sql:2: unrecognized value "error" for "\if <expr>": boolean
>> expected
>>  psql:test.sql:2: new \if is invalid, ignoring commands until next \endif
>>  psql:test.sql:2: found EOF before closing \endif(s)
>>
>> It stopped on line 2, which is expected, but it was not on EOF.
>>
>> I think that the message when stopping should be ", stopping as required
>> by ON_ERROR_STOP" or something like that instead of ", ignoring...", and
>> the EOF message should not be printed at all in this case.
>
>
> I agree, and will look into making that happen. Thanks for the test case.
>
>

I suppressed the endif-balance checking in cases where we're in an
already-failed situation.
In cases where there was a boolean parsing failure, and ON_ERROR_STOP is
on, the error message no longer speak of a future which the session does
not have. I could left the ParseVariableBool() message as the only one, but
wasn't sure that that was enough of an error message on its own.
Added the test case to the existing tap tests. Incidentally, the tap tests
aren't presently fooled into thinking they're interactive.

$ cat test2.sql
\if error
    \echo NO
\endif
\echo NOPE
$ psql test < test2.sql -v ON_ERROR_STOP=0
unrecognized value "error" for "\if <expr>": boolean expected
new \if is invalid, ignoring commands until next \endif
NOPE
$ psql test < test2.sql -v ON_ERROR_STOP=1
unrecognized value "error" for "\if <expr>": boolean expected
new \if is invalid.
$ psql test -f test2.sql -v ON_ERROR_STOP=0
psql:test2.sql:1: unrecognized value "error" for "\if <expr>": boolean
expected
psql:test2.sql:1: new \if is invalid, ignoring commands until next \endif
NOPE
$ psql test -f test2.sql -v ON_ERROR_STOP=1
psql:test2.sql:1: unrecognized value "error" for "\if <expr>": boolean
expected
psql:test2.sql:1: new \if is invalid.


Revised cumulative patch attached for those playing along at home.
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..f10d7ac 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,259 @@ 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. suppress 
this error in
+                                * cases where the script is already going to 
terminate.
+                                */
+                               if (pset.on_error_stop)
+                                       psql_error("new \\if is invalid.\n");
+                               else
+                                       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. 
suppress this error in
+                                        * cases where the script is already 
going to terminate.
+                                        */
+                                       if (pset.on_error_stop)
+                                               psql_error("\\elif is 
invalid.\n");
+                                       else
+                                               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..b599581 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,19 @@ MainLoop(FILE *source)
        destroyPQExpBuffer(previous_buf);
        destroyPQExpBuffer(history_buf);
 
+       /*
+        * check for unbalanced \if-\endifs unless user explicitly quit,
+        * or the script is erroring out
+        */
+       if (slashCmdStatus != PSQL_CMD_TERMINATE
+               && successResult != EXIT_USER
+               && !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..88f3013
--- /dev/null
+++ b/src/bin/psql/t/001_if.pl
@@ -0,0 +1,41 @@
+use strict;
+use warnings;
+
+use Config;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 21;
+
+#
+# test invalid \if respects ON_ERROR_STOP
+#
+my $node = get_new_node('master');
+$node->init;
+$node->start;
+
+my $tests = [
+    # syntax errors
+       [ "\\if invalid\n\\echo NO\n\\endif\n\\echo NO\n", '', 'boolean 
expected', 'syntax error' ],
+       [ "\\if false\n\\elif invalid\n\\echo NO\n\\endif\n\\echo NO\n", '', 
'boolean expected', 'syntax error' ],
+       # unmatched checks
+       [ "\\if true\n", '', 'found EOF before closing.*endif', 'unmatched \if' 
],
+       [ "\\elif true\n\\echo NO\n", '', 'encountered un-matched.*elif', 
'unmatched \elif' ],
+       [ "\\else\n\\echo NO\n", '', 'encountered un-matched.*else', 'unmatched 
\else' ],
+       [ "\\endif\n\\echo NO\n", '', 'encountered un-matched.*endif', 
'unmatched \endif' ],
+       # error stop messages
+       [ "\\set ON_ERROR_STOP on\n\\if error\n\\echo NO\n\\endif\n\\echo 
NO\n", '', 'new.*if is invalid.', 'invalid if'],
+];
+
+# 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 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

Reply via email to