diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index ae58708..9d245a9 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2035,6 +2035,83 @@ hello 10 + + \if expr + \elif expr + \else + \endif + + + This group of commands implements nestable conditional blocks, like + this: + + +-- set ON_ERROR_STOP in case the variables are not valid boolean expressions +\set ON_ERROR_STOP on +-- 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 should never print' + \endif +\endif + + + Conditional blocks must begin with a \if and end + with an \endif, and the pairs must be found in + the same source file. If an EOF is reached on the main file or an + \include-ed file before all local + \if-\endif are matched, then + psql will raise an error. + + + A conditional block can have any number of + \elif clauses, which may optionally be followed by a + single \else clause. + + + The \if and \elif 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: + true, false, 1, + 0, on, off, + yes, no. So + t, T, and tR + will all match true. + + Expressions that do not properly evaluate to true or false will + generate an error and cause the \if or + \elif command to fail. Because that behavior may + change branching context in undesirable ways (executing code which + was intended to be skipped, causing \elif, + \else, and \endif commands to + pair with the wrong \if, etc), it is + recommended that scripts which use conditionals also set + ON_ERROR_STOP. + + + Lines within false branches are parsed normally for query/command + completion, but any queries are not sent to the server, and any + commands other than conditionals (\if, + \elif, \else, + \endif) are ignored. Conditional commands are + still checked for valid nesting. + + + \ir or \include_relative filename @@ -3703,8 +3780,9 @@ testdb=> INSERT INTO my_table VALUES (:'content'); In prompt 1 normally =, - but ^ if in single-line mode, - or ! if the session is disconnected from the + but @ if the session is in a false conditional + block, or ^ if in single-line mode, or + ! if the session is disconnected from the database (which can happen if \connect fails). In prompt 2 %R is replaced by a character that depends on why psql expects more input: diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile index c53733f..1492b66 100644 --- a/src/bin/psql/Makefile +++ b/src/bin/psql/Makefile @@ -21,7 +21,7 @@ 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 \ +OBJS= command.o common.o conditional.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 \ @@ -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..5dcde39 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -38,6 +38,7 @@ #include "fe_utils/string_utils.h" #include "common.h" +#include "conditional.h" #include "copy.h" #include "crosstabview.h" #include "describe.h" @@ -62,6 +63,7 @@ typedef enum EditableObjectType /* functions for use in this file */ static backslashResult exec_command(const char *cmd, PsqlScanState scan_state, + ConditionalStack cstack, PQExpBuffer query_buf); static bool do_edit(const char *filename_arg, PQExpBuffer query_buf, int lineno, bool *edited); @@ -109,6 +111,7 @@ static void checkWin32Codepage(void); backslashResult HandleSlashCmds(PsqlScanState scan_state, + ConditionalStack cstack, PQExpBuffer query_buf) { backslashResult status = PSQL_CMD_SKIP_LINE; @@ -121,7 +124,7 @@ HandleSlashCmds(PsqlScanState scan_state, cmd = psql_scan_slash_command(scan_state); /* And try to execute it */ - status = exec_command(cmd, scan_state, query_buf); + status = exec_command(cmd, scan_state, cstack, query_buf); if (status == PSQL_CMD_UNKNOWN) { @@ -132,7 +135,7 @@ HandleSlashCmds(PsqlScanState scan_state, status = PSQL_CMD_ERROR; } - if (status != PSQL_CMD_ERROR) + if (status != PSQL_CMD_ERROR && conditional_active(cstack)) { /* eat any remaining arguments after a valid command */ /* note we suppress evaluation of backticks here */ @@ -194,6 +197,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. @@ -201,12 +228,25 @@ read_connect_arg(PsqlScanState scan_state) static backslashResult exec_command(const char *cmd, PsqlScanState scan_state, + ConditionalStack cstack, PQExpBuffer query_buf) { bool success = true; /* indicate here if the command ran ok or * failed */ backslashResult status = PSQL_CMD_SKIP_LINE; + if (!conditional_active(cstack) && !is_branching_command(cmd)) + { + if (pset.cur_cmd_interactive) + psql_error("command ignored, use \\endif or Ctrl-C to exit " + "current branch.\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 +1046,154 @@ exec_command(const char *cmd, } } + /* + * \if is the beginning of an \if..\endif block. must be a + * valid boolean expression, or the command 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) + { + bool if_true = false; + success = read_boolean_expression(scan_state, "\\if ", &if_true); + if (success) + { + ifState if_state = IFSTATE_IGNORED; + if (conditional_active(cstack)) + if_state = (if_true) ? IFSTATE_TRUE : IFSTATE_FALSE; + conditional_stack_push(cstack, if_state); + } + psql_scan_reset(scan_state); + } + + /* + * \elif is part of an \if..\endif block. must be a valid + * boolean expression, or the command will be ignored. + * + */ + else if (strcmp(cmd, "elif") == 0) + { + ifState if_state = conditional_stack_peek(cstack); + + /* check for invalid \elif contexts */ + switch (if_state) + { + case IFSTATE_NONE: + /* no if to elif from */ + psql_error("\\elif: no matching \\if\n"); + success = false; + break; + case IFSTATE_ELSE_TRUE: + case IFSTATE_ELSE_FALSE: + psql_error("\\elif: cannot occur after \\else\n"); + success = false; + break; + default: + break; + } + + if (success) + { + /* + * valid \elif context, check for valid expression + */ + bool elif_true = false; + success = read_boolean_expression(scan_state, "\\elif ", + &elif_true); + if (success) + { + /* + * got a valid boolean, what to do with it depends on current + * state + */ + switch (if_state) + { + case IFSTATE_IGNORED: + /* + * inactive branch, do nothing. + * either if-endif already had a true block, + * or whole parent block is false. + */ + break; + case IFSTATE_TRUE: + /* + * just finished true section of this if-endif, + * must ignore the rest until \endif + */ + conditional_stack_poke(cstack, IFSTATE_IGNORED); + break; + case IFSTATE_FALSE: + /* + * have not yet found a true block in this if-endif, + * this might be the first. + */ + if (elif_true) + conditional_stack_poke(cstack, IFSTATE_TRUE); + break; + default: + /* error cases all previously ruled out */ + 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 (conditional_stack_peek(cstack)) + { + case IFSTATE_FALSE: + /* just finished false section of an active branch */ + conditional_stack_poke(cstack, IFSTATE_ELSE_TRUE); + break; + 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 + */ + conditional_stack_poke(cstack, IFSTATE_ELSE_FALSE); + break; + case IFSTATE_NONE: + /* no if to else from */ + psql_error("\\else: no matching \\if\n"); + success = false; + break; + case IFSTATE_ELSE_TRUE: + case IFSTATE_ELSE_FALSE: + psql_error("\\else: cannot occur after \\else\n"); + success = 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 + */ + success = conditional_stack_pop(cstack); + if (!success) + psql_error("\\endif: no matching \\if\n"); + + 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/command.h b/src/bin/psql/command.h index d0c3264..a396f29 100644 --- a/src/bin/psql/command.h +++ b/src/bin/psql/command.h @@ -10,6 +10,7 @@ #include "fe_utils/print.h" #include "fe_utils/psqlscan.h" +#include "conditional.h" typedef enum _backslashResult @@ -25,6 +26,7 @@ typedef enum _backslashResult extern backslashResult HandleSlashCmds(PsqlScanState scan_state, + ConditionalStack cstack, PQExpBuffer query_buf); extern int process_file(char *filename, bool use_relative_path); diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index 5349c39..665bfef 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) diff --git a/src/bin/psql/conditional.c b/src/bin/psql/conditional.c new file mode 100644 index 0000000..f73ac7c --- /dev/null +++ b/src/bin/psql/conditional.c @@ -0,0 +1,103 @@ +/* + * 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() +{ + ConditionalStack cstack = pg_malloc0(sizeof(ConditionalStackData)); + cstack->head = NULL; + return cstack; +} + +/* + * destroy stack + */ +void +conditional_stack_destroy(const ConditionalStack cstack) +{ + while (conditional_stack_pop(cstack)) + continue; + free(cstack); +} + +/* + * Create a new conditional branch. + */ +bool +conditional_stack_push(const ConditionalStack cstack, + ifState new_state) +{ + IfStackElem *p = (IfStackElem*) pg_malloc0(sizeof(IfStackElem)); + p->if_state = new_state; + p->next = cstack->head; + cstack->head = p; + return true; +} + +/* + * Destroy the topmost conditional branch. + * Returns false if there was no branch to end. + */ +bool +conditional_stack_pop(const 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(const 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 branch state to set. + */ +bool +conditional_stack_poke(const 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-structures + */ +bool +conditional_stack_empty(const ConditionalStack cstack) +{ + return cstack->head == NULL; +} + +/* + * True if the current conditional block (if any) is true + */ +bool +conditional_active(const ConditionalStack cstack) +{ + ifState s = conditional_stack_peek(cstack); + return s == IFSTATE_NONE || s == IFSTATE_TRUE || s == IFSTATE_ELSE_TRUE; +} diff --git a/src/bin/psql/conditional.h b/src/bin/psql/conditional.h new file mode 100644 index 0000000..e947760 --- /dev/null +++ b/src/bin/psql/conditional.h @@ -0,0 +1,65 @@ +/* + * psql - the PostgreSQL interactive terminal + * + * Copyright (c) 2000-2017, PostgreSQL Global Development Group + * + * src/bin/psql/conditional.h + */ +#ifndef CONDITIONAL_H +#define CONDITIONAL_H + +#include "postgres_fe.h" + +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; + +/* + * A mutable stack needs an immutable structure to be passed around + */ +typedef struct ConditionalStackData +{ + IfStackElem *head; +} ConditionalStackData; + +typedef struct ConditionalStackData *ConditionalStack; + +extern ConditionalStack conditional_stack_create(void); + +extern void conditional_stack_destroy(ConditionalStack cstack); + +extern bool conditional_stack_empty(ConditionalStack cstack); + +extern bool 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_active(ConditionalStack cstack); + +#endif /* CONDITIONAL_H */ diff --git a/src/bin/psql/copy.c b/src/bin/psql/copy.c index 481031a..6cfd438 100644 --- a/src/bin/psql/copy.c +++ b/src/bin/psql/copy.c @@ -23,6 +23,7 @@ #include "common.h" #include "prompt.h" #include "stringutils.h" +#include "conditional.h" /* @@ -552,7 +553,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, (ConditionalStack) NULL); fputs(prompt, stdout); fflush(stdout); @@ -590,7 +591,8 @@ 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, + (ConditionalStack) NULL); fputs(prompt, stdout); fflush(stdout); 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 begin a conditional block\n")); + fprintf(output, _(" \\elif 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..0a5b87a 100644 --- a/src/bin/psql/mainloop.c +++ b/src/bin/psql/mainloop.c @@ -10,19 +10,35 @@ #include "command.h" #include "common.h" +#include "conditional.h" #include "input.h" #include "prompt.h" #include "settings.h" #include "mb/pg_wchar.h" - /* callback functions for our flex lexer */ const PsqlScanCallbacks psqlscan_callbacks = { psql_get_variable, psql_error }; +/* + * execute query if branch is active. + * warn interactive users about ignored queries. + */ +static +bool send_query(const char *query, ConditionalStack cstack) +{ + /* execute query if branch is active */ + if (conditional_active(cstack)) + return SendQuery(query); + + if (pset.cur_cmd_interactive) + psql_error("query ignored, use \\endif or Ctrl-C to exit current branch.\n"); + + return true; +} /* * Main processing loop for reading lines of input @@ -50,6 +66,7 @@ MainLoop(FILE *source) volatile promptStatus_t prompt_status = PROMPT_READY; volatile int count_eof = 0; volatile bool die_on_error = false; + ConditionalStack cond_stack; /* Save the prior command source */ FILE *prev_cmd_source; @@ -69,6 +86,7 @@ MainLoop(FILE *source) /* Create working state */ scan_state = psql_scan_create(&psqlscan_callbacks); + cond_stack = conditional_stack_create(); query_buf = createPQExpBuffer(); previous_buf = createPQExpBuffer(); @@ -122,7 +140,18 @@ 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 (!conditional_stack_empty(cond_stack)) + { + psql_error("\\if: escaped\n"); + conditional_stack_pop(cond_stack); + } + } else { successResult = EXIT_USER; @@ -140,7 +169,7 @@ 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 { @@ -296,8 +325,8 @@ MainLoop(FILE *source) line_saved_in_history = true; } - /* execute query */ - success = SendQuery(query_buf->data); + success = send_query(query_buf->data, cond_stack); + slashCmdStatus = success ? PSQL_CMD_SEND : PSQL_CMD_ERROR; pset.stmt_lineno = 1; @@ -343,6 +372,7 @@ MainLoop(FILE *source) /* execute backslash command */ slashCmdStatus = HandleSlashCmds(scan_state, + cond_stack, query_buf->len > 0 ? query_buf : previous_buf); @@ -358,7 +388,7 @@ MainLoop(FILE *source) if (slashCmdStatus == PSQL_CMD_SEND) { - success = SendQuery(query_buf->data); + success = send_query(query_buf->data, cond_stack); /* transfer query to previous_buf by pointer-swapping */ { @@ -367,6 +397,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 +460,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, cond_stack); if (!success && die_on_error) successResult = EXIT_USER; @@ -451,7 +481,21 @@ 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 + && !conditional_stack_empty(cond_stack)) + { + psql_error("found EOF before closing \\endif(s)\n"); + if (die_on_error && !pset.cur_cmd_interactive) + successResult = EXIT_USER; + } + psql_scan_destroy(scan_state); + conditional_stack_destroy(cond_stack); pset.cur_cmd_source = prev_cmd_source; pset.cur_cmd_interactive = prev_cmd_interactive; diff --git a/src/bin/psql/prompt.c b/src/bin/psql/prompt.c index f7930c4..e502ff3 100644 --- a/src/bin/psql/prompt.c +++ b/src/bin/psql/prompt.c @@ -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] = '='; diff --git a/src/bin/psql/prompt.h b/src/bin/psql/prompt.h index 977e754..b3d2d98 100644 --- a/src/bin/psql/prompt.h +++ b/src/bin/psql/prompt.h @@ -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 */ diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c index 88d686a..02be63e 100644 --- a/src/bin/psql/startup.c +++ b/src/bin/psql/startup.c @@ -335,19 +335,24 @@ 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); scan_state = psql_scan_create(&psqlscan_callbacks); + cond_stack = conditional_stack_create(); psql_scan_setup(scan_state, cell->val, strlen(cell->val), pset.encoding, standard_strings()); - successResult = HandleSlashCmds(scan_state, NULL) != PSQL_CMD_ERROR + successResult = HandleSlashCmds(scan_state, + cond_stack, + NULL) != PSQL_CMD_ERROR ? EXIT_SUCCESS : EXIT_FAILURE; psql_scan_destroy(scan_state); + conditional_stack_destroy(cond_stack); } else if (cell->action == ACT_FILE) { diff --git a/src/bin/psql/t/001_if.pl b/src/bin/psql/t/001_if.pl new file mode 100644 index 0000000..ced7e98 --- /dev/null +++ b/src/bin/psql/t/001_if.pl @@ -0,0 +1,42 @@ +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', '\if syntax error' ], + [ "\\if false\n\\elif invalid\n\\echo NO\n\\endif\n\\echo NO\n", '', 'boolean expected', '\elif syntax error' ], + # unmatched checks + [ "\\if true\n", '', 'found EOF before closing.*endif', 'unmatched \if' ], + [ "\\elif true\n\\echo NO\n", '', '.elif: no matching .if', 'unmatched \elif' ], + [ "\\else\n\\echo NO\n", '', '.else: no matching .if', 'unmatched \else' ], + [ "\\endif\n\\echo NO\n", '', '.endif: no matching .if', 'unmatched \endif' ], + # error stop messages + [ "\\set ON_ERROR_STOP on\n\\if error\n\\echo NO\n\\endif\n\\echo NO\n", '', + 'unrecognized value "error" for ".if ": boolean expected', 'if error'], +]; + +# 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/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 026a4f0..aa54184 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -2712,6 +2712,109 @@ 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 +-- invalid boolean expressions mean the \if is ignored +\if invalid_boolean_expression +unrecognized value "invalid_boolean_expression" for "\if ": boolean expected + \echo 'will print anyway #6-1' +will print anyway #6-1 +\else +\else: no matching \if + \echo 'will print anyway #6-2' +will print anyway #6-2 +\endif +\endif: no matching \if +-- 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 variables still expand even in false blocks +\set var 'ab''cd' +-- select :var; +\if false + select :var; +-- this will be skipped because of an unterminated string +\endif +-- fix the unterminated string +'; +-- now the if block can be properly ended +\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..c35516e 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -375,6 +375,106 @@ 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 + +-- invalid boolean expressions mean the \if is ignored +\if invalid_boolean_expression + \echo 'will print anyway #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 variables still expand even in false blocks +\set var 'ab''cd' +-- select :var; +\if false + select :var; +-- this will be skipped because of an unterminated string +\endif +-- fix the unterminated string +'; +-- now the if block can be properly ended +\endif + -- SHOW_CONTEXT \set SHOW_CONTEXT never