From 45df6a438c3bc2b0d65c6160756bc86a83496ae0 Mon Sep 17 00:00:00 2001 From: Rui Zhao Date: Sat, 17 Oct 2026 22:20:06 +0800 Subject: [PATCH 2/3] Stop script execution on Ctrl-D without exiting interactive psql In single-step mode, SendQuery ignores a NULL return from fgets when reading the user's confirmation. Ctrl-D therefore executes the query without approval. While reading a script, later queries also execute because stdin's EOF indicator remains set. With readline disabled, the interactive session then exits when MainLoop next reads from stdin. Treat failure to read confirmation as cancellation: clear stdin's EOF and error indicators, set cancel_pressed, and return without sending the query. MainLoop already stops reading a script when cancel_pressed is set, but only checks it at the start of the next input line. Also check it before processing each command on the current line, so subsequent SQL and backslash commands are not executed after cancellation. This applies to cancellation by Ctrl-C as well as Ctrl-D. Add TAP tests for multi-line scripts, SQL and backslash commands on one line, queries sent by \g, and nested scripts, with ON_ERROR_STOP both on and off. Check that a later script still asks for confirmation, that x skips only one query, and that Ctrl-D at the ordinary command prompt still exits psql. Run with readline enabled and disabled. --- src/bin/psql/common.c | 12 ++- src/bin/psql/mainloop.c | 3 +- src/bin/psql/meson.build | 1 + src/bin/psql/t/041_singlestep_eof.pl | 131 +++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 src/bin/psql/t/041_singlestep_eof.pl diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index 7b5bff8141d..9600c468aa2 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -1159,9 +1159,15 @@ SendQuery(const char *query, int num_copy_from_stdin) "/**(press return to proceed or enter x and return to cancel)*******************/\n"), query); fflush(stdout); - if (fgets(buf, sizeof(buf), stdin) != NULL) - if (buf[0] == 'x') - goto sendquery_cleanup; + if (fgets(buf, sizeof(buf), stdin) == NULL) + { + /* Cancel the script, but allow subsequent input from stdin. */ + clearerr(stdin); + cancel_pressed = true; + goto sendquery_cleanup; + } + if (buf[0] == 'x') + goto sendquery_cleanup; if (cancel_pressed) goto sendquery_cleanup; } diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c index cf1a967f56d..db02bdde178 100644 --- a/src/bin/psql/mainloop.c +++ b/src/bin/psql/mainloop.c @@ -385,7 +385,8 @@ MainLoop(FILE *source) success = true; line_saved_in_history = false; - while (success || !die_on_error) + /* Do not process the rest of the line after cancellation. */ + while (!cancel_pressed && (success || !die_on_error)) { PsqlScanResult scan_result; promptStatus_t prompt_tmp = prompt_status; diff --git a/src/bin/psql/meson.build b/src/bin/psql/meson.build index debdaf8294c..e78d53faeb6 100644 --- a/src/bin/psql/meson.build +++ b/src/bin/psql/meson.build @@ -79,6 +79,7 @@ tests += { 't/020_cancel.pl', 't/030_pager.pl', 't/040_interrupt.pl', + 't/041_singlestep_eof.pl', ], }, } diff --git a/src/bin/psql/t/041_singlestep_eof.pl b/src/bin/psql/t/041_singlestep_eof.pl new file mode 100644 index 00000000000..e19135bc997 --- /dev/null +++ b/src/bin/psql/t/041_singlestep_eof.pl @@ -0,0 +1,131 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +plan skip_all => 'IO::Pty is needed to run this test' + unless eval { require IO::Pty; 1 }; +plan skip_all => 'terminal EOF input is not supported by this test on Windows' + if $windows_os; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->start; +$node->safe_psql('postgres', 'CREATE TABLE step_log (id int)'); + +my $multiline = $node->basedir . '/multiline.sql'; +append_to_file($multiline, + "INSERT INTO step_log VALUES (1);\nINSERT INTO step_log VALUES (2);\n"); +my $same_line = $node->basedir . '/same_line.sql'; +append_to_file($same_line, + "INSERT INTO step_log VALUES (1); \\set eof_marker changed \\echo :eof_marker\n" + . "INSERT INTO step_log VALUES (2);\n"); +my $send_query = $node->basedir . '/send_query.sql'; +append_to_file($send_query, + "INSERT INTO step_log VALUES (1) \\g \\set eof_marker changed\n" + . "INSERT INTO step_log VALUES (2);\n"); +my $nested = $node->basedir . '/nested.sql'; +append_to_file($nested, + "\\i $same_line \\set eof_marker changed\n" + . "INSERT INTO step_log VALUES (4);\n"); + +my $confirm = qr/return to cancel[^\r\n]*\r?\n/; +my $ready = qr/postgres=# /; + +for my $no_readline (1, 0) +{ + next + if !$no_readline + && ((!defined($ENV{with_readline}) || $ENV{with_readline} ne 'yes') + || defined($ENV{SKIP_READLINE_TESTS})); + + my $mode = $no_readline ? 'without readline' : 'with readline'; + my $h = $node->interactive_psql('postgres', + extra_params => $no_readline ? ['-n'] : []); + $h->set_query_timer_restart; + $h->query_until(qr/eof-ready\r?\npostgres=# /, "\\echo eof-ready\n"); + + for my $on_error_stop ('off', 'on') + { + $h->query_until($ready, "\\set ON_ERROR_STOP $on_error_stop\n"); + for my $case ( + [ 'multiple lines', $multiline ], + [ 'multiple commands on one line', $same_line ], + [ 'query sent by \\g', $send_query ], + [ 'nested scripts', $nested ]) + { + my ($name, $script) = @$case; + my $description = "$name, ON_ERROR_STOP $on_error_stop, $mode"; + $node->safe_psql('postgres', 'TRUNCATE step_log'); + $h->query_until( + qr/eof-setup\r?\npostgres=# /, + "\\set SINGLESTEP on\n\\set eof_marker unchanged\n\\echo eof-setup\n" + ); + $h->query_until($confirm, "\\i $script\n"); + my $output = $h->query_until($ready, "\004"); + if ($script eq $same_line) + { + unlike( + $output, + qr/(?:^|\r?\n)(?:unchanged|changed)\r?\n/, + "Ctrl-D does not execute later \\echo: $description"); + } + is( $node->safe_psql('postgres', 'SELECT count(*) FROM step_log'), + '0', + "Ctrl-D does not execute SQL: $description"); + like( + $h->query_until($ready, "\\echo :eof_marker\n"), + qr/\r?\nunchanged\r?\n/, + "Ctrl-D does not execute later \\set: $description"); + } + } + + # A later script must still wait for confirmation before each query. + $h->query_until($ready, "\\set ON_ERROR_STOP off\n"); + $h->query_until($confirm, "\\i $multiline\n"); + $h->query_until($confirm, "\n"); + $h->query_until($ready, "\n"); + is( $node->safe_psql( + 'postgres', 'SELECT array_agg(id ORDER BY id) FROM step_log'), + '{1,2}', + "a later script accepts confirmation for each query, $mode"); + + # Unlike Ctrl-D, x skips just one query, not the rest of the script. + $node->safe_psql('postgres', 'TRUNCATE step_log'); + $h->query_until($confirm, "\\i $multiline\n"); + $h->query_until($confirm, "x\n"); + $h->query_until($ready, "\n"); + is($node->safe_psql('postgres', 'SELECT id FROM step_log'), + '2', "x still skips only the current query, $mode"); + + # The same cancellation must work for SQL entered interactively. + $node->safe_psql('postgres', 'TRUNCATE step_log'); + $h->query_until($confirm, + "INSERT INTO step_log VALUES (5); INSERT INTO step_log VALUES (6);\n" + ); + $h->query_until($ready, "\004"); + is($node->safe_psql('postgres', 'SELECT count(*) FROM step_log'), + '0', "Ctrl-D cancels the rest of an interactive SQL line, $mode"); + $h->query_until($confirm, "SELECT 777;\n"); + like($h->query_until($ready, "\n"), + qr/\r?\n777\r?\n/, "the interactive session remains usable, $mode"); + + ok($h->quit, "psql exits normally, $mode"); + + # EOF while reading a command must still end the interactive session. + my $exit_session = $node->interactive_psql('postgres', + extra_params => $no_readline ? ['-n'] : []); + $exit_session->set_query_timer_restart; + $exit_session->query_until(qr/eof-exit\r?\npostgres=# /, + "\\echo eof-exit\n"); + $exit_session->{stdin} .= "\004"; + ok($exit_session->{run}->finish, + "Ctrl-D at the command prompt still exits psql, $mode"); +} + +$node->stop; +done_testing(); -- 2.43.7