diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 9915731..2c3fccd 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2007,6 +2007,83 @@ hello 10 + + \if expr + \elseif expr + \else + \endif + + + This group of commands implements nestable conditional blocks, like + this: + + +\if true + \if 1 + \if yes + \if on + \echo 'all true' +all true + \endif + \endif + \endif +\endif +\if false +\elseif 0 +\elseif no +\elseif off +\else + \echo 'all false' +all false +\endif +\if true +\else + \echo 'should not print #1' +\endif +\if false +\elseif true +\else + \echo 'should not print #2' +\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 + \if-\endif are matched, then + psql will raise an error. + + + The \if and \elseif commands + read the rest of the line and evaluate it as a boolean expression. + Currently, expressions are limited to the same values allowed for + other option booleans (true, false, 1, 0, on, off, yes, no, etc). + + + Queries within a false branch of a conditional block will not be + sent to the server. + + + Non-conditional \-commands within a false branch + of a conditional block will not be evaluated for correctness. The + command will be ignored along with all remaining input to the end + of the line. + + + Expressions on \if and \elseif + commands within a false branch of a conditional block will not be + evaluated. + + + A conditional block can at most one \else command. + + + The \elseif command cannot follow the + \else command. + + + \ir or \include_relative filename diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 4139b77..d4e0bb8 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) && psqlscan_branch_active(scan_state)) { /* eat any remaining arguments after a valid command */ /* note we suppress evaluation of backticks here */ @@ -194,6 +195,32 @@ read_connect_arg(PsqlScanState scan_state) return result; } +/* + * Read and interpret argument as a boolean expression. + */ +static bool +read_boolean_expression(PsqlScanState scan_state, const char *action) +{ + bool result = false; + char *expr = psql_scan_slash_option(scan_state, + OT_NORMAL, NULL, false); + if (expr) + { + if (ParseVariableBool(expr, action)) + result = true; + free(expr); + } + return result; +} + +static bool +is_branching_command(const char *cmd) +{ + return ((strcmp(cmd, "if") == 0 || \ + strcmp(cmd, "elseif") == 0 || \ + strcmp(cmd, "else") == 0 || \ + strcmp(cmd, "endif") == 0)); +} /* * Subroutine to actually try to execute a backslash command. @@ -207,6 +234,14 @@ exec_command(const char *cmd, * failed */ backslashResult status = PSQL_CMD_SKIP_LINE; + if (!psqlscan_branch_active(scan_state) && !is_branching_command(cmd) ) + { + /* Continue with an empty buffer as if the command were never read */ + resetPQExpBuffer(query_buf); /* TODO: is this needed? */ + psql_scan_reset(scan_state); /* TODO: is this needed or just for interactive? */ + return status; + } + /* * \a -- toggle field alignment This makes little sense but we keep it * around. @@ -984,6 +1019,84 @@ exec_command(const char *cmd, } } + else if (strcmp(cmd, "if") == 0) + { + ifState new_if_state = IFSTATE_IGNORED; + if (psqlscan_branch_active(scan_state)) + { + if (read_boolean_expression(scan_state, "\\if")) + new_if_state = IFSTATE_TRUE; + else + new_if_state = IFSTATE_FALSE; + } + psqlscan_branch_push(scan_state,new_if_state); + psql_scan_reset(scan_state); + } + + else if (strcmp(cmd, "elseif") == 0) + { + if (psqlscan_branch_empty(scan_state)) + psql_error("encountered un-matched \\elseif\n"); + + switch (psqlscan_branch_get_state(scan_state)) + { + case IFSTATE_IGNORED: + /* inactive branch, do nothing */ + break; + case IFSTATE_TRUE: + /* just finished true section of active branch */ + psqlscan_branch_set_state(scan_state, IFSTATE_IGNORED); + break; + case IFSTATE_FALSE: + /* determine if this section is true or not */ + if (read_boolean_expression(scan_state, "\\elseif")) + psqlscan_branch_set_state(scan_state, IFSTATE_TRUE); + break; + case IFSTATE_ELSE_TRUE: + case IFSTATE_ELSE_FALSE: + psql_error("encountered \\elseif after \\else\n"); + break; + default: + break; + } + psql_scan_reset(scan_state); + } + + else if (strcmp(cmd, "else") == 0) + { + if (psqlscan_branch_empty(scan_state)) + psql_error("encountered un-matched \\else\n"); + + switch (psqlscan_branch_get_state(scan_state)) + { + case IFSTATE_TRUE: + /* just finished true section of active branch */ + case IFSTATE_IGNORED: + /* whole branch was inactive */ + psqlscan_branch_set_state(scan_state, IFSTATE_ELSE_FALSE); + break; + case IFSTATE_FALSE: + /* just finished true section of active branch */ + psqlscan_branch_set_state(scan_state, IFSTATE_ELSE_TRUE); + break; + case IFSTATE_ELSE_TRUE: + case IFSTATE_ELSE_FALSE: + psql_error("encountered \\else after \\else\n"); + break; + default: + break; + } + psql_scan_reset(scan_state); + } + + else if (strcmp(cmd, "endif") == 0) + { + if (psqlscan_branch_empty(scan_state)) + psql_error("encountered un-matched \\endif\n"); + psqlscan_branch_end_state(scan_state); + 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/mainloop.c b/src/bin/psql/mainloop.c index bb306a4..8f12e55 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,7 +23,6 @@ const PsqlScanCallbacks psqlscan_callbacks = { psql_error }; - /* * Main processing loop for reading lines of input * and sending them to the backend. @@ -51,6 +50,9 @@ MainLoop(FILE *source) volatile int count_eof = 0; volatile bool die_on_error = false; + /* only needed at the end to detect unbalanced ifs in scan_state */ + bool if_endifs_balanced = true; + /* Save the prior command source */ FILE *prev_cmd_source; bool prev_cmd_interactive; @@ -285,21 +287,28 @@ MainLoop(FILE *source) if (scan_result == PSCAN_SEMICOLON || (scan_result == PSCAN_EOL && pset.singleline)) { - /* - * Save query in history. We use history_buf to accumulate - * multi-line queries into a single history entry. - */ - if (pset.cur_cmd_interactive && !line_saved_in_history) + if (psqlscan_branch_active(scan_state)) { - pg_append_history(line, history_buf); - pg_send_history(history_buf); - line_saved_in_history = true; + /* + * Save query in history. We use history_buf to accumulate + * multi-line queries into a single history entry. + */ + if (pset.cur_cmd_interactive && !line_saved_in_history) + { + pg_append_history(line, history_buf); + pg_send_history(history_buf); + line_saved_in_history = true; + } + + /* execute query */ + success = SendQuery(query_buf->data); } + else + success = true; - /* execute query */ - success = SendQuery(query_buf->data); slashCmdStatus = success ? PSQL_CMD_SEND : PSQL_CMD_ERROR; pset.stmt_lineno = 1; + slashCmdStatus = success ? PSQL_CMD_SEND : PSQL_CMD_ERROR; /* transfer query to previous_buf by pointer-swapping */ { @@ -352,21 +361,28 @@ MainLoop(FILE *source) if ((slashCmdStatus == PSQL_CMD_SEND || slashCmdStatus == PSQL_CMD_NEWEDIT) && query_buf->len == 0) { + /* TODO check if-then-skip-state */ /* copy previous buffer to current for handling */ appendPQExpBufferStr(query_buf, previous_buf->data); } if (slashCmdStatus == PSQL_CMD_SEND) { - success = SendQuery(query_buf->data); - - /* transfer query to previous_buf by pointer-swapping */ + if (psqlscan_branch_active(scan_state)) { - PQExpBuffer swap_buf = previous_buf; + success = SendQuery(query_buf->data); - previous_buf = query_buf; - query_buf = swap_buf; + /* transfer query to previous_buf by pointer-swapping */ + { + PQExpBuffer swap_buf = previous_buf; + + previous_buf = query_buf; + query_buf = swap_buf; + } } + else + success = true; + resetPQExpBuffer(query_buf); /* flush any paren nesting info after forced send */ @@ -425,12 +441,17 @@ MainLoop(FILE *source) if (query_buf->len > 0 && !pset.cur_cmd_interactive && successResult == EXIT_SUCCESS) { - /* save query in history */ - if (pset.cur_cmd_interactive) - pg_send_history(history_buf); + if (psqlscan_branch_active(scan_state)) + { + /* save query in history */ + if (pset.cur_cmd_interactive) + pg_send_history(history_buf); - /* execute query */ - success = SendQuery(query_buf->data); + /* execute query */ + success = SendQuery(query_buf->data); + } + else + success = true; if (!success && die_on_error) successResult = EXIT_USER; @@ -451,11 +472,17 @@ MainLoop(FILE *source) destroyPQExpBuffer(previous_buf); destroyPQExpBuffer(history_buf); + if (slashCmdStatus != PSQL_CMD_TERMINATE) + if_endifs_balanced = psqlscan_branch_empty(scan_state); + psql_scan_destroy(scan_state); pset.cur_cmd_source = prev_cmd_source; pset.cur_cmd_interactive = prev_cmd_interactive; pset.lineno = prev_lineno; + if (! if_endifs_balanced ) + psql_error("found EOF before closing \\endif(s)\n"); + return successResult; } /* MainLoop() */ diff --git a/src/bin/psql/mainloop.h b/src/bin/psql/mainloop.h index 228a5e0..47f4c32 100644 --- a/src/bin/psql/mainloop.h +++ b/src/bin/psql/mainloop.h @@ -14,4 +14,5 @@ extern const PsqlScanCallbacks psqlscan_callbacks; extern int MainLoop(FILE *source); + #endif /* MAINLOOP_H */ diff --git a/src/fe_utils/psqlscan.l b/src/fe_utils/psqlscan.l index 1b29341..881c4f8 100644 --- a/src/fe_utils/psqlscan.l +++ b/src/fe_utils/psqlscan.l @@ -904,6 +904,9 @@ psql_scan_create(const PsqlScanCallbacks *callbacks) psql_scan_reset(state); + state->branch_stack = NULL; + state->branch_block_active = true; + return state; } @@ -919,6 +922,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 +1436,103 @@ psqlscan_escape_variable(PsqlScanState state, const char *txt, int len, psqlscan_emit(state, txt, len); } } + +/* + * psqlscan_branch_empty + * + * True if there are no active \if-structures + */ +bool +psqlscan_branch_empty(PsqlScanState state) +{ + return (state->branch_stack == NULL); +} + +/* + * 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) +{ + return state->branch_block_active; +} + +/* + * Fetch the current state of the top of the stack + */ +ifState +psqlscan_branch_get_state(PsqlScanState state) +{ + if (psqlscan_branch_empty(state)) + return IFSTATE_NONE; + return state->branch_stack->if_state; +} + +/* + * psqlscan_branch_update_active + * + * Scan the branch_stack to determine whether the next statements + * can execute or should be skipped. Cache this result in + * branch_block_active. + */ +static void +psqlscan_branch_update_active(PsqlScanState state) +{ + ifState s = psqlscan_branch_get_state(state); + state->branch_block_active = ( (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; + psqlscan_branch_update_active(state); + 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_empty(state)) + return false; + state->branch_stack->if_state = new_state; + psqlscan_branch_update_active(state); + return true; +} + +/* + * psqlscan_branch_end_state + * + * Destroy the topmost branch because and \endif was encountered. + * Returns false if there was no branch to end. + */ +bool +psqlscan_branch_end_state(PsqlScanState state) +{ + IfStackElem *p = state->branch_stack; + if (!p) + return false; + state->branch_stack = state->branch_stack->next; + free(p); + psqlscan_branch_update_active(state); + return true; +} diff --git a/src/include/fe_utils/psqlscan_int.h b/src/include/fe_utils/psqlscan_int.h index 0fddc7a..e9773ca 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 \elseif which is true + * and all parent branches (if any) are true */ + IFSTATE_FALSE, /* currently in an \if or \elseif which is false + * but no true branch has yet been seen, + * and all parent branches (if any) are true */ + IFSTATE_IGNORED, /* currently in an \elseif 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,12 @@ typedef struct PsqlScanStateData * Callback functions provided by the program making use of the lexer. */ const PsqlScanCallbacks *callbacks; + + /* + * \if branch state variables + */ + IfStackElem *branch_stack; + bool branch_block_active; } PsqlScanStateData; @@ -141,4 +171,21 @@ extern void psqlscan_escape_variable(PsqlScanState state, const char *txt, int len, bool as_ident); +/* + * branching commands + */ +extern bool psqlscan_branch_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_end_state(PsqlScanState state); + #endif /* PSQLSCAN_INT_H */ diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 464436a..7492a92 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -2686,6 +2686,33 @@ deallocate q; \pset format aligned \pset expanded off \pset border 1 +\if true + \if 1 + \if yes + \if on + \echo 'all true' +all true + \endif + \endif + \endif +\endif +\if false +\elseif 0 +\elseif no +\elseif off +\else + \echo 'all false' +all false +\endif +\if true +\else + \echo 'should not print #1' +\endif +\if false +\elseif true +\else + \echo 'should not print #2' +\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 900aa7e..ef1be30 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -357,6 +357,36 @@ deallocate q; \pset expanded off \pset border 1 +\if true + \if 1 + \if yes + \if on + \echo 'all true' + \endif + \endif + \endif +\endif + +\if false +\elseif 0 +\elseif no +\elseif off +\else + \echo 'all false' +\endif + +\if true +\else + \echo 'should not print #1' +\endif + +\if false +\elseif true +\else + \echo 'should not print #2' +\endif + + -- SHOW_CONTEXT \set SHOW_CONTEXT never