Hi,
v3 follow-up: one-page merge rationale, explicit review ask, and a TAP test
(as offered in the original post). Patch attached.
========================================================================
Why merge this (one page)
========================================================================
Problem
-------
Interactive psql prompts are static between PROMPT1/PROMPT2 redefinitions.
Users who want connection context, transaction state, or last-command status
in the prompt today either fork psql or abuse %`shell` on every redraw.
There
is no bash-shaped hook to regenerate the prompt once per readline.
Bash-shaped solution
--------------------
Mirror bash, not invent a plugin API:
* PROMPT_COMMAND — if set, run a shell command before each interactive
prompt; capture the first line of stdout (existing prompt buffer limit).
* %D — PROMPT1/PROMPT2 escape for that captured line (renderer output).
* SHELL_EXIT — last *user* command status (SQL or \!); PROMPT_COMMAND does
not overwrite it, so themes can show the prior command’s outcome.
* Prompt refresh — run_prompt_command() immediately before readline() in
input.c (not rl_pre_input_hook, which left the prompt blank until a
key).
* \connect housekeeping — reset ROW_COUNT / SHELL_EXIT after successful
\connect; optional :txid refresh when that variable already exists.
Zero behavior change unless configured
--------------------------------------
Defaults are inert:
| Knob | Default | Effect when unset/off |
|-------------------------|---------|------------------------------------|
| PROMPT_COMMAND | unset | no shell, no popen |
| %D | unused | empty substitution if referenced |
| PROMPT_SESSION_EXPORT | off | no PG*/PSQL_* export into subprocess |
| SHELL_EXIT | "0" | new variable only; scripts ignore it |
Non-interactive use, existing scripts, and default interactive sessions are
unchanged. No .psqlrc change ⇒ no new behavior.
Security / gating story (PROMPT_SESSION_EXPORT + shell)
-------------------------------------------------------
PROMPT_COMMAND uses popen() — the same trust model as existing %`command`
prompt escapes and \!. It only runs when the user (or their .psqlrc)
explicitly sets PROMPT_COMMAND.
PROMPT_SESSION_EXPORT is a separate, default-off gate:
* Off (default): PROMPT_COMMAND subprocess inherits the normal process
environment only; no extra session dump.
* On: export current-session PG* / PSQL_* (database, user, host, port,
PSQL_TXN, PSQL_ROW_COUNT, PSQL_SHELL_EXIT, PSQL_SUPERUSER, optional
PSQL_TXID) so external renderers need not re-query.
Keeping export opt-in means “I only want %D from a fixed command” never
leaks connection metadata into the child env. Reviewers who want a
narrower first commit can take PROMPT_COMMAND + %D + SHELL_EXIT alone and
leave PROMPT_SESSION_EXPORT for a follow-up.
========================================================================
Looking for a reviewer to walk this path
========================================================================
Please walk the psql + readline path (not a general “please review”):
1. src/bin/psql/input.c
— run_prompt_command() immediately before readline() / gets_fromFile
2. src/bin/psql/prompt.c + prompt.h
— run_prompt_command(), export_prompt_environment(),
prompt_session_export_enabled(), %D in get_prompt(),
reset_prompt_status_after_connect()
3. src/bin/psql/mainloop.c
— gets_interactive() call sites (when prompts are drawn)
4. src/bin/psql/common.c
— SetLastExitVariable() / SHELL_EXIT; confirm PROMPT_COMMAND does not
call it
5. src/bin/psql/command.c
— reset_prompt_status_after_connect() after successful \connect
6. src/bin/psql/help.c + startup.c
— --help=variables / initial SHELL_EXIT
7. src/bin/psql/t/040_prompt_command.pl
— TAP coverage (below)
Optional companion doc (not installed):
src/bin/psql/powerline-integration.md
========================================================================
Testing (v3: TAP added)
========================================================================
src/bin/psql/t/040_prompt_command.pl (registered in meson.build):
Non-interactive:
* SHELL_EXIT after successful SQL, failed SQL, \! true, \! false
* \connect clears ROW_COUNT and SHELL_EXIT
* --help=variables lists PROMPT_COMMAND, PROMPT_SESSION_EXPORT,
SHELL_EXIT
Interactive (IO::Pty + readline, same pattern as t/010 / t/030):
* PROMPT_COMMAND + %D in PROMPT1
* first line only of multi-line PROMPT_COMMAND stdout
* %D empty when PROMPT_COMMAND unset
* PROMPT_SESSION_EXPORT off → PSQL_TXN / PSQL_SUPERUSER not exported
* PROMPT_SESSION_EXPORT on → PSQL_TXN / PSQL_SUPERUSER exported
* PROMPT_COMMAND does not overwrite SHELL_EXIT after a failed SQL
Manual still useful for visual / powerline end-to-end; automation no longer
depends on it for the core knobs.
========================================================================
v3 patch notes
========================================================================
Relative to the original submission:
* Added src/bin/psql/t/040_prompt_command.pl
* Listed that test in src/bin/psql/meson.build
* No intentional functional change to the feature code
Happy to rename variables, drop PROMPT_SESSION_EXPORT from v1, or further
split if that helps commit.
Companion (not part of this patch):
https://github.com/powerline/powerline/pull/2287
Fork branch:
https://github.com/bithead2k/postgres/tree/psql-prompt-command
Comments welcome.
Regards,
Kirk Roybal
On Tue, Aug 4, 2026 at 10:49 AM Kirk Roybal <[email protected]> wrote:
> Hi,
>
> v2 follow-up: one-page merge rationale, explicit review ask, and a TAP test
> (as offered in the original post). Patch attached.
>
> ========================================================================
> Why merge this (one page)
> ========================================================================
>
> Problem
> -------
> Interactive psql prompts are static between PROMPT1/PROMPT2 redefinitions.
> Users who want connection context, transaction state, or last-command
> status
> in the prompt today either fork psql or abuse %`shell` on every redraw.
> There
> is no bash-shaped hook to regenerate the prompt once per readline.
>
> Bash-shaped solution
> --------------------
> Mirror bash, not invent a plugin API:
>
> * PROMPT_COMMAND — if set, run a shell command before each interactive
> prompt; capture the first line of stdout (existing prompt buffer
> limit).
> * %D — PROMPT1/PROMPT2 escape for that captured line (renderer output).
> * SHELL_EXIT — last *user* command status (SQL or \!); PROMPT_COMMAND
> does
> not overwrite it, so themes can show the prior command’s outcome.
> * Prompt refresh — run_prompt_command() immediately before readline() in
> input.c (not rl_pre_input_hook, which left the prompt blank until a
> key).
> * \connect housekeeping — reset ROW_COUNT / SHELL_EXIT after successful
> \connect; optional :txid refresh when that variable already exists.
>
> Zero behavior change unless configured
> --------------------------------------
> Defaults are inert:
>
> | Knob | Default | Effect when unset/off |
>
> |-------------------------|---------|------------------------------------|
> | PROMPT_COMMAND | unset | no shell, no popen |
> | %D | unused | empty substitution if referenced |
> | PROMPT_SESSION_EXPORT | off | no PG*/PSQL_* export into subprocess |
> | SHELL_EXIT | "0" | new variable only; scripts ignore it |
>
> Non-interactive use, existing scripts, and default interactive sessions are
> unchanged. No .psqlrc change ⇒ no new behavior.
>
> Security / gating story (PROMPT_SESSION_EXPORT + shell)
> -------------------------------------------------------
> PROMPT_COMMAND uses popen() — the same trust model as existing %`command`
> prompt escapes and \!. It only runs when the user (or their .psqlrc)
> explicitly sets PROMPT_COMMAND.
>
> PROMPT_SESSION_EXPORT is a separate, default-off gate:
>
> * Off (default): PROMPT_COMMAND subprocess inherits the normal process
> environment only; no extra session dump.
> * On: export current-session PG* / PSQL_* (database, user, host, port,
> PSQL_TXN, PSQL_ROW_COUNT, PSQL_SHELL_EXIT, PSQL_SUPERUSER, optional
> PSQL_TXID) so external renderers need not re-query.
>
> Keeping export opt-in means “I only want %D from a fixed command” never
> leaks connection metadata into the child env. Reviewers who want a
> narrower first commit can take PROMPT_COMMAND + %D + SHELL_EXIT alone and
> leave PROMPT_SESSION_EXPORT for a follow-up.
>
> ========================================================================
> Looking for a reviewer to walk this path
> ========================================================================
>
> Please walk the psql + readline path (not a general “please review”):
>
> 1. src/bin/psql/input.c
> — run_prompt_command() immediately before readline() / gets_fromFile
> 2. src/bin/psql/prompt.c + prompt.h
> — run_prompt_command(), export_prompt_environment(),
> prompt_session_export_enabled(), %D in get_prompt(),
> reset_prompt_status_after_connect()
> 3. src/bin/psql/mainloop.c
> — gets_interactive() call sites (when prompts are drawn)
> 4. src/bin/psql/common.c
> — SetLastExitVariable() / SHELL_EXIT; confirm PROMPT_COMMAND does not
> call it
> 5. src/bin/psql/command.c
> — reset_prompt_status_after_connect() after successful \connect
> 6. src/bin/psql/help.c + startup.c
> — --help=variables / initial SHELL_EXIT
> 7. src/bin/psql/t/040_prompt_command.pl
> — TAP coverage (below)
>
> Optional companion doc (not installed):
> src/bin/psql/powerline-integration.md
>
> ========================================================================
> Testing (v2: TAP added)
> ========================================================================
>
> src/bin/psql/t/040_prompt_command.pl (registered in meson.build):
>
> Non-interactive:
> * SHELL_EXIT after successful SQL, failed SQL, \! true, \! false
> * \connect clears ROW_COUNT and SHELL_EXIT
> * --help=variables lists PROMPT_COMMAND, PROMPT_SESSION_EXPORT,
> SHELL_EXIT
>
> Interactive (IO::Pty + readline, same pattern as t/010 / t/030):
> * PROMPT_COMMAND + %D in PROMPT1
> * first line only of multi-line PROMPT_COMMAND stdout
> * %D empty when PROMPT_COMMAND unset
> * PROMPT_SESSION_EXPORT off → PSQL_TXN / PSQL_SUPERUSER not exported
> * PROMPT_SESSION_EXPORT on → PSQL_TXN / PSQL_SUPERUSER exported
> * PROMPT_COMMAND does not overwrite SHELL_EXIT after a failed SQL
>
> Manual still useful for visual / powerline end-to-end; automation no longer
> depends on it for the core knobs.
>
> ========================================================================
> v2 patch notes
> ========================================================================
>
> Relative to the original submission:
>
> * Added src/bin/psql/t/040_prompt_command.pl
> * Listed that test in src/bin/psql/meson.build
> * No intentional functional change to the feature code
>
> Happy to rename variables, drop PROMPT_SESSION_EXPORT from v1, or further
> split if that helps commit.
>
> Companion (not part of this patch):
> https://github.com/powerline/powerline/pull/2287
>
> Fork branch:
> https://github.com/bithead2k/postgres/tree/psql-prompt-command
>
> Comments welcome.
>
> Regards,
> Kirk Roybal
>
>
From 703884fe51f5261601590e995971f411e55df5d1 Mon Sep 17 00:00:00 2001
From: bithead2k <[email protected]>
Date: Fri, 3 Jul 2026 18:56:59 -0500
Subject: [PATCH v3] Add PROMPT_COMMAND and dynamic prompt support to psql
Introduce bash-like PROMPT_COMMAND handling that runs before each
interactive readline prompt, with a %D escape to embed the first line
of stdout in PROMPT1/PROMPT2. Add SHELL_EXIT for last-command status
and PROMPT_SESSION_EXPORT (off by default) to optionally export PG*
and PSQL_* environment for external prompt renderers.
Includes reset_prompt_status_after_connect() after successful \connect,
prompt regeneration in input.c, and powerline-integration.md documenting
a companion patch for the powerline psql extension.
Backward compatible: no behavior change until PROMPT_COMMAND and related
variables are configured in .psqlrc.
---
scripts/PR-description-postgres.md | 57 +++++++
src/bin/psql/command.c | 2 +
src/bin/psql/common.c | 25 ++-
src/bin/psql/help.c | 7 +
src/bin/psql/input.c | 19 ++-
src/bin/psql/input.h | 5 +-
src/bin/psql/mainloop.c | 3 +-
src/bin/psql/powerline-integration.md | 67 ++++++++
src/bin/psql/prompt.c | 213 ++++++++++++++++++++++++--
src/bin/psql/prompt.h | 2 +
src/bin/psql/startup.c | 1 +
11 files changed, 384 insertions(+), 17 deletions(-)
create mode 100644 scripts/PR-description-postgres.md
create mode 100644 src/bin/psql/powerline-integration.md
diff --git a/scripts/PR-description-postgres.md b/scripts/PR-description-postgres.md
new file mode 100644
index 00000000000..e3dcd65d215
--- /dev/null
+++ b/scripts/PR-description-postgres.md
@@ -0,0 +1,57 @@
+# PostgreSQL patch: dynamic psql prompts (PROMPT_COMMAND)
+
+## Summary
+
+Add bash-like `PROMPT_COMMAND` support to psql so interactive prompts can be
+regenerated before each `readline()` call, with optional session export for
+external prompt renderers. A companion patch for the
+[powerline](https://github.com/powerline/powerline) project provides a
+reference implementation; see `src/bin/psql/powerline-integration.md`.
+
+## Backward compatibility
+
+- **Default behavior unchanged** for users who do not set new variables.
+- `PROMPT_COMMAND` and `%D` are inert until configured (same model as bash).
+- `PROMPT_SESSION_EXPORT` defaults to **off**; session environment is only
+ exported when explicitly enabled, so existing `PROMPT_COMMAND` uses are not
+ surprised by extra `setenv()` calls.
+- New psql variables (`SHELL_EXIT`, etc.) do not alter query execution.
+
+We are happy to adjust naming, defaults, or add further gating if reviewers
+prefer a different compatibility story.
+
+## Features
+
+1. **`PROMPT_COMMAND`** — run a shell command before each interactive prompt;
+ capture first line of stdout.
+2. **`%D` prompt escape** — insert `PROMPT_COMMAND` output into `PROMPT1`/`PROMPT2`.
+3. **`SHELL_EXIT`** — last command exit status (SQL failure → 1, `\!` → real
+ shell code); not updated by `PROMPT_COMMAND` itself.
+4. **`PROMPT_SESSION_EXPORT`** — when on, export `PG*` and `PSQL_*` env vars
+ for the `PROMPT_COMMAND` subprocess (connection info, txn state, row count,
+ superuser flag, optional `txid`).
+5. **Prompt refresh** — `run_prompt_command()` before each `readline()` (fixes
+ stale prompts without `rl_pre_input_hook` hacks).
+6. **Connect reset** — clear `ROW_COUNT`/`SHELL_EXIT` and refresh optional
+ `txid` after successful `\connect`.
+
+## Files touched
+
+- `src/bin/psql/prompt.c`, `prompt.h` — core prompt/PROMPT_COMMAND logic
+- `src/bin/psql/input.c`, `input.h` — regenerate prompt before readline
+- `src/bin/psql/common.c` — `SHELL_EXIT` tracking
+- `src/bin/psql/command.c` — connect hook
+- `src/bin/psql/mainloop.c`, `startup.c`, `help.c`
+- `src/bin/psql/powerline-integration.md` — integration notes (not installed)
+
+## Testing
+
+Manual: interactive psql with `PROMPT_COMMAND`, `%D`, `\c`, failed SQL,
+`\!`, and readline editing (e.g. `\c postgres` must retain spaces).
+
+Suggested follow-up: TAP test for `%D` and `SHELL_EXIT` in `src/bin/psql/t/`.
+
+## Companion patch
+
+powerline psql extension (separate PR): segments, theme, readline renderer.
+Cross-reference: `src/bin/psql/powerline-integration.md`.
\ No newline at end of file
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 2fc34b83a5a..11300397358 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -27,6 +27,7 @@
#include "catalog/pg_class_d.h"
#include "command.h"
#include "common.h"
+#include "prompt.h"
#include "common/logging.h"
#include "common/string.h"
#include "copy.h"
@@ -4336,6 +4337,7 @@ do_connect(enum trivalue reuse_previous_specification,
pset.db = n_conn;
SyncVariables();
connection_warnings(false); /* Must be after SyncVariables */
+ reset_prompt_status_after_connect();
/* Tell the user about the new connection */
if (!pset.quiet)
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index f220344daaf..59b6bcd1585 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -465,6 +465,22 @@ AcceptResult(const PGresult *result, bool show_error)
}
+/*
+ * Set SHELL_EXIT, the exit status of the last psql command (SQL or shell).
+ *
+ * SQL failures use exit code 1. Shell commands use the real exit code
+ * (0-127, 128+signal, or -1). PROMPT_COMMAND does not update this variable.
+ */
+static void
+SetLastExitVariable(int exit_code)
+{
+ char buf[32];
+
+ snprintf(buf, sizeof(buf), "%d", exit_code);
+ SetVariable(pset.vars, "SHELL_EXIT", buf);
+}
+
+
/*
* Set special variables from a query result
* - ERROR: true/false, whether an error occurred on this query
@@ -472,6 +488,7 @@ AcceptResult(const PGresult *result, bool show_error)
* - ROW_COUNT: how many rows were returned or affected, or "0"
* - LAST_ERROR_SQLSTATE: same for last error
* - LAST_ERROR_MESSAGE: message of last error
+ * - SHELL_EXIT: 0 on success, 1 on failure
*
* Note: current policy is to apply this only to the results of queries
* entered by the user, not queries generated by slash commands.
@@ -486,6 +503,7 @@ SetResultVariables(PGresult *result, bool success)
SetVariable(pset.vars, "ERROR", "false");
SetVariable(pset.vars, "SQLSTATE", "00000");
SetVariable(pset.vars, "ROW_COUNT", *ntuples ? ntuples : "0");
+ SetLastExitVariable(0);
}
else
{
@@ -504,6 +522,7 @@ SetResultVariables(PGresult *result, bool success)
SetVariable(pset.vars, "ROW_COUNT", "0");
SetVariable(pset.vars, "LAST_ERROR_SQLSTATE", code);
SetVariable(pset.vars, "LAST_ERROR_MESSAGE", mesg ? mesg : "");
+ SetLastExitVariable(1);
}
}
@@ -512,6 +531,7 @@ SetResultVariables(PGresult *result, bool success)
* Set special variables from a shell command result
* - SHELL_ERROR: true/false, whether command returned exit code 0
* - SHELL_EXIT_CODE: exit code according to shell conventions
+ * - SHELL_EXIT: same exit code as SHELL_EXIT_CODE
*
* The argument is a wait status as returned by wait(2) or waitpid(2),
* which also applies to pclose(3) and system(3).
@@ -520,11 +540,13 @@ void
SetShellResultVariables(int wait_result)
{
char buf[32];
+ int exit_code = wait_result_to_exit_code(wait_result);
SetVariable(pset.vars, "SHELL_ERROR",
(wait_result == 0) ? "false" : "true");
- snprintf(buf, sizeof(buf), "%d", wait_result_to_exit_code(wait_result));
+ snprintf(buf, sizeof(buf), "%d", exit_code);
SetVariable(pset.vars, "SHELL_EXIT_CODE", buf);
+ SetLastExitVariable(exit_code);
}
@@ -2121,6 +2143,7 @@ ExecQueryAndProcessResults(const char *query,
SetVariable(pset.vars, "SQLSTATE", "00000");
snprintf(buf, sizeof(buf), INT64_FORMAT, total_tuples);
SetVariable(pset.vars, "ROW_COUNT", buf);
+ SetLastExitVariable(0);
/* Prevent SetResultVariables call below */
is_chunked_result = true;
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 6b599c4705a..d8fb4120b06 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -432,6 +432,10 @@ helpVariables(unsigned short int pager)
" specifies the standard psql prompt\n");
HELP0(" PROMPT2\n"
" specifies the prompt used when a statement continues from a previous line\n");
+ HELP0(" PROMPT_COMMAND\n"
+ " shell command executed before each prompt; stdout available as %D\n");
+ HELP0(" PROMPT_SESSION_EXPORT\n"
+ " if on, export PG* and PSQL_* environment for PROMPT_COMMAND (off by default)\n");
HELP0(" PROMPT3\n"
" specifies the prompt used during COPY ... FROM STDIN\n");
HELP0(" QUIET\n"
@@ -443,6 +447,9 @@ helpVariables(unsigned short int pager)
" server's version (in short string or numeric format)\n");
HELP0(" SHELL_ERROR\n"
" \"true\" if the last shell command failed, \"false\" if it succeeded\n");
+ HELP0(" SHELL_EXIT\n"
+ " exit status of the last SQL or shell command (0 on success, 1 on SQL error,\n"
+ " or the shell exit code for \\!, \\copy, etc.)\n");
HELP0(" SHELL_EXIT_CODE\n"
" exit status of the last shell command\n");
HELP0(" SHOW_ALL_RESULTS\n"
diff --git a/src/bin/psql/input.c b/src/bin/psql/input.c
index 6d37359fc96..bb34196e36a 100644
--- a/src/bin/psql/input.c
+++ b/src/bin/psql/input.c
@@ -16,6 +16,7 @@
#include "common.h"
#include "common/logging.h"
#include "input.h"
+#include "prompt.h"
#include "settings.h"
#include "tab-complete.h"
@@ -55,7 +56,7 @@ static void finishInput(void);
*
* Gets a line of interactive input, using readline if desired.
*
- * prompt: the prompt string to be used
+ * status, cstack: prompt state passed to get_prompt()
* query_buf: buffer containing lines already read in the current command
* (query_buf is not modified here, but may be consulted for tab completion)
*
@@ -64,12 +65,14 @@ static void finishInput(void);
* Caller *must* have set up sigint_interrupt_jmp before calling.
*/
char *
-gets_interactive(const char *prompt, PQExpBuffer query_buf)
+gets_interactive(promptStatus_t status, ConditionalStack cstack,
+ PQExpBuffer query_buf)
{
#ifdef USE_READLINE
if (useReadline)
{
char *result;
+ const char *prompt;
/*
* Some versions of readline don't notice SIGWINCH signals that arrive
@@ -82,6 +85,15 @@ gets_interactive(const char *prompt, PQExpBuffer query_buf)
rl_reset_screen_size();
#endif
+ /*
+ * Regenerate the prompt immediately before readline(), the same way
+ * bash runs PROMPT_COMMAND and re-evaluates PS1. Using
+ * rl_pre_input_hook with readline("") leaves the prompt invisible
+ * until the user presses a key.
+ */
+ run_prompt_command();
+ prompt = get_prompt(status, cstack);
+
/* Make current query_buf available to tab completion callback */
tab_completion_query_buf = query_buf;
@@ -100,7 +112,8 @@ gets_interactive(const char *prompt, PQExpBuffer query_buf)
}
#endif
- fputs(prompt, stdout);
+ run_prompt_command();
+ fputs(get_prompt(status, cstack), stdout);
fflush(stdout);
return gets_fromFile(stdin);
}
diff --git a/src/bin/psql/input.h b/src/bin/psql/input.h
index 2a47166347e..14b73c12f5a 100644
--- a/src/bin/psql/input.h
+++ b/src/bin/psql/input.h
@@ -44,10 +44,13 @@
#endif /* HAVE_READLINE_READLINE_H, etc */
#endif /* HAVE_LIBREADLINE */
+#include "fe_utils/conditional.h"
+#include "fe_utils/psqlscan.h"
#include "pqexpbuffer.h"
-extern char *gets_interactive(const char *prompt, PQExpBuffer query_buf);
+extern char *gets_interactive(promptStatus_t status, ConditionalStack cstack,
+ PQExpBuffer query_buf);
extern char *gets_fromFile(FILE *source);
extern void initializeInput(int flags);
diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c
index c3f85a609bc..a3409312a1c 100644
--- a/src/bin/psql/mainloop.c
+++ b/src/bin/psql/mainloop.c
@@ -163,8 +163,7 @@ MainLoop(FILE *source)
need_redisplay = false;
}
/* Now we can fetch a line */
- line = gets_interactive(get_prompt(prompt_status, cond_stack),
- query_buf);
+ line = gets_interactive(prompt_status, cond_stack, query_buf);
}
else
{
diff --git a/src/bin/psql/powerline-integration.md b/src/bin/psql/powerline-integration.md
new file mode 100644
index 00000000000..b318d946f04
--- /dev/null
+++ b/src/bin/psql/powerline-integration.md
@@ -0,0 +1,67 @@
+# psql prompt extensions and Powerline integration
+
+This document describes optional prompt features added for dynamic prompt
+generation (similar in spirit to bash `PROMPT_COMMAND`) and their companion
+implementation in the [powerline](https://github.com/powerline/powerline) project.
+
+## Backward compatibility
+
+All behavior is **opt-in**. With no `.psqlrc` changes, psql behaves as before
+except for the addition of new psql variables (`SHELL_EXIT`, `PROMPT_COMMAND`,
+`PROMPT_SESSION_EXPORT`, and the `%D` prompt escape) that have no effect until
+configured.
+
+| Switch | Default | Purpose |
+|--------|---------|---------|
+| `PROMPT_COMMAND` | unset | Run a shell command before each interactive prompt; first line of stdout is inserted via `%D` |
+| `PROMPT_SESSION_EXPORT` | off | When on, export `PG*` / `PSQL_*` environment for `PROMPT_COMMAND` subprocesses |
+| `%D` in `PROMPT1`/`PROMPT2` | unused | Substitute `PROMPT_COMMAND` output (only when `PROMPT_COMMAND` is set) |
+
+`PROMPT_SESSION_EXPORT` exists so maintainers can keep session export separate
+from `PROMPT_COMMAND` itself: users who only need `%D` without polluting the
+subprocess environment leave it off.
+
+## PostgreSQL side (this tree)
+
+- `PROMPT_COMMAND` — shell command before each prompt (like bash)
+- `%D` — insert first line of `PROMPT_COMMAND` stdout into `PROMPT1`/`PROMPT2`
+- `SHELL_EXIT` — exit status of the last SQL or shell command (for prompt themes)
+- `PROMPT_SESSION_EXPORT` — gate for exporting session state to the subprocess
+- Exported when `PROMPT_SESSION_EXPORT` is on: `PGDATABASE`, `PGUSER`, `PGHOST`,
+ `PGPORT`, `PSQL_SHELL_EXIT`, `PSQL_TXN`, `PSQL_ROW_COUNT`, `PSQL_TXID` (if
+ the `txid` variable is set), `PSQL_SUPERUSER`
+
+## Powerline side (companion patch)
+
+A matching **psql extension** is prepared for submission to powerline:
+
+- Repository: https://github.com/powerline/powerline
+- Extension: `psql` (`powerline-render psql left`)
+- Segments: user (with PostgreSQL superuser highlighting), database, host,
+ port, transaction, row count, txid, last exit status
+- Renderer: readline non-printing markers (`\x01`/`\x02`)
+
+See powerline’s `docs/source/usage/psql-postgres-integration.rst` (added in the
+companion patch) for `.psqlrc` configuration.
+
+## Example `.psqlrc` (requires both patches)
+
+```sql
+\set PROMPT_SESSION_EXPORT on
+\set PROMPT_COMMAND 'powerline-render psql left --last-exit-code ${PSQL_SHELL_EXIT:-0} -w ${COLUMNS:-120} 2>/dev/null'
+\set PROMPT1 '%D %x%# '
+\set PROMPT2 '%w%R%x%# '
+```
+
+Optional: snapshot transaction id at connect for the txid segment:
+
+```sql
+SELECT txid_current() AS txid \gset
+```
+
+## Deployment note
+
+Neither patch depends on the other at build time. Full functionality appears
+when both are deployed and the user enables the variables above. Either
+project can merge independently; cross-references in code and docs point to
+the companion implementation.
\ No newline at end of file
diff --git a/src/bin/psql/prompt.c b/src/bin/psql/prompt.c
index 681574bee79..4ce856cb011 100644
--- a/src/bin/psql/prompt.c
+++ b/src/bin/psql/prompt.c
@@ -13,11 +13,184 @@
#endif
#include "common.h"
+#include "common/logging.h"
#include "common/string.h"
#include "input.h"
#include "libpq/pqcomm.h"
#include "prompt.h"
#include "settings.h"
+#include "variables.h"
+
+#define MAX_PROMPT_SIZE 8192
+#define MAX_PROMPT_SUBST_SIZE 8192
+
+/* stdout captured from the most recent PROMPT_COMMAND execution */
+static char prompt_command_result[MAX_PROMPT_SUBST_SIZE + 1];
+
+/*
+ * Export psql state for PROMPT_COMMAND subprocesses (powerline, etc.).
+ *
+ * Enabled only when the PROMPT_SESSION_EXPORT psql variable is on (see
+ * src/bin/psql/powerline-integration.md). A companion implementation
+ * lives in the powerline project (psql extension).
+ *
+ * Connection settings use the standard libpq PG* names, set from the
+ * current session (PQhost, PQdb, etc.) rather than stale startup env.
+ * There is no PG* variable for last-command exit status; use
+ * PSQL_SHELL_EXIT (from :SHELL_EXIT) for that. The psql variable ``txid`` is
+ * exported as PSQL_TXID when set (e.g. via ``\\gset`` at connect).
+ * PSQL_SUPERUSER is set to ``1`` or ``0`` from the connection's superuser
+ * status (same test used for the ``%#`` prompt escape).
+ */
+static bool
+prompt_session_export_enabled(void)
+{
+ const char *val = GetVariable(pset.vars, "PROMPT_SESSION_EXPORT");
+ bool on = false;
+
+ if (val != NULL && ParseVariableBool(val, "PROMPT_SESSION_EXPORT", &on))
+ return on;
+
+ return false;
+}
+
+static void
+export_prompt_environment(void)
+{
+ const char *val;
+
+ val = GetVariable(pset.vars, "SHELL_EXIT");
+ if (val)
+ setenv("PSQL_SHELL_EXIT", val, 1);
+
+ if (pset.db)
+ {
+ setenv("PGDATABASE", PQdb(pset.db), 1);
+ setenv("PGUSER", session_username(), 1);
+
+ val = PQhost(pset.db);
+ if (val && val[0] != '\0')
+ setenv("PGHOST", val, 1);
+ else
+ unsetenv("PGHOST");
+
+ val = PQport(pset.db);
+ if (val && val[0] != '\0')
+ setenv("PGPORT", val, 1);
+ else
+ unsetenv("PGPORT");
+
+ switch (PQtransactionStatus(pset.db))
+ {
+ case PQTRANS_IDLE:
+ setenv("PSQL_TXN", "idle", 1);
+ break;
+ case PQTRANS_ACTIVE:
+ case PQTRANS_INTRANS:
+ setenv("PSQL_TXN", "active", 1);
+ break;
+ case PQTRANS_INERROR:
+ setenv("PSQL_TXN", "error", 1);
+ break;
+ default:
+ setenv("PSQL_TXN", "unknown", 1);
+ break;
+ }
+
+ setenv("PSQL_SUPERUSER", is_superuser() ? "1" : "0", 1);
+ }
+ else
+ {
+ unsetenv("PGDATABASE");
+ unsetenv("PGUSER");
+ unsetenv("PGHOST");
+ unsetenv("PGPORT");
+ unsetenv("PSQL_TXN");
+ unsetenv("PSQL_SUPERUSER");
+ }
+
+ val = GetVariable(pset.vars, "ROW_COUNT");
+ if (val)
+ setenv("PSQL_ROW_COUNT", val, 1);
+ else
+ unsetenv("PSQL_ROW_COUNT");
+
+ val = GetVariable(pset.vars, "txid");
+ if (val && val[0] != '\0')
+ setenv("PSQL_TXID", val, 1);
+ else
+ unsetenv("PSQL_TXID");
+}
+
+/*
+ * Run PROMPT_COMMAND, if set, before generating a prompt.
+ *
+ * Like bash's PROMPT_COMMAND, this executes a shell command before each
+ * prompt is displayed. The first line of stdout is captured and can be
+ * inserted into PROMPT1/PROMPT2 via the %D escape. (Unlike bash, psql
+ * cannot run the command in-process, so "export" in PROMPT_COMMAND will
+ * not affect later %`command` substitutions.)
+ */
+void
+run_prompt_command(void)
+{
+ const char *cmd = GetVariable(pset.vars, "PROMPT_COMMAND");
+ FILE *fd;
+ size_t len = 0;
+ int c;
+
+ prompt_command_result[0] = '\0';
+
+ if (cmd == NULL || cmd[0] == '\0')
+ return;
+
+ if (prompt_session_export_enabled())
+ export_prompt_environment();
+
+ fflush(NULL);
+ fd = popen(cmd, "r");
+ if (fd == NULL)
+ return;
+
+ while (len < MAX_PROMPT_SUBST_SIZE && (c = fgetc(fd)) != EOF)
+ {
+ if (c == '\n' || c == '\r')
+ break;
+ prompt_command_result[len++] = (char) c;
+ }
+ prompt_command_result[len] = '\0';
+
+ /*
+ * Do not update SHELL_EXIT or SHELL_EXIT_CODE here: PROMPT_COMMAND is not
+ * a user command, and overwriting would hide the status of the last SQL
+ * or shell command (e.g. for powerline --last-exit-code).
+ */
+ (void) pclose(fd);
+}
+
+/*
+ * Reset prompt-related psql variables after a successful \connect.
+ *
+ * Clears stale row-count and exit-status from the previous session, and
+ * refreshes :varname:`txid` when that variable is already defined (e.g. via
+ * ``\\gset`` in .psqlrc for powerline).
+ */
+void
+reset_prompt_status_after_connect(void)
+{
+ PGresult *res;
+
+ SetVariable(pset.vars, "ROW_COUNT", "0");
+ SetVariable(pset.vars, "SHELL_EXIT", "0");
+
+ if (!pset.db || GetVariable(pset.vars, "txid") == NULL)
+ return;
+
+ res = PQexec(pset.db, "SELECT txid_current()::text");
+ if (PQresultStatus(res) == PGRES_TUPLES_OK && PQntuples(res) > 0)
+ SetVariable(pset.vars, "txid", PQgetvalue(res, 0, 0));
+ PQclear(res);
+}
/*--------------------------
* get_prompt
@@ -59,8 +232,16 @@
* newline stripped.
* %:name: - The value of the psql variable 'name'
* (those will not be rescanned for more escape sequences!)
+ * %D - stdout from PROMPT_COMMAND (first line, no trailing newline)
*
* %[ ... %] - tell readline that the contained text is invisible
+ * (use around ANSI color sequences from prompt utilities)
+ *
+ * PROMPT_COMMAND - if set, a shell command run before each prompt (see
+ * run_prompt_command()).
+ *
+ * PROMPT_SESSION_EXPORT - if on, export connection/session environment for
+ * PROMPT_COMMAND (see powerline-integration.md).
*
* If the application-wide prompts become NULL somehow, the returned string
* will be empty (not NULL!).
@@ -70,13 +251,20 @@
char *
get_prompt(promptStatus_t prompt_status, ConditionalStack cstack)
{
-#define MAX_PROMPT_SIZE 256
- static char destination[MAX_PROMPT_SIZE + 1];
- char buf[MAX_PROMPT_SIZE + 1];
+ static PQExpBuffer destination = NULL;
+ char buf[MAX_PROMPT_SUBST_SIZE + 1];
bool esc = false;
const char *prompt_string = "? ";
static size_t last_prompt1_width = 0;
+ if (destination == NULL)
+ destination = createPQExpBuffer();
+ else
+ resetPQExpBuffer(destination);
+
+ if (PQExpBufferBroken(destination))
+ pg_fatal("out of memory");
+
switch (prompt_status)
{
case PROMPT_READY:
@@ -97,10 +285,8 @@ get_prompt(promptStatus_t prompt_status, ConditionalStack cstack)
break;
}
- destination[0] = '\0';
-
for (const char *p = prompt_string;
- *p && strlen(destination) < sizeof(destination) - 1;
+ *p && destination->len < MAX_PROMPT_SIZE;
p++)
{
memset(buf, 0, sizeof(buf));
@@ -306,6 +492,10 @@ get_prompt(promptStatus_t prompt_status, ConditionalStack cstack)
/* not here yet */
break;
+ case 'D':
+ strlcpy(buf, prompt_command_result, sizeof(buf));
+ break;
+
case '#':
if (is_superuser())
buf[0] = '#';
@@ -383,14 +573,17 @@ get_prompt(promptStatus_t prompt_status, ConditionalStack cstack)
}
if (!esc)
- strlcat(destination, buf, sizeof(destination));
+ appendPQExpBufferStr(destination, buf);
}
+ if (PQExpBufferBroken(destination))
+ pg_fatal("out of memory");
+
/* Compute the visible width of PROMPT1, for PROMPT2's %w */
if (prompt_string == pset.prompt1)
{
- char *d = destination;
- char *end = d + strlen(d);
+ char *d = destination->data;
+ char *end = d + destination->len;
bool visible = true;
last_prompt1_width = 0;
@@ -432,5 +625,5 @@ get_prompt(promptStatus_t prompt_status, ConditionalStack cstack)
}
}
- return destination;
+ return destination->data;
}
diff --git a/src/bin/psql/prompt.h b/src/bin/psql/prompt.h
index b3fa4dba5e1..a10948519f8 100644
--- a/src/bin/psql/prompt.h
+++ b/src/bin/psql/prompt.h
@@ -13,5 +13,7 @@
#include "fe_utils/psqlscan.h"
char *get_prompt(promptStatus_t prompt_status, ConditionalStack cstack);
+void run_prompt_command(void);
+void reset_prompt_status_after_connect(void);
#endif /* PROMPT_H */
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index bb33c7c14d7..83f4236a150 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -198,6 +198,7 @@ main(int argc, char *argv[])
/* Initialize variables for last error */
SetVariable(pset.vars, "LAST_ERROR_MESSAGE", "");
SetVariable(pset.vars, "LAST_ERROR_SQLSTATE", "00000");
+ SetVariable(pset.vars, "SHELL_EXIT", "0");
/* Default values for variables (that don't match the result of \unset) */
SetVariableBool(pset.vars, "AUTOCOMMIT");
--
2.43.0