diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:15:05 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:15:05 +0000 |
commit | 46651ce6fe013220ed397add242004d764fc0153 (patch) | |
tree | 6e5299f990f88e60174a1d3ae6e48eedd2688b2b /src/bin/psql/mainloop.c | |
parent | Initial commit. (diff) | |
download | postgresql-14-upstream.tar.xz postgresql-14-upstream.zip |
Adding upstream version 14.5.upstream/14.5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/bin/psql/mainloop.c')
-rw-r--r-- | src/bin/psql/mainloop.c | 657 |
1 files changed, 657 insertions, 0 deletions
diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c new file mode 100644 index 0000000..e49ed02 --- /dev/null +++ b/src/bin/psql/mainloop.c @@ -0,0 +1,657 @@ +/* + * psql - the PostgreSQL interactive terminal + * + * Copyright (c) 2000-2021, PostgreSQL Global Development Group + * + * src/bin/psql/mainloop.c + */ +#include "postgres_fe.h" + +#include "command.h" +#include "common.h" +#include "common/logging.h" +#include "input.h" +#include "mainloop.h" +#include "mb/pg_wchar.h" +#include "prompt.h" +#include "settings.h" + +/* callback functions for our flex lexer */ +const PsqlScanCallbacks psqlscan_callbacks = { + psql_get_variable, +}; + + +/* + * Main processing loop for reading lines of input + * and sending them to the backend. + * + * This loop is re-entrant. May be called by \i command + * which reads input from a file. + */ +int +MainLoop(FILE *source) +{ + PsqlScanState scan_state; /* lexer working state */ + ConditionalStack cond_stack; /* \if status stack */ + volatile PQExpBuffer query_buf; /* buffer for query being accumulated */ + volatile PQExpBuffer previous_buf; /* if there isn't anything in the new + * buffer yet, use this one for \e, + * etc. */ + PQExpBuffer history_buf; /* earlier lines of a multi-line command, not + * yet saved to readline history */ + char *line; /* current line of input */ + int added_nl_pos; + bool success; + bool line_saved_in_history; + volatile int successResult = EXIT_SUCCESS; + volatile backslashResult slashCmdStatus = PSQL_CMD_UNKNOWN; + volatile promptStatus_t prompt_status = PROMPT_READY; + volatile bool need_redisplay = false; + volatile int count_eof = 0; + volatile bool die_on_error = false; + FILE *prev_cmd_source; + bool prev_cmd_interactive; + uint64 prev_lineno; + + /* Save the prior command source */ + prev_cmd_source = pset.cur_cmd_source; + prev_cmd_interactive = pset.cur_cmd_interactive; + prev_lineno = pset.lineno; + /* pset.stmt_lineno does not need to be saved and restored */ + + /* Establish new source */ + pset.cur_cmd_source = source; + pset.cur_cmd_interactive = ((source == stdin) && !pset.notty); + pset.lineno = 0; + pset.stmt_lineno = 1; + + /* Create working state */ + scan_state = psql_scan_create(&psqlscan_callbacks); + cond_stack = conditional_stack_create(); + psql_scan_set_passthrough(scan_state, (void *) cond_stack); + + query_buf = createPQExpBuffer(); + previous_buf = createPQExpBuffer(); + history_buf = createPQExpBuffer(); + if (PQExpBufferBroken(query_buf) || + PQExpBufferBroken(previous_buf) || + PQExpBufferBroken(history_buf)) + { + pg_log_error("out of memory"); + exit(EXIT_FAILURE); + } + + /* main loop to get queries and execute them */ + while (successResult == EXIT_SUCCESS) + { + /* + * Clean up after a previous Control-C + */ + if (cancel_pressed) + { + if (!pset.cur_cmd_interactive) + { + /* + * You get here if you stopped a script with Ctrl-C. + */ + successResult = EXIT_USER; + break; + } + + cancel_pressed = false; + } + + /* + * Establish longjmp destination for exiting from wait-for-input. We + * must re-do this each time through the loop for safety, since the + * jmpbuf might get changed during command execution. + */ + if (sigsetjmp(sigint_interrupt_jmp, 1) != 0) + { + /* got here with longjmp */ + + /* reset parsing state */ + psql_scan_finish(scan_state); + psql_scan_reset(scan_state); + resetPQExpBuffer(query_buf); + resetPQExpBuffer(history_buf); + count_eof = 0; + slashCmdStatus = PSQL_CMD_UNKNOWN; + prompt_status = PROMPT_READY; + need_redisplay = false; + pset.stmt_lineno = 1; + cancel_pressed = false; + + if (pset.cur_cmd_interactive) + { + putc('\n', stdout); + + /* + * if interactive user is in an \if block, then Ctrl-C will + * exit from the innermost \if. + */ + if (!conditional_stack_empty(cond_stack)) + { + pg_log_error("\\if: escaped"); + conditional_stack_pop(cond_stack); + } + } + else + { + successResult = EXIT_USER; + break; + } + } + + fflush(stdout); + + /* + * get another line + */ + if (pset.cur_cmd_interactive) + { + /* May need to reset prompt, eg after \r command */ + if (query_buf->len == 0) + prompt_status = PROMPT_READY; + /* If query buffer came from \e, redisplay it with a prompt */ + if (need_redisplay) + { + if (query_buf->len > 0) + { + fputs(get_prompt(PROMPT_READY, cond_stack), stdout); + fputs(query_buf->data, stdout); + fflush(stdout); + } + need_redisplay = false; + } + /* Now we can fetch a line */ + line = gets_interactive(get_prompt(prompt_status, cond_stack), + query_buf); + } + else + { + line = gets_fromFile(source); + if (!line && ferror(source)) + successResult = EXIT_FAILURE; + } + + /* + * query_buf holds query already accumulated. line is the malloc'd + * new line of input (note it must be freed before looping around!) + */ + + /* No more input. Time to quit, or \i done */ + if (line == NULL) + { + if (pset.cur_cmd_interactive) + { + /* This tries to mimic bash's IGNOREEOF feature. */ + count_eof++; + + if (count_eof < pset.ignoreeof) + { + if (!pset.quiet) + printf(_("Use \"\\q\" to leave %s.\n"), pset.progname); + continue; + } + + puts(pset.quiet ? "" : "\\q"); + } + break; + } + + count_eof = 0; + + pset.lineno++; + + /* ignore UTF-8 Unicode byte-order mark */ + if (pset.lineno == 1 && pset.encoding == PG_UTF8 && strncmp(line, "\xef\xbb\xbf", 3) == 0) + memmove(line, line + 3, strlen(line + 3) + 1); + + /* Detect attempts to run custom-format dumps as SQL scripts */ + if (pset.lineno == 1 && !pset.cur_cmd_interactive && + strncmp(line, "PGDMP", 5) == 0) + { + free(line); + puts(_("The input is a PostgreSQL custom-format dump.\n" + "Use the pg_restore command-line client to restore this dump to a database.\n")); + fflush(stdout); + successResult = EXIT_FAILURE; + break; + } + + /* no further processing of empty lines, unless within a literal */ + if (line[0] == '\0' && !psql_scan_in_quote(scan_state)) + { + free(line); + continue; + } + + /* Recognize "help", "quit", "exit" only in interactive mode */ + if (pset.cur_cmd_interactive) + { + char *first_word = line; + char *rest_of_line = NULL; + bool found_help = false; + bool found_exit_or_quit = false; + bool found_q = false; + + /* + * The assistance words, help/exit/quit, must have no whitespace + * before them, and only whitespace after, with an optional + * semicolon. This prevents indented use of these words, perhaps + * as identifiers, from invoking the assistance behavior. + */ + if (pg_strncasecmp(first_word, "help", 4) == 0) + { + rest_of_line = first_word + 4; + found_help = true; + } + else if (pg_strncasecmp(first_word, "exit", 4) == 0 || + pg_strncasecmp(first_word, "quit", 4) == 0) + { + rest_of_line = first_word + 4; + found_exit_or_quit = true; + } + else if (strncmp(first_word, "\\q", 2) == 0) + { + rest_of_line = first_word + 2; + found_q = true; + } + + /* + * If we found a command word, check whether the rest of the line + * contains only whitespace plus maybe one semicolon. If not, + * ignore the command word after all. These commands are only for + * compatibility with other SQL clients and are not documented. + */ + if (rest_of_line != NULL) + { + /* + * Ignore unless rest of line is whitespace, plus maybe one + * semicolon + */ + while (isspace((unsigned char) *rest_of_line)) + ++rest_of_line; + if (*rest_of_line == ';') + ++rest_of_line; + while (isspace((unsigned char) *rest_of_line)) + ++rest_of_line; + if (*rest_of_line != '\0') + { + found_help = false; + found_exit_or_quit = false; + } + } + + /* + * "help" is only a command when the query buffer is empty, but we + * emit a one-line message even when it isn't to help confused + * users. The text is still added to the query buffer in that + * case. + */ + if (found_help) + { + if (query_buf->len != 0) +#ifndef WIN32 + puts(_("Use \\? for help or press control-C to clear the input buffer.")); +#else + puts(_("Use \\? for help.")); +#endif + else + { + puts(_("You are using psql, the command-line interface to PostgreSQL.")); + printf(_("Type: \\copyright for distribution terms\n" + " \\h for help with SQL commands\n" + " \\? for help with psql commands\n" + " \\g or terminate with semicolon to execute query\n" + " \\q to quit\n")); + free(line); + fflush(stdout); + continue; + } + } + + /* + * "quit" and "exit" are only commands when the query buffer is + * empty, but we emit a one-line message even when it isn't to + * help confused users. The text is still added to the query + * buffer in that case. + */ + if (found_exit_or_quit) + { + if (query_buf->len != 0) + { + if (prompt_status == PROMPT_READY || + prompt_status == PROMPT_CONTINUE || + prompt_status == PROMPT_PAREN) + puts(_("Use \\q to quit.")); + else +#ifndef WIN32 + puts(_("Use control-D to quit.")); +#else + puts(_("Use control-C to quit.")); +#endif + } + else + { + /* exit app */ + free(line); + fflush(stdout); + successResult = EXIT_SUCCESS; + break; + } + } + + /* + * If they typed "\q" in a place where "\q" is not active, supply + * a hint. The text is still added to the query buffer. + */ + if (found_q && query_buf->len != 0 && + prompt_status != PROMPT_READY && + prompt_status != PROMPT_CONTINUE && + prompt_status != PROMPT_PAREN) +#ifndef WIN32 + puts(_("Use control-D to quit.")); +#else + puts(_("Use control-C to quit.")); +#endif + } + + /* echo back if flag is set, unless interactive */ + if (pset.echo == PSQL_ECHO_ALL && !pset.cur_cmd_interactive) + { + puts(line); + fflush(stdout); + } + + /* insert newlines into query buffer between source lines */ + if (query_buf->len > 0) + { + appendPQExpBufferChar(query_buf, '\n'); + added_nl_pos = query_buf->len; + } + else + added_nl_pos = -1; /* flag we didn't add one */ + + /* Setting this will not have effect until next line. */ + die_on_error = pset.on_error_stop; + + /* + * Parse line, looking for command separators. + */ + psql_scan_setup(scan_state, line, strlen(line), + pset.encoding, standard_strings()); + success = true; + line_saved_in_history = false; + + while (success || !die_on_error) + { + PsqlScanResult scan_result; + promptStatus_t prompt_tmp = prompt_status; + size_t pos_in_query; + char *tmp_line; + + pos_in_query = query_buf->len; + scan_result = psql_scan(scan_state, query_buf, &prompt_tmp); + prompt_status = prompt_tmp; + + if (PQExpBufferBroken(query_buf)) + { + pg_log_error("out of memory"); + exit(EXIT_FAILURE); + } + + /* + * Increase statement line number counter for each linebreak added + * to the query buffer by the last psql_scan() call. There only + * will be ones to add when navigating to a statement in + * readline's history containing newlines. + */ + tmp_line = query_buf->data + pos_in_query; + while (*tmp_line != '\0') + { + if (*(tmp_line++) == '\n') + pset.stmt_lineno++; + } + + if (scan_result == PSCAN_EOL) + pset.stmt_lineno++; + + /* + * Send command if semicolon found, or if end of line and we're in + * single-line mode. + */ + if (scan_result == PSCAN_SEMICOLON || + (scan_result == PSCAN_EOL && pset.singleline)) + { + /* + * Save line in history. We use history_buf to accumulate + * multi-line queries into a single history entry. Note that + * history accumulation works on input lines, so it doesn't + * matter whether the query will be ignored due to \if. + */ + if (pset.cur_cmd_interactive && !line_saved_in_history) + { + pg_append_history(line, history_buf); + pg_send_history(history_buf); + line_saved_in_history = true; + } + + /* execute query unless we're in an inactive \if branch */ + if (conditional_active(cond_stack)) + { + success = SendQuery(query_buf->data); + slashCmdStatus = success ? PSQL_CMD_SEND : PSQL_CMD_ERROR; + pset.stmt_lineno = 1; + + /* transfer query to previous_buf by pointer-swapping */ + { + PQExpBuffer swap_buf = previous_buf; + + previous_buf = query_buf; + query_buf = swap_buf; + } + resetPQExpBuffer(query_buf); + + added_nl_pos = -1; + /* we need not do psql_scan_reset() here */ + } + else + { + /* if interactive, warn about non-executed query */ + if (pset.cur_cmd_interactive) + pg_log_error("query ignored; use \\endif or Ctrl-C to exit current \\if block"); + /* fake an OK result for purposes of loop checks */ + success = true; + slashCmdStatus = PSQL_CMD_SEND; + pset.stmt_lineno = 1; + /* note that query_buf doesn't change state */ + } + } + else if (scan_result == PSCAN_BACKSLASH) + { + /* handle backslash command */ + + /* + * If we added a newline to query_buf, and nothing else has + * been inserted in query_buf by the lexer, then strip off the + * newline again. This avoids any change to query_buf when a + * line contains only a backslash command. Also, in this + * situation we force out any previous lines as a separate + * history entry; we don't want SQL and backslash commands + * intermixed in history if at all possible. + */ + if (query_buf->len == added_nl_pos) + { + query_buf->data[--query_buf->len] = '\0'; + pg_send_history(history_buf); + } + added_nl_pos = -1; + + /* save backslash command in history */ + if (pset.cur_cmd_interactive && !line_saved_in_history) + { + pg_append_history(line, history_buf); + pg_send_history(history_buf); + line_saved_in_history = true; + } + + /* execute backslash command */ + slashCmdStatus = HandleSlashCmds(scan_state, + cond_stack, + query_buf, + previous_buf); + + success = slashCmdStatus != PSQL_CMD_ERROR; + + /* + * Resetting stmt_lineno after a backslash command isn't + * always appropriate, but it's what we've done historically + * and there have been few complaints. + */ + pset.stmt_lineno = 1; + + if (slashCmdStatus == PSQL_CMD_SEND) + { + /* should not see this in inactive branch */ + Assert(conditional_active(cond_stack)); + + success = SendQuery(query_buf->data); + + /* transfer query to previous_buf by pointer-swapping */ + { + PQExpBuffer swap_buf = previous_buf; + + previous_buf = query_buf; + query_buf = swap_buf; + } + resetPQExpBuffer(query_buf); + + /* flush any paren nesting info after forced send */ + psql_scan_reset(scan_state); + } + else if (slashCmdStatus == PSQL_CMD_NEWEDIT) + { + /* should not see this in inactive branch */ + Assert(conditional_active(cond_stack)); + /* ensure what came back from editing ends in a newline */ + if (query_buf->len > 0 && + query_buf->data[query_buf->len - 1] != '\n') + appendPQExpBufferChar(query_buf, '\n'); + /* rescan query_buf as new input */ + psql_scan_finish(scan_state); + free(line); + line = pg_strdup(query_buf->data); + resetPQExpBuffer(query_buf); + /* reset parsing state since we are rescanning whole line */ + psql_scan_reset(scan_state); + psql_scan_setup(scan_state, line, strlen(line), + pset.encoding, standard_strings()); + line_saved_in_history = false; + prompt_status = PROMPT_READY; + /* we'll want to redisplay after parsing what we have */ + need_redisplay = true; + } + else if (slashCmdStatus == PSQL_CMD_TERMINATE) + break; + } + + /* fall out of loop if lexer reached EOL */ + if (scan_result == PSCAN_INCOMPLETE || + scan_result == PSCAN_EOL) + break; + } + + /* Add line to pending history if we didn't execute anything yet */ + if (pset.cur_cmd_interactive && !line_saved_in_history) + pg_append_history(line, history_buf); + + psql_scan_finish(scan_state); + free(line); + + if (slashCmdStatus == PSQL_CMD_TERMINATE) + { + successResult = EXIT_SUCCESS; + break; + } + + if (!pset.cur_cmd_interactive) + { + if (!success && die_on_error) + successResult = EXIT_USER; + /* Have we lost the db connection? */ + else if (!pset.db) + successResult = EXIT_BADCONN; + } + } /* while !endoffile/session */ + + /* + * If we have a non-semicolon-terminated query at the end of file, we + * process it unless the input source is interactive --- in that case it + * seems better to go ahead and quit. Also skip if this is an error exit. + */ + if (query_buf->len > 0 && !pset.cur_cmd_interactive && + successResult == EXIT_SUCCESS) + { + /* save query in history */ + /* currently unneeded since we don't use this block if interactive */ +#ifdef NOT_USED + if (pset.cur_cmd_interactive) + pg_send_history(history_buf); +#endif + + /* execute query unless we're in an inactive \if branch */ + if (conditional_active(cond_stack)) + { + success = SendQuery(query_buf->data); + } + else + { + if (pset.cur_cmd_interactive) + pg_log_error("query ignored; use \\endif or Ctrl-C to exit current \\if block"); + success = true; + } + + if (!success && die_on_error) + successResult = EXIT_USER; + else if (pset.db == NULL) + successResult = EXIT_BADCONN; + } + + /* + * Check for unbalanced \if-\endifs unless user explicitly quit, or the + * script is erroring out + */ + if (slashCmdStatus != PSQL_CMD_TERMINATE && + successResult != EXIT_USER && + !conditional_stack_empty(cond_stack)) + { + pg_log_error("reached EOF without finding closing \\endif(s)"); + if (die_on_error && !pset.cur_cmd_interactive) + successResult = EXIT_USER; + } + + /* + * Let's just make real sure the SIGINT handler won't try to use + * sigint_interrupt_jmp after we exit this routine. If there is an outer + * MainLoop instance, it will reset sigint_interrupt_jmp to point to + * itself at the top of its loop, before any further interactive input + * happens. + */ + sigint_interrupt_enabled = false; + + destroyPQExpBuffer(query_buf); + destroyPQExpBuffer(previous_buf); + destroyPQExpBuffer(history_buf); + + psql_scan_destroy(scan_state); + conditional_stack_destroy(cond_stack); + + pset.cur_cmd_source = prev_cmd_source; + pset.cur_cmd_interactive = prev_cmd_interactive; + pset.lineno = prev_lineno; + + return successResult; +} /* MainLoop() */ |