diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index ae58708..dac8e37 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2035,6 +2035,79 @@ 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 +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 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. + + + \ir or \include_relative filename 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..6c15c01 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,141 @@ 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) + { + /* + * only evaluate the expression for truth if the underlying + * branch is active + */ + if (conditional_active(cstack)) + { + bool if_true = false; + success = read_boolean_expression(scan_state, "\\if ", &if_true); + if (success) + conditional_stack_push(cstack, + (if_true) ? IFSTATE_TRUE : IFSTATE_FALSE); + } + psql_scan_reset(scan_state); + } + + /* + * \elif is part of an \if..\endif block + * 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 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 (conditional_stack_peek(cstack)) + { + 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 active branch + * do not evaluate expression, just skip + */ + conditional_stack_poke(cstack, IFSTATE_IGNORED); + 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. + */ + success = read_boolean_expression(scan_state, "\\elif ", + &elif_true); + if (success && elif_true) + conditional_stack_poke(cstack, IFSTATE_TRUE); + break; + case IFSTATE_ELSE_TRUE: + case IFSTATE_ELSE_FALSE: + psql_error("\\elif: cannot occur after \\else\n"); + success = false; + break; + case IFSTATE_NONE: + /* no if to elif from */ + psql_error("\\elif: no matching \\if\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 (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/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..9d3069e 100644 --- a/src/bin/psql/mainloop.c +++ b/src/bin/psql/mainloop.c @@ -10,12 +10,13 @@ #include "command.h" #include "common.h" +#include "conditional.h" #include "input.h" #include "prompt.h" #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 +24,22 @@ 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, 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 +67,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 +87,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 +141,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 +170,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 +326,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 +373,7 @@ MainLoop(FILE *source) /* execute backslash command */ slashCmdStatus = HandleSlashCmds(scan_state, + cond_stack, query_buf->len > 0 ? query_buf : previous_buf); @@ -358,7 +389,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 +398,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 +461,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 +482,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/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 026a4f0..2c56e3f 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -2712,6 +2712,86 @@ 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 -- 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..69fc57c 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -375,6 +375,82 @@ 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 + -- SHOW_CONTEXT \set SHOW_CONTEXT never