diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 9915731..20091e5 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2007,6 +2007,78 @@ hello 10 + + \if expr + \elif expr + \else + \endif + + + This group of commands implements nestable conditional blocks, like + this: + + +SELECT + EXISTS(SELECT 1 FROM customer) as has_customers, + EXISTS(SELECT 1 FROM employee) as has_employees +\gset +\if :has_users + SELECT * FROM customer ORDER BY creation_date LIMIT 5; +\elif :has_employees + \echo 'no customers found' + SELECT * FROM employee ORDER BY creation_date LIMIT 5; +\else + \if yes + \echo 'No customers or employees' + \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 + \if-\endif are matched, then + psql will raise an error. + + + 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 is evaluated like other options booleans, so the valid values + are any unabiguous case insensitive matches for one of: + true, false, 1, + 0, on, off, + yes, no. So + t, T, and tR + will all match true. + + + 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 \elif + commands within a false branch of a conditional block will not be + evaluated. + + + A conditional block can at most one \else command. + + + The \elif 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..feb9ddc 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,68 @@ 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) +{ + bool success = false; + char *value = psql_scan_slash_option(scan_state, + OT_NORMAL, NULL, false); + /* + * placeholder code until ParseVariableBool() ads error detection + * once that patch is in place, use that instead + */ + if (value) + { + size_t len; + + if (value == NULL) + return false; /* not set -> assume "off" */ + + len = strlen(value); + + if ((pg_strncasecmp(value, "true", len) == 0) || + (pg_strncasecmp(value, "yes", len) == 0) || + (pg_strncasecmp(value, "on", (len > 2 ? len : 2)) == 0) || + (pg_strcasecmp(value, "1") == 0)) + { + success = true; + *result = true; + } + else if ((pg_strncasecmp(value, "false", len) == 0) || + (pg_strncasecmp(value, "no", len) == 0) || + (pg_strncasecmp(value, "off", (len > 2 ? len : 2)) == 0) || + (pg_strcasecmp(value, "0") == 0)) + { + success = true; + *result = false; + } + else + { + psql_error("\\%s: invalid boolean expression: %s\n", + action, value); + } + free(value); + } + else + { + psql_error("\\%s: no expression given\n",action); + } + return success; +} + +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 +270,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); + psql_scan_reset(scan_state); + return status; + } + /* * \a -- toggle field alignment This makes little sense but we keep it * around. @@ -984,6 +1055,114 @@ exec_command(const char *cmd, } } + else if (strcmp(cmd, "if") == 0) + { + ifState new_if_state = IFSTATE_IGNORED; + if (psqlscan_branch_active(scan_state)) + { + bool if_true = false; + success = read_boolean_expression(scan_state, "if", &if_true); + if (success) + { + if (if_true) + new_if_state = IFSTATE_TRUE; + else + new_if_state = IFSTATE_FALSE; + } + } + if (success) + psqlscan_branch_push(scan_state,new_if_state); + psql_scan_reset(scan_state); + } + + else if (strcmp(cmd, "elif") == 0) + { + if (psqlscan_branch_empty(scan_state)) + { + psql_error("encountered un-matched \\elif\n"); + success = false; + } + else + { + bool elif_true = false; + 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 */ + success = read_boolean_expression(scan_state, "elif", + &elif_true); + if (success) + { + if (elif_true) + psqlscan_branch_set_state(scan_state, IFSTATE_TRUE); + } + break; + case IFSTATE_ELSE_TRUE: + case IFSTATE_ELSE_FALSE: + psql_error("encountered \\elif after \\else\n"); + success = false; + 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"); + success = false; + } + else + { + 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"); + success = false; + 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"); + success = false; + } + else + { + 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..7252824 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 */ { @@ -358,15 +367,21 @@ MainLoop(FILE *source) 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 +440,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 +471,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..f70841c 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..734e719 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,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..1dcaa46 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -2686,6 +2686,66 @@ deallocate q; \pset format aligned \pset expanded off \pset border 1 +\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 +\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 +\if true + \echo 'first thing true' +first thing true +\else + \echo 'should not print #3-1' +\endif +\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 +\endif +encountered un-matched \endif +\else +encountered un-matched \else +\elif +encountered un-matched \elif +\if true +\else +\else +encountered \else after \else +\endif +\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 900aa7e..4f41894 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -357,6 +357,66 @@ deallocate q; \pset expanded off \pset border 1 +\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 + +\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 + +\if true + \echo 'first thing true' +\else + \echo 'should not print #3-1' +\endif + +\if false + \echo 'should not print #4-1' +\elif true + \echo 'second thing true' +\else + \echo 'should not print #5-1' +\endif + +\endif + +\else + +\elif + +\if true +\else +\else +\endif + +\if false +\else +\elif +\endif + -- SHOW_CONTEXT \set SHOW_CONTEXT never