From e984ef5861df4bc9733b36271d05763e82de7c04 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Thu, 30 Mar 2017 12:59:11 -0400 Subject: [PATCH] Support \if ... \elif ... \else ... \endif in psql scripting. This patch adds nestable conditional blocks to psql. The control structure feature per se is complete, but the boolean expressions understood by \if and \elif are pretty primitive; basically, after variable substitution and backtick expansion, the result has to be "true" or "false" or one of the other standard spellings of a boolean value. But that's enough for many purposes, since you can always do the heavy lifting on the server side; and we can extend it later. Along the way, pay down some of the technical debt that had built up around psql/command.c: * Refactor exec_command() into a function per command, instead of being a 1500-line monstrosity. This makes the file noticeably longer because of repetitive function header/trailer overhead, but it seems much more readable. * Teach psql_get_variable() and psqlscanslash.l to suppress variable substitution and backtick expansion on the basis of the conditional stack state, thereby allowing removal of the OT_NO_EVAL kluge. * Fix the no-doubt-once-expedient hack of sometimes silently substituting mainloop.c's previous_buf for query_buf when calling HandleSlashCmds. (It's a bit remarkable that commands like \r worked at all with that.) Recall of a previous query is now done explicitly in the slash commands where that should happen. Corey Huinker, reviewed by Fabien Coelho, further hacking by me Discussion: https://postgr.es/m/CADkLM=c94OSRTnat=LX0ivNq4pxDNeoomFfYvBKM5N_xfmLtAA@mail.gmail.com --- doc/src/sgml/ref/psql-ref.sgml | 92 +- src/bin/psql/Makefile | 8 +- src/bin/psql/command.c | 1578 ++++++++++++++++++++++++---- src/bin/psql/command.h | 5 +- src/bin/psql/common.c | 7 +- src/bin/psql/conditional.c | 153 +++ src/bin/psql/conditional.h | 83 ++ src/bin/psql/copy.c | 4 +- src/bin/psql/help.c | 9 +- src/bin/psql/mainloop.c | 119 ++- src/bin/psql/prompt.c | 6 +- src/bin/psql/prompt.h | 3 +- src/bin/psql/psqlscanslash.h | 7 +- src/bin/psql/psqlscanslash.l | 46 +- src/bin/psql/startup.c | 9 +- src/test/regress/expected/psql.out | 169 +++ src/test/regress/sql/psql.sql | 144 +++ 17 files changed, 2172 insertions(+), 270 deletions(-) create mode 100644 src/bin/psql/conditional.c create mode 100644 src/bin/psql/conditional.h diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 2a9c412020..b51b11baa3 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2063,6 +2063,95 @@ hello 10 + + \if expression + \elif expression + \else + \endif + + + This group of commands implements nestable conditional blocks. + A conditional block must begin with an \if and end + with an \endif. In between there may be any number + of \elif clauses, which may optionally be followed + by a single \else clause. Ordinary queries and + other types of backslash commands may (and usually do) appear between + the commands forming a conditional block. + + + The \if and \elif commands read + their argument(s) and evaluate them as a boolean expression. If the + expression yields true then processing continues + normally; otherwise, lines are skipped until a + matching \elif, \else, + or \endif is reached. Once + an \if or \elif test has + succeeded, the arguments of later \elif commands in + the same block are not evaluated but are treated as false. Lines + following an \else are processed only if no earlier + matching \if or \elif succeeded. + + + The expression argument + of an \if or \elif command + is subject to variable interpolation and backquote expansion, just + like any other backslash command argument. After that it is evaluated + like the value of an on/off option variable. So a valid value + is any unambiguous case-insensitive match for one of: + true, false, 1, + 0, on, off, + yes, no. For example, + t, T, and tR + will all be considered to be true. + + + Expressions that do not properly evaluate to true or false will + generate a warning and be treated as false. + + + Lines being skipped are parsed normally to identify queries and + backslash commands, but queries are not sent to the server, and + backslash commands other than conditionals + (\if, \elif, + \else, \endif) are + ignored. Conditional commands are checked only for valid nesting. + Variable references in skipped lines are not expanded, and backquote + expansion is not performed either. + + + All the backslash commands of a given conditional block must appear in + the same source file. If EOF is reached on the main input file or an + \include-ed file before all local + \if-blocks have been closed, + then psql will raise an error. + + + Here is an example: + + +-- 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 will never print' + \endif +\endif + + + + + \l[+] or \list[+] [ pattern ] @@ -3715,7 +3804,8 @@ testdb=> INSERT INTO my_table VALUES (:'content'); In prompt 1 normally =, - but ^ if in single-line mode, + but @ if the session is in an inactive branch of a + 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 diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile index f8e31eacbe..ab2cfa6353 100644 --- a/src/bin/psql/Makefile +++ b/src/bin/psql/Makefile @@ -21,10 +21,10 @@ 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 \ - startup.o prompt.o variables.o large_obj.o describe.o \ - crosstabview.o tab-complete.o \ - sql_help.o psqlscanslash.o \ +OBJS= command.o common.o conditional.o copy.o crosstabview.o \ + describe.o help.o input.o large_obj.o mainloop.o \ + prompt.o psqlscanslash.o sql_help.o startup.o stringutils.o \ + tab-complete.o variables.o \ $(WIN32RES) diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 4f4a0aa9bd..94a3cfce90 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -56,14 +56,103 @@ typedef enum EditableObjectType EditableView } EditableObjectType; -/* functions for use in this file */ +/* local function declarations */ static backslashResult exec_command(const char *cmd, PsqlScanState scan_state, - PQExpBuffer query_buf); -static bool do_edit(const char *filename_arg, PQExpBuffer query_buf, - int lineno, bool *edited); + ConditionalStack cstack, + PQExpBuffer query_buf, + PQExpBuffer previous_buf); +static backslashResult exec_command_a(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_C(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_connect(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_cd(PsqlScanState scan_state, bool active_branch, + const char *cmd); +static backslashResult exec_command_conninfo(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_copy(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_copyright(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_crosstabview(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_d(PsqlScanState scan_state, bool active_branch, + const char *cmd); +static backslashResult exec_command_edit(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf, PQExpBuffer previous_buf); +static backslashResult exec_command_ef(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf); +static backslashResult exec_command_ev(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf); +static backslashResult exec_command_echo(PsqlScanState scan_state, bool active_branch, + const char *cmd); +static backslashResult exec_command_elif(PsqlScanState scan_state, ConditionalStack cstack, + PQExpBuffer query_buf); +static backslashResult exec_command_else(PsqlScanState scan_state, ConditionalStack cstack, + PQExpBuffer query_buf); +static backslashResult exec_command_endif(PsqlScanState scan_state, ConditionalStack cstack, + PQExpBuffer query_buf); +static backslashResult exec_command_encoding(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_errverbose(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_f(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_g(PsqlScanState scan_state, bool active_branch, + const char *cmd); +static backslashResult exec_command_gexec(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_gset(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_help(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_html(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_include(PsqlScanState scan_state, bool active_branch, + const char *cmd); +static backslashResult exec_command_if(PsqlScanState scan_state, ConditionalStack cstack, + PQExpBuffer query_buf); +static backslashResult exec_command_list(PsqlScanState scan_state, bool active_branch, + const char *cmd); +static backslashResult exec_command_lo(PsqlScanState scan_state, bool active_branch, + const char *cmd); +static backslashResult exec_command_out(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_print(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf); +static backslashResult exec_command_password(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_prompt(PsqlScanState scan_state, bool active_branch, + const char *cmd); +static backslashResult exec_command_pset(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_quit(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_reset(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf); +static backslashResult exec_command_s(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_set(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_setenv(PsqlScanState scan_state, bool active_branch, + const char *cmd); +static backslashResult exec_command_sf(PsqlScanState scan_state, bool active_branch, + const char *cmd); +static backslashResult exec_command_sv(PsqlScanState scan_state, bool active_branch, + const char *cmd); +static backslashResult exec_command_t(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_T(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_timing(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_unset(PsqlScanState scan_state, bool active_branch, + const char *cmd); +static backslashResult exec_command_write(PsqlScanState scan_state, bool active_branch, + const char *cmd, + PQExpBuffer query_buf, PQExpBuffer previous_buf); +static backslashResult exec_command_watch(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf, PQExpBuffer previous_buf); +static backslashResult exec_command_x(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_z(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_shell_escape(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_slash_command_help(PsqlScanState scan_state, bool active_branch); +static char *read_connect_arg(PsqlScanState scan_state); +static PQExpBuffer gather_boolean_expression(PsqlScanState scan_state); +static bool is_true_boolean_expression(PsqlScanState scan_state, const char *name); +static void ignore_boolean_expression(PsqlScanState scan_state); +static void ignore_slash_options(PsqlScanState scan_state); +static void ignore_slash_filepipe(PsqlScanState scan_state); +static void ignore_slash_whole_line(PsqlScanState scan_state); +static bool is_branching_command(const char *cmd); +static void save_query_text_state(PsqlScanState scan_state, ConditionalStack cstack, + PQExpBuffer query_buf); +static void discard_query_text(PsqlScanState scan_state, ConditionalStack cstack, + PQExpBuffer query_buf); +static void copy_previous_query(PQExpBuffer query_buf, PQExpBuffer previous_buf); static bool do_connect(enum trivalue reuse_previous_specification, char *dbname, char *user, char *host, char *port); +static bool do_edit(const char *filename_arg, PQExpBuffer query_buf, + int lineno, bool *edited); static bool do_shell(const char *command); static bool do_watch(PQExpBuffer query_buf, double sleep); static bool lookup_object_oid(EditableObjectType obj_type, const char *desc, @@ -96,9 +185,18 @@ static void checkWin32Codepage(void); * just after the '\'. The lexer is advanced past the command and all * arguments on return. * - * 'query_buf' contains the query-so-far, which may be modified by + * cstack is the current \if stack state. This will be examined, and + * possibly modified by conditional commands. + * + * query_buf contains the query-so-far, which may be modified by * execution of the backslash command (for example, \r clears it). - * query_buf can be NULL if there is no query so far. + * + * previous_buf contains the query most recently sent to the server + * (empty if none yet). This should not be modified here, but some + * commands copy its content into query_buf. + * + * query_buf and previous_buf will be NULL when executing a "-c" + * command-line option. * * Returns a status code indicating what action is desired, see command.h. *---------- @@ -106,19 +204,22 @@ static void checkWin32Codepage(void); backslashResult HandleSlashCmds(PsqlScanState scan_state, - PQExpBuffer query_buf) + ConditionalStack cstack, + PQExpBuffer query_buf, + PQExpBuffer previous_buf) { - backslashResult status = PSQL_CMD_SKIP_LINE; + backslashResult status; char *cmd; char *arg; Assert(scan_state != NULL); + Assert(cstack != NULL); /* Parse off the command name */ 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, previous_buf); if (status == PSQL_CMD_UNKNOWN) { @@ -131,14 +232,22 @@ HandleSlashCmds(PsqlScanState scan_state, if (status != PSQL_CMD_ERROR) { - /* eat any remaining arguments after a valid command */ - /* note we suppress evaluation of backticks here */ + /* + * Eat any remaining arguments after a valid command. We want to + * suppress evaluation of backticks in this situation, so transiently + * push an inactive conditional-stack entry. + */ + bool active_branch = conditional_active(cstack); + + conditional_stack_push(cstack, IFSTATE_IGNORED); while ((arg = psql_scan_slash_option(scan_state, - OT_NO_EVAL, NULL, false))) + OT_NORMAL, NULL, false))) { - psql_error("\\%s: extra argument \"%s\" ignored\n", cmd, arg); + if (active_branch) + psql_error("\\%s: extra argument \"%s\" ignored\n", cmd, arg); free(arg); } + conditional_stack_pop(cstack); } else { @@ -159,56 +268,169 @@ HandleSlashCmds(PsqlScanState scan_state, return status; } + /* - * Read and interpret an argument to the \connect slash command. + * Subroutine to actually try to execute a backslash command. + * + * The typical "success" result code is PSQL_CMD_SKIP_LINE, although some + * commands return something else. Failure results are PSQL_CMD_ERROR, + * unless PSQL_CMD_UNKNOWN is more appropriate. */ -static char * -read_connect_arg(PsqlScanState scan_state) +static backslashResult +exec_command(const char *cmd, + PsqlScanState scan_state, + ConditionalStack cstack, + PQExpBuffer query_buf, + PQExpBuffer previous_buf) { - char *result; - char quote; + backslashResult status; + bool active_branch = conditional_active(cstack); /* - * Ideally we should treat the arguments as SQL identifiers. But for - * backwards compatibility with 7.2 and older pg_dump files, we have to - * take unquoted arguments verbatim (don't downcase them). For now, - * double-quoted arguments may be stripped of double quotes (as if SQL - * identifiers). By 7.4 or so, pg_dump files can be expected to - * double-quote all mixed-case \connect arguments, and then we can get rid - * of OT_SQLIDHACK. + * In interactive mode, warn when we're ignoring a command within a false + * \if-branch. But we continue on, so as to parse and discard the right + * amount of parameter text. Each individual backslash command subroutine + * is responsible for doing nothing after discarding appropriate + * arguments, if !active_branch. */ - result = psql_scan_slash_option(scan_state, OT_SQLIDHACK, "e, true); - - if (!result) - return NULL; + if (pset.cur_cmd_interactive && !active_branch && + !is_branching_command(cmd)) + { + psql_error("\\%s command ignored; use \\endif or Ctrl-C to exit current \\if block\n", + cmd); + } - if (quote) - return result; + if (strcmp(cmd, "a") == 0) + status = exec_command_a(scan_state, active_branch); + else if (strcmp(cmd, "C") == 0) + status = exec_command_C(scan_state, active_branch); + else if (strcmp(cmd, "c") == 0 || strcmp(cmd, "connect") == 0) + status = exec_command_connect(scan_state, active_branch); + else if (strcmp(cmd, "cd") == 0) + status = exec_command_cd(scan_state, active_branch, cmd); + else if (strcmp(cmd, "conninfo") == 0) + status = exec_command_conninfo(scan_state, active_branch); + else if (pg_strcasecmp(cmd, "copy") == 0) + status = exec_command_copy(scan_state, active_branch); + else if (strcmp(cmd, "copyright") == 0) + status = exec_command_copyright(scan_state, active_branch); + else if (strcmp(cmd, "crosstabview") == 0) + status = exec_command_crosstabview(scan_state, active_branch); + else if (cmd[0] == 'd') + status = exec_command_d(scan_state, active_branch, cmd); + else if (strcmp(cmd, "e") == 0 || strcmp(cmd, "edit") == 0) + status = exec_command_edit(scan_state, active_branch, + query_buf, previous_buf); + else if (strcmp(cmd, "ef") == 0) + status = exec_command_ef(scan_state, active_branch, query_buf); + else if (strcmp(cmd, "ev") == 0) + status = exec_command_ev(scan_state, active_branch, query_buf); + else if (strcmp(cmd, "echo") == 0 || strcmp(cmd, "qecho") == 0) + status = exec_command_echo(scan_state, active_branch, cmd); + else if (strcmp(cmd, "elif") == 0) + status = exec_command_elif(scan_state, cstack, query_buf); + else if (strcmp(cmd, "else") == 0) + status = exec_command_else(scan_state, cstack, query_buf); + else if (strcmp(cmd, "endif") == 0) + status = exec_command_endif(scan_state, cstack, query_buf); + else if (strcmp(cmd, "encoding") == 0) + status = exec_command_encoding(scan_state, active_branch); + else if (strcmp(cmd, "errverbose") == 0) + status = exec_command_errverbose(scan_state, active_branch); + else if (strcmp(cmd, "f") == 0) + status = exec_command_f(scan_state, active_branch); + else if (strcmp(cmd, "g") == 0 || strcmp(cmd, "gx") == 0) + status = exec_command_g(scan_state, active_branch, cmd); + else if (strcmp(cmd, "gexec") == 0) + status = exec_command_gexec(scan_state, active_branch); + else if (strcmp(cmd, "gset") == 0) + status = exec_command_gset(scan_state, active_branch); + else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0) + status = exec_command_help(scan_state, active_branch); + else if (strcmp(cmd, "H") == 0 || strcmp(cmd, "html") == 0) + status = exec_command_html(scan_state, active_branch); + else if (strcmp(cmd, "i") == 0 || strcmp(cmd, "include") == 0 || + strcmp(cmd, "ir") == 0 || strcmp(cmd, "include_relative") == 0) + status = exec_command_include(scan_state, active_branch, cmd); + else if (strcmp(cmd, "if") == 0) + status = exec_command_if(scan_state, cstack, query_buf); + else if (strcmp(cmd, "l") == 0 || strcmp(cmd, "list") == 0 || + strcmp(cmd, "l+") == 0 || strcmp(cmd, "list+") == 0) + status = exec_command_list(scan_state, active_branch, cmd); + else if (strncmp(cmd, "lo_", 3) == 0) + status = exec_command_lo(scan_state, active_branch, cmd); + else if (strcmp(cmd, "o") == 0 || strcmp(cmd, "out") == 0) + status = exec_command_out(scan_state, active_branch); + else if (strcmp(cmd, "p") == 0 || strcmp(cmd, "print") == 0) + status = exec_command_print(scan_state, active_branch, query_buf); + else if (strcmp(cmd, "password") == 0) + status = exec_command_password(scan_state, active_branch); + else if (strcmp(cmd, "prompt") == 0) + status = exec_command_prompt(scan_state, active_branch, cmd); + else if (strcmp(cmd, "pset") == 0) + status = exec_command_pset(scan_state, active_branch); + else if (strcmp(cmd, "q") == 0 || strcmp(cmd, "quit") == 0) + status = exec_command_quit(scan_state, active_branch); + else if (strcmp(cmd, "r") == 0 || strcmp(cmd, "reset") == 0) + status = exec_command_reset(scan_state, active_branch, query_buf); + else if (strcmp(cmd, "s") == 0) + status = exec_command_s(scan_state, active_branch); + else if (strcmp(cmd, "set") == 0) + status = exec_command_set(scan_state, active_branch); + else if (strcmp(cmd, "setenv") == 0) + status = exec_command_setenv(scan_state, active_branch, cmd); + else if (strcmp(cmd, "sf") == 0 || strcmp(cmd, "sf+") == 0) + status = exec_command_sf(scan_state, active_branch, cmd); + else if (strcmp(cmd, "sv") == 0 || strcmp(cmd, "sv+") == 0) + status = exec_command_sv(scan_state, active_branch, cmd); + else if (strcmp(cmd, "t") == 0) + status = exec_command_t(scan_state, active_branch); + else if (strcmp(cmd, "T") == 0) + status = exec_command_T(scan_state, active_branch); + else if (strcmp(cmd, "timing") == 0) + status = exec_command_timing(scan_state, active_branch); + else if (strcmp(cmd, "unset") == 0) + status = exec_command_unset(scan_state, active_branch, cmd); + else if (strcmp(cmd, "w") == 0 || strcmp(cmd, "write") == 0) + status = exec_command_write(scan_state, active_branch, cmd, + query_buf, previous_buf); + else if (strcmp(cmd, "watch") == 0) + status = exec_command_watch(scan_state, active_branch, + query_buf, previous_buf); + else if (strcmp(cmd, "x") == 0) + status = exec_command_x(scan_state, active_branch); + else if (strcmp(cmd, "z") == 0) + status = exec_command_z(scan_state, active_branch); + else if (strcmp(cmd, "!") == 0) + status = exec_command_shell_escape(scan_state, active_branch); + else if (strcmp(cmd, "?") == 0) + status = exec_command_slash_command_help(scan_state, active_branch); + else + status = PSQL_CMD_UNKNOWN; - if (*result == '\0' || strcmp(result, "-") == 0) - return NULL; + /* + * All the commands that return PSQL_CMD_SEND want to execute previous_buf + * if query_buf is empty. For convenience we implement that here, not in + * the individual command subroutines. + */ + if (status == PSQL_CMD_SEND) + copy_previous_query(query_buf, previous_buf); - return result; + return status; } /* - * Subroutine to actually try to execute a backslash command. + * \a -- toggle field alignment + * + * This makes little sense but we keep it around. */ static backslashResult -exec_command(const char *cmd, - PsqlScanState scan_state, - PQExpBuffer query_buf) +exec_command_a(PsqlScanState scan_state, bool active_branch) { - bool success = true; /* indicate here if the command ran ok or - * failed */ - backslashResult status = PSQL_CMD_SKIP_LINE; + bool success = true; - /* - * \a -- toggle field alignment This makes little sense but we keep it - * around. - */ - if (strcmp(cmd, "a") == 0) + if (active_branch) { if (pset.popt.topt.format != PRINT_ALIGNED) success = do_pset("format", "aligned", &pset.popt, pset.quiet); @@ -216,8 +438,18 @@ exec_command(const char *cmd, success = do_pset("format", "unaligned", &pset.popt, pset.quiet); } - /* \C -- override table title (formerly change HTML caption) */ - else if (strcmp(cmd, "C") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \C -- override table title (formerly change HTML caption) + */ +static backslashResult +exec_command_C(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); @@ -225,20 +457,32 @@ exec_command(const char *cmd, success = do_pset("title", opt, &pset.popt, pset.quiet); free(opt); } + else + ignore_slash_options(scan_state); - /* - * \c or \connect -- connect to database using the specified parameters. - * - * \c [-reuse-previous=BOOL] dbname user host port - * - * Specifying a parameter as '-' is equivalent to omitting it. Examples: - * - * \c - - hst Connect to current database on current port of host - * "hst" as current user. \c - usr - prt Connect to current database on - * "prt" port of current host as user "usr". \c dbs Connect to - * "dbs" database on current port of current host as current user. - */ - else if (strcmp(cmd, "c") == 0 || strcmp(cmd, "connect") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \c or \connect -- connect to database using the specified parameters. + * + * \c [-reuse-previous=BOOL] dbname user host port + * + * Specifying a parameter as '-' is equivalent to omitting it. Examples: + * + * \c - - hst Connect to current database on current port of + * host "hst" as current user. + * \c - usr - prt Connect to current database on port "prt" of current host + * as user "usr". + * \c dbs Connect to database "dbs" on current port of current host + * as current user. + */ +static backslashResult +exec_command_connect(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { static const char prefix[] = "-reuse-previous="; char *opt1, @@ -277,9 +521,21 @@ exec_command(const char *cmd, } free(opt1); } + else + ignore_slash_options(scan_state); - /* \cd */ - else if (strcmp(cmd, "cd") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \cd -- change directory + */ +static backslashResult +exec_command_cd(PsqlScanState scan_state, bool active_branch, const char *cmd) +{ + bool success = true; + + if (active_branch) { char *opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); @@ -323,9 +579,19 @@ exec_command(const char *cmd, if (opt) free(opt); } + else + ignore_slash_options(scan_state); - /* \conninfo -- display information about the current connection */ - else if (strcmp(cmd, "conninfo") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \conninfo -- display information about the current connection + */ +static backslashResult +exec_command_conninfo(PsqlScanState scan_state, bool active_branch) +{ + if (active_branch) { char *db = PQdb(pset.db); @@ -366,8 +632,18 @@ exec_command(const char *cmd, } } - /* \copy */ - else if (pg_strcasecmp(cmd, "copy") == 0) + return PSQL_CMD_SKIP_LINE; +} + +/* + * \copy -- run a COPY command + */ +static backslashResult +exec_command_copy(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *opt = psql_scan_slash_option(scan_state, OT_WHOLE_LINE, NULL, false); @@ -375,13 +651,33 @@ exec_command(const char *cmd, success = do_copy(opt); free(opt); } + else + ignore_slash_whole_line(scan_state); - /* \copyright */ - else if (strcmp(cmd, "copyright") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \copyright -- print copyright notice + */ +static backslashResult +exec_command_copyright(PsqlScanState scan_state, bool active_branch) +{ + if (active_branch) print_copyright(); - /* \crosstabview -- execute a query and display results in crosstab */ - else if (strcmp(cmd, "crosstabview") == 0) + return PSQL_CMD_SKIP_LINE; +} + +/* + * \crosstabview -- execute a query and display results in crosstab + */ +static backslashResult +exec_command_crosstabview(PsqlScanState scan_state, bool active_branch) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) { int i; @@ -391,9 +687,22 @@ exec_command(const char *cmd, pset.crosstab_flag = true; status = PSQL_CMD_SEND; } + else + ignore_slash_options(scan_state); - /* \d* commands */ - else if (cmd[0] == 'd') + return status; +} + +/* + * \d* commands + */ +static backslashResult +exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + bool success = true; + + if (active_branch) { char *pattern; bool show_verbose, @@ -502,7 +811,7 @@ exec_command(const char *cmd, success = listDbRoleSettings(pattern, pattern2); } else - success = PSQL_CMD_UNKNOWN; + status = PSQL_CMD_UNKNOWN; break; case 'R': switch (cmd[2]) @@ -580,13 +889,26 @@ exec_command(const char *cmd, if (pattern) free(pattern); } + else + ignore_slash_options(scan_state); + if (!success) + status = PSQL_CMD_ERROR; - /* - * \e or \edit -- edit the current query buffer, or edit a file and make - * it the query buffer - */ - else if (strcmp(cmd, "e") == 0 || strcmp(cmd, "edit") == 0) + return status; +} + +/* + * \e or \edit -- edit the current query buffer, or edit a file and + * make it the query buffer + */ +static backslashResult +exec_command_edit(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf, PQExpBuffer previous_buf) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) { if (!query_buf) { @@ -632,6 +954,10 @@ exec_command(const char *cmd, expand_tilde(&fname); if (fname) canonicalize_path(fname); + + /* Applies to previous query if current buffer is empty */ + copy_previous_query(query_buf, previous_buf); + if (do_edit(fname, query_buf, lineno, NULL)) status = PSQL_CMD_NEWEDIT; else @@ -643,13 +969,26 @@ exec_command(const char *cmd, free(ln); } } + else + ignore_slash_options(scan_state); - /* - * \ef -- edit the named function, or present a blank CREATE FUNCTION - * template if no argument is given - */ - else if (strcmp(cmd, "ef") == 0) + return status; +} + +/* + * \ef -- edit the named function, or present a blank CREATE FUNCTION + * template if no argument is given + */ +static backslashResult +exec_command_ef(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) { + char *func = psql_scan_slash_option(scan_state, + OT_WHOLE_LINE, NULL, true); int lineno = -1; if (pset.sversion < 80400) @@ -668,11 +1007,8 @@ exec_command(const char *cmd, } else { - char *func; Oid foid = InvalidOid; - func = psql_scan_slash_option(scan_state, - OT_WHOLE_LINE, NULL, true); lineno = strip_lineno_from_objdesc(func); if (lineno == 0) { @@ -725,9 +1061,6 @@ exec_command(const char *cmd, lines++; } } - - if (func) - free(func); } if (status != PSQL_CMD_ERROR) @@ -741,14 +1074,30 @@ exec_command(const char *cmd, else status = PSQL_CMD_NEWEDIT; } + + if (func) + free(func); } + else + ignore_slash_whole_line(scan_state); - /* - * \ev -- edit the named view, or present a blank CREATE VIEW template if - * no argument is given - */ - else if (strcmp(cmd, "ev") == 0) + return status; +} + +/* + * \ev -- edit the named view, or present a blank CREATE VIEW + * template if no argument is given + */ +static backslashResult +exec_command_ev(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) { + char *view = psql_scan_slash_option(scan_state, + OT_WHOLE_LINE, NULL, true); int lineno = -1; if (pset.sversion < 70400) @@ -767,11 +1116,8 @@ exec_command(const char *cmd, } else { - char *view; Oid view_oid = InvalidOid; - view = psql_scan_slash_option(scan_state, - OT_WHOLE_LINE, NULL, true); lineno = strip_lineno_from_objdesc(view); if (lineno == 0) { @@ -796,9 +1142,6 @@ exec_command(const char *cmd, /* error already reported */ status = PSQL_CMD_ERROR; } - - if (view) - free(view); } if (status != PSQL_CMD_ERROR) @@ -812,10 +1155,23 @@ exec_command(const char *cmd, else status = PSQL_CMD_NEWEDIT; } + + if (view) + free(view); } + else + ignore_slash_whole_line(scan_state); - /* \echo and \qecho */ - else if (strcmp(cmd, "echo") == 0 || strcmp(cmd, "qecho") == 0) + return status; +} + +/* + * \echo and \qecho -- echo arguments to stdout or query output + */ +static backslashResult +exec_command_echo(PsqlScanState scan_state, bool active_branch, const char *cmd) +{ + if (active_branch) { char *value; char quoted; @@ -846,9 +1202,19 @@ exec_command(const char *cmd, if (!no_newline) fputs("\n", fout); } + else + ignore_slash_options(scan_state); - /* \encoding -- set/show client side encoding */ - else if (strcmp(cmd, "encoding") == 0) + return PSQL_CMD_SKIP_LINE; +} + +/* + * \encoding -- set/show client side encoding + */ +static backslashResult +exec_command_encoding(PsqlScanState scan_state, bool active_branch) +{ + if (active_branch) { char *encoding = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false); @@ -874,9 +1240,19 @@ exec_command(const char *cmd, free(encoding); } } + else + ignore_slash_options(scan_state); - /* \errverbose -- display verbose message from last failed query */ - else if (strcmp(cmd, "errverbose") == 0) + return PSQL_CMD_SKIP_LINE; +} + +/* + * \errverbose -- display verbose message from last failed query + */ +static backslashResult +exec_command_errverbose(PsqlScanState scan_state, bool active_branch) +{ + if (active_branch) { if (pset.last_error_result) { @@ -897,8 +1273,18 @@ exec_command(const char *cmd, puts(_("There is no previous error.")); } - /* \f -- change field separator */ - else if (strcmp(cmd, "f") == 0) + return PSQL_CMD_SKIP_LINE; +} + +/* + * \f -- change field separator + */ +static backslashResult +exec_command_f(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *fname = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false); @@ -906,12 +1292,22 @@ exec_command(const char *cmd, success = do_pset("fieldsep", fname, &pset.popt, pset.quiet); free(fname); } + else + ignore_slash_options(scan_state); - /* - * \g [filename] -- send query, optionally with output to file/pipe - * \gx [filename] -- same as \g, with expanded mode forced - */ - else if (strcmp(cmd, "g") == 0 || strcmp(cmd, "gx") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \g [filename] -- send query, optionally with output to file/pipe + * \gx [filename] -- same as \g, with expanded mode forced + */ +static backslashResult +exec_command_g(PsqlScanState scan_state, bool active_branch, const char *cmd) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) { char *fname = psql_scan_slash_option(scan_state, OT_FILEPIPE, NULL, false); @@ -928,16 +1324,38 @@ exec_command(const char *cmd, pset.g_expanded = true; status = PSQL_CMD_SEND; } + else + ignore_slash_filepipe(scan_state); - /* \gexec -- send query and execute each field of result */ - else if (strcmp(cmd, "gexec") == 0) + return status; +} + +/* + * \gexec -- send query and execute each field of result + */ +static backslashResult +exec_command_gexec(PsqlScanState scan_state, bool active_branch) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) { pset.gexec_flag = true; status = PSQL_CMD_SEND; } - /* \gset [prefix] -- send query and store result into variables */ - else if (strcmp(cmd, "gset") == 0) + return status; +} + +/* + * \gset [prefix] -- send query and store result into variables + */ +static backslashResult +exec_command_gset(PsqlScanState scan_state, bool active_branch) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) { char *prefix = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false); @@ -952,9 +1370,19 @@ exec_command(const char *cmd, /* gset_prefix is freed later */ status = PSQL_CMD_SEND; } + else + ignore_slash_options(scan_state); - /* help */ - else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0) + return status; +} + +/* + * \help [topic] -- print help about SQL commands + */ +static backslashResult +exec_command_help(PsqlScanState scan_state, bool active_branch) +{ + if (active_branch) { char *opt = psql_scan_slash_option(scan_state, OT_WHOLE_LINE, NULL, false); @@ -973,9 +1401,21 @@ exec_command(const char *cmd, helpSQL(opt, pset.popt.topt.pager); free(opt); } + else + ignore_slash_whole_line(scan_state); - /* HTML mode */ - else if (strcmp(cmd, "H") == 0 || strcmp(cmd, "html") == 0) + return PSQL_CMD_SKIP_LINE; +} + +/* + * \H and \html -- toggle HTML formatting + */ +static backslashResult +exec_command_html(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { if (pset.popt.topt.format != PRINT_HTML) success = do_pset("format", "html", &pset.popt, pset.quiet); @@ -983,10 +1423,18 @@ exec_command(const char *cmd, success = do_pset("format", "aligned", &pset.popt, pset.quiet); } + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} - /* \i and \ir include files */ - else if (strcmp(cmd, "i") == 0 || strcmp(cmd, "include") == 0 - || strcmp(cmd, "ir") == 0 || strcmp(cmd, "include_relative") == 0) +/* + * \i and \ir -- include a file + */ +static backslashResult +exec_command_include(PsqlScanState scan_state, bool active_branch, const char *cmd) +{ + bool success = true; + + if (active_branch) { char *fname = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); @@ -1007,10 +1455,254 @@ exec_command(const char *cmd, free(fname); } } + else + ignore_slash_options(scan_state); - /* \l is list databases */ - else if (strcmp(cmd, "l") == 0 || strcmp(cmd, "list") == 0 || - strcmp(cmd, "l+") == 0 || strcmp(cmd, "list+") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \if -- beginning of an \if..\endif block + * + * is parsed as a boolean expression. Invalid expressions will emit a + * warning and be treated as false. Statements that follow a false expression + * will be parsed but ignored. Note that in the case where an \if statement + * is itself within an inactive section of a block, then the entire inner + * \if..\endif block will be parsed but ignored. + */ +static backslashResult +exec_command_if(PsqlScanState scan_state, ConditionalStack cstack, + PQExpBuffer query_buf) +{ + if (conditional_active(cstack)) + { + /* + * First, push a new active stack entry; this ensures that the lexer + * will perform variable substitution and backtick evaluation while + * scanning the expression. (That should happen anyway, since we know + * we're in an active outer branch, but let's be sure.) + */ + conditional_stack_push(cstack, IFSTATE_TRUE); + + /* Remember current query state in case we need to restore later */ + save_query_text_state(scan_state, cstack, query_buf); + + /* + * Evaluate the expression; if it's false, change to inactive state. + */ + if (!is_true_boolean_expression(scan_state, "\\if expression")) + conditional_stack_poke(cstack, IFSTATE_FALSE); + } + else + { + /* + * We're within an inactive outer branch, so this entire \if block + * will be ignored. We don't want to evaluate the expression, so push + * the "ignored" stack state before scanning it. + */ + conditional_stack_push(cstack, IFSTATE_IGNORED); + + /* Remember current query state in case we need to restore later */ + save_query_text_state(scan_state, cstack, query_buf); + + ignore_boolean_expression(scan_state); + } + + return PSQL_CMD_SKIP_LINE; +} + +/* + * \elif -- alternative branch in an \if..\endif block + * + * is evaluated the same as in \if . + */ +static backslashResult +exec_command_elif(PsqlScanState scan_state, ConditionalStack cstack, + PQExpBuffer query_buf) +{ + bool success = true; + + switch (conditional_stack_peek(cstack)) + { + case IFSTATE_TRUE: + + /* + * Just finished active branch of this \if block. Update saved + * state so we will keep whatever data was put in query_buf by the + * active branch. + */ + save_query_text_state(scan_state, cstack, query_buf); + + /* + * Discard \elif expression and ignore the rest until \endif. + * Switch state before reading expression to ensure proper lexer + * behavior. + */ + conditional_stack_poke(cstack, IFSTATE_IGNORED); + ignore_boolean_expression(scan_state); + break; + case IFSTATE_FALSE: + + /* + * Discard any query text added by the just-skipped branch. + */ + discard_query_text(scan_state, cstack, query_buf); + + /* + * Have not yet found a true expression in this \if block, so this + * might be the first. We have to change state before examining + * the expression, or the lexer won't do the right thing. + */ + conditional_stack_poke(cstack, IFSTATE_TRUE); + if (!is_true_boolean_expression(scan_state, "\\elif expression")) + conditional_stack_poke(cstack, IFSTATE_FALSE); + break; + case IFSTATE_IGNORED: + + /* + * Discard any query text added by the just-skipped branch. + */ + discard_query_text(scan_state, cstack, query_buf); + + /* + * Skip expression and move on. Either the \if block already had + * an active section, or whole block is being skipped. + */ + ignore_boolean_expression(scan_state); + 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; + } + + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \else -- final alternative in an \if..\endif block + * + * Statements within an \else branch will only be executed if + * all previous \if and \elif expressions evaluated to false + * and the block was not itself being ignored. + */ +static backslashResult +exec_command_else(PsqlScanState scan_state, ConditionalStack cstack, + PQExpBuffer query_buf) +{ + bool success = true; + + switch (conditional_stack_peek(cstack)) + { + case IFSTATE_TRUE: + + /* + * Just finished active branch of this \if block. Update saved + * state so we will keep whatever data was put in query_buf by the + * active branch. + */ + save_query_text_state(scan_state, cstack, query_buf); + + /* Now skip the \else branch */ + conditional_stack_poke(cstack, IFSTATE_ELSE_FALSE); + break; + case IFSTATE_FALSE: + + /* + * Discard any query text added by the just-skipped branch. + */ + discard_query_text(scan_state, cstack, query_buf); + + /* + * We've not found any true \if or \elif expression, so execute + * the \else branch. + */ + conditional_stack_poke(cstack, IFSTATE_ELSE_TRUE); + break; + case IFSTATE_IGNORED: + + /* + * Discard any query text added by the just-skipped branch. + */ + discard_query_text(scan_state, cstack, query_buf); + + /* + * Either we previously processed the active branch of this \if, + * or the whole \if block is being skipped. Either way, skip the + * \else branch. + */ + conditional_stack_poke(cstack, IFSTATE_ELSE_FALSE); + break; + case IFSTATE_ELSE_TRUE: + case IFSTATE_ELSE_FALSE: + psql_error("\\else: cannot occur after \\else\n"); + success = false; + break; + case IFSTATE_NONE: + /* no \if to else from */ + psql_error("\\else: no matching \\if\n"); + success = false; + break; + } + + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \endif -- ends an \if...\endif block + */ +static backslashResult +exec_command_endif(PsqlScanState scan_state, ConditionalStack cstack, + PQExpBuffer query_buf) +{ + bool success = true; + + switch (conditional_stack_peek(cstack)) + { + case IFSTATE_TRUE: + case IFSTATE_ELSE_TRUE: + /* Close the \if block, keeping the query text */ + success = conditional_stack_pop(cstack); + Assert(success); + break; + case IFSTATE_FALSE: + case IFSTATE_IGNORED: + case IFSTATE_ELSE_FALSE: + + /* + * Discard any query text added by the just-skipped branch. + */ + discard_query_text(scan_state, cstack, query_buf); + + /* Close the \if block */ + success = conditional_stack_pop(cstack); + Assert(success); + break; + case IFSTATE_NONE: + /* no \if to end */ + psql_error("\\endif: no matching \\if\n"); + success = false; + break; + } + + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \l -- list databases + */ +static backslashResult +exec_command_list(PsqlScanState scan_state, bool active_branch, const char *cmd) +{ + bool success = true; + + if (active_branch) { char *pattern; bool show_verbose; @@ -1025,11 +1717,22 @@ exec_command(const char *cmd, if (pattern) free(pattern); } + else + ignore_slash_options(scan_state); - /* - * large object things - */ - else if (strncmp(cmd, "lo_", 3) == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \lo_* -- large object operations + */ +static backslashResult +exec_command_lo(PsqlScanState scan_state, bool active_branch, const char *cmd) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + bool success = true; + + if (active_branch) { char *opt1, *opt2; @@ -1087,10 +1790,24 @@ exec_command(const char *cmd, free(opt1); free(opt2); } + else + ignore_slash_options(scan_state); + if (!success) + status = PSQL_CMD_ERROR; - /* \o -- set query output */ - else if (strcmp(cmd, "o") == 0 || strcmp(cmd, "out") == 0) + return status; +} + +/* + * \o -- set query output + */ +static backslashResult +exec_command_out(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *fname = psql_scan_slash_option(scan_state, OT_FILEPIPE, NULL, true); @@ -1099,9 +1816,20 @@ exec_command(const char *cmd, success = setQFout(fname); free(fname); } + else + ignore_slash_filepipe(scan_state); - /* \p prints the current query buffer */ - else if (strcmp(cmd, "p") == 0 || strcmp(cmd, "print") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \p -- print the current query buffer + */ +static backslashResult +exec_command_print(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf) +{ + if (active_branch) { if (query_buf && query_buf->len > 0) puts(query_buf->data); @@ -1110,9 +1838,21 @@ exec_command(const char *cmd, fflush(stdout); } - /* \password -- set user password */ - else if (strcmp(cmd, "password") == 0) + return PSQL_CMD_SKIP_LINE; +} + +/* + * \password -- set user password + */ +static backslashResult +exec_command_password(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { + char *opt0 = psql_scan_slash_option(scan_state, + OT_SQLID, NULL, true); char pw1[100]; char pw2[100]; @@ -1126,7 +1866,6 @@ exec_command(const char *cmd, } else { - char *opt0 = psql_scan_slash_option(scan_state, OT_SQLID, NULL, true); char *user; char *encrypted_password; @@ -1159,14 +1898,27 @@ exec_command(const char *cmd, PQclear(res); PQfreemem(encrypted_password); } - - if (opt0) - free(opt0); } + + if (opt0) + free(opt0); } + else + ignore_slash_options(scan_state); - /* \prompt -- prompt and set variable */ - else if (strcmp(cmd, "prompt") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \prompt -- prompt and set variable + */ +static backslashResult +exec_command_prompt(PsqlScanState scan_state, bool active_branch, + const char *cmd) +{ + bool success = true; + + if (active_branch) { char *opt, *prompt_text = NULL; @@ -1225,9 +1977,21 @@ exec_command(const char *cmd, free(opt); } } + else + ignore_slash_options(scan_state); - /* \pset -- set printing parameters */ - else if (strcmp(cmd, "pset") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \pset -- set printing parameters + */ +static backslashResult +exec_command_pset(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *opt0 = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false); @@ -1267,13 +2031,34 @@ exec_command(const char *cmd, free(opt0); free(opt1); } + else + ignore_slash_options(scan_state); - /* \q or \quit */ - else if (strcmp(cmd, "q") == 0 || strcmp(cmd, "quit") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \q or \quit -- exit psql + */ +static backslashResult +exec_command_quit(PsqlScanState scan_state, bool active_branch) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) status = PSQL_CMD_TERMINATE; - /* reset(clear) the buffer */ - else if (strcmp(cmd, "r") == 0 || strcmp(cmd, "reset") == 0) + return status; +} + +/* + * \r -- reset (clear) the query buffer + */ +static backslashResult +exec_command_reset(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf) +{ + if (active_branch) { resetPQExpBuffer(query_buf); psql_scan_reset(scan_state); @@ -1281,8 +2066,18 @@ exec_command(const char *cmd, puts(_("Query buffer reset (cleared).")); } - /* \s save history in a file or show it on the screen */ - else if (strcmp(cmd, "s") == 0) + return PSQL_CMD_SKIP_LINE; +} + +/* + * \s -- save history in a file or show it on the screen + */ +static backslashResult +exec_command_s(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *fname = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); @@ -1295,9 +2090,21 @@ exec_command(const char *cmd, putchar('\n'); free(fname); } + else + ignore_slash_options(scan_state); - /* \set -- generalized set variable/option command */ - else if (strcmp(cmd, "set") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \set -- set variable + */ +static backslashResult +exec_command_set(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *opt0 = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false); @@ -1336,10 +2143,22 @@ exec_command(const char *cmd, } free(opt0); } + else + ignore_slash_options(scan_state); + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} - /* \setenv -- set environment command */ - else if (strcmp(cmd, "setenv") == 0) +/* + * \setenv -- set environment variable + */ +static backslashResult +exec_command_setenv(PsqlScanState scan_state, bool active_branch, + const char *cmd) +{ + bool success = true; + + if (active_branch) { char *envvar = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false); @@ -1381,9 +2200,21 @@ exec_command(const char *cmd, free(envvar); free(envval); } + else + ignore_slash_options(scan_state); - /* \sf -- show a function's source code */ - else if (strcmp(cmd, "sf") == 0 || strcmp(cmd, "sf+") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \sf -- show a function's source code + */ +static backslashResult +exec_command_sf(PsqlScanState scan_state, bool active_branch, const char *cmd) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) { bool show_linenumbers = (strcmp(cmd, "sf+") == 0); PQExpBuffer func_buf; @@ -1463,9 +2294,21 @@ exec_command(const char *cmd, free(func); destroyPQExpBuffer(func_buf); } + else + ignore_slash_whole_line(scan_state); - /* \sv -- show a view's source code */ - else if (strcmp(cmd, "sv") == 0 || strcmp(cmd, "sv+") == 0) + return status; +} + +/* + * \sv -- show a view's source code + */ +static backslashResult +exec_command_sv(PsqlScanState scan_state, bool active_branch, const char *cmd) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) { bool show_linenumbers = (strcmp(cmd, "sv+") == 0); PQExpBuffer view_buf; @@ -1539,9 +2382,21 @@ exec_command(const char *cmd, free(view); destroyPQExpBuffer(view_buf); } + else + ignore_slash_whole_line(scan_state); - /* \t -- turn off headers and row count */ - else if (strcmp(cmd, "t") == 0) + return status; +} + +/* + * \t -- turn off table headers and row count + */ +static backslashResult +exec_command_t(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); @@ -1549,9 +2404,21 @@ exec_command(const char *cmd, success = do_pset("tuples_only", opt, &pset.popt, pset.quiet); free(opt); } + else + ignore_slash_options(scan_state); - /* \T -- define html attributes */ - else if (strcmp(cmd, "T") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \T -- define html
attributes + */ +static backslashResult +exec_command_T(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *value = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false); @@ -1559,9 +2426,21 @@ exec_command(const char *cmd, success = do_pset("tableattr", value, &pset.popt, pset.quiet); free(value); } + else + ignore_slash_options(scan_state); - /* \timing -- toggle timing of queries */ - else if (strcmp(cmd, "timing") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \timing -- enable/disable timing of queries + */ +static backslashResult +exec_command_timing(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false); @@ -1579,9 +2458,22 @@ exec_command(const char *cmd, } free(opt); } + else + ignore_slash_options(scan_state); - /* \unset */ - else if (strcmp(cmd, "unset") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \unset -- unset variable + */ +static backslashResult +exec_command_unset(PsqlScanState scan_state, bool active_branch, + const char *cmd) +{ + bool success = true; + + if (active_branch) { char *opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false); @@ -1596,13 +2488,28 @@ exec_command(const char *cmd, free(opt); } + else + ignore_slash_options(scan_state); - /* \w -- write query buffer to file */ - else if (strcmp(cmd, "w") == 0 || strcmp(cmd, "write") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \w -- write query buffer to file + */ +static backslashResult +exec_command_write(PsqlScanState scan_state, bool active_branch, + const char *cmd, + PQExpBuffer query_buf, PQExpBuffer previous_buf) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) { + char *fname = psql_scan_slash_option(scan_state, + OT_FILEPIPE, NULL, true); FILE *fd = NULL; bool is_pipe = false; - char *fname = NULL; if (!query_buf) { @@ -1611,17 +2518,14 @@ exec_command(const char *cmd, } else { - fname = psql_scan_slash_option(scan_state, - OT_FILEPIPE, NULL, true); - expand_tilde(&fname); - if (!fname) { psql_error("\\%s: missing required argument\n", cmd); - success = false; + status = PSQL_CMD_ERROR; } else { + expand_tilde(&fname); if (fname[0] == '|') { is_pipe = true; @@ -1636,7 +2540,7 @@ exec_command(const char *cmd, if (!fd) { psql_error("%s: %s\n", fname, strerror(errno)); - success = false; + status = PSQL_CMD_ERROR; } } } @@ -1647,6 +2551,9 @@ exec_command(const char *cmd, if (query_buf && query_buf->len > 0) fprintf(fd, "%s\n", query_buf->data); + /* Applies to previous query if current buffer is empty */ + else if (previous_buf && previous_buf->len > 0) + fprintf(fd, "%s\n", previous_buf->data); if (is_pipe) result = pclose(fd); @@ -1656,7 +2563,7 @@ exec_command(const char *cmd, if (result == EOF) { psql_error("%s: %s\n", fname, strerror(errno)); - success = false; + status = PSQL_CMD_ERROR; } } @@ -1665,9 +2572,22 @@ exec_command(const char *cmd, free(fname); } + else + ignore_slash_filepipe(scan_state); - /* \watch -- execute a query every N seconds */ - else if (strcmp(cmd, "watch") == 0) + return status; +} + +/* + * \watch -- execute a query every N seconds + */ +static backslashResult +exec_command_watch(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf, PQExpBuffer previous_buf) +{ + bool success = true; + + if (active_branch) { char *opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); @@ -1682,15 +2602,30 @@ exec_command(const char *cmd, free(opt); } + /* Applies to previous query if current buffer is empty */ + copy_previous_query(query_buf, previous_buf); + success = do_watch(query_buf, sleep); /* Reset the query buffer as though for \r */ resetPQExpBuffer(query_buf); psql_scan_reset(scan_state); } + else + ignore_slash_options(scan_state); - /* \x -- set or toggle expanded table representation */ - else if (strcmp(cmd, "x") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \x -- set or toggle expanded table representation + */ +static backslashResult +exec_command_x(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); @@ -1698,9 +2633,21 @@ exec_command(const char *cmd, success = do_pset("expanded", opt, &pset.popt, pset.quiet); free(opt); } + else + ignore_slash_options(scan_state); - /* \z -- list table rights (equivalent to \dp) */ - else if (strcmp(cmd, "z") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \z -- list table privileges (equivalent to \dp) + */ +static backslashResult +exec_command_z(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *pattern = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); @@ -1709,9 +2656,21 @@ exec_command(const char *cmd, if (pattern) free(pattern); } + else + ignore_slash_options(scan_state); - /* \! -- shell escape */ - else if (strcmp(cmd, "!") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \! -- execute shell command + */ +static backslashResult +exec_command_shell_escape(PsqlScanState scan_state, bool active_branch) +{ + bool success = true; + + if (active_branch) { char *opt = psql_scan_slash_option(scan_state, OT_WHOLE_LINE, NULL, false); @@ -1719,9 +2678,19 @@ exec_command(const char *cmd, success = do_shell(opt); free(opt); } + else + ignore_slash_whole_line(scan_state); - /* \? -- slash command help */ - else if (strcmp(cmd, "?") == 0) + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} + +/* + * \? -- print help about backslash commands + */ +static backslashResult +exec_command_slash_command_help(PsqlScanState scan_state, bool active_branch) +{ + if (active_branch) { char *opt0 = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false); @@ -1734,34 +2703,231 @@ exec_command(const char *cmd, helpVariables(pset.popt.topt.pager); else slashUsage(pset.popt.topt.pager); + + if (opt0) + free(opt0); } + else + ignore_slash_options(scan_state); -#if 0 + return PSQL_CMD_SKIP_LINE; +} + + +/* + * Read and interpret an argument to the \connect slash command. + */ +static char * +read_connect_arg(PsqlScanState scan_state) +{ + char *result; + char quote; /* - * These commands don't do anything. I just use them to test the parser. + * Ideally we should treat the arguments as SQL identifiers. But for + * backwards compatibility with 7.2 and older pg_dump files, we have to + * take unquoted arguments verbatim (don't downcase them). For now, + * double-quoted arguments may be stripped of double quotes (as if SQL + * identifiers). By 7.4 or so, pg_dump files can be expected to + * double-quote all mixed-case \connect arguments, and then we can get rid + * of OT_SQLIDHACK. */ - else if (strcmp(cmd, "void") == 0 || strcmp(cmd, "#") == 0) - { - int i = 0; - char *value; + result = psql_scan_slash_option(scan_state, OT_SQLIDHACK, "e, true); - while ((value = psql_scan_slash_option(scan_state, - OT_NORMAL, NULL, true))) - { - psql_error("+ opt(%d) = |%s|\n", i++, value); - free(value); - } + if (!result) + return NULL; + + if (quote) + return result; + + if (*result == '\0' || strcmp(result, "-") == 0) + return NULL; + + return result; +} + +/* + * Read a boolean expression, return it as a PQExpBuffer string. + * + * Note: anything more or less than one token will certainly fail to be + * parsed by ParseVariableBool, so we don't worry about complaining here. + * This routine's return data structure will need to be rethought anyway + * to support likely future extensions such as "\if defined VARNAME". + */ +static PQExpBuffer +gather_boolean_expression(PsqlScanState scan_state) +{ + PQExpBuffer exp_buf = createPQExpBuffer(); + int num_options = 0; + char *value; + + /* collect all arguments for the conditional command into exp_buf */ + while ((value = psql_scan_slash_option(scan_state, + OT_NORMAL, NULL, false)) != NULL) + { + /* add spaces between tokens */ + if (num_options > 0) + appendPQExpBufferChar(exp_buf, ' '); + appendPQExpBufferStr(exp_buf, value); + num_options++; + free(value); } -#endif - else - status = PSQL_CMD_UNKNOWN; + return exp_buf; +} - if (!success) - status = PSQL_CMD_ERROR; +/* + * Read a boolean expression, return true if the expression + * was a valid boolean expression that evaluated to true. + * Otherwise return false. + * + * Note: conditional stack's top state must be active, else lexer will + * fail to expand variables and backticks. + */ +static bool +is_true_boolean_expression(PsqlScanState scan_state, const char *name) +{ + PQExpBuffer buf = gather_boolean_expression(scan_state); + bool value = false; + bool success = ParseVariableBool(buf->data, name, &value); - return status; + destroyPQExpBuffer(buf); + return success && value; +} + +/* + * Read a boolean expression, but do nothing with it. + * + * Note: conditional stack's top state must be INACTIVE, else lexer will + * expand variables and backticks, which we do not want here. + */ +static void +ignore_boolean_expression(PsqlScanState scan_state) +{ + PQExpBuffer buf = gather_boolean_expression(scan_state); + + destroyPQExpBuffer(buf); +} + +/* + * Read and discard "normal" slash command options. + * + * This should be used for inactive-branch processing of any slash command + * that eats one or more OT_NORMAL, OT_SQLID, or OT_SQLIDHACK parameters. + * We don't need to worry about exactly how many it would eat, since the + * cleanup logic in HandleSlashCmds would silently discard any extras anyway. + */ +static void +ignore_slash_options(PsqlScanState scan_state) +{ + char *arg; + + while ((arg = psql_scan_slash_option(scan_state, + OT_NORMAL, NULL, false)) != NULL) + free(arg); +} + +/* + * Read and discard FILEPIPE slash command argument. + * + * This *MUST* be used for inactive-branch processing of any slash command + * that takes an OT_FILEPIPE option. Otherwise we might consume a different + * amount of option text in active and inactive cases. + */ +static void +ignore_slash_filepipe(PsqlScanState scan_state) +{ + char *arg = psql_scan_slash_option(scan_state, + OT_FILEPIPE, NULL, false); + + if (arg) + free(arg); +} + +/* + * Read and discard whole-line slash command argument. + * + * This *MUST* be used for inactive-branch processing of any slash command + * that takes an OT_WHOLE_LINE option. Otherwise we might consume a different + * amount of option text in active and inactive cases. + */ +static void +ignore_slash_whole_line(PsqlScanState scan_state) +{ + char *arg = psql_scan_slash_option(scan_state, + OT_WHOLE_LINE, NULL, false); + + if (arg) + free(arg); +} + +/* + * 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); +} + +/* + * Prepare to possibly restore query buffer to its current state + * (cf. discard_query_text). + * + * We need to remember the length of the query buffer, and the lexer's + * notion of the parenthesis nesting depth. + */ +static void +save_query_text_state(PsqlScanState scan_state, ConditionalStack cstack, + PQExpBuffer query_buf) +{ + if (query_buf) + conditional_stack_set_query_len(cstack, query_buf->len); + conditional_stack_set_paren_depth(cstack, + psql_scan_get_paren_depth(scan_state)); +} + +/* + * Discard any query text absorbed during an inactive conditional branch. + * + * We must discard data that was appended to query_buf during an inactive + * \if branch. We don't have to do anything there if there's no query_buf. + * + * Also, reset the lexer state to the same paren depth there was before. + * (The rest of its state doesn't need attention, since we could not be + * inside a comment or literal or partial token.) + */ +static void +discard_query_text(PsqlScanState scan_state, ConditionalStack cstack, + PQExpBuffer query_buf) +{ + if (query_buf) + { + int new_len = conditional_stack_get_query_len(cstack); + + Assert(new_len >= 0 && new_len <= query_buf->len); + query_buf->len = new_len; + query_buf->data[new_len] = '\0'; + } + psql_scan_set_paren_depth(scan_state, + conditional_stack_get_paren_depth(cstack)); +} + +/* + * If query_buf is empty, copy previous_buf into it. + * + * This is used by various slash commands for which re-execution of a + * previous query is a common usage. For convenience, we allow the + * case of query_buf == NULL (and do nothing). + */ +static void +copy_previous_query(PQExpBuffer query_buf, PQExpBuffer previous_buf) +{ + if (query_buf && query_buf->len == 0) + appendPQExpBufferStr(query_buf, previous_buf->data); } /* diff --git a/src/bin/psql/command.h b/src/bin/psql/command.h index d0c32645f1..e8ea8473e8 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,7 +26,9 @@ typedef enum _backslashResult extern backslashResult HandleSlashCmds(PsqlScanState scan_state, - PQExpBuffer query_buf); + ConditionalStack cstack, + PQExpBuffer query_buf, + PQExpBuffer previous_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 e9d4fe6786..b06ae9779d 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -121,7 +121,8 @@ setQFout(const char *fname) * (Failure in escaping should lead to returning NULL.) * * "passthrough" is the pointer previously given to psql_scan_set_passthrough. - * psql currently doesn't use this. + * In psql, passthrough points to a ConditionalStack, which we check to + * determine whether variable expansion is allowed. */ char * psql_get_variable(const char *varname, bool escape, bool as_ident, @@ -130,6 +131,10 @@ psql_get_variable(const char *varname, bool escape, bool as_ident, char *result; const char *value; + /* In an inactive \if branch, suppress all variable substitutions */ + if (passthrough && !conditional_active((ConditionalStack) passthrough)) + return NULL; + value = GetVariable(pset.vars, varname); if (!value) return NULL; diff --git a/src/bin/psql/conditional.c b/src/bin/psql/conditional.c new file mode 100644 index 0000000000..63977ce5dd --- /dev/null +++ b/src/bin/psql/conditional.c @@ -0,0 +1,153 @@ +/* + * 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(void) +{ + ConditionalStack cstack = pg_malloc(sizeof(ConditionalStackData)); + + cstack->head = NULL; + return cstack; +} + +/* + * destroy stack + */ +void +conditional_stack_destroy(ConditionalStack cstack) +{ + while (conditional_stack_pop(cstack)) + continue; + free(cstack); +} + +/* + * Create a new conditional branch. + */ +void +conditional_stack_push(ConditionalStack cstack, ifState new_state) +{ + IfStackElem *p = (IfStackElem *) pg_malloc(sizeof(IfStackElem)); + + p->if_state = new_state; + p->query_len = -1; + p->paren_depth = -1; + p->next = cstack->head; + cstack->head = p; +} + +/* + * Destroy the topmost conditional branch. + * Returns false if there was no branch to end. + */ +bool +conditional_stack_pop(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(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 no branch state to set. + */ +bool +conditional_stack_poke(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-blocks. + */ +bool +conditional_stack_empty(ConditionalStack cstack) +{ + return cstack->head == NULL; +} + +/* + * True if we should execute commands normally; that is, the current + * conditional branch is active, or there is no open \if block. + */ +bool +conditional_active(ConditionalStack cstack) +{ + ifState s = conditional_stack_peek(cstack); + + return s == IFSTATE_NONE || s == IFSTATE_TRUE || s == IFSTATE_ELSE_TRUE; +} + +/* + * Save current query buffer length in topmost stack entry. + */ +void +conditional_stack_set_query_len(ConditionalStack cstack, int len) +{ + Assert(!conditional_stack_empty(cstack)); + cstack->head->query_len = len; +} + +/* + * Fetch last-recorded query buffer length from topmost stack entry. + * Will return -1 if no stack or it was never saved. + */ +int +conditional_stack_get_query_len(ConditionalStack cstack) +{ + if (conditional_stack_empty(cstack)) + return -1; + return cstack->head->query_len; +} + +/* + * Save current parenthesis nesting depth in topmost stack entry. + */ +void +conditional_stack_set_paren_depth(ConditionalStack cstack, int depth) +{ + Assert(!conditional_stack_empty(cstack)); + cstack->head->paren_depth = depth; +} + +/* + * Fetch last-recorded parenthesis nesting depth from topmost stack entry. + * Will return -1 if no stack or it was never saved. + */ +int +conditional_stack_get_paren_depth(ConditionalStack cstack) +{ + if (conditional_stack_empty(cstack)) + return -1; + return cstack->head->paren_depth; +} diff --git a/src/bin/psql/conditional.h b/src/bin/psql/conditional.h new file mode 100644 index 0000000000..90e4d93b40 --- /dev/null +++ b/src/bin/psql/conditional.h @@ -0,0 +1,83 @@ +/* + * psql - the PostgreSQL interactive terminal + * + * Copyright (c) 2000-2017, PostgreSQL Global Development Group + * + * src/bin/psql/conditional.h + */ +#ifndef CONDITIONAL_H +#define CONDITIONAL_H + +/* + * Possible states of a single level of \if block. + */ +typedef enum ifState +{ + IFSTATE_NONE = 0, /* not currently in an \if block */ + IFSTATE_TRUE, /* currently in an \if or \elif that is true + * and all parent branches (if any) are true */ + IFSTATE_FALSE, /* currently in an \if or \elif that is false + * but no true branch has yet been seen, and + * all parent branches (if any) are true */ + IFSTATE_IGNORED, /* currently in an \elif that follows a true + * branch, or the whole \if is a child of a + * false parent branch */ + IFSTATE_ELSE_TRUE, /* currently in an \else that is true and all + * parent branches (if any) are true */ + IFSTATE_ELSE_FALSE /* currently in an \else that is false or + * ignored */ +} ifState; + +/* + * The state of nested \ifs is stored in a stack. + * + * query_len is used to determine what accumulated text to throw away at the + * end of an inactive branch. (We could, perhaps, teach the lexer to not add + * stuff to the query buffer in the first place when inside an inactive branch; + * but that would be very invasive.) We also need to save and restore the + * lexer's parenthesis nesting depth when throwing away text. (We don't need + * to save and restore any of its other state, such as comment nesting depth, + * because a backslash command could never appear inside a comment or SQL + * literal.) + */ +typedef struct IfStackElem +{ + ifState if_state; /* current state, see enum above */ + int query_len; /* length of query_buf at last branch start */ + int paren_depth; /* parenthesis depth at last branch start */ + struct IfStackElem *next; /* next surrounding \if, if any */ +} IfStackElem; + +typedef struct ConditionalStackData +{ + IfStackElem *head; +} ConditionalStackData; + +typedef struct ConditionalStackData *ConditionalStack; + + +extern ConditionalStack conditional_stack_create(void); + +extern void conditional_stack_destroy(ConditionalStack cstack); + +extern void 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_stack_empty(ConditionalStack cstack); + +extern bool conditional_active(ConditionalStack cstack); + +extern void conditional_stack_set_query_len(ConditionalStack cstack, int len); + +extern int conditional_stack_get_query_len(ConditionalStack cstack); + +extern void conditional_stack_set_paren_depth(ConditionalStack cstack, int depth); + +extern int conditional_stack_get_paren_depth(ConditionalStack cstack); + +#endif /* CONDITIONAL_H */ diff --git a/src/bin/psql/copy.c b/src/bin/psql/copy.c index 481031a229..2005b9a0bf 100644 --- a/src/bin/psql/copy.c +++ b/src/bin/psql/copy.c @@ -552,7 +552,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, NULL); fputs(prompt, stdout); fflush(stdout); @@ -590,7 +590,7 @@ 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, NULL); fputs(prompt, stdout); fflush(stdout); diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index ba14df0344..ac435220e6 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -167,7 +167,7 @@ slashUsage(unsigned short int pager) * Use "psql --help=commands | wc" to count correctly. It's okay to count * the USE_READLINE line even in builds without that. */ - output = PageOutput(113, pager ? &(pset.popt.topt) : NULL); + output = PageOutput(122, pager ? &(pset.popt.topt) : NULL); fprintf(output, _("General\n")); fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n")); @@ -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, _("Conditional\n")); + fprintf(output, _(" \\if EXPR begin conditional block\n")); + fprintf(output, _(" \\elif EXPR alternative within current conditional block\n")); + fprintf(output, _(" \\else final alternative within current conditional block\n")); + fprintf(output, _(" \\endif end 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 6e358e2e1b..2bc2f43b4e 100644 --- a/src/bin/psql/mainloop.c +++ b/src/bin/psql/mainloop.c @@ -35,6 +35,7 @@ int MainLoop(FILE *source) { PsqlScanState scan_state; /* lexer working state */ + ConditionalStack cond_stack; /* \if status stack */ volatile PQExpBuffer query_buf; /* buffer for query being accumulated */ volatile PQExpBuffer previous_buf; /* if there isn't anything in the new * buffer yet, use this one for \e, @@ -50,16 +51,15 @@ MainLoop(FILE *source) volatile promptStatus_t prompt_status = PROMPT_READY; volatile int count_eof = 0; volatile bool die_on_error = false; - - /* Save the prior command source */ FILE *prev_cmd_source; bool prev_cmd_interactive; uint64 prev_lineno; - /* Save old settings */ + /* Save the prior command source */ prev_cmd_source = pset.cur_cmd_source; prev_cmd_interactive = pset.cur_cmd_interactive; prev_lineno = pset.lineno; + /* pset.stmt_lineno does not need to be saved and restored */ /* Establish new source */ pset.cur_cmd_source = source; @@ -69,6 +69,8 @@ MainLoop(FILE *source) /* Create working state */ scan_state = psql_scan_create(&psqlscan_callbacks); + cond_stack = conditional_stack_create(); + psql_scan_set_passthrough(scan_state, (void *) cond_stack); query_buf = createPQExpBuffer(); previous_buf = createPQExpBuffer(); @@ -122,7 +124,19 @@ MainLoop(FILE *source) cancel_pressed = false; if (pset.cur_cmd_interactive) + { putc('\n', stdout); + + /* + * if interactive user is in an \if block, then Ctrl-C will + * exit from the innermost \if. + */ + if (!conditional_stack_empty(cond_stack)) + { + psql_error("\\if: escaped\n"); + conditional_stack_pop(cond_stack); + } + } else { successResult = EXIT_USER; @@ -140,7 +154,8 @@ 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 { @@ -286,8 +301,10 @@ MainLoop(FILE *source) (scan_result == PSCAN_EOL && pset.singleline)) { /* - * Save query in history. We use history_buf to accumulate - * multi-line queries into a single history entry. + * Save line in history. We use history_buf to accumulate + * multi-line queries into a single history entry. Note that + * history accumulation works on input lines, so it doesn't + * matter whether the query will be ignored due to \if. */ if (pset.cur_cmd_interactive && !line_saved_in_history) { @@ -296,22 +313,36 @@ MainLoop(FILE *source) line_saved_in_history = true; } - /* execute query */ - success = SendQuery(query_buf->data); - slashCmdStatus = success ? PSQL_CMD_SEND : PSQL_CMD_ERROR; - pset.stmt_lineno = 1; - - /* transfer query to previous_buf by pointer-swapping */ + /* execute query unless we're in an inactive \if branch */ + if (conditional_active(cond_stack)) { - PQExpBuffer swap_buf = previous_buf; + success = SendQuery(query_buf->data); + slashCmdStatus = success ? PSQL_CMD_SEND : PSQL_CMD_ERROR; + pset.stmt_lineno = 1; - previous_buf = query_buf; - query_buf = swap_buf; - } - resetPQExpBuffer(query_buf); + /* transfer query to previous_buf by pointer-swapping */ + { + PQExpBuffer swap_buf = previous_buf; - added_nl_pos = -1; - /* we need not do psql_scan_reset() here */ + previous_buf = query_buf; + query_buf = swap_buf; + } + resetPQExpBuffer(query_buf); + + added_nl_pos = -1; + /* we need not do psql_scan_reset() here */ + } + else + { + /* if interactive, warn about non-executed query */ + if (pset.cur_cmd_interactive) + psql_error("query ignored; use \\endif or Ctrl-C to exit current \\if block\n"); + /* fake an OK result for purposes of loop checks */ + success = true; + slashCmdStatus = PSQL_CMD_SEND; + pset.stmt_lineno = 1; + /* note that query_buf doesn't change state */ + } } else if (scan_result == PSCAN_BACKSLASH) { @@ -343,21 +374,24 @@ MainLoop(FILE *source) /* execute backslash command */ slashCmdStatus = HandleSlashCmds(scan_state, - query_buf->len > 0 ? - query_buf : previous_buf); + cond_stack, + query_buf, + previous_buf); success = slashCmdStatus != PSQL_CMD_ERROR; - pset.stmt_lineno = 1; - if ((slashCmdStatus == PSQL_CMD_SEND || slashCmdStatus == PSQL_CMD_NEWEDIT) && - query_buf->len == 0) - { - /* copy previous buffer to current for handling */ - appendPQExpBufferStr(query_buf, previous_buf->data); - } + /* + * Resetting stmt_lineno after a backslash command isn't + * always appropriate, but it's what we've done historically + * and there have been few complaints. + */ + pset.stmt_lineno = 1; if (slashCmdStatus == PSQL_CMD_SEND) { + /* should not see this in inactive branch */ + Assert(conditional_active(cond_stack)); + success = SendQuery(query_buf->data); /* transfer query to previous_buf by pointer-swapping */ @@ -374,6 +408,8 @@ MainLoop(FILE *source) } else if (slashCmdStatus == PSQL_CMD_NEWEDIT) { + /* should not see this in inactive branch */ + Assert(conditional_active(cond_stack)); /* rescan query_buf as new input */ psql_scan_finish(scan_state); free(line); @@ -429,8 +465,17 @@ MainLoop(FILE *source) if (pset.cur_cmd_interactive) pg_send_history(history_buf); - /* execute query */ - success = SendQuery(query_buf->data); + /* execute query unless we're in an inactive \if branch */ + if (conditional_active(cond_stack)) + { + success = SendQuery(query_buf->data); + } + else + { + if (pset.cur_cmd_interactive) + psql_error("query ignored; use \\endif or Ctrl-C to exit current \\if block\n"); + success = true; + } if (!success && die_on_error) successResult = EXIT_USER; @@ -438,6 +483,19 @@ MainLoop(FILE *source) successResult = EXIT_BADCONN; } + /* + * 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("reached EOF without finding closing \\endif(s)\n"); + if (die_on_error && !pset.cur_cmd_interactive) + successResult = EXIT_USER; + } + /* * Let's just make real sure the SIGINT handler won't try to use * sigint_interrupt_jmp after we exit this routine. If there is an outer @@ -452,6 +510,7 @@ MainLoop(FILE *source) destroyPQExpBuffer(history_buf); 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 f7930c4a83..e502ff3f6e 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 977e754293..b3d2d98fd7 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/psqlscanslash.h b/src/bin/psql/psqlscanslash.h index 266e93af44..db76061332 100644 --- a/src/bin/psql/psqlscanslash.h +++ b/src/bin/psql/psqlscanslash.h @@ -18,8 +18,7 @@ enum slash_option_type OT_SQLID, /* treat as SQL identifier */ OT_SQLIDHACK, /* SQL identifier, but don't downcase */ OT_FILEPIPE, /* it's a filename or pipe */ - OT_WHOLE_LINE, /* just snarf the rest of the line */ - OT_NO_EVAL /* no expansion of backticks or variables */ + OT_WHOLE_LINE /* just snarf the rest of the line */ }; @@ -32,6 +31,10 @@ extern char *psql_scan_slash_option(PsqlScanState state, extern void psql_scan_slash_command_end(PsqlScanState state); +extern int psql_scan_get_paren_depth(PsqlScanState state); + +extern void psql_scan_set_paren_depth(PsqlScanState state, int depth); + extern void dequote_downcase_identifier(char *str, bool downcase, int encoding); #endif /* PSQLSCANSLASH_H */ diff --git a/src/bin/psql/psqlscanslash.l b/src/bin/psql/psqlscanslash.l index ba4a08d000..319afdc744 100644 --- a/src/bin/psql/psqlscanslash.l +++ b/src/bin/psql/psqlscanslash.l @@ -19,6 +19,7 @@ #include "postgres_fe.h" #include "psqlscanslash.h" +#include "conditional.h" #include "libpq-fe.h" } @@ -230,8 +231,7 @@ other . :{variable_char}+ { /* Possible psql variable substitution */ - if (option_type == OT_NO_EVAL || - cur_state->callbacks->get_variable == NULL) + if (cur_state->callbacks->get_variable == NULL) ECHO; else { @@ -268,25 +268,15 @@ other . } :'{variable_char}+' { - if (option_type == OT_NO_EVAL) - ECHO; - else - { - psqlscan_escape_variable(cur_state, yytext, yyleng, false); - *option_quote = ':'; - } + psqlscan_escape_variable(cur_state, yytext, yyleng, false); + *option_quote = ':'; unquoted_option_chars = 0; } :\"{variable_char}+\" { - if (option_type == OT_NO_EVAL) - ECHO; - else - { - psqlscan_escape_variable(cur_state, yytext, yyleng, true); - *option_quote = ':'; - } + psqlscan_escape_variable(cur_state, yytext, yyleng, true); + *option_quote = ':'; unquoted_option_chars = 0; } @@ -353,8 +343,9 @@ other . */ "`" { - /* In NO_EVAL mode, don't evaluate the command */ - if (option_type != OT_NO_EVAL) + /* In an inactive \if branch, don't evaluate the command */ + if (cur_state->cb_passthrough == NULL || + conditional_active((ConditionalStack) cur_state->cb_passthrough)) evaluate_backtick(cur_state); BEGIN(xslasharg); } @@ -641,6 +632,25 @@ psql_scan_slash_command_end(PsqlScanState state) psql_scan_reselect_sql_lexer(state); } +/* + * Fetch current paren nesting depth + */ +int +psql_scan_get_paren_depth(PsqlScanState state) +{ + return state->paren_depth; +} + +/* + * Set paren nesting depth + */ +void +psql_scan_set_paren_depth(PsqlScanState state, int depth) +{ + Assert(depth >= 0); + state->paren_depth = depth; +} + /* * De-quote and optionally downcase a SQL identifier. * diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c index 694f0ef257..8068a28b4e 100644 --- a/src/bin/psql/startup.c +++ b/src/bin/psql/startup.c @@ -331,6 +331,7 @@ 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); @@ -339,11 +340,17 @@ main(int argc, char *argv[]) psql_scan_setup(scan_state, cell->val, strlen(cell->val), pset.encoding, standard_strings()); + cond_stack = conditional_stack_create(); + psql_scan_set_passthrough(scan_state, (void *) cond_stack); - successResult = HandleSlashCmds(scan_state, NULL) != PSQL_CMD_ERROR + successResult = HandleSlashCmds(scan_state, + cond_stack, + NULL, + 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 eb7f197b12..8aa914fa95 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -2735,6 +2735,175 @@ deallocate q; \pset format aligned \pset expanded off \pset border 1 +-- tests for \if ... \endif +\if true + select 'okay'; + ?column? +---------- + okay +(1 row) + + select 'still okay'; + ?column? +------------ + still okay +(1 row) + +\else + not okay; + still not okay +\endif +-- at this point query buffer should still have last valid line +\g + ?column? +------------ + still okay +(1 row) + +-- \if should work okay on part of a query +select + \if true + 42 + \else + (bogus + \endif + forty_two; + forty_two +----------- + 42 +(1 row) + +select \if false \\ (bogus \else \\ 42 \endif \\ forty_two; + forty_two +----------- + 42 +(1 row) + +-- 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 are false +\if invalid boolean expression +unrecognized value "invalid boolean expression" for "\if expression": boolean expected + \echo 'will not print #6-1' +\else + \echo 'will print anyway #6-2' +will print anyway #6-2 +\endif +-- 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 vars and backticks are not expanded when ignoring extra args +\set foo bar +\echo :foo :'foo' :"foo" +bar 'bar' "bar" +\pset fieldsep | `nosuchcommand` :foo :'foo' :"foo" +\pset: extra argument "nosuchcommand" ignored +\pset: extra argument ":foo" ignored +\pset: extra argument ":'foo'" ignored +\pset: extra argument ":"foo"" ignored +-- show that vars and backticks are not expanded and commands are ignored +-- when in a false if-branch +\set try_to_quit '\\q' +\if false + :try_to_quit + \echo `nosuchcommand` :foo :'foo' :"foo" + \pset fieldsep | `nosuchcommand` :foo :'foo' :"foo" + \a \C arg1 \c arg1 arg2 arg3 arg4 \cd arg1 \conninfo + \copy arg1 arg2 arg3 arg4 arg5 arg6 + \copyright \dt arg1 \e arg1 arg2 + \ef whole_line + \ev whole_line + \echo arg1 arg2 arg3 arg4 arg5 \echo arg1 \encoding arg1 \errverbose + \g arg1 \gx arg1 \gexec \h \html \i arg1 \ir arg1 \l arg1 \lo arg1 arg2 + \o arg1 \p \password arg1 \prompt arg1 arg2 \pset arg1 arg2 \q + \reset \s arg1 \set arg1 arg2 arg3 arg4 arg5 arg6 arg7 \setenv arg1 arg2 + \sf whole_line + \sv whole_line + \t arg1 \T arg1 \timing arg1 \unset arg1 \w arg1 \watch arg1 \x arg1 + -- \else here is eaten as part of OT_FILEPIPE argument + \w |/no/such/file \else + -- \endif here is eaten as part of whole-line argument + \! whole_line \endif +\else + \echo 'should print #8-1' +should print #8-1 +\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 8f8e17a87c..0ae4dd84ea 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -382,6 +382,150 @@ deallocate q; \pset expanded off \pset border 1 +-- tests for \if ... \endif + +\if true + select 'okay'; + select 'still okay'; +\else + not okay; + still not okay +\endif + +-- at this point query buffer should still have last valid line +\g + +-- \if should work okay on part of a query +select + \if true + 42 + \else + (bogus + \endif + forty_two; + +select \if false \\ (bogus \else \\ 42 \endif \\ forty_two; + +-- 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 are false +\if invalid boolean expression + \echo 'will not print #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 vars and backticks are not expanded when ignoring extra args +\set foo bar +\echo :foo :'foo' :"foo" +\pset fieldsep | `nosuchcommand` :foo :'foo' :"foo" + +-- show that vars and backticks are not expanded and commands are ignored +-- when in a false if-branch +\set try_to_quit '\\q' +\if false + :try_to_quit + \echo `nosuchcommand` :foo :'foo' :"foo" + \pset fieldsep | `nosuchcommand` :foo :'foo' :"foo" + \a \C arg1 \c arg1 arg2 arg3 arg4 \cd arg1 \conninfo + \copy arg1 arg2 arg3 arg4 arg5 arg6 + \copyright \dt arg1 \e arg1 arg2 + \ef whole_line + \ev whole_line + \echo arg1 arg2 arg3 arg4 arg5 \echo arg1 \encoding arg1 \errverbose + \g arg1 \gx arg1 \gexec \h \html \i arg1 \ir arg1 \l arg1 \lo arg1 arg2 + \o arg1 \p \password arg1 \prompt arg1 arg2 \pset arg1 arg2 \q + \reset \s arg1 \set arg1 arg2 arg3 arg4 arg5 arg6 arg7 \setenv arg1 arg2 + \sf whole_line + \sv whole_line + \t arg1 \T arg1 \timing arg1 \unset arg1 \w arg1 \watch arg1 \x arg1 + -- \else here is eaten as part of OT_FILEPIPE argument + \w |/no/such/file \else + -- \endif here is eaten as part of whole-line argument + \! whole_line \endif +\else + \echo 'should print #8-1' +\endif + -- SHOW_CONTEXT \set SHOW_CONTEXT never -- 2.40.0