Attached is a rebase after TestLib.pm got a documentation in 6fcc40b1.

The attached patch improves psql code coverage by adding a specific TAP test. The 1709 tests take 4 seconds CPU (6.3 elapsed time) on my laptop.

The infrastructure is updated to require perl module "Expect", allowing to test interactive features such as tab completion and prompt changes.

Coverage in "src/bin/psql" jumps from 40.0% of lines and 41.9% of functions to 78.4% of lines and 98.1% of functions with "check-world".

--
Fabien.
diff --git a/src/bin/pg_basebackup/t/010_pg_basebackup.pl b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
index b7d36b65dd..aabc27ab6f 100644
--- a/src/bin/pg_basebackup/t/010_pg_basebackup.pl
+++ b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
@@ -506,6 +506,7 @@ system_or_bail 'pg_ctl', '-D', $pgdata, 'start';
 $node->command_checks_all(
 	[ 'pg_basebackup', '-D', "$tempdir/backup_corrupt" ],
 	1,
+	'',
 	[qr{^$}],
 	[qr/^WARNING.*checksum verification failed/s],
 	'pg_basebackup reports checksum mismatch');
@@ -526,6 +527,7 @@ system_or_bail 'pg_ctl', '-D', $pgdata, 'start';
 $node->command_checks_all(
 	[ 'pg_basebackup', '-D', "$tempdir/backup_corrupt2" ],
 	1,
+	'',
 	[qr{^$}],
 	[qr/^WARNING.*further.*failures.*will.not.be.reported/s],
 	'pg_basebackup does not report more than 5 checksum mismatches');
@@ -542,6 +544,7 @@ system_or_bail 'pg_ctl', '-D', $pgdata, 'start';
 $node->command_checks_all(
 	[ 'pg_basebackup', '-D', "$tempdir/backup_corrupt3" ],
 	1,
+	'',
 	[qr{^$}],
 	[qr/^WARNING.*7 total checksum verification failures/s],
 	'pg_basebackup correctly report the total number of checksum mismatches');
diff --git a/src/bin/pg_checksums/t/002_actions.pl b/src/bin/pg_checksums/t/002_actions.pl
index 59228b916c..cf4811d382 100644
--- a/src/bin/pg_checksums/t/002_actions.pl
+++ b/src/bin/pg_checksums/t/002_actions.pl
@@ -62,6 +62,7 @@ sub check_relation_corruption
 			'--filenode',   $relfilenode_corrupted
 		],
 		1,
+		'',
 		[qr/Bad checksums:.*1/],
 		[qr/checksum verification failed/],
 		"fails with corrupted data for single relfilenode on tablespace $tablespace"
@@ -71,6 +72,7 @@ sub check_relation_corruption
 	$node->command_checks_all(
 		[ 'pg_checksums', '--check', '-D', $pgdata ],
 		1,
+		'',
 		[qr/Bad checksums:.*1/],
 		[qr/checksum verification failed/],
 		"fails with corrupted data on tablespace $tablespace");
@@ -203,6 +205,7 @@ sub fail_corrupt
 	$node->command_checks_all(
 		[ 'pg_checksums', '--check', '-D', $pgdata ],
 		1,
+		'',
 		[qr/^$/],
 		[qr/could not read block 0 in file.*$file\":/],
 		"fails for corrupted data in $file");
diff --git a/src/bin/pg_controldata/t/001_pg_controldata.pl b/src/bin/pg_controldata/t/001_pg_controldata.pl
index 3b63ad230f..ebe9b80a52 100644
--- a/src/bin/pg_controldata/t/001_pg_controldata.pl
+++ b/src/bin/pg_controldata/t/001_pg_controldata.pl
@@ -33,6 +33,7 @@ close $fh;
 command_checks_all(
 	[ 'pg_controldata', $node->data_dir ],
 	0,
+	'',
 	[
 		qr/WARNING: Calculated CRC checksum does not match value stored in file/,
 		qr/WARNING: invalid WAL segment size/
diff --git a/src/bin/pg_resetwal/t/002_corrupted.pl b/src/bin/pg_resetwal/t/002_corrupted.pl
index f9940d7fc5..1990669d26 100644
--- a/src/bin/pg_resetwal/t/002_corrupted.pl
+++ b/src/bin/pg_resetwal/t/002_corrupted.pl
@@ -30,6 +30,7 @@ close $fh;
 command_checks_all(
 	[ 'pg_resetwal', '-n', $node->data_dir ],
 	0,
+	'',
 	[qr/pg_control version number/],
 	[
 		qr/pg_resetwal: warning: pg_control exists but is broken or wrong version; ignoring it/
@@ -46,6 +47,7 @@ close $fh;
 command_checks_all(
 	[ 'pg_resetwal', '-n', $node->data_dir ],
 	0,
+	'',
 	[qr/pg_control version number/],
 	[
 		qr/\Qpg_resetwal: warning: pg_control specifies invalid WAL segment size (0 bytes); proceed with caution\E/
diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl
index b82d3f65c4..01010914fe 100644
--- a/src/bin/pgbench/t/001_pgbench_with_server.pl
+++ b/src/bin/pgbench/t/001_pgbench_with_server.pl
@@ -50,7 +50,7 @@ sub pgbench
 
 	push @cmd, @args;
 
-	$node->command_checks_all(\@cmd, $stat, $out, $err, $name);
+	$node->command_checks_all(\@cmd, $stat, '', $out, $err, $name);
 
 	# cleanup?
 	#unlink @filenames or die "cannot unlink files (@filenames): $!";
diff --git a/src/bin/pgbench/t/002_pgbench_no_server.pl b/src/bin/pgbench/t/002_pgbench_no_server.pl
index f7fa18418b..b58f3548c3 100644
--- a/src/bin/pgbench/t/002_pgbench_no_server.pl
+++ b/src/bin/pgbench/t/002_pgbench_no_server.pl
@@ -25,7 +25,7 @@ sub pgbench
 	my ($opts, $stat, $out, $err, $name) = @_;
 	print STDERR "opts=$opts, stat=$stat, out=$out, err=$err, name=$name";
 	command_checks_all([ 'pgbench', split(/\s+/, $opts) ],
-		$stat, $out, $err, $name);
+		$stat, '', $out, $err, $name);
 	return;
 }
 
@@ -52,7 +52,7 @@ sub pgbench_scripts
 			push @cmd, '-f', $filename;
 		}
 	}
-	command_checks_all(\@cmd, $stat, $out, $err, $name);
+	command_checks_all(\@cmd, $stat, '', $out, $err, $name);
 	return;
 }
 
diff --git a/src/bin/psql/.gitignore b/src/bin/psql/.gitignore
index c2862b12d6..d324c1c1fa 100644
--- a/src/bin/psql/.gitignore
+++ b/src/bin/psql/.gitignore
@@ -3,3 +3,4 @@
 /sql_help.c
 
 /psql
+/tmp_check
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index 69bb297fe7..9473ab01cb 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -60,8 +60,15 @@ uninstall:
 
 clean distclean:
 	rm -f psql$(X) $(OBJS) lex.backup
+	rm -rf tmp_check
 
 # files removed here are supposed to be in the distribution tarball,
 # so do not clean them in the clean/distclean rules
 maintainer-clean: distclean
 	rm -f sql_help.h sql_help.c psqlscanslash.c
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 774cc764ff..d7c0fc0c1e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -307,6 +307,7 @@ describeTablespaces(const char *pattern, bool verbose)
  *
  * a for aggregates
  * n for normal
+ * p for procedure
  * t for trigger
  * w for window
  *
diff --git a/src/bin/psql/t/001_psql.pl b/src/bin/psql/t/001_psql.pl
new file mode 100644
index 0000000000..dafa68e63e
--- /dev/null
+++ b/src/bin/psql/t/001_psql.pl
@@ -0,0 +1,869 @@
+use strict;
+use warnings;
+
+use PostgresNode;
+use TestLib;
+use Test::More;
+
+my $node = get_new_node('main');
+$node->init();
+$node->start();
+
+# create a file under the test directory
+# return its full path
+sub create_test_file
+{
+	my ($fname, $contents) = @_;
+	my $fn = $node->basedir . '/' . $fname;
+	#ok(not -e $fn, "$fn must not already exists");
+	append_to_file($fn, $contents);
+	return $fn;
+}
+
+# invoke psql
+# - opts: space-separated options and arguments
+#         -X is appended, unless opts starts with !
+# - stat: expected exit status
+# - in: input stream
+# - out: list of re to check on stdout
+# - err: list of re to check on stderr
+# - name: of the test
+# - more: more raw arguments
+sub psql
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($opts, $stat, $in, $out, $err, $name, @more) = @_;
+	$opts =~ s/^!// or push @more, '-X';
+	my @cmd = ('psql', split(/\s+/, $opts), @more);
+	$node->command_checks_all(\@cmd, $stat, $in, $out, $err, $name);
+	return;
+}
+
+# invoke psql interactively, making it believe it is connected to a tty
+# - opts: space-separated options and arguments
+#         -X is appended, unless opts starts with !
+# - stat: expected final exit status
+# - timeout: for interactions, in second
+# - inout: list of input expected re
+# - name: of the test
+# - remove: tell to remove init/start/end checks
+sub ipsql
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+	my ($opts, $stat, $timeout, $inout, $name, $remove) = @_;
+
+	# build command to run
+	my @more = ();
+	@more = ('-X') unless $opts =~ /^!/;
+	$opts =~ s/^!//;
+	my @cmd = ('psql', split(/\s+/, $opts), @more);
+
+	# check it is running at start and end, and exit cleanly
+	unless ($remove)
+	{
+		unshift @$inout, [ "\\echo STARTING :VERSION_NUM\n", qr/STARTING \d+.*postgres=\# /s ];
+		push @$inout, [ "\\echo ENDING :VERSION_NUM\\q\n", qr/ENDING \d+$/s ];
+	}
+
+	# call the stuff
+	$node->icommand_checks(
+		\@cmd, $stat, $timeout,
+		$remove ? undef : qr/psql .*Type "help" for help..*postgres=# /s,
+		$inout, $name);
+	return;
+}
+
+my $EMPTY = [ qr/^$/ ];
+
+#
+# check various options
+#
+# this cannot be tested from within a SQL script
+psql('!--help', 0, '',
+	 [ qr/interactive terminal/, qr/Usage:/, qr/Connection options:/ ],
+	 $EMPTY, 'psql --help');
+psql('!-?', 0, '',
+	 [ qr/interactive terminal/, qr/Usage:/, qr/Connection options:/ ],
+	 $EMPTY, 'psql -?');
+psql('!--version', 0, '',
+	 [ qr{^psql \(PostgreSQL\) \d+} ], $EMPTY, 'psql --version');
+psql('--help=variables', 0, '',
+	 [ qr{VERSION_NAME}, qr{unicode_border_linestyle}, qr{PSQL_EDITOR} ],
+	 $EMPTY, 'psql --help=variables');
+# look for plenty of backslash-commands
+psql('--help=commands', 0, '',
+	 [ qr{\\copyright}, qr{\\pset}, qr{(?!VERSION_NAME)} ],
+	 $EMPTY, 'psql --help=commands');
+
+# tested elsewhere: -a -A -d -e -h -L
+psql('-b', 0, "SELECT 1/0;\n",
+	 $EMPTY, [ qr{STATEMENT:  SELECT 1/0}, qr{ERROR:  division by zero} ], 'psql -b');
+psql('-c \\q', 0, '', $EMPTY, $EMPTY, 'psql -c');
+psql('-c \\l -c \\l', 0, '', [ qr{template0.*template0}s ], $EMPTY, 'psql -c -c');
+psql('', 0, '', [ qr{(?!1234)}, qr{5678}s ], $EMPTY, 'psql -c ;;', ('-c', 'SELECT 1234; SELECT 5678;'));
+psql('-E', 0, "\\l\n", [ qr{\* QUERY \*}, qr{template0} ], $EMPTY, 'psql -E');
+psql('-A -F _', 0, "SELECT 54, 32;\n", [ qr{54_32} ], $EMPTY, 'psql -F _');
+psql('-H', 0, "SELECT 5432 AS pg\n", [ qr{>5432<} ], $EMPTY, 'psql -H');
+psql('-l', 0, '', [ qr{template0}, qr{template1} ], $EMPTY, 'psql -l');
+psql('-n', 0, "\\q\n", $EMPTY, $EMPTY, 'psql -n');
+psql('-P format=html', 0, "SELECT 5432 AS pg\n", [ qr{>5432<} ], $EMPTY, 'psql -P format=html');
+psql('-P tuples_only', 0, "\\pset\n", [ qr{tuples_only.*on} ], $EMPTY, 'psql -P tuples_only');
+psql('-A -R_', 0, "VALUES (1), (2);\n", [ qr{_1_2_} ], $EMPTY, 'psql -R _');
+psql('-s', 0, "SELECT 1;\n", [ qr{verify command}, qr{SELECT 1}, qr{press return to proceed} ], $EMPTY, 'psql -s');
+psql('-S', 0, "SELECT 1 AS one\nSELECT 2 AS two\n", [ qr{one.*1.*two.*2}s ], $EMPTY, 'psql -S');
+psql('-H -T width=100', 0, "SELECT 1 AS one;\n", [ qr{table.*width=100} ], $EMPTY, 'psql -T ...');
+psql('-X -V', 0, '', [ qr{^psql \(PostgreSQL\) \d+} ], $EMPTY, 'psql -X -V');
+psql('', 0, "\\echo :VERSION_NUM\n", [ qr{\d{6}} ], $EMPTY, 'psql -- show VERSION_NUM');
+psql('-v VERSION_NUM', 0, "\\echo :VERSION_NUM\n", [ qr{:VERSION_NUM} ], $EMPTY, 'psql -v ...');
+psql('-v VERSION_NUM=bla', 0, "\\echo :VERSION_NUM\n", [ qr{bla} ], $EMPTY, 'psql -v ...=.');
+psql('-w', 0, '', $EMPTY, $EMPTY, 'psql -w');
+# nope, interacts on tty
+#psql('-W', 0, "foo\n", [ qr{^$} ], [ qr{^$} ], 'psql -W');
+psql('-x', 0, "SELECT 1 AS one, 2 AS two;\n", [ qr{one \| 1.*two \| 2}s ], $EMPTY, 'psql -x');
+# some issue, \0 is not welcome somewhere
+#psql('-A -z', "SELECT 1 AS one, 2 AS two;\n", [ qr{one.two}s, qr{1.2}s ], [ qr{^$} ], 'psql -z');
+#psql('-A -0', "SELECT 1 AS one, 2 AS two;\n", [ qr{two.1}s ], [ qr{^$} ], 'psql -0');
+psql('-1', 0, "SELECT 54;\nSELECT 32;\n", [ qr{54}, qr{32} ], $EMPTY, 'psql -1');
+psql('-X -?', 0, '',
+	 [ qr{interactive terminal}, qr{Usage:}, qr{Connection options:} ], $EMPTY, 'psql -X -?');
+psql('--csv', 0, "SELECT 1 AS one, 2 AS two\n", [ qr{one,two}, qr{1,2} ], $EMPTY, 'psql --csv');
+
+#
+# create some objects to help \d tests later
+#
+psql('', 0, '-- create some objects
+CREATE USER regress_psql_tap_1;
+ALTER ROLE regress_psql_tap_1 SET enable_indexscan TO off;
+CREATE TABLE regress_psql_tap_1_t1(data TEXT NOT NULL);
+CREATE FUNCTION regress_psql_tap_1_f1() RETURNS INTEGER AS $$ SELECT 54321 $$ LANGUAGE SQL;
+CREATE VIEW regress_psql_tap_1_v1 AS SELECT 54321 AS one;
+', [ qr{CREATE ROLE}, qr{ALTER ROLE} ], [ qr{^$} ], 'psql -- create stuff');
+
+#
+# check interactive help
+#
+# this is mostly for coverage, including occasional negatives.
+# we certainly do not want to have an output file with the whole doc.
+my @backslash_help = (
+	# \h command, list of stdout matches
+	[ 'no-help', [ qr{No help available for} ] ],
+	[ '' , [ qr{ABORT}, qr{ALTER}, qr{CREATE}, qr{DROP}, qr{VALUES}, qr{WITH} ] ],
+	[ ' ' , [ qr{ABORT}, qr{ALTER}, qr{CREATE}, qr{DROP}, qr{VALUES}, qr{WITH} ] ],
+	# lists
+	[ 'ALTER', [ qr{AGGREGATE}, qr{ROLE}, qr{VIEW}, qr{(?!ACCESS METHOD)}, qr{(?!TRANSFORM)} ] ],
+	[ 'ALTER TEXT SEARCH', [ qr{CONFIGURATION}, qr{DICTIONARY}, qr{PARSER}, qr{TEMPLATE} ] ],
+	[ 'ALTER FOREIGN', [ qr{DATA WRAPPER}, qr{TABLE} ] ],
+	[ 'CREATE', [ qr{ACCESS METHOD}, qr{TRANSFORM} ] ],
+	[ 'DROP', [ qr{ACCESS METHOD}, qr{VIEW} ] ],
+	# details
+	[ 'ABORT', [ qr{TRANSACTION}, qr{CHAIN} ] ],
+	[ 'ALTER AGGREGATE', [ qr{RENAME TO}, qr{(?!CONVERSION)} ] ],
+	[ 'ALTER CONVERSION', [ qr{SET SCHEMA}, qr{(?!AGGREGATE)} ] ],
+	[ 'ALTER COLLATION', [ qr{REFRESH VERSION} ] ],
+	[ 'ALTER DATABASE', [ qr{CONNECTION LIMIT} ] ],
+	[ 'ALTER DEFAULT PRIVILEGES', [ qr{GRANT} ] ],
+	[ 'ALTER DOMAIN', [ qr{VALIDATE CONSTRAINT} ] ],
+	[ 'ALTER EVENT TRIGGER', [ qr{DISABLE} ] ],
+	[ 'ALTER EXTENSION', [ qr{OPERATOR FAMILY} ] ],
+	[ 'ALTER FOREIGN DATA WRAPPER', [ qr{VALIDATOR} ] ],
+	[ 'ALTER FOREIGN TABLE', [ qr{SET STORAGE} ] ],
+	[ 'ALTER FUNCTION', [ qr{PARALLEL} ] ],
+	[ 'ALTER GROUP', [ qr{DROP USER} ] ],
+	[ 'ALTER INDEX', [ qr{ALL IN TABLESPACE} ] ],
+	[ 'ALTER LANGUAGE', [ qr{PROCEDURAL}, qr{(?!INDEX)} ] ],
+	[ 'ALTER LARGE OBJECT', [ qr{OWNER TO} ] ],
+	[ 'ALTER MATERIALIZED VIEW', [ qr{SET TABLESPACE} ] ],
+	[ 'ALTER OPERATOR', [ qr{JOIN =} ] ],
+	[ 'ALTER OPERATOR CLASS', [ qr{SET SCHEMA}, qr{(?!LANGUAGE)} ] ],
+	[ 'ALTER OPERATOR FAMILY', [ qr{FUNCTION} ] ],
+	[ 'ALTER POLICY', [ qr{WITH CHECK} ] ],
+	[ 'ALTER PROCEDURE', [ qr{DEPENDS ON EXTENSION} ] ],
+	[ 'ALTER PUBLICATION', [ qr{DROP TABLE} ] ],
+	[ 'ALTER ROLE', [ qr{BYPASSRLS} ] ],
+	[ 'ALTER ROUTINE', [ qr{LEAKPROOF} ] ],
+	[ 'ALTER RULE', [ qr{RENAME TO}, qr{(?!OWNER TO)} ] ],
+	[ 'ALTER SCHEMA', [ qr{OWNER TO} ] ],
+	[ 'ALTER SEQUENCE', [ qr{NO MAXVALUE} ] ],
+	[ 'ALTER SERVER', [ qr{VERSION} ] ],
+	[ 'ALTER STATISTICS', [ qr{SET SCHEMA} ] ],
+	[ 'ALTER SUBSCRIPTION', [ qr{REFRESH PUBLICATION} ] ],
+	[ 'ALTER SYSTEM', [ qr{RESET ALL} ] ],
+	[ 'ALTER TABLE', [ qr{ALL IN TABLESPACE}, qr{DISABLE ROW LEVEL SECURITY} ] ],
+	[ 'ALTER TABLESPACE', [ qr{RESET} ] ],
+	[ 'ALTER TEXT SEARCH CONFIGURATION', [ qr{ALTER MAPPING FOR} ] ],
+	[ 'ALTER TEXT SEARCH DICTIONARY', [ qr{OWNER TO} ] ],
+	[ 'ALTER TEXT SEARCH PARSER', [ qr{RENAME TO}, qr{(?!OWNER TO)} ] ],
+	[ 'ALTER TEXT SEARCH TEMPLATE', [ qr{RENAME TO}, qr{(?!OWNER TO)} ] ],
+	[ 'ALTER TRIGGER', [ qr{DEPENDS ON EXTENSION} ] ],
+	[ 'ALTER TYPE', [ qr{RENAME ATTRIBUTE} ] ],
+	[ 'ALTER USER', [ qr{NOREPLICATION}, qr{(?!MAPPING)} ] ],
+	[ 'ALTER USER MAPPING', [ qr{SERVER} ] ],
+	[ 'ALTER VIEW', [ qr{DROP DEFAULT} ] ],
+	[ 'ANALYZE', [ qr{VERBOSE}, qr{SKIP_LOCKED} ] ],
+	[ 'BEGIN', [ qr{ISOLATION LEVEL} ] ],
+	[ 'CALL', [] ],
+	[ 'CHECKPOINT', [] ],
+	[ 'CLOSE', [ qr{ALL} ] ],
+	[ 'CLUSTER', [ qr{VERBOSE} ] ],
+	[ 'COMMENT', [ qr{FOREIGN DATA WRAPPER} ] ],
+	[ 'COMMIT', [ qr{TRANSACTION}, qr{CHAIN} ] ],
+	[ 'COMMIT PREPARED', [] ],
+	[ 'COPY', [ qr{FORMAT}, qr{FORCE_NOT_NULL} ] ],
+	[ 'CREATE ACCESS METHOD', [ qr{HANDLER} ] ],
+	[ 'CREATE AGGREGATE', [ qr{SSPACE}, qr{SORTOP}, qr{FINALFUNC} ] ],
+	[ 'CREATE CAST', [ qr{AS IMPLICIT} ] ],
+	[ 'CREATE COLLATION', [ qr{LC_CTYPE}, qr{DETERMINISTIC} ] ],
+	[ 'CREATE CONVERSION', [ qr{FOR} ] ],
+	[ 'CREATE DATABASE', [ qr{ALLOW_CONNECTIONS} ] ],
+	[ 'CREATE DOMAIN', [ qr{COLLATE} ] ],
+	[ 'CREATE EVENT TRIGGER', [ qr{EXECUTE} ] ],
+	[ 'CREATE EXTENSION', [ qr{FROM} ] ],
+	[ 'CREATE FOREIGN DATA WRAPPER', [ qr{NO VALIDATOR} ] ],
+	[ 'CREATE FOREIGN TABLE', [ qr{SERVER} ] ],
+	[ 'CREATE FUNCTION', [ qr{CALLED ON NULL INPUT}, qr{(?!PROCEDURE)} ] ],
+	[ 'CREATE GROUP', [ qr{ADMIN} ] ],
+	[ 'CREATE INDEX', [ qr{CONCURRENTLY}, qr{WHERE} ] ],
+	[ 'CREATE LANGUAGE', [ qr{TRUSTED} ] ],
+	[ 'CREATE MATERIALIZED VIEW', [ qr{DATA} ] ],
+	[ 'CREATE OPERATOR', [ qr{LEFTARG} ] ],
+	[ 'CREATE OPERATOR CLASS', [ qr{FOR ORDER BY} ] ],
+	[ 'CREATE OPERATOR FAMILY', [ qr{USING} ] ],
+	[ 'CREATE POLICY', [ qr{PERMISSIVE} ] ],
+	[ 'CREATE PROCEDURE', [ qr{SECURITY INVOKER}, qr{(?!FUNCTION)} ] ],
+	[ 'CREATE PUBLICATION', [ qr{FOR ALL TABLES} ] ],
+	[ 'CREATE ROLE', [ qr{SYSID} ] ],
+	[ 'CREATE RULE', [ qr{ALSO} ] ],
+	[ 'CREATE SCHEMA', [ qr{AUTHORIZATION} ] ],
+	[ 'CREATE SEQUENCE', [ qr{NO MINVALUE} ] ],
+	[ 'CREATE SERVER', [ qr{FOREIGN DATA WRAPPER} ] ],
+	[ 'CREATE STATISTICS', [ qr{FROM} ] ],
+	[ 'CREATE SUBSCRIPTION', [ qr{CONNECTION} ] ],
+	[ 'CREATE TABLE', [ qr{PARTITION BY} ] ],
+	[ 'CREATE TABLE AS', [ qr{ON COMMIT} ] ],
+	[ 'CREATE TABLESPACE', [ qr{LOCATION} ] ],
+	[ 'CREATE TEXT SEARCH CONFIGURATION', [ qr{PARSER} ] ],
+	[ 'CREATE TEXT SEARCH DICTIONARY', [ qr{TEMPLATE} ] ],
+	[ 'CREATE TEXT SEARCH PARSER', [ qr{GETTOKEN} ] ],
+	[ 'CREATE TEXT SEARCH TEMPLATE', [ qr{LEXIZE} ] ],
+	[ 'CREATE TRANSFORM', [ qr{FROM SQL WITH FUNCTION} ] ],
+	[ 'CREATE TRIGGER', [ qr{EXECUTE} ] ],
+	[ 'CREATE TYPE', [ qr{SUBTYPE} ] ],
+	[ 'CREATE USER', [ qr{PASSWORD NULL} ] ],
+	[ 'CREATE USER MAPPING', [ qr{SERVER} ] ],
+	[ 'CREATE VIEW', [ qr{CASCADED} ] ],
+	[ 'DEALLOCATE', [ qr{PREPARE} ] ],
+	[ 'DECLARE', [ qr{INSENSITIVE} ] ],
+	[ 'DELETE', [ qr{WHERE CURRENT OF} ] ],
+	[ 'DISCARD', [ qr{PLANS} ] ],
+	[ 'DO', [ qr{LANGUAGE} ] ],
+	[ 'DROP ACCESS METHOD', [ qr{CASCADE} ] ],
+	[ 'DROP AGGREGATE', [ qr{ORDER BY} ] ],
+	[ 'DROP CAST', [ qr{RESTRICT} ] ],
+	[ 'DROP COLLATION', [ qr{RESTRICT} ] ],
+	[ 'DROP CONVERSION', [ qr{RESTRICT} ] ],
+	[ 'DROP DATABASE', [ qr{IF EXISTS} ] ],
+	[ 'DROP DOMAIN', [ qr{RESTRICT} ] ],
+	[ 'DROP EVENT TRIGGER', [ qr{RESTRICT} ] ],
+	[ 'DROP EXTENSION', [ qr{RESTRICT} ] ],
+	[ 'DROP FOREIGN DATA WRAPPER', [ qr{RESTRICT} ] ],
+	[ 'DROP FOREIGN TABLE', [ qr{RESTRICT} ] ],
+	[ 'DROP FUNCTION', [ qr{RESTRICT} ] ],
+	[ 'DROP GROUP', [ qr{IF EXISTS} ] ],
+	[ 'DROP INDEX', [ qr{RESTRICT} ] ],
+	[ 'DROP LANGUAGE', [ qr{RESTRICT} ] ],
+	[ 'DROP MATERIALIZED VIEW', [ qr{RESTRICT} ] ],
+	[ 'DROP OPERATOR', [ qr{RESTRICT} ] ],
+	[ 'DROP OPERATOR CLASS', [ qr{RESTRICT} ] ],
+	[ 'DROP OPERATOR FAMILY', [ qr{RESTRICT} ] ],
+	[ 'DROP OWNED', [ qr{CURRENT_USER} ] ],
+	[ 'DROP POLICY', [ qr{RESTRICT} ] ],
+	[ 'DROP PROCEDURE', [ qr{RESTRICT} ] ],
+	[ 'DROP PUBLICATION', [ qr{RESTRICT} ] ],
+	[ 'DROP ROLE', [ qr{IF EXISTS} ] ],
+	[ 'DROP ROUTINE', [ qr{RESTRICT} ] ],
+	[ 'DROP RULE', [ qr{RESTRICT} ] ],
+	[ 'DROP SCHEMA', [ qr{RESTRICT} ] ],
+	[ 'DROP SEQUENCE', [ qr{RESTRICT} ] ],
+	[ 'DROP SERVER', [ qr{RESTRICT} ] ],
+	[ 'DROP STATISTICS', [ qr{IF EXISTS} ] ],
+	[ 'DROP SUBSCRIPTION', [ qr{RESTRICT} ] ],
+	[ 'DROP TABLE', [ qr{RESTRICT} ] ],
+	[ 'DROP TABLESPACE', [ qr{IF EXISTS} ] ],
+	[ 'DROP TEXT SEARCH CONFIGURATION', [ qr{RESTRICT} ] ],
+	[ 'DROP TEXT SEARCH DICTIONARY', [ qr{RESTRICT} ] ],
+	[ 'DROP TEXT SEARCH PARSER', [ qr{RESTRICT} ] ],
+	[ 'DROP TEXT SEARCH TEMPLATE', [ qr{RESTRICT} ] ],
+	[ 'DROP TRANSFORM', [ qr{LANGUAGE} ] ],
+	[ 'DROP TRIGGER', [ qr{RESTRICT} ] ],
+	[ 'DROP TYPE', [ qr{RESTRICT} ] ],
+	[ 'DROP USER', [ qr{IF EXISTS} ] ],
+	[ 'DROP USER MAPPING', [ qr{SERVER} ] ],
+	[ 'DROP VIEW', [ qr{RESTRICT} ] ],
+	[ 'END', [ qr{TRANSACTION}, qr{CHAIN}, qr{(?!COMMIT)} ] ],
+	[ 'EXECUTE', [ ] ],
+	[ 'EXPLAIN', [ qr{VERBOSE} ] ],
+	[ 'FETCH', [ qr{RELATIVE} ] ],
+	[ 'GRANT', [ qr{ALL TABLES} ] ],
+	[ 'IMPORT FOREIGN SCHEMA', [ qr{FROM SERVER} ] ],
+	[ 'INSERT', [ qr{ON CONFLICT} ] ],
+	[ 'LISTEN', [ ] ],
+	[ 'LOAD', [ ] ],
+	[ 'LOCK', [ qr{NOWAIT} ] ],
+	[ 'MOVE', [ qr{FORWARD} ] ],
+	[ 'NOTIFY', [ ] ],
+	[ 'PREPARE', [ qr{AS} ] ],
+	[ 'PREPARE TRANSACTION', [ ] ],
+	[ 'REASSIGN OWNED', [ qr{SESSION_USER} ] ],
+	[ 'REFRESH MATERIALIZED VIEW', [ qr{CONCURRENTLY} ] ],
+	[ 'REINDEX', [ qr{DATABASE} ] ],
+	[ 'RELEASE', [ qr{SAVEPOINT} ] ],
+	[ 'RESET', [ qr{RESET ALL} ] ],
+	[ 'REVOKE', [ qr{PRIVILEGES} ] ],
+	[ 'ROLLBACK', [ qr{CHAIN} ] ],
+	[ 'ROLLBACK PREPARED', [ ]],
+	[ 'SAVEPOINT', [ ] ],
+	[ 'SECURITY LABEL', [ qr{SUBSCRIPTION} ] ],
+	[ 'SELECT', [ qr{RECURSIVE} ] ],
+	[ 'SET', [ qr{SESSION} ] ],
+	[ 'SET CONSTRAINTS', [ qr{IMMEDIATE} ] ],
+	[ 'SET ROLE', [ qr{ROLE NONE} ] ],
+	[ 'SET SESSION AUTHORIZATION', [ qr{DEFAULT} ] ],
+	[ 'SET TRANSACTION', [ qr{SNAPSHOT} ] ],
+	[ 'SHOW', [ qr{SHOW ALL} ] ],
+	[ 'START TRANSACTION', [ qr{ISOLATION LEVEL}, qr{(?!BEGIN)} ] ],
+	[ 'TABLE', [ qr{ONLY} ] ], # hmmm...
+	[ 'TRUNCATE', [ qr{CONTINUE IDENTITY} ] ],
+	[ 'UNLISTEN', [ ] ],
+	[ 'UPDATE', [ qr{RETURNING} ] ],
+	[ 'VACUUM', [ qr{FREEZE} ] ],
+	[ 'VALUES', [ qr{ORDER BY} ] ],
+	[ 'WITH', [ qr{RECURSIVE} ] ], # SELECT duplicate?
+);
+
+for my $h (@backslash_help)
+{
+	my ($cmd, $out) = @$h;
+	push @$out, qr{$cmd};
+	psql('', 0, "\\h $cmd\n", $out, [ qr{^$} ], "psql -- \\h $cmd");
+}
+
+# special cases
+psql('', 0, "\\h ROLLBACK TO SAVEPOINT\n", [ qr{ROLLBACK}, qr{SAVEPOINT} ],
+	 [ qr{^$} ], "psql -- \\h ROLLBACK TO SAVEPOINT");
+psql('', 0, "\\h SELECT INTO\n", [ qr{SELECT}, qr{INTO} ],
+	 [ qr{^$} ], "psql -- \\h SELECT INTO");
+
+#
+# check describe and other backslash commands
+#
+# the output can vary significantly, especially with +
+my @backslash_out = (
+	# empty
+	[ "\\d\n", [ qr{List of relations}, qr{regress_psql_tap_1_t1}, qr{(?!pg_locks)} ] ],
+	[ "\\dS\n", [ qr{List of relations}, qr{pg_locks} ] ],
+	# aggregates
+	[ "\\da\n", [ qr{List of aggregate functions}, qr{(?!Description)} ] ],
+	[ "\\da+\n", [ qr{List of aggregate functions}, qr{(Description)} ] ],
+	[ "\\daS\n", [ qr{array_agg}, qr{avg}, qr{bit_and}, qr{bit_or}, qr{count}, qr{max} ] ],
+	# access methods
+	[ "\\dA\n", [ qr{List of access methods}, qr{btree}, qr{hash}, qr{(?!Description)} ] ],
+	[ "\\dA+\n", [ qr{List of access methods}, qr{btree}, qr{hash}, qr{Description} ] ],
+	# tablespaces
+	[ "\\db\n", [ qr{List of tablespaces}, qr{pg_default}, qr{pg_global}, qr{(?!Size)} ] ],
+	[ "\\db+ pg_def*\n", [ qr{List of tablespaces}, qr{pg_default}, qr{(?!pg_global)}, qr{Size} ] ],
+	# functions
+	[ "\\df\n", [ qr{List of functions} ] ],
+	[ "\\df+\n", [ qr{List of functions} ] ],
+	[ "\\dftS\n", [ qr{RI_}, qr{Type}, qr{(?!Volatility)}, qr{(?!Parallel)} ] ],
+	[ "\\dftS+\n", [ qr{Volatility}, qr{Parallel} ] ],
+	[ "\\dfwS\n", [ qr{lag}, qr{dense_rank}, qr{(?!Owner)}, qr{(?!Language)} ] ],
+	[ "\\dfwS+\n", [ qr{cume_dist}, qr{Owner}, qr{Language} ] ],
+	[ "\\dfnS\n", [ qr{abbrev}, qr{(?!bit_or)} ] ],
+	[ "\\dfaS\n", [ qr{bit_and}, qr{(?!abs)} ] ],
+	[ "\\dfpS\n", [ qr{List of functions} ] ],
+	[ "\\dfwtS\n", [ qr{RI_FKey_}, qr{last_value}, qr{nth_value}, qr{(?!bit_and)} ] ],
+	# type
+	[ "\\dTS charac*\n", [ qr{List of data types}, qr{character varying}, qr{(?!double)}, qr{(?!Size)} ] ],
+	[ "\\dTS+\n", [ qr{aclitem}, qr{Size} ] ],
+	# operator
+	[ "\\doS\n", [ qr{List of operators}, qr{!~\*} ] ],
+	[ "\\doS+\n", [ qr{<->}, qr{Result type}] ],
+	# databases
+	[ "\\l\n", [ qr{List of databases}, qr{template0}, qr{template1}, qr{(?!Size)} ] ],
+	[ "\\l a*\n", [ qr{(?!template[01])} ] ],
+	[ "\\l+\n", [ qr{Size}, qr{Encoding} ] ],
+	[ "\\list\n", [ qr{List of databases} ] ],
+	# permissions: well tested
+	# default acls (empty)
+	[ "\\ddp\n", [ qr{Default access privileges} ] ],
+	# descriptions (empty)
+	[ "\\dd\n", [ qr{Object descriptions} ] ],
+	# tables: well tested
+	# roles/users/groups
+	[ "\\du\n", [ qr{Superuser}, qr{regress_psql_tap_1}, qr{List of roles}, qr{(?!Description)} ] ],
+	[ "\\du+\n", [ qr{Superuser}, qr{List of roles}, qr{Description} ] ],
+	[ "\\dg\n", [ qr{Superuser}, qr{List of roles} ] ],
+	# role settings
+	[ "\\drds\n", [ qr{List of settings} ] ],
+	[ "\\drds regress_* *\n", [ qr{List of settings} ] ],
+	# index/...
+	[ "\\diS\n", [ qr{List of relations}, qr{pg_am_oid_index} ] ],
+	[ "\\diS+\n", [ qr{Table} ] ],
+	# partition tables: well tested
+	# large objects
+	[ "\\dl", [ qr{Large objects} ] ],
+	# languages
+	[ "\\dL\n", [ qr{List of languages}, qr{plpgsql}, qr{(?!Trusted)} ] ],
+	[ "\\dL+\n", [ qr{plpgsql}, qr{Trusted} ] ],
+	# domains
+	[ "\\dD\n", [ qr{List of domains}, qr{(?!Access privileges)} ] ],
+	[ "\\dD+\n", [ qr{List of domains}, qr{Access privileges} ] ],
+	# conversions
+	[ "\\dcS\n", [ qr{List of conversions}, qr{UTF8}, qr{(?!Description)} ] ],
+	[ "\\dcS+\n", [ qr{List of conversions}, qr{LATIN1}, qr{Description} ] ],
+	# event triggers (empty)
+	[ "\\dy\n", [ qr{List of event triggers}, qr{(?!Description)} ] ],
+	[ "\\dy+\n", [ qr{List of event triggers}, qr{Description} ] ],
+	# casts
+	[ "\\dC\n", [ qr{List of casts}, qr{timestamp without time zone}, qr{(?!Description)} ] ],
+	[ "\\dC+\n", [ qr{Function}, qr{Implicit}, qr{Description} ] ],
+	# collations
+	[ "\\dOS\n", [ qr{List of collations}, qr{Deterministic}, qr{(?!Description)} ] ],
+	[ "\\dOS+\n", [ qr{POSIX}, qr{Description} ] ],
+	# schemas
+	[ "\\dn\n", [ qr{List of schemas}, qr{Owner}, qr{(?!Description)} ] ],
+	[ "\\dn+\n", [ qr{Access privileges}, qr{Description} ] ],
+	# text search misc.
+	[ "\\dFp\n", [ qr{List of text search parsers}, qr{default} ] ],
+	[ "\\dFp+\n", [ qr{hword}, qr{numword} ] ],
+	[ "\\dFd\n", [ qr{List of text search dictionaries}, qr{simple} ] ],
+	[ "\\dFd+\n", [ qr{Init options}, qr{Description} ] ],
+	[ "\\dFt\n", [ qr{List of text search templates}, qr{simple} ] ],
+	[ "\\dFt+\n", [ qr{Lexize}, qr{Description} ] ],
+	[ "\\dF\n", [ qr{List of text search configurations}, qr{simple} ] ],
+	[ "\\dF+\n", [ qr{email}, qr{url_path} ] ],
+	# extensions
+	[ "\\dx\n", [ qr{List of installed extensions}, qr{Version}, qr{plpgsql} ] ],
+	[ "\\dx+\n", [ qr{Objects in extension}, qr{plpgsql_call_handler} ] ],
+	# other backslash commands
+	[ "\\f\n", [ qr{Field separator is} ] ],
+	[ "\\H\\H\n", [ qr{Output format is html.* is aligned}s ] ],
+	[ "\\\?\n", [ qr{General}, qr{Help}, qr{Variables}, qr{Large Objects} ] ],
+	[ "\\C foobar\n", [ qr{Title is "foobar"} ] ],
+	[ "\\cd /\n\\cd\n", [ qr{^$} ] ],
+	[ "\\conninfo\n", [ qr{You are connected to database} ] ],
+	[ "\\copy (SELECT 5432 UNION SELECT 2345 ORDER BY 1) TO STDOUT\n", [ qr/\b2345\b.*\b5432\b/s ] ],
+	[ "\\copyright\n", [ qr/The Regents of the University of California/ ] ],
+	[ "\\encoding\n", [ qr/./ ] ],
+	[ "\\encoding :ENCODING\n", $EMPTY ],
+	[ "\\errverbose\n", [ qr{There is no previous error} ] ],
+	[ "\\lo_list\n", [ qr{Large objects} ] ],
+	[ "\\if true\\q\\endif\n", $EMPTY ],
+	# ???
+	#[ "SELECT md5('hello world');\n\\s\n", [ qr{5eb63bbbe0}, qr{SELECT md5} ] ],
+	[ "\\set\n", [ qr{ENCODING = }, qr{VERSION_NUM = } ] ],
+	[ "\\set COMP_KEYWORD_CASE preserve-lower\n\\set COMP_KEYWORD_CASE lower\n" .
+	  "\\set COMP_KEYWORD_CASE upper\n\\echo :COMP_KEYWORD_CASE", [ qr/upper/ ] ],
+	[ "\\set HISTCONTROL ignorespace\n\\set HISTCONTROL ignoredups\n" .
+	  "\\set HISTCONTROL ignoreboth\n\\echo :HISTCONTROL", [ qr/ignoreboth/ ] ],
+	[ "\\set ECHO_HIDDEN on\n\\l\n", [ qr/\* QUERY \*/, qr/pg_catalog\.pg_database/, qr/template0/ ] ],
+	[ "\\set ECHO_HIDDEN noexec\n\\l\n", [ qr/\* QUERY \*/, qr/pg_catalog\.pg_database/, qr/(?!template0)/ ] ],
+	[ "\\set LAST_ERROR_MESSAGE foo bla\n\\echo :LAST_ERROR_MESSAGE\n", [ qr{foobla} ] ],
+	[ "\\set AUTOCOMMIT off\n\\set ON_ERROR_ROLLBACK on\nSELECT 5432 AS pg;\nSELECT 2345 AS pg;\n",
+		[ qr{5432}, qr{2345} ] ],
+	[ "\\set ON_ERROR_ROLLBACK interactive\n\\echo :ON_ERROR_ROLLBACK\n", [ qr{interactive} ] ],
+	[ "\\set VERBOSITY verbose\n\\echo :VERBOSITY\n", [ qr{verbose} ] ],
+	[ "\\set foo bla\n\\echo :foo\\unset foo\\echo :foo\n", [ qr{^bla.*:foo}s ] ],
+	[ "\\set foo bla\n\\echo :'foo' :\"foo\"\n", [ qr{'bla'}, qr{"bla"} ] ],
+	[ "\\setenv FOO bla\n\\setenv FOO\n", [ qr{^$} ] ],
+	[ "\\timing\nSELECT 1;\n\\timing\n", [ qr{Timing is on.*Timing is off}s, qr(Time: \d+\.\d{3} ms) ] ],
+	[ "\\timing ON\n\\timing OFF\n", [ qr{Timing is on.*Timing is off}s ] ],
+	[ "\\T foo\n", [ qr{Table attributes are "foo"} ] ],
+	[ "\\T\n", [ qr{Table attributes unset} ] ],
+	# help variants
+	[ "\\?\n",
+		[ qr{General}, qr{Help}, qr{Query}, qr{Input}, qr{Conditional},
+		  qr{Informational}, qr{Formatting}, qr{Connection},
+		  qr{Operating System}, qr{Variables}, qr{Large Objects} ] ],
+	[ "\\? commands\n",
+		[ qr{General}, qr{Help}, qr{Query}, qr{Input}, qr{Conditional},
+		  qr{Informational}, qr{Formatting}, qr{Connection},
+		  qr{Operating System}, qr{Variables}, qr{Large Objects} ] ],
+	[ "\\? bad\n", [ qr{General} ] ],
+	[ "\\? variables\n",
+		  [ qr{psql variables:}, qr{Display settings:}, qr{Environment variables} ] ],
+	[ "\\? options\n",
+		  [ qr{interactive terminal}, qr{Report bugs} ] ],
+	# pset format
+	[ "\\pset format aligned\nSELECT 1 AS one, 2 AS two;\n",
+		[ qr{Output format is aligned}, qr{   1 \|   2} ] ],
+	[ "\\pset format asciidoc\nSELECT 1 AS one, 2 AS two;\n",
+		[ qr{Output format is asciidoc}, qr{\|1 \|2} ] ],
+	[ "\\pset format csv\nSELECT 1 AS one, 2 AS two;\n",
+		[ qr{Output format is csv}, qr{1,2} ] ],
+	[ "\\pset format html\nSELECT 1 AS one, 2 AS two;\n",
+		[ qr{Output format is html}, qr{<td align="right">1</td>} ] ],
+	[ "\\pset format latex\nSELECT 1 AS one, 2 AS two;\n",
+		[ qr{Output format is latex}, qr{1 \& 2} ] ],
+	[ "\\pset format latex-longtable\n\\pset format\nSELECT 1 AS one, 2 AS two;\n",
+		[ qr{Output format is latex-longtable}, qr{\\raggedright\{1\}} ] ],
+	[ "\\pset format troff-ms\nSELECT 1 AS one, 2 AS two;\n",
+		[ qr{Output format is troff-ms}, qr{1\t2} ] ],
+	[ "\\pset format unaligned\nSELECT 1 AS one, 2 AS two;\n",
+		[ qr{Output format is unaligned}, qr{1\|2} ] ],
+	[ "\\pset format wrapped\nSELECT 1 AS one, 2 AS two;\n",
+		[ qr{Output format is wrapped}, qr{   1 \|   2} ] ],
+	[ "\\pset format\n", [ qr{Output format is aligned} ] ],
+	# pset linestyle
+	[ "\\pset linestyle ascii\n", [ qr{Line style is ascii} ] ],
+	[ "\\pset linestyle old-ascii\n", [ qr{Line style is old-ascii} ] ],
+	[ "\\pset linestyle unicode\n", [ qr{Line style is unicode} ] ],
+	# pset unicode_*_linestyle
+	[ "\\pset unicode_header_linestyle single\n", [ qr{"single"} ] ],
+	[ "\\pset unicode_header_linestyle double\n", [ qr{"double"} ] ],
+	[ "\\pset unicode_border_linestyle single\n", [ qr{"single"} ] ],
+	[ "\\pset unicode_border_linestyle double\n", [ qr{"double"} ] ],
+	[ "\\pset unicode_column_linestyle single\n", [ qr{"single"} ] ],
+	[ "\\pset unicode_column_linestyle double\n", [ qr{"double"} ] ],
+);
+
+for my $h (@backslash_out)
+{
+	my ($in, $out) = @$h;
+	psql('', 0, $in, $out, [ qr{^$} ], "psql -- $in");
+}
+
+# test some errors
+my @backslash_err = (
+	[ "\\unknown stuff\n", [ qr/invalid command / ] ],
+	[ "\\cd /no/such/dir\n", [ qr/could not change directory to/ ] ],
+	[ "\\copy\n", [ qr/arguments required/ ] ],
+	[ "\\copy bad stuff on the line\n", [ qr/parse error at \"/ ] ],
+	[ "\\copy binary (\n", [ qr/parse error at end of line/ ] ],
+	[ "\\copy public.regress_psql_tap_1_t1(data) to '/no/such/file'", [ qr{/no/such/file} ] ],
+	[ "\\encoding no-such-encoding\n", [ qr/invalid encoding name/ ] ],
+	[ "BAD;\n\\errverbose\n", [ qr/syntax error at or near "BAD"/, qr/\b42601\b/ ] ],
+	[ "\\dfX\n", [ qr/invalid command/ ] ],
+	[ "\\dPX\n", [ qr/invalid command/ ] ],
+	[ "\\drX\n", [ qr/invalid command/ ] ],
+	[ "\\dRX\n", [ qr/invalid command/ ] ],
+	[ "\\dFX\n", [ qr/invalid command/ ] ],
+	[ "\\deX\n", [ qr/invalid command/ ] ],
+	[ "\\dX\n", [ qr/invalid command/ ] ],
+	[ "\\i\n", [ qr/missing required argument/ ] ],
+	[ "\\ir\n", [ qr/missing required argument/] ],
+	[ "\\lo_bad\n", [ qr/invalid command/ ] ],
+	[ "\\lo_export\n", [ qr/missing required argument/ ] ],
+	[ "\\lo_import\n", [ qr/missing required argument/ ] ],
+	[ "\\lo_import /no/such/file\n", [ qr{no/such/file} ] ],
+	[ "\\lo_unlink\n", [ qr/missing required argument/ ] ],
+	[ "\\lo_unlink -1\n", [ qr/large object 4294967295 does not exist/ ] ],
+	[ "\\setenv\n", [ qr/missing required argument/ ] ],
+	[ "\\setenv PG_REGRESS_TEST=1\n", [ qr/environment variable name must not contain "="/ ] ],
+	[ "\\unset\n", [ qr/missing required argument/ ] ],
+	[ "\\set foo \`no-such-command\`\n", [ qr/no-such-command/ ] ],
+	[ "\\set === xxx\n", [ qr/invalid variable name/ ] ],
+	[ "\\w\n", [ qr/missing required argument/ ] ],
+	[ "\\pset format bad\n", [ qr/allowed formats are aligned, asciidoc/ ] ],
+	[ "\\pset linestyle bad\n", [ qr/allowed line styles are ascii, old-ascii, unicode/ ] ],
+	[ "\\pset unicode_border_linestyle bad\n", [ qr/allowed Unicode border line styles are single, double/ ] ],
+	[ "\\pset unicode_column_linestyle bad\n", [ qr/allowed Unicode column line styles are single, double/ ] ],
+	[ "\\pset unicode_header_linestyle bad\n", [ qr/allowed Unicode header line styles are single, double/ ] ],
+	[ "COPY regress_psql_tap_1_t1 FROM STDIN \\watch 0.01\n", [ qr/watch cannot be used with COPY/ ] ],
+	[ "\\watch 0.01\n", [ qr/watch cannot be used with an empty query/ ] ],
+	# check variable constraints values
+	[ "\\set ECHO bad\n", [ qr{none, errors, queries, all} ] ],
+	[ "\\set ECHO_HIDDEN bad\n", [ qr{on, off, noexec} ] ],
+	[ "\\set ON_ERROR_ROLLBACK bad\n", [ qr{on, off, interactive} ] ],
+	[ "\\set COMP_KEYWORD_CASE bad\n", [ qr{lower, upper, preserve-lower, preserve-upper} ] ],
+	[ "\\set HISTCONTROL bad\n", [ qr{none, ignorespace, ignoredups, ignoreboth} ] ],
+	[ "\\set VERBOSITY bad\n", [ qr{default, verbose, terse, sqlstate} ] ],
+	[ "\\set SHOW_CONTEXT bad\n", [ qr{never, errors, always} ] ],
+);
+
+for my $h (@backslash_err)
+{
+	my ($in, $err) = @$h;
+	psql('-L /dev/null', 0, $in, [ qr{^$} ], $err, "psql -- $in");
+}
+
+# other errors
+
+# \timing
+psql('', 0, "\\timing bad\n",
+	 [ qr{Timing is off} ], [ qr{unrecognized value} ], 'psql \timing error');
+
+# check stdout vs stderr output
+psql('', 0, "\\echo hello\n\\warn world\n\\q\n",
+	 [ qr{^hello$} ], [ qr{^world$} ], 'psql in/out/err');
+
+# \watch test
+psql('', 0,
+	 "CREATE TEMP SEQUENCE tmp_seq MAXVALUE 3 NO CYCLE;\n" .
+	 "\\t on\n" .
+	 "\\timing\n" .
+	 "SELECT 'x=' || NEXTVAL('tmp_seq') \\watch 0.01\n",
+	 [ qr/x=1\b.*x=2\b.*x=3\b/s, qr/(?!x=[04-9])/ ],
+	 [ qr/nextval: reached maximum value of sequence/ ],
+	 'psql watch');
+
+# skip un*x-specific zone if it does not look like un*x
+goto END_UNIX_ZONE
+	unless -e "/dev/null" and -d "/tmp" and -x "/bin/cat" and -x "/bin/echo";
+
+# this probably only works under UN*X-like systems
+psql('', 0, "\\setenv PSQL_EDITOR echo\nSELECT 1 AS one;\n\\e\n\\p\n",
+	 [ qr/one.*one/s, qr/SELECT 1/ ], $EMPTY, 'psql edit');
+psql('', 0,
+	 "\\setenv PSQL_EDITOR echo\n\\ef regress_psql_tap_1_f1\n",
+	 [ qr/No changes/ ], $EMPTY, 'psql \ef');
+psql('', 0,
+	 "\\setenv PSQL_EDITOR echo\n\\ev regress_psql_tap_1_v1\n",
+	 [ qr/No changes/ ], $EMPTY, 'psql \ev');
+psql('', 0, "\\setenv FOO bla\\set foo \`echo \$FOO\`\n\\echo :foo\n",
+	 [ qr/bla/ ], $EMPTY, 'psql backtick');
+psql('', 0, "\\setenv FOO blabla\n\\! echo \$FOO\n",
+	 [ qr/blabla/ ], $EMPTY, 'psql !');
+
+psql('-o /dev/null', 0, "SELECT 5432 AS pg\n", $EMPTY, $EMPTY, 'psql -o null');
+psql('', 0, "\\o /dev/null\nSELECT 1;\n", $EMPTY, $EMPTY, 'psql \o null');
+psql('', 0, "\\o | cat\nSELECT 5432 AS pg;\n\\o\n",
+	 [ qr/\b5432\b/ ], $EMPTY, 'psql \o cat');
+
+psql('', 0, "\\s /dev/null\n", $EMPTY, $EMPTY, 'psql \s null');
+
+psql('', 0, "SELECT 5432 AS pg;\n\\w /dev/null\n",
+	 [ qr/pg.*\b5432\b/s ], $EMPTY, 'psql \w null');
+
+psql('', 0, "SELECT 1\\g /dev/null\n",
+	 $EMPTY, $EMPTY, 'psql \g null');
+psql('', 0, "SELECT 5432\\g | cat\n",
+	 [ qr/\b5432\n/ ], $EMPTY, 'psql \g cat');
+
+psql('--log-file=/dev/null', 0, "SELECT 5432 AS pg\n",
+	 [ qr/\b5432\b/ ], $EMPTY, 'psql -L null');
+
+psql('', 0, "\\copy public.regress_psql_tap_1_t1(data) FROM PROGRAM 'echo moe'\n",
+	[ qr/COPY 1\b/ ], $EMPTY, 'psql copy echo');
+psql('', 0, "\\copy public.regress_psql_tap_1_t1(data) TO PROGRAM 'cat'\n",
+	[ qr/COPY 1\b/ ], $EMPTY, 'psql copy cat'); # :-)
+
+END_UNIX_ZONE:
+
+psql('', 0, "\\i /no/such/file\n",
+	 [ qr{^$} ], [ qr{/no/such/file} ], 'psql \i error');
+psql('', 0, "\\i ~/there/is/no/such/file\n",
+	 [ qr{^$} ], [ qr{there/is/no/such/file} ], 'psql \i tilde');
+
+# no such file
+psql('', 0, "\\o /no/such/file\n",
+	 [ qr{^$} ], [ qr{/no/such/file} ], 'psql \o error');
+
+psql('', 0, "\\w /no/such/file\n",
+	 [ qr{^$} ], [ qr{/no/such/file} ], 'psql \w error');
+
+psql('-L /no/such/file', 1, '',
+	 [ qr{^$} ], [ qr{could not open log file} ], 'psql -L bad');
+
+# \copy errors
+psql('', 0, "\\copy regress_psql_tap_1_t1 to program '/no/such/prgm'",
+	 [ qr/COPY 1/ ], [ qr{/no/such/prgm} ], 'psql copy to bad prgm');
+psql('', 0, "\\copy regress_psql_tap_1_t1 from program '/no/such/prgm'",
+	 [ qr/COPY 0/ ], [ qr{/no/such/prgm} ], 'psql copy from bad prgm');
+psql('', 0, "\\copy regress_psql_tap_1_t1 to '/tmp'",
+	 $EMPTY, [ qr{directory} ], 'psql copy to tmp');
+psql('', 0, "\\copy regress_psql_tap_1_t1 from '/tmp'",
+	 $EMPTY, [ qr/directory/ ], 'psql copy to tmp');
+
+# misc errors
+psql('-P format=bad', 1, '', $EMPTY,
+	 [ qr/allowed formats are aligned/, qr/could not set printing parameter/ ], 'psql -P bad');
+psql('-Q', 1, '', $EMPTY, [ qr/Try ".* --help" for more information/ ], 'psql -BAD');
+
+#
+# .psqlrc
+#
+# note: reading the system wide psqlrc cannot be avoided
+#
+#import env;
+my $psqlrc = create_test_file('regress_psql_tap_1_psqlrc', "SELECT 1 AS one\n");
+$ENV{PSQLRC} = $psqlrc;
+psql('!', 0, "SELECT 2 AS two\n", [ qr{one.*1.*two.*2}s ], [ qr{^$} ], 'psql .psqlrc');
+delete $ENV{PSQLRC};
+ok(unlink $psqlrc, "unlink $psqlrc");
+
+#
+# large objects
+#
+my $lofile = create_test_file('regress_psql_tap_1_lo', '0123456789ABCDEF' x 1024);
+my $esc_lofile = $lofile;
+$esc_lofile =~ s/'/''/g;
+psql('-H', 0,
+	 "\\lo_import '$esc_lofile' 'regress psql TAP lo'\n\\lo_list\n",
+	 [ qr{lo_import \d+}, qr{Large objects}, qr{regress psql TAP lo} ], [ qr{^$} ], 'psql lo 1');
+ok(unlink $lofile, "unlink $lofile (1)");
+psql('', 0,
+	 "SELECT oid AS loboid\n" .
+	 "FROM pg_catalog.pg_largeobject_metadata\n" .
+	 "WHERE pg_catalog.obj_description(oid, 'pg_largeobject') = 'regress psql TAP lo'\\gset\n" .
+	 "\\echo loboid = :loboid\n" .
+	 "\\pset tuples_only on\n" .
+	 "SELECT 'lo extract: ' || lo_get(:loboid, 826, 3)::TEXT AS extract;\n" .
+	 "\\lo_export :loboid '/no/such/dir/stuff'\n" . # this one fails
+	 "\\lo_export :loboid '$esc_lofile'\n" .
+	 "\\lo_unlink :loboid\n",
+	 [ qr{loboid = \d+}, qr{lo extract: \\x414243} ],
+	 [ qr{could not open file "/no/such/dir/stuff"} ],
+	 'psql lo 2');
+ok(-e $lofile, "re-created $lofile");
+ok(unlink $lofile, "unlink $lofile (2)");
+# TODO
+#ok(not -e $lofile, "file $lofile is removed");
+
+#
+# INTERACTIVE STUFF
+#
+# pretty slow... espacially the tab-completion thing
+#
+# disable pager to work around pagination interactions
+
+# basic test
+ipsql('-P pager', 0, 5,
+	  [
+		[ "\\echo pg :VERSION_NUM\n", qr/pg \d+.*postgres=\# /s ],
+		[ "\\echo hello     world\n", qr/hello world.*postgres=\# /s ],
+		[ "help\n", qr/You are using.*\\copyright.*quit.*postgres=\# /s ],
+		[ "SELECT\nhelp\n\\r\n", qr/help or press control-[CD].*postgres=\# /s ],
+		[ "SELECT\nquit  ;  \n\\r\n", qr/Use \\q to quit.*buffer reset.*postgres=\# /s ],
+		[ "SELECT (\nquit  ;  \n\\r\n", qr/Use \\q to quit.*buffer reset.*postgres=\# /s ],
+		[ "SELECT \$\$\nquit\n\$\$\\r\n", qr/Use control-[CD] to quit.*buffer reset.*postgres=\# /s ],
+		[ "SELECT '\nquit'\\r\n", qr/quit'.*postgres=\# /s ],
+		[ "SELECT '\n\\q\n", qr/Use control-[CD] to quit.*postgres'\# /s ],
+		[ "'\\r\n", qr/buffer reset.*postgres=\# /s ],
+		[ "SELECT 1\n\\p\n\\r\n", qr/SELECT 1.*SELECT 1.*buffer reset.*postgres=\#/s ],
+		#[ "\\prompt tmp_var\nfoobla\n\\echo hello :tmp_var\n\\unset foo\n\\echo world :tmp_var\n",
+		#	qr/hello foobla.*world :tmp_var.*postgres=\# /s ],
+		[ "\\password\n", qr/Enter new password: / ],
+		[ "foo\n", qr/Enter it again: / ],
+		[ "bla\n", qr/Passwords didn't match.*postgres=\# /s ],
+		[ "exit\n" ],
+	  ],
+	  'ipsql basic', 1);
+
+# signals
+#TODO: reliably check for un*x vs windows
+my $stop = -e '/dev/null' ? "\004" : "\003";
+ipsql('-P pager', 0, 5,
+	  [
+		[ "\\echo pg :VERSION_NUM\n", qr/pg \d+.*postgres=\# /s ],
+		[ $stop, qr/\\q/ ],
+	  ],
+	  'ipsql signals 1', 1);
+
+# prompt
+ipsql('-P pager', 0, 5,
+	  [ # default prompts
+		[ "SELECT 1 +\n", qr/postgres-\# / ],
+		[ "(2 * \n", qr/postgres\(\# / ],
+		[ "3) + '\n", qr/postgres\'\# / ],
+		[ "4' + \$\$\n", qr/postgres\$\# /],
+		[ "5\$\$ AS \"\n", qr/postgres\"\# / ],
+		[ "seize\" /*\n", qr/postgres\*\# / ],
+		[ " ignored */;\n", qr/\bseize\b.*\b16\b.*postgres=\# /s ],
+		[ "\\if false\n", qr/postgres\@\# / ],
+		[ "\\echo IGNORED\n", qr/command ignored.*if block.*postgres\@\# /s ],
+		# BUG: prompt with - instead of @
+		[ "SELECT 1;\n", qr/query ignored.*if block.*postgres.\# /s ],
+		[ "\\endif\n", qr/postgres=\# / ],
+		[ "\\set SINGLELINE ON\n", qr/postgres\^\# / ],
+		[ "\\set SINGLELINE OFF\n", qr/postgres=\# / ],
+		[ "COPY regress_psql_tap_1_t1(data) FROM STDIN;\n", qr/>> / ],
+		[ "hello\nworld\n\\.\n", qr/COPY 2\b.*postgres=\# /s ],
+		# set new prompts (not tested: %`)
+		[ "\\set PROMPT1 '%%%[%]%?%M%m%>%n%/%~%#%p%R%x%l%:QUIET:%040'\n", qr/%.*\d+(on|off) /s ],
+		[ "\\set PROMPT1 '1> '\n", qr/\b1> / ],
+		[ "\\set PROMPT2 '2> '\n", qr/\b1> / ],
+		[ "\\set PROMPT3 '3> '\n", qr/\b1> / ],
+		[ "COPY regress_psql_tap_1_t1(data)\n", qr/\b2> / ],
+		[ "FROM STDIN;\n", qr/\b3> / ],
+		[ "calvin\nhobbes\nsusie\nmoe\n\\.\n", qr/COPY 4\b.*\b1> /s ],
+		# reset prompts
+		[ "\\set PROMPT1 '%/%R%# '\n", qr/PROMPT1.*postgres=# /s ],
+		[ "\\set PROMPT2 '%/%R%# '\n", qr/PROMPT2.*postgres=# /s ],
+		[ "\\set PROMPT3 '>> '\n", qr/PROMPT3.*postgres=# /s ],
+	  ],
+	  'ipsql prompt 1');
+
+# tab-complation
+ipsql('-P pager', 0, 5,
+	  [ # commands
+		[ "SEL\t", qr/SELECT / ],
+		[ "* FROM pg_catalog.pg_ca\t", qr/pg_catalog\.pg_cast / ],
+		[ "LIMIT 17;\n", qr/\b17 rows.*postgres=\# /s ],
+		# backslash commands
+		[ "\\d\t", qr/(\\da.*\\dy.*postgres=# )?\\d/s ],
+		[ "y\n", qr/List of event triggers.*postgres=\# /s ],
+		# pset
+		[ "\\pset \t", qr/(unicode_column_linestyle.*postgres=\# )?\\pset/s ],
+		[ "tu\t", qr/tuples_only / ],
+		[ "off\n", qr/postgres=\# / ],
+		[ "\\pset unicode_b\t", qr/unicode_border_linestyle / ],
+		[ "\t", qr/(double.*single.*postgres=\# )?/s ],
+		[ "double\n", qr/Unicode border line style is "double".*postgres=\# /s ],
+		# set
+		[ "\\set VERB\t", qr/VERBOSITY / ],
+		[ "\t", qr/(default.*verbose.*postgres=\# )?/s ],
+		[ "d\t\n", qr/default.*postgres=\# /s ],
+		# misc
+		[ "\\cd \t", qr/(.*postgres=\# )?\\cd /s ],
+		[ "\\r\n", qr/Query buffer reset.*postgres=\# /s ],
+		# object type completion,
+		# strange: the list is not seen from the test driver
+		# but the coverage works as expected.
+		#[ "CREATE \t", qr/i(MATERIALIZED VIEW.*postgres=\# )?/s ],
+		#[ "\\r\n", qr/Query buffer reset.*postgres=\# /s ],
+		[ "CREATE \t\\r\n", qr/Query buffer reset.*postgres=\# /s ],
+		#[ "DROP \t", qr/(UNLOGGED.*postgres=\# )?/s ],
+		#[ "\\r\n", qr/Query buffer reset.*postgres=\# /s ],
+		[ "DROP \t\\r\n", qr/Query buffer reset.*postgres=\# /s ],
+		#[ "ALTER \t", qr/(TABLESPACE.*postgres=\# )/s ],
+		#[ "\\r\n", qr/Query buffer reset.*postgres=\# /s ],
+		[ "ALTER \t\\r\n", qr/Query buffer reset.*postgres=\# /s ],
+		[ "ANALYZE (\t", qr/(SKIP_LOCKED.*postgres=\# )?ANALYZE \(/s ],
+		[ "V\tOF\t", qr/VERBOSE OFF/ ],
+		[ "\\r\n", qr/Query buffer reset.*postgres=\# /s ],
+		# SET
+		[ "SET \t", qr/(zero_damaged_pages.*postgres=\# )?SET /s ],
+		[ "da\t", qr/datestyle / ],
+		[ "\t", qr/TO / ],
+		#[ "\t", qr/(YMD.*postgres=\# SET datestyle TO )?/s ],
+		[ "\tDE\t", qr/DEFAULT/ ],
+		[ ";\n", qr/SET.*postgres=\# /s ],
+		# SET completion on a GUC variable,
+		#[ "SET default_with\t", qr/default_with_oids / ],
+		#[ "TO \t", qr/(YES.*postgres=\# SET default_with_oids )?TO /s ],
+		#[ "D\t", qr/DEFAULT/ ],
+		#[ ";\n", qr/SET.*postgres=\# / ],
+		# psql variables
+		[ "\\ec\t", qr/\\echo / ],
+		[ ":q\t", qr/QUIET/ ],
+		[ ":'PO\t", qr/:'PORT'/ ],
+		[ ":\"HISTC\t", qr/:"HISTCONTROL"/ ],
+		[ "\n", qr/(on|off).*'\d+'.*"[a-z]+".*postgres=\# /s ],
+		# file
+		[ "COPY regress_psql_tap_1_t1 FROM \t", qr/(postgres=\# COPY .* FROM )?/ ],
+		[ "\\r\n", qr/Query buffer reset.*postgres=\# /s ],
+	  ],
+	  'ipsql tab 1');
+
+
+# final cleanup
+psql('', 0, '-- cleanup objects
+DROP VIEW regress_psql_tap_1_v1;
+DROP FUNCTION regress_psql_tap_1_f1;
+DROP TABLE regress_psql_tap_1_t1;
+DROP USER regress_psql_tap_1;
+', [ qr{DROP VIEW}, qr{DROP FUNCTION}, qr{DROP TABLE}, qr{DROP ROLE} ], [ qr{^$} ], 'psql -- drop stuff');
+
+$node->stop();
+done_testing();
diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl
index b685b35282..2692c99e17 100644
--- a/src/bin/scripts/t/100_vacuumdb.pl
+++ b/src/bin/scripts/t/100_vacuumdb.pl
@@ -92,6 +92,7 @@ $node->issues_sql_like(
 $node->command_checks_all(
 	[ 'vacuumdb', '--analyze', '--table', 'vacview', 'postgres' ],
 	0,
+	'',
 	[qr/^.*vacuuming database "postgres"/],
 	[qr/^WARNING.*cannot vacuum non-tables or special system tables/s],
 	'vacuumdb with view');
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index 270bd6c856..5faafce12e 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -1676,6 +1676,25 @@ sub command_checks_all
 
 =pod
 
+=item $node->icommand_checks(cmd, ...)
+
+=cut
+
+sub icommand_checks
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my $self = shift;
+
+	local $ENV{PGHOST} = $self->host;
+	local $ENV{PGPORT} = $self->port;
+
+	TestLib::icommand_checks(@_);
+	return;
+}
+
+=pod
+
 =item $node->issues_sql_like(cmd, expected_sql, test_name)
 
 Run a command on the node, then verify that $expected_sql appears in the
diff --git a/src/test/perl/TestLib.pm b/src/test/perl/TestLib.pm
index 92199792eb..fa9dabbe7b 100644
--- a/src/test/perl/TestLib.pm
+++ b/src/test/perl/TestLib.pm
@@ -53,6 +53,7 @@ use File::stat qw(stat);
 use File::Temp ();
 use IPC::Run;
 use SimpleTee;
+use Expect;
 
 # specify a recent enough version of Test::More to support the
 # done_testing() function
@@ -81,6 +82,7 @@ our @EXPORT = qw(
   command_like_safe
   command_fails_like
   command_checks_all
+  icommand_checks
 
   $windows_os
 );
@@ -792,6 +794,8 @@ Arguments:
 
 =item C<ret>: Expected exit code
 
+=item C<in>: Stdin for command
+
 =item C<out>: Expected stdout from command
 
 =item C<err>: Expected stderr from command
@@ -806,12 +810,13 @@ sub command_checks_all
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 
-	my ($cmd, $expected_ret, $out, $err, $test_name) = @_;
+	my ($cmd, $expected_ret, $in, $out, $err, $test_name) = @_;
+	$in = '' if not defined $in;
 
 	# run command
 	my ($stdout, $stderr);
 	print("# Running: " . join(" ", @{$cmd}) . "\n");
-	IPC::Run::run($cmd, '>', \$stdout, '2>', \$stderr);
+	IPC::Run::run($cmd, '<', \$in, '>', \$stdout, '2>', \$stderr);
 
 	# See http://perldoc.perl.org/perlvar.html#%24CHILD_ERROR
 	my $ret = $?;
@@ -840,6 +845,82 @@ sub command_checks_all
 
 =pod
 
+=item icommand_checks(cmd, ret, timeout, init, inout, name)
+
+Run a command and check its status and outputs.
+Arguments:
+
+=over
+
+=item C<cmd> ref to list for command, options and arguments to run
+
+=item C<ret> expected exit status
+
+=item C<timeout> for interactions
+
+=item C<init> initial output before interacting
+
+=item C<inout> [ [ input, list of output checks ], ... ]
+
+=item C<name> of the test
+
+=back
+
+=cut
+
+sub icommand_checks
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($cmd, $expected_ret, $timeout, $init, $inout, $name) = @_;
+
+
+	my $ps = Expect->spawn(@$cmd);
+	ok(defined $ps, "$name command spawn: @$cmd");
+	return unless defined $ps;
+
+	#$ps->slave->stty(qw(raw -echo));
+	$ps->slave->stty(qw(raw));
+	for my $test (@$inout)
+	{
+		#warn "test: @$test";
+		my ($in, @out) = @$test;
+		#warn "in: $in";
+		#warn "out: @out";
+		$ps->send($in);
+		for my $o (@out)
+		{
+			my $ok = $ps->expect($timeout, '-re', $o);
+			# check header on first output
+			if (defined $init)
+			{
+				# beware of race conditions...
+				ok($ps->before =~ $init,
+				   "$name header matches /$init/");
+				undef $init;
+			}
+			ok($ok, "$name output matches /$o/");
+			if (not $ok)
+			{
+				print STDERR "before: ###" . $ps->before . "###\n" if $ps->before;
+				print STDERR "match: ###" . $ps->match . "###\n" if $ps->match;
+				print STDERR "after: ###" . $ps->after . "###\n" if $ps->after;
+				print STDERR "error: ###" . $ps->error . "###\n" if $ps->error;
+			}
+		}
+	}
+	$ps->soft_close();
+
+	# check status
+	my $ret = $ps->exitstatus;
+	ok($ret == $expected_ret,
+	   "$name status (got $ret vs expected $expected_ret)");
+
+	return;
+}
+
+=pod
+
 =back
 
 =cut
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index ef534a36a0..bbe4a69370 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4348,22 +4348,60 @@ bar 'bar' "bar"
 	:try_to_quit
 	\echo `nosuchcommand` :foo :'foo' :"foo"
 	\pset fieldsep | `nosuchcommand` :foo :'foo' :"foo"
-	\a \C arg1 \c arg1 arg2 arg3 arg4 \cd arg1 \conninfo
+	\a
+    \C arg1
+    \c arg1 arg2 arg3 arg4
+    \cd arg1
+    \conninfo
 	\copy arg1 arg2 arg3 arg4 arg5 arg6
-	\copyright \dt arg1 \e arg1 arg2
+    \copyright
+	SELECT 1 as one, 2, 3 \crosstabview
+    \dt arg1
+    \e arg1 arg2
 	\ef whole_line
 	\ev whole_line
-	\echo arg1 arg2 arg3 arg4 arg5 \echo arg1 \encoding arg1 \errverbose
-	\g arg1 \gx arg1 \gexec \h \html \i arg1 \ir arg1 \l arg1 \lo arg1 arg2
-	\o arg1 \p \password arg1 \prompt arg1 arg2 \pset arg1 arg2 \q
-	\reset \s arg1 \set arg1 arg2 arg3 arg4 arg5 arg6 arg7 \setenv arg1 arg2
+	\echo arg1 arg2 arg3 arg4 arg5
+    \echo arg1
+    \encoding arg1
+    \errverbose
+	\f arg1
+	\g arg1
+    \gx arg1
+    \gexec
+	SELECT 1 AS one \gset
+    \h
+	\?
+    \html
+    \i arg1
+    \ir arg1
+    \l arg1
+    \lo arg1 arg2
+invalid command \lo
+	\lo_list
+	\o arg1
+    \p
+    \password arg1
+    \prompt arg1 arg2
+    \pset arg1 arg2
+    \q
+	\reset
+    \s arg1
+    \set arg1 arg2 arg3 arg4 arg5 arg6 arg7
+    \setenv arg1 arg2
 	\sf whole_line
 	\sv whole_line
-	\t arg1 \T arg1 \timing arg1 \unset arg1 \w arg1 \watch arg1 \x arg1
+	\t arg1
+    \T arg1
+    \timing arg1
+    \unset arg1
+    \w arg1
+    \watch arg1
+    \x arg1
 	-- \else here is eaten as part of OT_FILEPIPE argument
 	\w |/no/such/file \else
 	-- \endif here is eaten as part of whole-line argument
 	\! whole_line \endif
+	\z
 \else
 	\echo 'should print #8-1'
 should print #8-1
@@ -4382,6 +4420,17 @@ should print #8-1
   \echo '#10-2 ok, variable no_such_variable is not defined'
 #10-2 ok, variable no_such_variable is not defined
 \endif
+-- elif after true branch
+\if true
+	\echo 'should print #11-1'
+should print #11-1
+\elif true
+	\echo 'should not print #11-2'
+\elif false
+	\echo 'should not print #11-3'
+\else
+	\echo 'should not print #11-4'
+\endif
 SELECT :{?i} AS i_is_defined;
  i_is_defined 
 --------------
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index 2e37984962..1160855e01 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -911,22 +911,59 @@ select \if false \\ (bogus \else \\ 42 \endif \\ forty_two;
 	:try_to_quit
 	\echo `nosuchcommand` :foo :'foo' :"foo"
 	\pset fieldsep | `nosuchcommand` :foo :'foo' :"foo"
-	\a \C arg1 \c arg1 arg2 arg3 arg4 \cd arg1 \conninfo
+	\a
+    \C arg1
+    \c arg1 arg2 arg3 arg4
+    \cd arg1
+    \conninfo
 	\copy arg1 arg2 arg3 arg4 arg5 arg6
-	\copyright \dt arg1 \e arg1 arg2
+    \copyright
+	SELECT 1 as one, 2, 3 \crosstabview
+    \dt arg1
+    \e arg1 arg2
 	\ef whole_line
 	\ev whole_line
-	\echo arg1 arg2 arg3 arg4 arg5 \echo arg1 \encoding arg1 \errverbose
-	\g arg1 \gx arg1 \gexec \h \html \i arg1 \ir arg1 \l arg1 \lo arg1 arg2
-	\o arg1 \p \password arg1 \prompt arg1 arg2 \pset arg1 arg2 \q
-	\reset \s arg1 \set arg1 arg2 arg3 arg4 arg5 arg6 arg7 \setenv arg1 arg2
+	\echo arg1 arg2 arg3 arg4 arg5
+    \echo arg1
+    \encoding arg1
+    \errverbose
+	\f arg1
+	\g arg1
+    \gx arg1
+    \gexec
+	SELECT 1 AS one \gset
+    \h
+	\?
+    \html
+    \i arg1
+    \ir arg1
+    \l arg1
+    \lo arg1 arg2
+	\lo_list
+	\o arg1
+    \p
+    \password arg1
+    \prompt arg1 arg2
+    \pset arg1 arg2
+    \q
+	\reset
+    \s arg1
+    \set arg1 arg2 arg3 arg4 arg5 arg6 arg7
+    \setenv arg1 arg2
 	\sf whole_line
 	\sv whole_line
-	\t arg1 \T arg1 \timing arg1 \unset arg1 \w arg1 \watch arg1 \x arg1
+	\t arg1
+    \T arg1
+    \timing arg1
+    \unset arg1
+    \w arg1
+    \watch arg1
+    \x arg1
 	-- \else here is eaten as part of OT_FILEPIPE argument
 	\w |/no/such/file \else
 	-- \endif here is eaten as part of whole-line argument
 	\! whole_line \endif
+	\z
 \else
 	\echo 'should print #8-1'
 \endif
@@ -945,6 +982,17 @@ select \if false \\ (bogus \else \\ 42 \endif \\ forty_two;
   \echo '#10-2 ok, variable no_such_variable is not defined'
 \endif
 
+-- elif after true branch
+\if true
+	\echo 'should print #11-1'
+\elif true
+	\echo 'should not print #11-2'
+\elif false
+	\echo 'should not print #11-3'
+\else
+	\echo 'should not print #11-4'
+\endif
+
 SELECT :{?i} AS i_is_defined;
 
 SELECT NOT :{?no_such_var} AS no_such_var_is_not_defined;

Reply via email to