mirror of
https://github.com/postgres/postgres.git
synced 2025-05-12 16:21:30 +03:00
More than twenty years ago (79fcde48b), we hacked the postmaster to avoid a core-dump on systems that didn't support fflush(NULL). We've mostly, though not completely, hewed to that rule ever since. But such systems are surely gone in the wild, so in the spirit of cleaning out no-longer-needed portability hacks let's get rid of multiple per-file fflush() calls in favor of using fflush(NULL). Also, we were fairly inconsistent about whether to fflush() before popen() and system() calls. While we've received no bug reports about that, it seems likely that at least some of these call sites are at risk of odd behavior, such as error messages appearing in an unexpected order. Rather than expend a lot of brain cells figuring out which places are at hazard, let's just establish a uniform coding rule that we should fflush(NULL) before these calls. A no-op fflush() is surely of trivial cost compared to launching a sub-process via a shell; while if it's not a no-op then we likely need it. Discussion: https://postgr.es/m/2923412.1661722825@sss.pgh.pa.us
385 lines
8.3 KiB
C
385 lines
8.3 KiB
C
/*
|
|
* psql - the PostgreSQL interactive terminal
|
|
*
|
|
* Copyright (c) 2000-2022, PostgreSQL Global Development Group
|
|
*
|
|
* src/bin/psql/prompt.c
|
|
*/
|
|
#include "postgres_fe.h"
|
|
|
|
#ifdef WIN32
|
|
#include <io.h>
|
|
#include <win32.h>
|
|
#endif
|
|
|
|
#include "common.h"
|
|
#include "common/string.h"
|
|
#include "input.h"
|
|
#include "libpq/pqcomm.h"
|
|
#include "prompt.h"
|
|
#include "settings.h"
|
|
|
|
/*--------------------------
|
|
* get_prompt
|
|
*
|
|
* Returns a statically allocated prompt made by interpolating certain
|
|
* tcsh style escape sequences into pset.vars "PROMPT1|2|3".
|
|
* (might not be completely multibyte safe)
|
|
*
|
|
* Defined interpolations are:
|
|
* %M - database server "hostname.domainname", "[local]" for AF_UNIX
|
|
* sockets, "[local:/dir/name]" if not default
|
|
* %m - like %M, but hostname only (before first dot), or always "[local]"
|
|
* %p - backend pid
|
|
* %> - database server port number
|
|
* %n - database user name
|
|
* %/ - current database
|
|
* %~ - like %/ but "~" when database name equals user name
|
|
* %w - whitespace of the same width as the most recent output of PROMPT1
|
|
* %# - "#" if superuser, ">" otherwise
|
|
* %R - in prompt1 normally =, or ^ if single line mode,
|
|
* or a ! if session is not connected to a database;
|
|
* in prompt2 -, *, ', or ";
|
|
* in prompt3 nothing
|
|
* %x - transaction status: empty, *, !, ? (unknown or no connection)
|
|
* %l - The line number inside the current statement, starting from 1.
|
|
* %? - the error code of the last query (not yet implemented)
|
|
* %% - a percent sign
|
|
*
|
|
* %[0-9] - the character with the given decimal code
|
|
* %0[0-7] - the character with the given octal code
|
|
* %0x[0-9A-Fa-f] - the character with the given hexadecimal code
|
|
*
|
|
* %`command` - The result of executing command in /bin/sh with trailing
|
|
* newline stripped.
|
|
* %:name: - The value of the psql variable 'name'
|
|
* (those will not be rescanned for more escape sequences!)
|
|
*
|
|
* %[ ... %] - tell readline that the contained text is invisible
|
|
*
|
|
* If the application-wide prompts become NULL somehow, the returned string
|
|
* will be empty (not NULL!).
|
|
*--------------------------
|
|
*/
|
|
|
|
char *
|
|
get_prompt(promptStatus_t status, ConditionalStack cstack)
|
|
{
|
|
#define MAX_PROMPT_SIZE 256
|
|
static char destination[MAX_PROMPT_SIZE + 1];
|
|
char buf[MAX_PROMPT_SIZE + 1];
|
|
bool esc = false;
|
|
const char *p;
|
|
const char *prompt_string = "? ";
|
|
static size_t last_prompt1_width = 0;
|
|
|
|
switch (status)
|
|
{
|
|
case PROMPT_READY:
|
|
prompt_string = pset.prompt1;
|
|
break;
|
|
|
|
case PROMPT_CONTINUE:
|
|
case PROMPT_SINGLEQUOTE:
|
|
case PROMPT_DOUBLEQUOTE:
|
|
case PROMPT_DOLLARQUOTE:
|
|
case PROMPT_COMMENT:
|
|
case PROMPT_PAREN:
|
|
prompt_string = pset.prompt2;
|
|
break;
|
|
|
|
case PROMPT_COPY:
|
|
prompt_string = pset.prompt3;
|
|
break;
|
|
}
|
|
|
|
destination[0] = '\0';
|
|
|
|
for (p = prompt_string;
|
|
*p && strlen(destination) < sizeof(destination) - 1;
|
|
p++)
|
|
{
|
|
memset(buf, 0, sizeof(buf));
|
|
if (esc)
|
|
{
|
|
switch (*p)
|
|
{
|
|
/* Current database */
|
|
case '/':
|
|
if (pset.db)
|
|
strlcpy(buf, PQdb(pset.db), sizeof(buf));
|
|
break;
|
|
case '~':
|
|
if (pset.db)
|
|
{
|
|
const char *var;
|
|
|
|
if (strcmp(PQdb(pset.db), PQuser(pset.db)) == 0 ||
|
|
((var = getenv("PGDATABASE")) && strcmp(var, PQdb(pset.db)) == 0))
|
|
strlcpy(buf, "~", sizeof(buf));
|
|
else
|
|
strlcpy(buf, PQdb(pset.db), sizeof(buf));
|
|
}
|
|
break;
|
|
|
|
/* Whitespace of the same width as the last PROMPT1 */
|
|
case 'w':
|
|
if (pset.db)
|
|
memset(buf, ' ',
|
|
Min(last_prompt1_width, sizeof(buf) - 1));
|
|
break;
|
|
|
|
/* DB server hostname (long/short) */
|
|
case 'M':
|
|
case 'm':
|
|
if (pset.db)
|
|
{
|
|
const char *host = PQhost(pset.db);
|
|
|
|
/* INET socket */
|
|
if (host && host[0] && !is_unixsock_path(host))
|
|
{
|
|
strlcpy(buf, host, sizeof(buf));
|
|
if (*p == 'm')
|
|
buf[strcspn(buf, ".")] = '\0';
|
|
}
|
|
/* UNIX socket */
|
|
else
|
|
{
|
|
if (!host
|
|
|| strcmp(host, DEFAULT_PGSOCKET_DIR) == 0
|
|
|| *p == 'm')
|
|
strlcpy(buf, "[local]", sizeof(buf));
|
|
else
|
|
snprintf(buf, sizeof(buf), "[local:%s]", host);
|
|
}
|
|
}
|
|
break;
|
|
/* DB server port number */
|
|
case '>':
|
|
if (pset.db && PQport(pset.db))
|
|
strlcpy(buf, PQport(pset.db), sizeof(buf));
|
|
break;
|
|
/* DB server user name */
|
|
case 'n':
|
|
if (pset.db)
|
|
strlcpy(buf, session_username(), sizeof(buf));
|
|
break;
|
|
/* backend pid */
|
|
case 'p':
|
|
if (pset.db)
|
|
{
|
|
int pid = PQbackendPID(pset.db);
|
|
|
|
if (pid)
|
|
snprintf(buf, sizeof(buf), "%d", pid);
|
|
}
|
|
break;
|
|
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
*buf = (char) strtol(p, unconstify(char **, &p), 8);
|
|
--p;
|
|
break;
|
|
case 'R':
|
|
switch (status)
|
|
{
|
|
case PROMPT_READY:
|
|
if (cstack != NULL && !conditional_active(cstack))
|
|
buf[0] = '@';
|
|
else if (!pset.db)
|
|
buf[0] = '!';
|
|
else if (!pset.singleline)
|
|
buf[0] = '=';
|
|
else
|
|
buf[0] = '^';
|
|
break;
|
|
case PROMPT_CONTINUE:
|
|
buf[0] = '-';
|
|
break;
|
|
case PROMPT_SINGLEQUOTE:
|
|
buf[0] = '\'';
|
|
break;
|
|
case PROMPT_DOUBLEQUOTE:
|
|
buf[0] = '"';
|
|
break;
|
|
case PROMPT_DOLLARQUOTE:
|
|
buf[0] = '$';
|
|
break;
|
|
case PROMPT_COMMENT:
|
|
buf[0] = '*';
|
|
break;
|
|
case PROMPT_PAREN:
|
|
buf[0] = '(';
|
|
break;
|
|
default:
|
|
buf[0] = '\0';
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'x':
|
|
if (!pset.db)
|
|
buf[0] = '?';
|
|
else
|
|
switch (PQtransactionStatus(pset.db))
|
|
{
|
|
case PQTRANS_IDLE:
|
|
buf[0] = '\0';
|
|
break;
|
|
case PQTRANS_ACTIVE:
|
|
case PQTRANS_INTRANS:
|
|
buf[0] = '*';
|
|
break;
|
|
case PQTRANS_INERROR:
|
|
buf[0] = '!';
|
|
break;
|
|
default:
|
|
buf[0] = '?';
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'l':
|
|
snprintf(buf, sizeof(buf), UINT64_FORMAT, pset.stmt_lineno);
|
|
break;
|
|
|
|
case '?':
|
|
/* not here yet */
|
|
break;
|
|
|
|
case '#':
|
|
if (is_superuser())
|
|
buf[0] = '#';
|
|
else
|
|
buf[0] = '>';
|
|
break;
|
|
|
|
/* execute command */
|
|
case '`':
|
|
{
|
|
int cmdend = strcspn(p + 1, "`");
|
|
char *file = pnstrdup(p + 1, cmdend);
|
|
FILE *fd;
|
|
|
|
fflush(NULL);
|
|
fd = popen(file, "r");
|
|
if (fd)
|
|
{
|
|
if (fgets(buf, sizeof(buf), fd) == NULL)
|
|
buf[0] = '\0';
|
|
pclose(fd);
|
|
}
|
|
|
|
/* strip trailing newline and carriage return */
|
|
(void) pg_strip_crlf(buf);
|
|
|
|
free(file);
|
|
p += cmdend + 1;
|
|
break;
|
|
}
|
|
|
|
/* interpolate variable */
|
|
case ':':
|
|
{
|
|
int nameend = strcspn(p + 1, ":");
|
|
char *name = pnstrdup(p + 1, nameend);
|
|
const char *val;
|
|
|
|
val = GetVariable(pset.vars, name);
|
|
if (val)
|
|
strlcpy(buf, val, sizeof(buf));
|
|
free(name);
|
|
p += nameend + 1;
|
|
break;
|
|
}
|
|
|
|
case '[':
|
|
case ']':
|
|
#if defined(USE_READLINE) && defined(RL_PROMPT_START_IGNORE)
|
|
|
|
/*
|
|
* readline >=4.0 undocumented feature: non-printing
|
|
* characters in prompt strings must be marked as such, in
|
|
* order to properly display the line during editing.
|
|
*/
|
|
buf[0] = (*p == '[') ? RL_PROMPT_START_IGNORE : RL_PROMPT_END_IGNORE;
|
|
buf[1] = '\0';
|
|
#endif /* USE_READLINE */
|
|
break;
|
|
|
|
default:
|
|
buf[0] = *p;
|
|
buf[1] = '\0';
|
|
break;
|
|
}
|
|
esc = false;
|
|
}
|
|
else if (*p == '%')
|
|
esc = true;
|
|
else
|
|
{
|
|
buf[0] = *p;
|
|
buf[1] = '\0';
|
|
esc = false;
|
|
}
|
|
|
|
if (!esc)
|
|
strlcat(destination, buf, sizeof(destination));
|
|
}
|
|
|
|
/* Compute the visible width of PROMPT1, for PROMPT2's %w */
|
|
if (prompt_string == pset.prompt1)
|
|
{
|
|
char *p = destination;
|
|
char *end = p + strlen(p);
|
|
bool visible = true;
|
|
|
|
last_prompt1_width = 0;
|
|
while (*p)
|
|
{
|
|
#if defined(USE_READLINE) && defined(RL_PROMPT_START_IGNORE)
|
|
if (*p == RL_PROMPT_START_IGNORE)
|
|
{
|
|
visible = false;
|
|
++p;
|
|
}
|
|
else if (*p == RL_PROMPT_END_IGNORE)
|
|
{
|
|
visible = true;
|
|
++p;
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
int chlen,
|
|
chwidth;
|
|
|
|
chlen = PQmblen(p, pset.encoding);
|
|
if (p + chlen > end)
|
|
break; /* Invalid string */
|
|
|
|
if (visible)
|
|
{
|
|
chwidth = PQdsplen(p, pset.encoding);
|
|
|
|
if (*p == '\n')
|
|
last_prompt1_width = 0;
|
|
else if (chwidth > 0)
|
|
last_prompt1_width += chwidth;
|
|
}
|
|
|
|
p += chlen;
|
|
}
|
|
}
|
|
}
|
|
|
|
return destination;
|
|
}
|