X-Git-Url: https://granicus.if.org/sourcecode?a=blobdiff_plain;ds=sidebyside;f=src%2Fbin%2Fpsql%2Fcommand.c;h=a2c0ec0b7ff7d7e4ed6750aae45aa3cb377db637;hb=fc9a62af3f87f4bec1e8c904ea99ae50f3c881ef;hp=5d90ca28edf446ad9ac944d14d9fc2d50d119ef4;hpb=07c8651dd91d5aea38bee268acb582674ca4b5ea;p=postgresql diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 5d90ca28ed..a2c0ec0b7f 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1,24 +1,17 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2014, PostgreSQL Global Development Group + * Copyright (c) 2000-2019, PostgreSQL Global Development Group * * src/bin/psql/command.c */ #include "postgres_fe.h" #include "command.h" -#ifdef __BORLANDC__ /* needed for BCC */ -#undef mkdir -#endif - #include #include -#ifdef HAVE_PWD_H #include -#endif #ifndef WIN32 -#include /* for umask() */ #include /* for stat() */ #include /* open() flags */ #include /* for geteuid(), getpid(), stat() */ @@ -27,48 +20,149 @@ #include #include #include -#include /* for umask() */ #include /* for stat() */ #endif -#ifdef USE_OPENSSL -#include -#endif +#include "catalog/pg_class_d.h" #include "portability/instr_time.h" #include "libpq-fe.h" #include "pqexpbuffer.h" -#include "dumputils.h" +#include "common/logging.h" +#include "fe_utils/print.h" +#include "fe_utils/string_utils.h" #include "common.h" #include "copy.h" +#include "crosstabview.h" #include "describe.h" #include "help.h" #include "input.h" #include "large_obj.h" #include "mainloop.h" -#include "print.h" -#include "psqlscan.h" +#include "psqlscanslash.h" #include "settings.h" #include "variables.h" +/* + * Editable database object types. + */ +typedef enum EditableObjectType +{ + EditableFunction, + EditableView +} EditableObjectType; -/* functions for use in this file */ +/* local function declarations */ static backslashResult exec_command(const char *cmd, PsqlScanState scan_state, - PQExpBuffer query_buf); + 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_ev(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf, bool is_func); +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_gdesc(PsqlScanState scan_state, bool active_branch); +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, PQExpBuffer previous_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_sv(PsqlScanState scan_state, bool active_branch, + const char *cmd, bool is_func); +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_connect(char *dbname, char *user, char *host, char *port); static bool do_shell(const char *command); -static bool do_watch(PQExpBuffer query_buf, long sleep); -static bool lookup_function_oid(PGconn *conn, const char *desc, Oid *foid); -static bool get_create_function_cmd(PGconn *conn, Oid oid, PQExpBuffer buf); -static int strip_lineno_from_funcdesc(char *func); +static bool do_watch(PQExpBuffer query_buf, double sleep); +static bool lookup_object_oid(EditableObjectType obj_type, const char *desc, + Oid *obj_oid); +static bool get_create_object_cmd(EditableObjectType obj_type, Oid oid, + PQExpBuffer buf); +static int strip_lineno_from_objdesc(char *obj); +static int count_lines_in_buf(PQExpBuffer buf); +static void print_with_linenumbers(FILE *output, char *lines, + const char *header_keyword); static void minimal_error_message(PGresult *res); static void printSSLInfo(void); +static void printGSSInfo(void); static bool printPsetInfo(const char *param, struct printQueryOpt *popt); +static char *pset_value_string(const char *param, struct printQueryOpt *popt); #ifdef WIN32 static void checkWin32Codepage(void); @@ -86,9 +180,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. *---------- @@ -96,39 +199,49 @@ 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) { + pg_log_error("invalid command \\%s", cmd); if (pset.cur_cmd_interactive) - psql_error("Invalid command \\%s. Try \\? for help.\n", cmd); - else - psql_error("invalid command \\%s\n", cmd); + pg_log_info("Try \\? for help."); status = PSQL_CMD_ERROR; } 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) + pg_log_warning("\\%s: extra argument \"%s\" ignored", cmd, arg); free(arg); } + conditional_stack_pop(cstack); } else { @@ -149,56 +262,172 @@ 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)) + { + pg_log_warning("\\%s command ignored; use \\endif or Ctrl-C to exit current \\if block", + 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_ev(scan_state, active_branch, query_buf, true); + else if (strcmp(cmd, "ev") == 0) + status = exec_command_ef_ev(scan_state, active_branch, query_buf, false); + 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, "gdesc") == 0) + status = exec_command_gdesc(scan_state, active_branch); + 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, previous_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_sv(scan_state, active_branch, cmd, true); + else if (strcmp(cmd, "sv") == 0 || strcmp(cmd, "sv+") == 0) + status = exec_command_sf_sv(scan_state, active_branch, cmd, false); + 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); @@ -206,8 +435,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); @@ -215,44 +454,85 @@ 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 dbname user host port - * - * If any of these parameters are omitted or specified as '-', the current - * value of the parameter will be used instead. If the parameter has no - * current value, the default value for that parameter will be used. Some - * 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, *opt2, *opt3, *opt4; + enum trivalue reuse_previous = TRI_DEFAULT; opt1 = read_connect_arg(scan_state); - opt2 = read_connect_arg(scan_state); - opt3 = read_connect_arg(scan_state); - opt4 = read_connect_arg(scan_state); + if (opt1 != NULL && strncmp(opt1, prefix, sizeof(prefix) - 1) == 0) + { + bool on_off; + + success = ParseVariableBool(opt1 + sizeof(prefix) - 1, + "-reuse-previous", + &on_off); + if (success) + { + reuse_previous = on_off ? TRI_YES : TRI_NO; + free(opt1); + opt1 = read_connect_arg(scan_state); + } + } - success = do_connect(opt1, opt2, opt3, opt4); + if (success) /* give up if reuse_previous was invalid */ + { + opt2 = read_connect_arg(scan_state); + opt3 = read_connect_arg(scan_state); + opt4 = read_connect_arg(scan_state); + + success = do_connect(reuse_previous, opt1, opt2, opt3, opt4); + free(opt2); + free(opt3); + free(opt4); + } free(opt1); - free(opt2); - free(opt3); - free(opt4); } + 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); @@ -270,9 +550,9 @@ exec_command(const char *cmd, pw = getpwuid(user_id); if (!pw) { - psql_error("could not get home directory for user ID %ld: %s\n", + pg_log_error("could not get home directory for user ID %ld: %s", (long) user_id, - errno ? strerror(errno) : _("user does not exist")); + errno ? strerror(errno) : _("user does not exist")); exit(EXIT_FAILURE); } dir = pw->pw_dir; @@ -283,45 +563,81 @@ exec_command(const char *cmd, * directory, so if someone wants to code this here instead... */ dir = "/"; -#endif /* WIN32 */ +#endif /* WIN32 */ } if (chdir(dir) == -1) { - psql_error("\\%s: could not change directory to \"%s\": %s\n", - cmd, dir, strerror(errno)); + pg_log_error("\\%s: could not change directory to \"%s\": %m", + cmd, dir); success = false; } 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); - char *host = (PQhostaddr(pset.db) != NULL) ? PQhostaddr(pset.db) : PQhost(pset.db); if (db == NULL) printf(_("You are currently not connected to a database.\n")); else { - if (host == NULL) - host = DEFAULT_PGSOCKET_DIR; - /* If the host is an absolute path, the connection is via socket */ + char *host = PQhost(pset.db); + char *hostaddr = PQhostaddr(pset.db); + + /* + * If the host is an absolute path, the connection is via socket + * unless overridden by hostaddr + */ if (is_absolute_path(host)) - printf(_("You are connected to database \"%s\" as user \"%s\" via socket in \"%s\" at port \"%s\".\n"), - db, PQuser(pset.db), host, PQport(pset.db)); + { + if (hostaddr && *hostaddr) + printf(_("You are connected to database \"%s\" as user \"%s\" on address \"%s\" at port \"%s\".\n"), + db, PQuser(pset.db), hostaddr, PQport(pset.db)); + else + printf(_("You are connected to database \"%s\" as user \"%s\" via socket in \"%s\" at port \"%s\".\n"), + db, PQuser(pset.db), host, PQport(pset.db)); + } else - printf(_("You are connected to database \"%s\" as user \"%s\" on host \"%s\" at port \"%s\".\n"), - db, PQuser(pset.db), host, PQport(pset.db)); + { + if (hostaddr && *hostaddr && strcmp(host, hostaddr) != 0) + printf(_("You are connected to database \"%s\" as user \"%s\" on host \"%s\" (address \"%s\") at port \"%s\".\n"), + db, PQuser(pset.db), host, hostaddr, PQport(pset.db)); + else + printf(_("You are connected to database \"%s\" as user \"%s\" on host \"%s\" at port \"%s\".\n"), + db, PQuser(pset.db), host, PQport(pset.db)); + } printSSLInfo(); + printGSSInfo(); } } - /* \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); @@ -329,13 +645,58 @@ 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(); - /* \d* commands */ - else if (cmd[0] == 'd') + 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; + + for (i = 0; i < lengthof(pset.ctv_args); i++) + pset.ctv_args[i] = psql_scan_slash_option(scan_state, + OT_NORMAL, NULL, true); + pset.crosstab_flag = true; + status = PSQL_CMD_SEND; + } + else + ignore_slash_options(scan_state); + + 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, @@ -359,6 +720,9 @@ exec_command(const char *cmd, /* standard listing of interesting things */ success = listTables("tvmsE", NULL, show_verbose, show_system); break; + case 'A': + success = describeAccessMethods(pattern, show_verbose); + break; case 'a': success = describeAggregates(pattern, show_verbose, show_system); break; @@ -388,6 +752,7 @@ exec_command(const char *cmd, case 'S': case 'a': case 'n': + case 'p': case 't': case 'w': success = describeFunctions(&cmd[2], pattern, show_verbose, show_system); @@ -399,7 +764,7 @@ exec_command(const char *cmd, break; case 'g': /* no longer distinct from \du */ - success = describeRoles(pattern, show_verbose); + success = describeRoles(pattern, show_verbose, show_system); break; case 'l': success = do_lo_list(); @@ -419,6 +784,23 @@ exec_command(const char *cmd, case 'p': success = permissionsList(pattern); break; + case 'P': + { + switch (cmd[2]) + { + case '\0': + case '+': + case 't': + case 'i': + case 'n': + success = listPartitionedTables(&cmd[2], pattern, show_verbose); + break; + default: + status = PSQL_CMD_UNKNOWN; + break; + } + } + break; case 'T': success = describeTypes(pattern, show_verbose, show_system); break; @@ -437,14 +819,33 @@ exec_command(const char *cmd, if (pattern) pattern2 = psql_scan_slash_option(scan_state, - OT_NORMAL, NULL, true); + OT_NORMAL, NULL, true); success = listDbRoleSettings(pattern, pattern2); + + if (pattern2) + free(pattern2); } else - success = PSQL_CMD_UNKNOWN; + status = PSQL_CMD_UNKNOWN; + break; + case 'R': + switch (cmd[2]) + { + case 'p': + if (show_verbose) + success = describePublications(pattern); + else + success = listPublications(pattern); + break; + case 's': + success = describeSubscriptions(pattern, show_verbose); + break; + default: + status = PSQL_CMD_UNKNOWN; + } break; case 'u': - success = describeRoles(pattern, show_verbose); + success = describeRoles(pattern, show_verbose, show_system); break; case 'F': /* text search subsystem */ switch (cmd[2]) @@ -503,17 +904,30 @@ 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) { - psql_error("no query buffer\n"); + pg_log_error("no query buffer"); status = PSQL_CMD_ERROR; } else @@ -546,7 +960,7 @@ exec_command(const char *cmd, lineno = atoi(ln); if (lineno < 1) { - psql_error("invalid line number: %s\n", ln); + pg_log_error("invalid line number: %s", ln); status = PSQL_CMD_ERROR; } } @@ -555,6 +969,10 @@ exec_command(const char *cmd, expand_tilde(&fname); if (fname) canonicalize_path(fname); + + /* If query_buf is empty, recall previous query for editing */ + copy_previous_query(query_buf, previous_buf); + if (do_edit(fname, query_buf, lineno, NULL)) status = PSQL_CMD_NEWEDIT; else @@ -566,61 +984,88 @@ 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/\ev -- edit the named function/view, or + * present a blank CREATE FUNCTION/VIEW template if no argument is given + */ +static backslashResult +exec_command_ef_ev(PsqlScanState scan_state, bool active_branch, + PQExpBuffer query_buf, bool is_func) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) { + char *obj_desc = psql_scan_slash_option(scan_state, + OT_WHOLE_LINE, + NULL, true); int lineno = -1; - if (pset.sversion < 80400) + if (pset.sversion < (is_func ? 80400 : 70400)) { - psql_error("The server (version %d.%d) does not support editing function source.\n", - pset.sversion / 10000, (pset.sversion / 100) % 100); + char sverbuf[32]; + + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf)); + if (is_func) + pg_log_error("The server (version %s) does not support editing function source.", + sverbuf); + else + pg_log_error("The server (version %s) does not support editing view definitions.", + sverbuf); status = PSQL_CMD_ERROR; } else if (!query_buf) { - psql_error("no query buffer\n"); + pg_log_error("no query buffer"); status = PSQL_CMD_ERROR; } else { - char *func; - Oid foid = InvalidOid; + Oid obj_oid = InvalidOid; + EditableObjectType eot = is_func ? EditableFunction : EditableView; - func = psql_scan_slash_option(scan_state, - OT_WHOLE_LINE, NULL, true); - lineno = strip_lineno_from_funcdesc(func); + lineno = strip_lineno_from_objdesc(obj_desc); if (lineno == 0) { /* error already reported */ status = PSQL_CMD_ERROR; } - else if (!func) + else if (!obj_desc) { /* set up an empty command to fill in */ - printfPQExpBuffer(query_buf, - "CREATE FUNCTION ( )\n" - " RETURNS \n" - " LANGUAGE \n" - " -- common options: IMMUTABLE STABLE STRICT SECURITY DEFINER\n" - "AS $function$\n" - "\n$function$\n"); + resetPQExpBuffer(query_buf); + if (is_func) + appendPQExpBufferStr(query_buf, + "CREATE FUNCTION ( )\n" + " RETURNS \n" + " LANGUAGE \n" + " -- common options: IMMUTABLE STABLE STRICT SECURITY DEFINER\n" + "AS $function$\n" + "\n$function$\n"); + else + appendPQExpBufferStr(query_buf, + "CREATE VIEW AS\n" + " SELECT \n" + " -- something...\n"); } - else if (!lookup_function_oid(pset.db, func, &foid)) + else if (!lookup_object_oid(eot, obj_desc, &obj_oid)) { /* error already reported */ status = PSQL_CMD_ERROR; } - else if (!get_create_function_cmd(pset.db, foid, query_buf)) + else if (!get_create_object_cmd(eot, obj_oid, query_buf)) { /* error already reported */ status = PSQL_CMD_ERROR; } - else if (lineno > 0) + else if (is_func && lineno > 0) { /* * lineno "1" should correspond to the first line of the @@ -645,9 +1090,6 @@ exec_command(const char *cmd, lines++; } } - - if (func) - free(func); } if (status != PSQL_CMD_ERROR) @@ -661,10 +1103,23 @@ exec_command(const char *cmd, else status = PSQL_CMD_NEWEDIT; } - } - /* \echo and \qecho */ - else if (strcmp(cmd, "echo") == 0 || strcmp(cmd, "qecho") == 0) + if (obj_desc) + free(obj_desc); + } + else + ignore_slash_whole_line(scan_state); + + 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; @@ -695,9 +1150,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); @@ -711,7 +1176,7 @@ exec_command(const char *cmd, { /* set encoding */ if (PQsetClientEncoding(pset.db, encoding) == -1) - psql_error("%s: invalid encoding name or conversion procedure not found\n", encoding); + pg_log_error("%s: invalid encoding name or conversion procedure not found", encoding); else { /* save encoding info into psql internal data */ @@ -723,9 +1188,51 @@ exec_command(const char *cmd, free(encoding); } } + else + ignore_slash_options(scan_state); - /* \f -- change field separator */ - else if (strcmp(cmd, "f") == 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) + { + char *msg; + + msg = PQresultVerboseErrorMessage(pset.last_error_result, + PQERRORS_VERBOSE, + PQSHOW_CONTEXT_ALWAYS); + if (msg) + { + pg_log_error("%s", msg); + PQfreemem(msg); + } + else + puts(_("out of memory")); + } + else + puts(_("There is no previous error.")); + } + + 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); @@ -733,9 +1240,22 @@ exec_command(const char *cmd, success = do_pset("fieldsep", fname, &pset.popt, pset.quiet); free(fname); } + else + ignore_slash_options(scan_state); + + 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; - /* \g [filename] -- send query, optionally with output to file/pipe */ - else if (strcmp(cmd, "g") == 0) + if (active_branch) { char *fname = psql_scan_slash_option(scan_state, OT_FILEPIPE, NULL, false); @@ -748,11 +1268,59 @@ exec_command(const char *cmd, pset.gfname = pg_strdup(fname); } free(fname); + if (strcmp(cmd, "gx") == 0) + pset.g_expanded = true; status = PSQL_CMD_SEND; } + else + ignore_slash_filepipe(scan_state); - /* \gset [prefix] -- send query and store result into variables */ - else if (strcmp(cmd, "gset") == 0) + return status; +} + +/* + * \gdesc -- describe query result + */ +static backslashResult +exec_command_gdesc(PsqlScanState scan_state, bool active_branch) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) + { + pset.gdesc_flag = true; + status = PSQL_CMD_SEND; + } + + 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; + } + + 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); @@ -767,9 +1335,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); @@ -788,9 +1366,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); @@ -798,17 +1388,25 @@ 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 a file + */ +static backslashResult +exec_command_include(PsqlScanState scan_state, bool active_branch, const char *cmd) +{ + bool success = true; - /* \i and \ir include files */ - else if (strcmp(cmd, "i") == 0 || strcmp(cmd, "include") == 0 - || strcmp(cmd, "ir") == 0 || strcmp(cmd, "include_relative") == 0) + if (active_branch) { char *fname = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); if (!fname) { - psql_error("\\%s: missing required argument\n", cmd); + pg_log_error("\\%s: missing required argument", cmd); success = false; } else @@ -818,14 +1416,258 @@ exec_command(const char *cmd, include_relative = (strcmp(cmd, "ir") == 0 || strcmp(cmd, "include_relative") == 0); expand_tilde(&fname); - success = (process_file(fname, false, include_relative) == EXIT_SUCCESS); + success = (process_file(fname, include_relative) == EXIT_SUCCESS); 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: + pg_log_error("\\elif: cannot occur after \\else"); + success = false; + break; + case IFSTATE_NONE: + /* no \if to elif from */ + pg_log_error("\\elif: no matching \\if"); + 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: + pg_log_error("\\else: cannot occur after \\else"); + success = false; + break; + case IFSTATE_NONE: + /* no \if to else from */ + pg_log_error("\\else: no matching \\if"); + 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 */ + pg_log_error("\\endif: no matching \\if"); + 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; @@ -840,11 +1682,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; @@ -858,7 +1711,7 @@ exec_command(const char *cmd, { if (!opt2) { - psql_error("\\%s: missing required argument\n", cmd); + pg_log_error("\\%s: missing required argument", cmd); success = false; } else @@ -872,7 +1725,7 @@ exec_command(const char *cmd, { if (!opt1) { - psql_error("\\%s: missing required argument\n", cmd); + pg_log_error("\\%s: missing required argument", cmd); success = false; } else @@ -889,7 +1742,7 @@ exec_command(const char *cmd, { if (!opt1) { - psql_error("\\%s: missing required argument\n", cmd); + pg_log_error("\\%s: missing required argument", cmd); success = false; } else @@ -902,10 +1755,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); @@ -914,34 +1781,63 @@ 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, PQExpBuffer previous_buf) +{ + if (active_branch) { + /* + * We want to print the same thing \g would execute, but not to change + * the query buffer state; so we can't use copy_previous_query(). + * Also, beware of possibility that buffer pointers are NULL. + */ if (query_buf && query_buf->len > 0) puts(query_buf->data); + else if (previous_buf && previous_buf->len > 0) + puts(previous_buf->data); else if (!pset.quiet) puts(_("Query buffer is empty.")); 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 *pw1; - char *pw2; + char *opt0 = psql_scan_slash_option(scan_state, + OT_SQLID, NULL, true); + char pw1[100]; + char pw2[100]; - pw1 = simple_prompt("Enter new password: ", 100, false); - pw2 = simple_prompt("Enter it again: ", 100, false); + simple_prompt("Enter new password: ", pw1, sizeof(pw1), false); + simple_prompt("Enter it again: ", pw2, sizeof(pw2), false); if (strcmp(pw1, pw2) != 0) { - psql_error("Passwords didn't match.\n"); + pg_log_error("Passwords didn't match."); success = false; } else { - char *opt0 = psql_scan_slash_option(scan_state, OT_SQLID, NULL, true); char *user; char *encrypted_password; @@ -950,11 +1846,11 @@ exec_command(const char *cmd, else user = PQuser(pset.db); - encrypted_password = PQencryptPassword(pw1, user); + encrypted_password = PQencryptPasswordConn(pset.db, pw1, user, NULL); if (!encrypted_password) { - psql_error("Password encryption failed.\n"); + pg_log_info("%s", PQerrorMessage(pset.db)); success = false; } else @@ -966,7 +1862,7 @@ exec_command(const char *cmd, printfPQExpBuffer(&buf, "ALTER USER %s PASSWORD ", fmtId(user)); appendStringLiteralConn(&buf, encrypted_password, pset.db); - res = PSQLexec(buf.data, false); + res = PSQLexec(buf.data); termPQExpBuffer(&buf); if (!res) success = false; @@ -974,17 +1870,27 @@ exec_command(const char *cmd, PQclear(res); PQfreemem(encrypted_password); } - - if (opt0) - free(opt0); } - free(pw1); - free(pw2); + 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; @@ -996,7 +1902,7 @@ exec_command(const char *cmd, if (!arg1) { - psql_error("\\%s: missing required argument\n", cmd); + pg_log_error("\\%s: missing required argument", cmd); success = false; } else @@ -1012,7 +1918,10 @@ exec_command(const char *cmd, opt = arg1; if (!pset.inputfile) - result = simple_prompt(prompt_text, 4096, true); + { + result = (char *) pg_malloc(4096); + simple_prompt(prompt_text, result, 4096, true); + } else { if (prompt_text) @@ -1021,23 +1930,40 @@ exec_command(const char *cmd, fflush(stdout); } result = gets_fromFile(stdin); + if (!result) + { + pg_log_error("\\%s: could not read value for variable", + cmd); + success = false; + } } - if (!SetVariable(pset.vars, opt, result)) - { - psql_error("\\%s: error while setting variable\n", cmd); + if (result && + !SetVariable(pset.vars, opt, result)) success = false; - } - free(result); + if (result) + free(result); if (prompt_text) free(prompt_text); 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); @@ -1050,15 +1976,24 @@ exec_command(const char *cmd, int i; static const char *const my_list[] = { - "border", "columns", "expanded", "fieldsep", - "footer", "format", "linestyle", "null", - "numericlocale", "pager", "recordsep", + "border", "columns", "csv_fieldsep", "expanded", "fieldsep", + "fieldsep_zero", "footer", "format", "linestyle", "null", + "numericlocale", "pager", "pager_min_lines", + "recordsep", "recordsep_zero", "tableattr", "title", "tuples_only", + "unicode_border_linestyle", + "unicode_column_linestyle", + "unicode_header_linestyle", NULL }; for (i = 0; my_list[i] != NULL; i++) - printPsetInfo(my_list[i], &pset.popt); + { + char *val = pset_value_string(my_list[i], &pset.popt); + + printf("%-24s %s\n", my_list[i], val); + free(val); + } success = true; } @@ -1068,13 +2003,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); @@ -1082,8 +2038,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); @@ -1096,9 +2062,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); @@ -1125,29 +2103,34 @@ exec_command(const char *cmd, while ((opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false))) { - newval = realloc(newval, strlen(newval) + strlen(opt) + 1); - if (!newval) - { - psql_error("out of memory\n"); - exit(EXIT_FAILURE); - } + newval = pg_realloc(newval, strlen(newval) + strlen(opt) + 1); strcat(newval, opt); free(opt); } if (!SetVariable(pset.vars, opt0, newval)) - { - psql_error("\\%s: error while setting variable\n", cmd); success = false; - } + free(newval); } 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); @@ -1156,12 +2139,12 @@ exec_command(const char *cmd, if (!envvar) { - psql_error("\\%s: missing required argument\n", cmd); + pg_log_error("\\%s: missing required argument", cmd); success = false; } else if (strchr(envvar, '=') != NULL) { - psql_error("\\%s: environment variable name must not contain \"=\"\n", + pg_log_error("\\%s: environment variable name must not contain \"=\"", cmd); success = false; } @@ -1189,35 +2172,60 @@ 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) - { - bool show_linenumbers = (strcmp(cmd, "sf+") == 0); - PQExpBuffer func_buf; - char *func; - Oid foid = InvalidOid; + return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; +} - func_buf = createPQExpBuffer(); - func = psql_scan_slash_option(scan_state, - OT_WHOLE_LINE, NULL, true); - if (pset.sversion < 80400) +/* + * \sf/\sv -- show a function/view's source code + */ +static backslashResult +exec_command_sf_sv(PsqlScanState scan_state, bool active_branch, + const char *cmd, bool is_func) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + + if (active_branch) + { + bool show_linenumbers = (strchr(cmd, '+') != NULL); + PQExpBuffer buf; + char *obj_desc; + Oid obj_oid = InvalidOid; + EditableObjectType eot = is_func ? EditableFunction : EditableView; + + buf = createPQExpBuffer(); + obj_desc = psql_scan_slash_option(scan_state, + OT_WHOLE_LINE, NULL, true); + if (pset.sversion < (is_func ? 80400 : 70400)) { - psql_error("The server (version %d.%d) does not support showing function source.\n", - pset.sversion / 10000, (pset.sversion / 100) % 100); + char sverbuf[32]; + + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf)); + if (is_func) + pg_log_error("The server (version %s) does not support showing function source.", + sverbuf); + else + pg_log_error("The server (version %s) does not support showing view definitions.", + sverbuf); status = PSQL_CMD_ERROR; } - else if (!func) + else if (!obj_desc) { - psql_error("function name is required\n"); + if (is_func) + pg_log_error("function name is required"); + else + pg_log_error("view name is required"); status = PSQL_CMD_ERROR; } - else if (!lookup_function_oid(pset.db, func, &foid)) + else if (!lookup_object_oid(eot, obj_desc, &obj_oid)) { /* error already reported */ status = PSQL_CMD_ERROR; } - else if (!get_create_function_cmd(pset.db, foid, func_buf)) + else if (!get_create_object_cmd(eot, obj_oid, buf)) { /* error already reported */ status = PSQL_CMD_ERROR; @@ -1231,20 +2239,9 @@ exec_command(const char *cmd, if (pset.queryFout == stdout) { /* count lines in function to see if pager is needed */ - int lineno = 0; - const char *lines = func_buf->data; - - while (*lines != '\0') - { - lineno++; - /* find start of next line */ - lines = strchr(lines, '\n'); - if (!lines) - break; - lines++; - } + int lineno = count_lines_in_buf(buf); - output = PageOutput(lineno, pset.popt.topt.pager); + output = PageOutput(lineno, &(pset.popt.topt)); is_pager = true; } else @@ -1256,63 +2253,45 @@ exec_command(const char *cmd, if (show_linenumbers) { - bool in_header = true; - int lineno = 0; - char *lines = func_buf->data; - /* - * lineno "1" should correspond to the first line of the - * function body. We expect that pg_get_functiondef() will - * emit that on a line beginning with "AS ", and that there - * can be no such line before the real start of the function - * body. - * - * Note that this loop scribbles on func_buf. + * For functions, lineno "1" should correspond to the first + * line of the function body. We expect that + * pg_get_functiondef() will emit that on a line beginning + * with "AS ", and that there can be no such line before the + * real start of the function body. */ - while (*lines != '\0') - { - char *eol; - - if (in_header && strncmp(lines, "AS ", 3) == 0) - in_header = false; - /* increment lineno only for body's lines */ - if (!in_header) - lineno++; - - /* find and mark end of current line */ - eol = strchr(lines, '\n'); - if (eol != NULL) - *eol = '\0'; - - /* show current line as appropriate */ - if (in_header) - fprintf(output, " %s\n", lines); - else - fprintf(output, "%-7d %s\n", lineno, lines); - - /* advance to next line, if any */ - if (eol == NULL) - break; - lines = ++eol; - } + print_with_linenumbers(output, buf->data, + is_func ? "AS " : NULL); } else { - /* just send the function definition to output */ - fputs(func_buf->data, output); + /* just send the definition to output */ + fputs(buf->data, output); } if (is_pager) ClosePager(output); } - if (func) - free(func); - destroyPQExpBuffer(func_buf); + if (obj_desc) + free(obj_desc); + destroyPQExpBuffer(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); @@ -1320,9 +2299,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); @@ -1330,15 +2321,27 @@ 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); if (opt) - pset.timing = ParseVariableBool(opt); + success = ParseVariableBool(opt, "\\timing", &pset.timing); else pset.timing = !pset.timing; if (!pset.quiet) @@ -1350,54 +2353,78 @@ 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); if (!opt) { - psql_error("\\%s: missing required argument\n", cmd); + pg_log_error("\\%s: missing required argument", cmd); success = false; } else if (!SetVariable(pset.vars, opt, NULL)) - { - psql_error("\\%s: error while setting variable\n", cmd); success = false; - } + 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) { - psql_error("no query buffer\n"); + pg_log_error("no query buffer"); status = PSQL_CMD_ERROR; } 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; + pg_log_error("\\%s: missing required argument", cmd); + status = PSQL_CMD_ERROR; } else { + expand_tilde(&fname); if (fname[0] == '|') { is_pipe = true; + disable_sigpipe_trap(); fd = popen(&fname[1], "w"); } else @@ -1407,8 +2434,8 @@ exec_command(const char *cmd, } if (!fd) { - psql_error("%s: %s\n", fname, strerror(errno)); - success = false; + pg_log_error("%s: %m", fname); + status = PSQL_CMD_ERROR; } } } @@ -1417,8 +2444,16 @@ exec_command(const char *cmd, { int result; + /* + * We want to print the same thing \g would execute, but not to + * change the query buffer state; so we can't use + * copy_previous_query(). Also, beware of possibility that buffer + * pointers are NULL. + */ if (query_buf && query_buf->len > 0) fprintf(fd, "%s\n", query_buf->data); + else if (previous_buf && previous_buf->len > 0) + fprintf(fd, "%s\n", previous_buf->data); if (is_pipe) result = pclose(fd); @@ -1427,39 +2462,70 @@ exec_command(const char *cmd, if (result == EOF) { - psql_error("%s: %s\n", fname, strerror(errno)); - success = false; + pg_log_error("%s: %m", fname); + status = PSQL_CMD_ERROR; } } + if (is_pipe) + restore_sigpipe_trap(); + 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); - long sleep = 2; + double sleep = 2; /* Convert optional sleep-length argument */ if (opt) { - sleep = strtol(opt, NULL, 10); + sleep = strtod(opt, NULL); if (sleep <= 0) sleep = 1; free(opt); } + /* If query_buf is empty, recall and execute previous query */ + 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); @@ -1467,9 +2533,21 @@ exec_command(const char *cmd, success = do_pset("expanded", opt, &pset.popt, pset.quiet); free(opt); } + else + ignore_slash_options(scan_state); + + 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; - /* \z -- list table rights (equivalent to \dp) */ - else if (strcmp(cmd, "z") == 0) + if (active_branch) { char *pattern = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); @@ -1478,9 +2556,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); @@ -1488,12 +2578,22 @@ 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); + OT_NORMAL, NULL, false); if (!opt0 || strcmp(opt0, "commands") == 0) slashUsage(pset.popt.topt.pager); @@ -1503,34 +2603,236 @@ 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); + + return PSQL_CMD_SKIP_LINE; +} -#if 0 + +/* + * Read and interpret an argument to the \connect slash command. + * + * Returns a malloc'd string, or NULL if no/empty argument. + */ +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) + result = psql_scan_slash_option(scan_state, OT_SQLIDHACK, "e, true); + + if (!result) + return NULL; + + if (quote) + return result; + + if (*result == '\0' || strcmp(result, "-") == 0) { - int i = 0; - char *value; + free(result); + return NULL; + } - while ((value = psql_scan_slash_option(scan_state, - OT_NORMAL, NULL, true))) - { - psql_error("+ opt(%d) = |%s|\n", i++, value); - free(value); - } + 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); } /* @@ -1541,20 +2843,19 @@ exec_command(const char *cmd, static char * prompt_for_password(const char *username) { - char *result; + char buf[100]; - if (username == NULL) - result = simple_prompt("Password: ", 100, false); + if (username == NULL || username[0] == '\0') + simple_prompt("Password: ", buf, sizeof(buf), false); else { char *prompt_text; prompt_text = psprintf(_("Password for user %s: "), username); - result = simple_prompt(prompt_text, 100, false); + simple_prompt(prompt_text, buf, sizeof(buf), false); free(prompt_text); } - - return result; + return pg_strdup(buf); } static bool @@ -1572,20 +2873,27 @@ param_is_newly_set(const char *old_val, const char *new_val) /* * do_connect -- handler for \connect * - * Connects to a database with given parameters. If there exists an - * established connection, NULL values will be replaced with the ones - * in the current connection. Otherwise NULL will be passed for that - * parameter to PQconnectdbParams(), so the libpq defaults will be used. + * Connects to a database with given parameters. Absent an established + * connection, all parameters are required. Given -reuse-previous=off or a + * connection string without -reuse-previous=on, NULL values will pass through + * to PQconnectdbParams(), so the libpq defaults will be used. Otherwise, NULL + * values will be replaced with the ones in the current connection. * * In interactive mode, if connection fails with the given parameters, * the old connection will be kept. */ static bool -do_connect(char *dbname, char *user, char *host, char *port) +do_connect(enum trivalue reuse_previous_specification, + char *dbname, char *user, char *host, char *port) { PGconn *o_conn = pset.db, *n_conn; char *password = NULL; + char *hostaddr = NULL; + bool keep_password; + bool has_connection_string; + bool reuse_previous; + PQExpBufferData connstr; if (!o_conn && (!dbname || !user || !host || !port)) { @@ -1594,25 +2902,89 @@ do_connect(char *dbname, char *user, char *host, char *port) * connect to the wrong database by using defaults, so require all * parameters to be specified. */ - psql_error("All connection parameters must be supplied because no " - "database connection exists\n"); + pg_log_error("All connection parameters must be supplied because no " + "database connection exists"); return false; } - if (!dbname) - dbname = PQdb(o_conn); - if (!user) - user = PQuser(o_conn); - if (!host) - host = PQhost(o_conn); - if (!port) - port = PQport(o_conn); + has_connection_string = dbname ? + recognized_connection_string(dbname) : false; + switch (reuse_previous_specification) + { + case TRI_YES: + reuse_previous = true; + break; + case TRI_NO: + reuse_previous = false; + break; + default: + reuse_previous = !has_connection_string; + break; + } + /* Silently ignore arguments subsequent to a connection string. */ + if (has_connection_string) + { + user = NULL; + host = NULL; + port = NULL; + } + + /* grab missing values from the old connection */ + if (reuse_previous) + { + if (!user) + user = PQuser(o_conn); + if (host && strcmp(host, PQhost(o_conn)) == 0) + { + /* + * if we are targetting the same host, reuse its hostaddr for + * consistency + */ + hostaddr = PQhostaddr(o_conn); + } + if (!host) + { + host = PQhost(o_conn); + /* also set hostaddr for consistency */ + hostaddr = PQhostaddr(o_conn); + } + if (!port) + port = PQport(o_conn); + } + + /* + * Any change in the parameters read above makes us discard the password. + * We also discard it if we're to use a conninfo rather than the + * positional syntax. + */ + if (has_connection_string) + keep_password = false; + else + keep_password = + (user && PQuser(o_conn) && strcmp(user, PQuser(o_conn)) == 0) && + (host && PQhost(o_conn) && strcmp(host, PQhost(o_conn)) == 0) && + (port && PQport(o_conn) && strcmp(port, PQport(o_conn)) == 0); + + /* + * Grab missing dbname from old connection. No password discard if this + * changes: passwords aren't (usually) database-specific. + */ + if (!dbname && reuse_previous) + { + initPQExpBuffer(&connstr); + appendPQExpBuffer(&connstr, "dbname="); + appendConnStrVal(&connstr, PQdb(o_conn)); + dbname = connstr.data; + /* has_connection_string=true would be a dead store */ + } + else + connstr.data = NULL; /* * If the user asked to be prompted for a password, ask for one now. If * not, use the password from the old connection, provided the username - * has not changed. Otherwise, try to connect without a password first, - * and then ask for a password if needed. + * etc have not changed. Otherwise, try to connect without a password + * first, and then ask for a password if needed. * * XXX: this behavior leads to spurious connection attempts recorded in * the postmaster's log. But libpq offers no API that would let us obtain @@ -1620,44 +2992,74 @@ do_connect(char *dbname, char *user, char *host, char *port) */ if (pset.getPassword == TRI_YES) { - password = prompt_for_password(user); + /* + * If a connstring or URI is provided, we can't be sure we know which + * username will be used, since we haven't parsed that argument yet. + * Don't risk issuing a misleading prompt. As in startup.c, it does + * not seem worth working harder, since this getPassword option is + * normally only used in noninteractive cases. + */ + password = prompt_for_password(has_connection_string ? NULL : user); } - else if (o_conn && user && strcmp(PQuser(o_conn), user) == 0) + else if (o_conn && keep_password) { - password = pg_strdup(PQpass(o_conn)); + password = PQpass(o_conn); + if (password && *password) + password = pg_strdup(password); + else + password = NULL; } while (true) { -#define PARAMS_ARRAY_SIZE 8 +#define PARAMS_ARRAY_SIZE 9 const char **keywords = pg_malloc(PARAMS_ARRAY_SIZE * sizeof(*keywords)); const char **values = pg_malloc(PARAMS_ARRAY_SIZE * sizeof(*values)); + int paramnum = -1; + + keywords[++paramnum] = "host"; + values[paramnum] = host; + if (hostaddr && *hostaddr) + { + keywords[++paramnum] = "hostaddr"; + values[paramnum] = hostaddr; + } + keywords[++paramnum] = "port"; + values[paramnum] = port; + keywords[++paramnum] = "user"; + values[paramnum] = user; - keywords[0] = "host"; - values[0] = host; - keywords[1] = "port"; - values[1] = port; - keywords[2] = "user"; - values[2] = user; - keywords[3] = "password"; - values[3] = password; - keywords[4] = "dbname"; - values[4] = dbname; - keywords[5] = "fallback_application_name"; - values[5] = pset.progname; - keywords[6] = "client_encoding"; - values[6] = (pset.notty || getenv("PGCLIENTENCODING")) ? NULL : "auto"; - keywords[7] = NULL; - values[7] = NULL; + /* + * Position in the array matters when the dbname is a connection + * string, because settings in a connection string override earlier + * array entries only. Thus, user= in the connection string always + * takes effect, but client_encoding= often will not. + * + * If you change this code, also change the initial-connection code in + * main(). For no good reason, a connection string password= takes + * precedence in main() but not here. + */ + keywords[++paramnum] = "dbname"; + values[paramnum] = dbname; + keywords[++paramnum] = "password"; + values[paramnum] = password; + keywords[++paramnum] = "fallback_application_name"; + values[paramnum] = pset.progname; + keywords[++paramnum] = "client_encoding"; + values[paramnum] = (pset.notty || getenv("PGCLIENTENCODING")) ? NULL : "auto"; + + /* add array terminator */ + keywords[++paramnum] = NULL; + values[paramnum] = NULL; n_conn = PQconnectdbParams(keywords, values, true); - free(keywords); - free(values); + pg_free(keywords); + pg_free(values); /* We can immediately discard the password -- no longer needed */ if (password) - free(password); + pg_free(password); if (PQstatus(n_conn) == CONNECTION_OK) break; @@ -1668,8 +3070,12 @@ do_connect(char *dbname, char *user, char *host, char *port) */ if (!password && PQconnectionNeedsPassword(n_conn) && pset.getPassword != TRI_NO) { + /* + * Prompt for password using the username we actually connected + * with --- it might've come out of "dbname" rather than "user". + */ + password = prompt_for_password(PQuser(n_conn)); PQfinish(n_conn); - password = prompt_for_password(user); continue; } @@ -1680,15 +3086,15 @@ do_connect(char *dbname, char *user, char *host, char *port) */ if (pset.cur_cmd_interactive) { - psql_error("%s", PQerrorMessage(n_conn)); + pg_log_info("%s", PQerrorMessage(n_conn)); /* pset.db is left unmodified */ if (o_conn) - psql_error("Previous connection kept\n"); + pg_log_info("Previous connection kept"); } else { - psql_error("\\connect: %s", PQerrorMessage(n_conn)); + pg_log_error("\\connect: %s", PQerrorMessage(n_conn)); if (o_conn) { PQfinish(o_conn); @@ -1697,8 +3103,12 @@ do_connect(char *dbname, char *user, char *host, char *port) } PQfinish(n_conn); + if (connstr.data) + termPQExpBuffer(&connstr); return false; } + if (connstr.data) + termPQExpBuffer(&connstr); /* * Replace the old connection with the new one, and update @@ -1712,20 +3122,32 @@ do_connect(char *dbname, char *user, char *host, char *port) /* Tell the user about the new connection */ if (!pset.quiet) { - if (param_is_newly_set(PQhost(o_conn), PQhost(pset.db)) || + if (!o_conn || + param_is_newly_set(PQhost(o_conn), PQhost(pset.db)) || param_is_newly_set(PQport(o_conn), PQport(pset.db))) { char *host = PQhost(pset.db); + char *hostaddr = PQhostaddr(pset.db); - if (host == NULL) - host = DEFAULT_PGSOCKET_DIR; /* If the host is an absolute path, the connection is via socket */ if (is_absolute_path(host)) - printf(_("You are now connected to database \"%s\" as user \"%s\" via socket in \"%s\" at port \"%s\".\n"), - PQdb(pset.db), PQuser(pset.db), host, PQport(pset.db)); + { + if (hostaddr && *hostaddr) + printf(_("You are now connected to database \"%s\" as user \"%s\" on address \"%s\" at port \"%s\".\n"), + PQdb(pset.db), PQuser(pset.db), hostaddr, PQport(pset.db)); + else + printf(_("You are now connected to database \"%s\" as user \"%s\" via socket in \"%s\" at port \"%s\".\n"), + PQdb(pset.db), PQuser(pset.db), host, PQport(pset.db)); + } else - printf(_("You are now connected to database \"%s\" as user \"%s\" on host \"%s\" at port \"%s\".\n"), - PQdb(pset.db), PQuser(pset.db), host, PQport(pset.db)); + { + if (hostaddr && *hostaddr && strcmp(host, hostaddr) != 0) + printf(_("You are now connected to database \"%s\" as user \"%s\" on host \"%s\" (address \"%s\") at port \"%s\".\n"), + PQdb(pset.db), PQuser(pset.db), host, hostaddr, PQport(pset.db)); + else + printf(_("You are now connected to database \"%s\" as user \"%s\" on host \"%s\" at port \"%s\".\n"), + PQdb(pset.db), PQuser(pset.db), host, PQport(pset.db)); + } } else printf(_("You are now connected to database \"%s\" as user \"%s\".\n"), @@ -1744,22 +3166,21 @@ connection_warnings(bool in_startup) if (!pset.quiet && !pset.notty) { int client_ver = PG_VERSION_NUM; + char cverbuf[32]; + char sverbuf[32]; if (pset.sversion != client_ver) { const char *server_version; - char server_ver_str[16]; /* Try to get full text form, might include "devel" etc */ server_version = PQparameterStatus(pset.db, "server_version"); + /* Otherwise fall back on pset.sversion */ if (!server_version) { - snprintf(server_ver_str, sizeof(server_ver_str), - "%d.%d.%d", - pset.sversion / 10000, - (pset.sversion / 100) % 100, - pset.sversion % 100); - server_version = server_ver_str; + formatPGVersionNumber(pset.sversion, true, + sverbuf, sizeof(sverbuf)); + server_version = sverbuf; } printf(_("%s (%s, server %s)\n"), @@ -1770,15 +3191,19 @@ connection_warnings(bool in_startup) printf("%s (%s)\n", pset.progname, PG_VERSION); if (pset.sversion / 100 > client_ver / 100) - printf(_("WARNING: %s major version %d.%d, server major version %d.%d.\n" + printf(_("WARNING: %s major version %s, server major version %s.\n" " Some psql features might not work.\n"), - pset.progname, client_ver / 10000, (client_ver / 100) % 100, - pset.sversion / 10000, (pset.sversion / 100) % 100); + pset.progname, + formatPGVersionNumber(client_ver, false, + cverbuf, sizeof(cverbuf)), + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); #ifdef WIN32 checkWin32Codepage(); #endif printSSLInfo(); + printGSSInfo(); } } @@ -1791,28 +3216,38 @@ connection_warnings(bool in_startup) static void printSSLInfo(void) { -#ifdef USE_OPENSSL - int sslbits = -1; - SSL *ssl; + const char *protocol; + const char *cipher; + const char *bits; + const char *compression; - ssl = PQgetssl(pset.db); - if (!ssl) + if (!PQsslInUse(pset.db)) return; /* no SSL */ - SSL_get_cipher_bits(ssl, &sslbits); - printf(_("SSL connection (protocol: %s, cipher: %s, bits: %d, compression: %s)\n"), - SSL_get_version(ssl), SSL_get_cipher(ssl), sslbits, - SSL_get_current_compression(ssl) ? _("on") : _("off")); -#else + protocol = PQsslAttribute(pset.db, "protocol"); + cipher = PQsslAttribute(pset.db, "cipher"); + bits = PQsslAttribute(pset.db, "key_bits"); + compression = PQsslAttribute(pset.db, "compression"); - /* - * If psql is compiled without SSL but is using a libpq with SSL, we - * cannot figure out the specifics about the connection. But we know it's - * SSL secured. - */ - if (PQgetssl(pset.db)) - printf(_("SSL connection (unknown cipher)\n")); -#endif + printf(_("SSL connection (protocol: %s, cipher: %s, bits: %s, compression: %s)\n"), + protocol ? protocol : _("unknown"), + cipher ? cipher : _("unknown"), + bits ? bits : _("unknown"), + (compression && strcmp(compression, "off") != 0) ? _("on") : _("off")); +} + +/* + * printGSSInfo + * + * Prints information about the current GSSAPI connection, if GSSAPI encryption is in use + */ +static void +printGSSInfo(void) +{ + if (!PQgssEncInUse(pset.db)) + return; /* no GSSAPI encryption in use */ + + printf(_("GSSAPI Encrypted connection\n")); } @@ -1850,6 +3285,9 @@ checkWin32Codepage(void) void SyncVariables(void) { + char vbuf[32]; + const char *server_version; + /* get stuff from connection */ pset.encoding = PQclientEncoding(pset.db); pset.popt.topt.encoding = pset.encoding; @@ -1861,8 +3299,23 @@ SyncVariables(void) SetVariable(pset.vars, "PORT", PQport(pset.db)); SetVariable(pset.vars, "ENCODING", pg_encoding_to_char(pset.encoding)); + /* this bit should match connection_warnings(): */ + /* Try to get full text form of version, might include "devel" etc */ + server_version = PQparameterStatus(pset.db, "server_version"); + /* Otherwise fall back on pset.sversion */ + if (!server_version) + { + formatPGVersionNumber(pset.sversion, true, vbuf, sizeof(vbuf)); + server_version = vbuf; + } + SetVariable(pset.vars, "SERVER_VERSION_NAME", server_version); + + snprintf(vbuf, sizeof(vbuf), "%d", pset.sversion); + SetVariable(pset.vars, "SERVER_VERSION_NUM", vbuf); + /* send stuff to it, too */ PQsetErrorVerbosity(pset.db, pset.verbosity); + PQsetErrorContextVisibility(pset.db, pset.show_context); } /* @@ -1878,6 +3331,8 @@ UnsyncVariables(void) SetVariable(pset.vars, "HOST", NULL); SetVariable(pset.vars, "PORT", NULL); SetVariable(pset.vars, "ENCODING", NULL); + SetVariable(pset.vars, "SERVER_VERSION_NAME", NULL); + SetVariable(pset.vars, "SERVER_VERSION_NUM", NULL); } @@ -1916,7 +3371,7 @@ editFile(const char *fname, int lineno) #endif if (!editor_lineno_arg) { - psql_error("environment variable PSQL_EDITOR_LINENUMBER_ARG must be set to specify a line number\n"); + pg_log_error("environment variable PSQL_EDITOR_LINENUMBER_ARG must be set to specify a line number"); return false; } } @@ -1945,9 +3400,9 @@ editFile(const char *fname, int lineno) #endif result = system(sys); if (result == -1) - psql_error("could not start editor \"%s\"\n", editorName); + pg_log_error("could not start editor \"%s\"", editorName); else if (result == 127) - psql_error("could not start /bin/sh\n"); + pg_log_error("could not start /bin/sh"); free(sys); return result == 0; @@ -1985,7 +3440,7 @@ do_edit(const char *filename_arg, PQExpBuffer query_buf, ret = GetTempPath(MAXPGPATH, tmpdir); if (ret == 0 || ret > MAXPGPATH) { - psql_error("could not locate temporary directory: %s\n", + pg_log_error("could not locate temporary directory: %s", !ret ? strerror(errno) : ""); return false; } @@ -2001,7 +3456,7 @@ do_edit(const char *filename_arg, PQExpBuffer query_buf, "/", (int) getpid()); #else snprintf(fnametmp, sizeof(fnametmp), "%s%spsql.edit.%d.sql", tmpdir, - "" /* trailing separator already present */ , (int) getpid()); + "" /* trailing separator already present */ , (int) getpid()); #endif fname = (const char *) fnametmp; @@ -2012,7 +3467,7 @@ do_edit(const char *filename_arg, PQExpBuffer query_buf, if (fd == -1 || !stream) { - psql_error("could not open temporary file \"%s\": %s\n", fname, strerror(errno)); + pg_log_error("could not open temporary file \"%s\": %m", fname); error = true; } else @@ -2027,21 +3482,21 @@ do_edit(const char *filename_arg, PQExpBuffer query_buf, if (fwrite(query_buf->data, 1, ql, stream) != ql) { - psql_error("%s: %s\n", fname, strerror(errno)); + pg_log_error("%s: %m", fname); if (fclose(stream) != 0) - psql_error("%s: %s\n", fname, strerror(errno)); + pg_log_error("%s: %m", fname); if (remove(fname) != 0) - psql_error("%s: %s\n", fname, strerror(errno)); + pg_log_error("%s: %m", fname); error = true; } else if (fclose(stream) != 0) { - psql_error("%s: %s\n", fname, strerror(errno)); + pg_log_error("%s: %m", fname); if (remove(fname) != 0) - psql_error("%s: %s\n", fname, strerror(errno)); + pg_log_error("%s: %m", fname); error = true; } } @@ -2049,7 +3504,7 @@ do_edit(const char *filename_arg, PQExpBuffer query_buf, if (!error && stat(fname, &before) != 0) { - psql_error("%s: %s\n", fname, strerror(errno)); + pg_log_error("%s: %m", fname); error = true; } @@ -2059,7 +3514,7 @@ do_edit(const char *filename_arg, PQExpBuffer query_buf, if (!error && stat(fname, &after) != 0) { - psql_error("%s: %s\n", fname, strerror(errno)); + pg_log_error("%s: %m", fname); error = true; } @@ -2068,7 +3523,7 @@ do_edit(const char *filename_arg, PQExpBuffer query_buf, stream = fopen(fname, PG_BINARY_R); if (!stream) { - psql_error("%s: %s\n", fname, strerror(errno)); + pg_log_error("%s: %m", fname); error = true; } else @@ -2082,7 +3537,7 @@ do_edit(const char *filename_arg, PQExpBuffer query_buf, if (ferror(stream)) { - psql_error("%s: %s\n", fname, strerror(errno)); + pg_log_error("%s: %m", fname); error = true; } else if (edited) @@ -2099,7 +3554,7 @@ do_edit(const char *filename_arg, PQExpBuffer query_buf, { if (remove(fname) == -1) { - psql_error("%s: %s\n", fname, strerror(errno)); + pg_log_error("%s: %m", fname); error = true; } } @@ -2120,13 +3575,12 @@ do_edit(const char *filename_arg, PQExpBuffer query_buf, * the file from where the currently processed file (if any) is located. */ int -process_file(char *filename, bool single_txn, bool use_relative_path) +process_file(char *filename, bool use_relative_path) { FILE *fd; int result; char *oldfilename; char relpath[MAXPGPATH]; - PGresult *res; if (!filename) { @@ -2158,7 +3612,7 @@ process_file(char *filename, bool single_txn, bool use_relative_path) if (!fd) { - psql_error("%s: %s\n", filename, strerror(errno)); + pg_log_error("%s: %m", filename); return EXIT_FAILURE; } } @@ -2171,50 +3625,22 @@ process_file(char *filename, bool single_txn, bool use_relative_path) oldfilename = pset.inputfile; pset.inputfile = filename; - if (single_txn) - { - if ((res = PSQLexec("BEGIN", false)) == NULL) - { - if (pset.on_error_stop) - { - result = EXIT_USER; - goto error; - } - } - else - PQclear(res); - } + pg_logging_config(pset.inputfile ? 0 : PG_LOG_FLAG_TERSE); result = MainLoop(fd); - if (single_txn) - { - if ((res = PSQLexec("COMMIT", false)) == NULL) - { - if (pset.on_error_stop) - { - result = EXIT_USER; - goto error; - } - } - else - PQclear(res); - } - -error: if (fd != stdin) fclose(fd); pset.inputfile = oldfilename; + + pg_logging_config(pset.inputfile ? 0 : PG_LOG_FLAG_TERSE); + return result; } -/* - * do_pset - * - */ static const char * _align2string(enum printFormat in) { @@ -2223,14 +3649,14 @@ _align2string(enum printFormat in) case PRINT_NOTHING: return "nothing"; break; - case PRINT_UNALIGNED: - return "unaligned"; - break; case PRINT_ALIGNED: return "aligned"; break; - case PRINT_WRAPPED: - return "wrapped"; + case PRINT_ASCIIDOC: + return "asciidoc"; + break; + case PRINT_CSV: + return "csv"; break; case PRINT_HTML: return "html"; @@ -2241,14 +3667,55 @@ _align2string(enum printFormat in) case PRINT_LATEX_LONGTABLE: return "latex-longtable"; break; - case PRINT_TROFF_MS: - return "troff-ms"; + case PRINT_TROFF_MS: + return "troff-ms"; + break; + case PRINT_UNALIGNED: + return "unaligned"; + break; + case PRINT_WRAPPED: + return "wrapped"; + break; + } + return "unknown"; +} + +/* + * Parse entered Unicode linestyle. If ok, update *linestyle and return + * true, else return false. + */ +static bool +set_unicode_line_style(const char *value, size_t vallen, + unicode_linestyle *linestyle) +{ + if (pg_strncasecmp("single", value, vallen) == 0) + *linestyle = UNICODE_LINESTYLE_SINGLE; + else if (pg_strncasecmp("double", value, vallen) == 0) + *linestyle = UNICODE_LINESTYLE_DOUBLE; + else + return false; + return true; +} + +static const char * +_unicode_linestyle2string(int linestyle) +{ + switch (linestyle) + { + case UNICODE_LINESTYLE_SINGLE: + return "single"; + break; + case UNICODE_LINESTYLE_DOUBLE: + return "double"; break; } return "unknown"; } - +/* + * do_pset + * + */ bool do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) { @@ -2262,28 +3729,61 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) /* set format */ if (strcmp(param, "format") == 0) { + static const struct fmt + { + const char *name; + enum printFormat number; + } formats[] = + { + /* remember to update error message below when adding more */ + {"aligned", PRINT_ALIGNED}, + {"asciidoc", PRINT_ASCIIDOC}, + {"csv", PRINT_CSV}, + {"html", PRINT_HTML}, + {"latex", PRINT_LATEX}, + {"troff-ms", PRINT_TROFF_MS}, + {"unaligned", PRINT_UNALIGNED}, + {"wrapped", PRINT_WRAPPED} + }; + if (!value) ; - else if (pg_strncasecmp("unaligned", value, vallen) == 0) - popt->topt.format = PRINT_UNALIGNED; - else if (pg_strncasecmp("aligned", value, vallen) == 0) - popt->topt.format = PRINT_ALIGNED; - else if (pg_strncasecmp("wrapped", value, vallen) == 0) - popt->topt.format = PRINT_WRAPPED; - else if (pg_strncasecmp("html", value, vallen) == 0) - popt->topt.format = PRINT_HTML; - else if (pg_strncasecmp("latex", value, vallen) == 0) - popt->topt.format = PRINT_LATEX; - else if (pg_strncasecmp("latex-longtable", value, vallen) == 0) - popt->topt.format = PRINT_LATEX_LONGTABLE; - else if (pg_strncasecmp("troff-ms", value, vallen) == 0) - popt->topt.format = PRINT_TROFF_MS; else { - psql_error("\\pset: allowed formats are unaligned, aligned, wrapped, html, latex, troff-ms\n"); - return false; - } + int match_pos = -1; + for (int i = 0; i < lengthof(formats); i++) + { + if (pg_strncasecmp(formats[i].name, value, vallen) == 0) + { + if (match_pos < 0) + match_pos = i; + else + { + pg_log_error("\\pset: ambiguous abbreviation \"%s\" matches both \"%s\" and \"%s\"", + value, + formats[match_pos].name, formats[i].name); + return false; + } + } + } + if (match_pos >= 0) + popt->topt.format = formats[match_pos].number; + else if (pg_strncasecmp("latex-longtable", value, vallen) == 0) + { + /* + * We must treat latex-longtable specially because latex is a + * prefix of it; if both were in the table above, we'd think + * "latex" is ambiguous. + */ + popt->topt.format = PRINT_LATEX_LONGTABLE; + } + else + { + pg_log_error("\\pset: allowed formats are aligned, asciidoc, csv, html, latex, latex-longtable, troff-ms, unaligned, wrapped"); + return false; + } + } } /* set table line style */ @@ -2299,10 +3799,54 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) popt->topt.line_style = &pg_utf8format; else { - psql_error("\\pset: allowed line styles are ascii, old-ascii, unicode\n"); + pg_log_error("\\pset: allowed line styles are ascii, old-ascii, unicode"); + return false; + } + } + + /* set unicode border line style */ + else if (strcmp(param, "unicode_border_linestyle") == 0) + { + if (!value) + ; + else if (set_unicode_line_style(value, vallen, + &popt->topt.unicode_border_linestyle)) + refresh_utf8format(&(popt->topt)); + else + { + pg_log_error("\\pset: allowed Unicode border line styles are single, double"); + return false; + } + } + + /* set unicode column line style */ + else if (strcmp(param, "unicode_column_linestyle") == 0) + { + if (!value) + ; + else if (set_unicode_line_style(value, vallen, + &popt->topt.unicode_column_linestyle)) + refresh_utf8format(&(popt->topt)); + else + { + pg_log_error("\\pset: allowed Unicode column line styles are single, double"); return false; } + } + /* set unicode header line style */ + else if (strcmp(param, "unicode_header_linestyle") == 0) + { + if (!value) + ; + else if (set_unicode_line_style(value, vallen, + &popt->topt.unicode_header_linestyle)) + refresh_utf8format(&(popt->topt)); + else + { + pg_log_error("\\pset: allowed Unicode header line styles are single, double"); + return false; + } } /* set border style/width */ @@ -2310,25 +3854,56 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) { if (value) popt->topt.border = atoi(value); - } /* set expanded/vertical mode */ - else if (strcmp(param, "x") == 0 || strcmp(param, "expanded") == 0 || strcmp(param, "vertical") == 0) + else if (strcmp(param, "x") == 0 || + strcmp(param, "expanded") == 0 || + strcmp(param, "vertical") == 0) { if (value && pg_strcasecmp(value, "auto") == 0) popt->topt.expanded = 2; else if (value) - popt->topt.expanded = ParseVariableBool(value); + { + bool on_off; + + if (ParseVariableBool(value, NULL, &on_off)) + popt->topt.expanded = on_off ? 1 : 0; + else + { + PsqlVarEnumError(param, value, "on, off, auto"); + return false; + } + } else popt->topt.expanded = !popt->topt.expanded; } + /* field separator for CSV format */ + else if (strcmp(param, "csv_fieldsep") == 0) + { + if (value) + { + /* CSV separator has to be a one-byte character */ + if (strlen(value) != 1) + { + pg_log_error("\\pset: csv_fieldsep must be a single one-byte character"); + return false; + } + if (value[0] == '"' || value[0] == '\n' || value[0] == '\r') + { + pg_log_error("\\pset: csv_fieldsep cannot be a double quote, a newline, or a carriage return"); + return false; + } + popt->topt.csvFieldSep[0] = value[0]; + } + } + /* locale-aware numeric output */ else if (strcmp(param, "numericlocale") == 0) { if (value) - popt->topt.numericLocale = ParseVariableBool(value); + return ParseVariableBool(value, param, &popt->topt.numericLocale); else popt->topt.numericLocale = !popt->topt.numericLocale; } @@ -2383,13 +3958,13 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) else if (strcmp(param, "t") == 0 || strcmp(param, "tuples_only") == 0) { if (value) - popt->topt.tuples_only = ParseVariableBool(value); + return ParseVariableBool(value, param, &popt->topt.tuples_only); else popt->topt.tuples_only = !popt->topt.tuples_only; } /* set title override */ - else if (strcmp(param, "title") == 0) + else if (strcmp(param, "C") == 0 || strcmp(param, "title") == 0) { free(popt->title); if (!value) @@ -2414,21 +3989,34 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) if (value && pg_strcasecmp(value, "always") == 0) popt->topt.pager = 2; else if (value) - if (ParseVariableBool(value)) - popt->topt.pager = 1; - else - popt->topt.pager = 0; + { + bool on_off; + + if (!ParseVariableBool(value, NULL, &on_off)) + { + PsqlVarEnumError(param, value, "on, off, always"); + return false; + } + popt->topt.pager = on_off ? 1 : 0; + } else if (popt->topt.pager == 1) popt->topt.pager = 0; else popt->topt.pager = 1; } + /* set minimum lines for pager use */ + else if (strcmp(param, "pager_min_lines") == 0) + { + if (value) + popt->topt.pager_min_lines = atoi(value); + } + /* disable "(x rows)" footer */ else if (strcmp(param, "footer") == 0) { if (value) - popt->topt.default_footer = ParseVariableBool(value); + return ParseVariableBool(value, param, &popt->topt.default_footer); else popt->topt.default_footer = !popt->topt.default_footer; } @@ -2441,7 +4029,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) } else { - psql_error("\\pset: unknown option: %s\n", param); + pg_log_error("\\pset: unknown option: %s", param); return false; } @@ -2459,80 +4047,76 @@ printPsetInfo(const char *param, struct printQueryOpt *popt) /* show border style/width */ if (strcmp(param, "border") == 0) - { - if (!popt->topt.border) - printf(_("Border style (%s) unset.\n"), param); - else - printf(_("Border style (%s) is %d.\n"), param, - popt->topt.border); - } + printf(_("Border style is %d.\n"), popt->topt.border); /* show the target width for the wrapped format */ else if (strcmp(param, "columns") == 0) { if (!popt->topt.columns) - printf(_("Target width (%s) unset.\n"), param); + printf(_("Target width is unset.\n")); else - printf(_("Target width (%s) is %d.\n"), param, - popt->topt.columns); + printf(_("Target width is %d.\n"), popt->topt.columns); } /* show expanded/vertical mode */ else if (strcmp(param, "x") == 0 || strcmp(param, "expanded") == 0 || strcmp(param, "vertical") == 0) { if (popt->topt.expanded == 1) - printf(_("Expanded display (%s) is on.\n"), param); + printf(_("Expanded display is on.\n")); else if (popt->topt.expanded == 2) - printf(_("Expanded display (%s) is used automatically.\n"), param); + printf(_("Expanded display is used automatically.\n")); else - printf(_("Expanded display (%s) is off.\n"), param); + printf(_("Expanded display is off.\n")); + } + + /* show field separator for CSV format */ + else if (strcmp(param, "csv_fieldsep") == 0) + { + printf(_("Field separator for CSV is \"%s\".\n"), + popt->topt.csvFieldSep); } /* show field separator for unaligned text */ else if (strcmp(param, "fieldsep") == 0) { if (popt->topt.fieldSep.separator_zero) - printf(_("Field separator (%s) is zero byte.\n"), param); + printf(_("Field separator is zero byte.\n")); else - printf(_("Field separator (%s) is \"%s\".\n"), param, + printf(_("Field separator is \"%s\".\n"), popt->topt.fieldSep.separator); } else if (strcmp(param, "fieldsep_zero") == 0) { - printf(_("Field separator (%s) is zero byte.\n"), param); + printf(_("Field separator is zero byte.\n")); } /* show disable "(x rows)" footer */ else if (strcmp(param, "footer") == 0) { if (popt->topt.default_footer) - printf(_("Default footer (%s) is on.\n"), param); + printf(_("Default footer is on.\n")); else - printf(_("Default footer (%s) is off.\n"), param); + printf(_("Default footer is off.\n")); } /* show format */ else if (strcmp(param, "format") == 0) { - if (!popt->topt.format) - printf(_("Output format (%s) is aligned.\n"), param); - else - printf(_("Output format (%s) is %s.\n"), param, - _align2string(popt->topt.format)); + printf(_("Output format is %s.\n"), _align2string(popt->topt.format)); } /* show table line style */ else if (strcmp(param, "linestyle") == 0) { - printf(_("Line style (%s) is %s.\n"), param, + printf(_("Line style is %s.\n"), get_line_style(&popt->topt)->name); } /* show null display */ else if (strcmp(param, "null") == 0) { - printf(_("Null display (%s) is \"%s\".\n"), param, + printf(_("Null display is \"%s\".\n"), popt->nullPrint ? popt->nullPrint : ""); } @@ -2540,70 +4124,98 @@ printPsetInfo(const char *param, struct printQueryOpt *popt) else if (strcmp(param, "numericlocale") == 0) { if (popt->topt.numericLocale) - printf(_("Locale-adjusted numeric output (%s) is on.\n"), param); + printf(_("Locale-adjusted numeric output is on.\n")); else - printf(_("Locale-adjusted numeric output (%s) is off.\n"), param); + printf(_("Locale-adjusted numeric output is off.\n")); } /* show toggle use of pager */ else if (strcmp(param, "pager") == 0) { if (popt->topt.pager == 1) - printf(_("Pager (%s) is used for long output.\n"), param); + printf(_("Pager is used for long output.\n")); else if (popt->topt.pager == 2) - printf(_("Pager (%s) is always used.\n"), param); + printf(_("Pager is always used.\n")); else - printf(_("Pager usage (%s) is off.\n"), param); + printf(_("Pager usage is off.\n")); + } + + /* show minimum lines for pager use */ + else if (strcmp(param, "pager_min_lines") == 0) + { + printf(ngettext("Pager won't be used for less than %d line.\n", + "Pager won't be used for less than %d lines.\n", + popt->topt.pager_min_lines), + popt->topt.pager_min_lines); } /* show record separator for unaligned text */ else if (strcmp(param, "recordsep") == 0) { if (popt->topt.recordSep.separator_zero) - printf(_("Record separator (%s) is zero byte.\n"), param); + printf(_("Record separator is zero byte.\n")); else if (strcmp(popt->topt.recordSep.separator, "\n") == 0) - printf(_("Record separator (%s) is .\n"), param); + printf(_("Record separator is .\n")); else - printf(_("Record separator (%s) is \"%s\".\n"), param, + printf(_("Record separator is \"%s\".\n"), popt->topt.recordSep.separator); } else if (strcmp(param, "recordsep_zero") == 0) { - printf(_("Record separator (%s) is zero byte.\n"), param); + printf(_("Record separator is zero byte.\n")); } /* show HTML table tag options */ else if (strcmp(param, "T") == 0 || strcmp(param, "tableattr") == 0) { if (popt->topt.tableAttr) - printf(_("Table attributes (%s) are \"%s\".\n"), param, + printf(_("Table attributes are \"%s\".\n"), popt->topt.tableAttr); else - printf(_("Table attributes (%s) unset.\n"), param); + printf(_("Table attributes unset.\n")); } /* show title override */ - else if (strcmp(param, "title") == 0) + else if (strcmp(param, "C") == 0 || strcmp(param, "title") == 0) { if (popt->title) - printf(_("Title (%s) is \"%s\".\n"), param, popt->title); + printf(_("Title is \"%s\".\n"), popt->title); else - printf(_("Title (%s) unset.\n"), param); + printf(_("Title is unset.\n")); } /* show toggle between full and tuples-only format */ else if (strcmp(param, "t") == 0 || strcmp(param, "tuples_only") == 0) { if (popt->topt.tuples_only) - printf(_("Tuples only (%s) is on.\n"), param); + printf(_("Tuples only is on.\n")); else - printf(_("Tuples only (%s) is off.\n"), param); + printf(_("Tuples only is off.\n")); + } + + /* Unicode style formatting */ + else if (strcmp(param, "unicode_border_linestyle") == 0) + { + printf(_("Unicode border line style is \"%s\".\n"), + _unicode_linestyle2string(popt->topt.unicode_border_linestyle)); + } + + else if (strcmp(param, "unicode_column_linestyle") == 0) + { + printf(_("Unicode column line style is \"%s\".\n"), + _unicode_linestyle2string(popt->topt.unicode_column_linestyle)); + } + + else if (strcmp(param, "unicode_header_linestyle") == 0) + { + printf(_("Unicode header line style is \"%s\".\n"), + _unicode_linestyle2string(popt->topt.unicode_header_linestyle)); } else { - psql_error("\\pset: unknown option: %s\n", param); + pg_log_error("\\pset: unknown option: %s", param); return false; } @@ -2611,6 +4223,111 @@ printPsetInfo(const char *param, struct printQueryOpt *popt) } +static const char * +pset_bool_string(bool val) +{ + return val ? "on" : "off"; +} + + +static char * +pset_quoted_string(const char *str) +{ + char *ret = pg_malloc(strlen(str) * 2 + 3); + char *r = ret; + + *r++ = '\''; + + for (; *str; str++) + { + if (*str == '\n') + { + *r++ = '\\'; + *r++ = 'n'; + } + else if (*str == '\'') + { + *r++ = '\\'; + *r++ = '\''; + } + else + *r++ = *str; + } + + *r++ = '\''; + *r = '\0'; + + return ret; +} + + +/* + * Return a malloc'ed string for the \pset value. + * + * Note that for some string parameters, print.c distinguishes between unset + * and empty string, but for others it doesn't. This function should produce + * output that produces the correct setting when fed back into \pset. + */ +static char * +pset_value_string(const char *param, struct printQueryOpt *popt) +{ + Assert(param != NULL); + + if (strcmp(param, "border") == 0) + return psprintf("%d", popt->topt.border); + else if (strcmp(param, "columns") == 0) + return psprintf("%d", popt->topt.columns); + else if (strcmp(param, "csv_fieldsep") == 0) + return pset_quoted_string(popt->topt.csvFieldSep); + else if (strcmp(param, "expanded") == 0) + return pstrdup(popt->topt.expanded == 2 + ? "auto" + : pset_bool_string(popt->topt.expanded)); + else if (strcmp(param, "fieldsep") == 0) + return pset_quoted_string(popt->topt.fieldSep.separator + ? popt->topt.fieldSep.separator + : ""); + else if (strcmp(param, "fieldsep_zero") == 0) + return pstrdup(pset_bool_string(popt->topt.fieldSep.separator_zero)); + else if (strcmp(param, "footer") == 0) + return pstrdup(pset_bool_string(popt->topt.default_footer)); + else if (strcmp(param, "format") == 0) + return psprintf("%s", _align2string(popt->topt.format)); + else if (strcmp(param, "linestyle") == 0) + return psprintf("%s", get_line_style(&popt->topt)->name); + else if (strcmp(param, "null") == 0) + return pset_quoted_string(popt->nullPrint + ? popt->nullPrint + : ""); + else if (strcmp(param, "numericlocale") == 0) + return pstrdup(pset_bool_string(popt->topt.numericLocale)); + else if (strcmp(param, "pager") == 0) + return psprintf("%d", popt->topt.pager); + else if (strcmp(param, "pager_min_lines") == 0) + return psprintf("%d", popt->topt.pager_min_lines); + else if (strcmp(param, "recordsep") == 0) + return pset_quoted_string(popt->topt.recordSep.separator + ? popt->topt.recordSep.separator + : ""); + else if (strcmp(param, "recordsep_zero") == 0) + return pstrdup(pset_bool_string(popt->topt.recordSep.separator_zero)); + else if (strcmp(param, "tableattr") == 0) + return popt->topt.tableAttr ? pset_quoted_string(popt->topt.tableAttr) : pstrdup(""); + else if (strcmp(param, "title") == 0) + return popt->title ? pset_quoted_string(popt->title) : pstrdup(""); + else if (strcmp(param, "tuples_only") == 0) + return pstrdup(pset_bool_string(popt->topt.tuples_only)); + else if (strcmp(param, "unicode_border_linestyle") == 0) + return pstrdup(_unicode_linestyle2string(popt->topt.unicode_border_linestyle)); + else if (strcmp(param, "unicode_column_linestyle") == 0) + return pstrdup(_unicode_linestyle2string(popt->topt.unicode_column_linestyle)); + else if (strcmp(param, "unicode_header_linestyle") == 0) + return pstrdup(_unicode_linestyle2string(popt->topt.unicode_header_linestyle)); + else + return pstrdup("ERROR"); +} + + #ifndef WIN32 #define DEFAULT_SHELL "/bin/sh" @@ -2654,7 +4371,7 @@ do_shell(const char *command) if (result == 127 || result == -1) { - psql_error("\\!: failed\n"); + pg_log_error("\\!: failed"); return false; } return true; @@ -2667,50 +4384,75 @@ do_shell(const char *command) * onto a bunch of exec_command's variables to silence stupider compilers. */ static bool -do_watch(PQExpBuffer query_buf, long sleep) +do_watch(PQExpBuffer query_buf, double sleep) { + long sleep_ms = (long) (sleep * 1000); printQueryOpt myopt = pset.popt; - char title[50]; + const char *strftime_fmt; + const char *user_title; + char *title; + int title_len; + int res = 0; if (!query_buf || query_buf->len <= 0) { - psql_error(_("\\watch cannot be used with an empty query\n")); + pg_log_error("\\watch cannot be used with an empty query"); return false; } + /* + * Choose format for timestamps. We might eventually make this a \pset + * option. In the meantime, using a variable for the format suppresses + * overly-anal-retentive gcc warnings about %c being Y2K sensitive. + */ + strftime_fmt = "%c"; + /* * Set up rendering options, in particular, disable the pager, because * nobody wants to be prompted while watching the output of 'watch'. */ - myopt.nullPrint = NULL; myopt.topt.pager = 0; + /* + * If there's a title in the user configuration, make sure we have room + * for it in the title buffer. Allow 128 bytes for the timestamp plus 128 + * bytes for the rest. + */ + user_title = myopt.title; + title_len = (user_title ? strlen(user_title) : 0) + 256; + title = pg_malloc(title_len); + for (;;) { - int res; time_t timer; + char timebuf[128]; long i; /* - * Prepare title for output. XXX would it be better to use the time - * of completion of the command? + * Prepare title for output. Note that we intentionally include a + * newline at the end of the title; this is somewhat historical but it + * makes for reasonably nicely formatted output in simple cases. */ timer = time(NULL); - snprintf(title, sizeof(title), _("Watch every %lds\t%s"), - sleep, asctime(localtime(&timer))); + strftime(timebuf, sizeof(timebuf), strftime_fmt, localtime(&timer)); + + if (user_title) + snprintf(title, title_len, _("%s\t%s (every %gs)\n"), + user_title, timebuf, sleep); + else + snprintf(title, title_len, _("%s (every %gs)\n"), + timebuf, sleep); myopt.title = title; /* Run the query and print out the results */ res = PSQLexecWatch(query_buf->data, &myopt); /* - * PSQLexecWatch handles the case where we can no longer - * repeat the query, and returns 0 or -1. + * PSQLexecWatch handles the case where we can no longer repeat the + * query, and returns 0 or -1. */ - if (res == 0) + if (res <= 0) break; - if (res == -1) - return false; /* * Set up cancellation of 'watch' via SIGINT. We redo this each time @@ -2722,47 +4464,107 @@ do_watch(PQExpBuffer query_buf, long sleep) /* * Enable 'watch' cancellations and wait a while before running the - * query again. Break the sleep into short intervals since pg_usleep - * isn't interruptible on some platforms. + * query again. Break the sleep into short intervals (at most 1s) + * since pg_usleep isn't interruptible on some platforms. */ sigint_interrupt_enabled = true; - for (i = 0; i < sleep; i++) + i = sleep_ms; + while (i > 0) { - pg_usleep(1000000L); + long s = Min(i, 1000L); + + pg_usleep(s * 1000L); if (cancel_pressed) break; + i -= s; } sigint_interrupt_enabled = false; } + pg_free(title); + return (res >= 0); +} + +/* + * a little code borrowed from PSQLexec() to manage ECHO_HIDDEN output. + * returns true unless we have ECHO_HIDDEN_NOEXEC. + */ +static bool +echo_hidden_command(const char *query) +{ + if (pset.echo_hidden != PSQL_ECHO_HIDDEN_OFF) + { + printf(_("********* QUERY **********\n" + "%s\n" + "**************************\n\n"), query); + fflush(stdout); + if (pset.logfile) + { + fprintf(pset.logfile, + _("********* QUERY **********\n" + "%s\n" + "**************************\n\n"), query); + fflush(pset.logfile); + } + + if (pset.echo_hidden == PSQL_ECHO_HIDDEN_NOEXEC) + return false; + } return true; } /* - * This function takes a function description, e.g. "x" or "x(int)", and - * issues a query on the given connection to retrieve the function's OID - * using a cast to regproc or regprocedure (as appropriate). The result, - * if there is one, is returned at *foid. Note that we'll fail if the - * function doesn't exist OR if there are multiple matching candidates - * OR if there's something syntactically wrong with the function description; - * unfortunately it can be hard to tell the difference. + * Look up the object identified by obj_type and desc. If successful, + * store its OID in *obj_oid and return true, else return false. + * + * Note that we'll fail if the object doesn't exist OR if there are multiple + * matching candidates OR if there's something syntactically wrong with the + * object description; unfortunately it can be hard to tell the difference. */ static bool -lookup_function_oid(PGconn *conn, const char *desc, Oid *foid) +lookup_object_oid(EditableObjectType obj_type, const char *desc, + Oid *obj_oid) { bool result = true; - PQExpBuffer query; + PQExpBuffer query = createPQExpBuffer(); PGresult *res; - query = createPQExpBuffer(); - appendPQExpBufferStr(query, "SELECT "); - appendStringLiteralConn(query, desc, conn); - appendPQExpBuffer(query, "::pg_catalog.%s::pg_catalog.oid", - strchr(desc, '(') ? "regprocedure" : "regproc"); + switch (obj_type) + { + case EditableFunction: + + /* + * We have a function description, e.g. "x" or "x(int)". Issue a + * query to retrieve the function's OID using a cast to regproc or + * regprocedure (as appropriate). + */ + appendPQExpBufferStr(query, "SELECT "); + appendStringLiteralConn(query, desc, pset.db); + appendPQExpBuffer(query, "::pg_catalog.%s::pg_catalog.oid", + strchr(desc, '(') ? "regprocedure" : "regproc"); + break; + + case EditableView: - res = PQexec(conn, query->data); + /* + * Convert view name (possibly schema-qualified) to OID. Note: + * this code doesn't check if the relation is actually a view. + * We'll detect that in get_create_object_cmd(). + */ + appendPQExpBufferStr(query, "SELECT "); + appendStringLiteralConn(query, desc, pset.db); + appendPQExpBuffer(query, "::pg_catalog.regclass::pg_catalog.oid"); + break; + } + + if (!echo_hidden_command(query->data)) + { + destroyPQExpBuffer(query); + return false; + } + res = PQexec(pset.db, query->data); if (PQresultStatus(res) == PGRES_TUPLES_OK && PQntuples(res) == 1) - *foid = atooid(PQgetvalue(res, 0, 0)); + *obj_oid = atooid(PQgetvalue(res, 0, 0)); else { minimal_error_message(res); @@ -2776,24 +4578,160 @@ lookup_function_oid(PGconn *conn, const char *desc, Oid *foid) } /* - * Fetches the "CREATE OR REPLACE FUNCTION ..." command that describes the - * function with the given OID. If successful, the result is stored in buf. + * Construct a "CREATE OR REPLACE ..." command that describes the specified + * database object. If successful, the result is stored in buf. */ static bool -get_create_function_cmd(PGconn *conn, Oid oid, PQExpBuffer buf) +get_create_object_cmd(EditableObjectType obj_type, Oid oid, + PQExpBuffer buf) { bool result = true; - PQExpBuffer query; + PQExpBuffer query = createPQExpBuffer(); PGresult *res; - query = createPQExpBuffer(); - printfPQExpBuffer(query, "SELECT pg_catalog.pg_get_functiondef(%u)", oid); + switch (obj_type) + { + case EditableFunction: + printfPQExpBuffer(query, + "SELECT pg_catalog.pg_get_functiondef(%u)", + oid); + break; + + case EditableView: + + /* + * pg_get_viewdef() just prints the query, so we must prepend + * CREATE for ourselves. We must fully qualify the view name to + * ensure the right view gets replaced. Also, check relation kind + * to be sure it's a view. + * + * Starting with 9.2, views may have reloptions (security_barrier) + * and from 9.4 onwards they may also have WITH [LOCAL|CASCADED] + * CHECK OPTION. These are not part of the view definition + * returned by pg_get_viewdef() and so need to be retrieved + * separately. Materialized views (introduced in 9.3) may have + * arbitrary storage parameter reloptions. + */ + if (pset.sversion >= 90400) + { + printfPQExpBuffer(query, + "SELECT nspname, relname, relkind, " + "pg_catalog.pg_get_viewdef(c.oid, true), " + "pg_catalog.array_remove(pg_catalog.array_remove(c.reloptions,'check_option=local'),'check_option=cascaded') AS reloptions, " + "CASE WHEN 'check_option=local' = ANY (c.reloptions) THEN 'LOCAL'::text " + "WHEN 'check_option=cascaded' = ANY (c.reloptions) THEN 'CASCADED'::text ELSE NULL END AS checkoption " + "FROM pg_catalog.pg_class c " + "LEFT JOIN pg_catalog.pg_namespace n " + "ON c.relnamespace = n.oid WHERE c.oid = %u", + oid); + } + else if (pset.sversion >= 90200) + { + printfPQExpBuffer(query, + "SELECT nspname, relname, relkind, " + "pg_catalog.pg_get_viewdef(c.oid, true), " + "c.reloptions AS reloptions, " + "NULL AS checkoption " + "FROM pg_catalog.pg_class c " + "LEFT JOIN pg_catalog.pg_namespace n " + "ON c.relnamespace = n.oid WHERE c.oid = %u", + oid); + } + else + { + printfPQExpBuffer(query, + "SELECT nspname, relname, relkind, " + "pg_catalog.pg_get_viewdef(c.oid, true), " + "NULL AS reloptions, " + "NULL AS checkoption " + "FROM pg_catalog.pg_class c " + "LEFT JOIN pg_catalog.pg_namespace n " + "ON c.relnamespace = n.oid WHERE c.oid = %u", + oid); + } + break; + } - res = PQexec(conn, query->data); + if (!echo_hidden_command(query->data)) + { + destroyPQExpBuffer(query); + return false; + } + res = PQexec(pset.db, query->data); if (PQresultStatus(res) == PGRES_TUPLES_OK && PQntuples(res) == 1) { resetPQExpBuffer(buf); - appendPQExpBufferStr(buf, PQgetvalue(res, 0, 0)); + switch (obj_type) + { + case EditableFunction: + appendPQExpBufferStr(buf, PQgetvalue(res, 0, 0)); + break; + + case EditableView: + { + char *nspname = PQgetvalue(res, 0, 0); + char *relname = PQgetvalue(res, 0, 1); + char *relkind = PQgetvalue(res, 0, 2); + char *viewdef = PQgetvalue(res, 0, 3); + char *reloptions = PQgetvalue(res, 0, 4); + char *checkoption = PQgetvalue(res, 0, 5); + + /* + * If the backend ever supports CREATE OR REPLACE + * MATERIALIZED VIEW, allow that here; but as of today it + * does not, so editing a matview definition in this way + * is impossible. + */ + switch (relkind[0]) + { +#ifdef NOT_USED + case RELKIND_MATVIEW: + appendPQExpBufferStr(buf, "CREATE OR REPLACE MATERIALIZED VIEW "); + break; +#endif + case RELKIND_VIEW: + appendPQExpBufferStr(buf, "CREATE OR REPLACE VIEW "); + break; + default: + pg_log_error("\"%s.%s\" is not a view", + nspname, relname); + result = false; + break; + } + appendPQExpBuffer(buf, "%s.", fmtId(nspname)); + appendPQExpBufferStr(buf, fmtId(relname)); + + /* reloptions, if not an empty array "{}" */ + if (reloptions != NULL && strlen(reloptions) > 2) + { + appendPQExpBufferStr(buf, "\n WITH ("); + if (!appendReloptionsArray(buf, reloptions, "", + pset.encoding, + standard_strings())) + { + pg_log_error("could not parse reloptions array"); + result = false; + } + appendPQExpBufferChar(buf, ')'); + } + + /* View definition from pg_get_viewdef (a SELECT query) */ + appendPQExpBuffer(buf, " AS\n%s", viewdef); + + /* Get rid of the semicolon that pg_get_viewdef appends */ + if (buf->len > 0 && buf->data[buf->len - 1] == ';') + buf->data[--(buf->len)] = '\0'; + + /* WITH [LOCAL|CASCADED] CHECK OPTION */ + if (checkoption && checkoption[0] != '\0') + appendPQExpBuffer(buf, "\n WITH %s CHECK OPTION", + checkoption); + } + break; + } + /* Make sure result ends with a newline */ + if (buf->len > 0 && buf->data[buf->len - 1] != '\n') + appendPQExpBufferChar(buf, '\n'); } else { @@ -2808,24 +4746,24 @@ get_create_function_cmd(PGconn *conn, Oid oid, PQExpBuffer buf) } /* - * If the given argument of \ef ends with a line number, delete the line + * If the given argument of \ef or \ev ends with a line number, delete the line * number from the argument string and return it as an integer. (We need - * this kluge because we're too lazy to parse \ef's function name argument - * carefully --- we just slop it up in OT_WHOLE_LINE mode.) + * this kluge because we're too lazy to parse \ef's function or \ev's view + * argument carefully --- we just slop it up in OT_WHOLE_LINE mode.) * * Returns -1 if no line number is present, 0 on error, or a positive value * on success. */ static int -strip_lineno_from_funcdesc(char *func) +strip_lineno_from_objdesc(char *obj) { char *c; int lineno; - if (!func || func[0] == '\0') + if (!obj || obj[0] == '\0') return -1; - c = func + strlen(func) - 1; + c = obj + strlen(obj) - 1; /* * This business of parsing backwards is dangerous as can be in a @@ -2838,20 +4776,20 @@ strip_lineno_from_funcdesc(char *func) */ /* skip trailing whitespace */ - while (c > func && isascii((unsigned char) *c) && isspace((unsigned char) *c)) + while (c > obj && isascii((unsigned char) *c) && isspace((unsigned char) *c)) c--; /* must have a digit as last non-space char */ - if (c == func || !isascii((unsigned char) *c) || !isdigit((unsigned char) *c)) + if (c == obj || !isascii((unsigned char) *c) || !isdigit((unsigned char) *c)) return -1; /* find start of digit string */ - while (c > func && isascii((unsigned char) *c) && isdigit((unsigned char) *c)) + while (c > obj && isascii((unsigned char) *c) && isdigit((unsigned char) *c)) c--; - /* digits must be separated from func name by space or closing paren */ - /* notice also that we are not allowing an empty func name ... */ - if (c == func || !isascii((unsigned char) *c) || + /* digits must be separated from object name by space or closing paren */ + /* notice also that we are not allowing an empty object name ... */ + if (c == obj || !isascii((unsigned char) *c) || !(isspace((unsigned char) *c) || *c == ')')) return -1; @@ -2860,16 +4798,84 @@ strip_lineno_from_funcdesc(char *func) lineno = atoi(c); if (lineno < 1) { - psql_error("invalid line number: %s\n", c); + pg_log_error("invalid line number: %s", c); return 0; } - /* strip digit string from func */ + /* strip digit string from object name */ *c = '\0'; return lineno; } +/* + * Count number of lines in the buffer. + * This is used to test if pager is needed or not. + */ +static int +count_lines_in_buf(PQExpBuffer buf) +{ + int lineno = 0; + const char *lines = buf->data; + + while (*lines != '\0') + { + lineno++; + /* find start of next line */ + lines = strchr(lines, '\n'); + if (!lines) + break; + lines++; + } + + return lineno; +} + +/* + * Write text at *lines to output with line numbers. + * + * If header_keyword isn't NULL, then line 1 should be the first line beginning + * with header_keyword; lines before that are unnumbered. + * + * Caution: this scribbles on *lines. + */ +static void +print_with_linenumbers(FILE *output, char *lines, + const char *header_keyword) +{ + bool in_header = (header_keyword != NULL); + size_t header_sz = in_header ? strlen(header_keyword) : 0; + int lineno = 0; + + while (*lines != '\0') + { + char *eol; + + if (in_header && strncmp(lines, header_keyword, header_sz) == 0) + in_header = false; + + /* increment lineno only for body's lines */ + if (!in_header) + lineno++; + + /* find and mark end of current line */ + eol = strchr(lines, '\n'); + if (eol != NULL) + *eol = '\0'; + + /* show current line as appropriate */ + if (in_header) + fprintf(output, " %s\n", lines); + else + fprintf(output, "%-7d %s\n", lineno, lines); + + /* advance to next line, if any */ + if (eol == NULL) + break; + lines = ++eol; + } +} + /* * Report just the primary error; this is to avoid cluttering the output * with, for instance, a redisplay of the internally generated query @@ -2892,9 +4898,9 @@ minimal_error_message(PGresult *res) appendPQExpBufferStr(msg, fld); else appendPQExpBufferStr(msg, "(not available)"); - appendPQExpBufferStr(msg, "\n"); + appendPQExpBufferChar(msg, '\n'); - psql_error("%s", msg->data); + pg_log_error("%s", msg->data); destroyPQExpBuffer(msg); }