From 5e4fdbab07812167f68ae993910d6bbf313e55ef Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <x4mmm@flight.local>
Date: Thu, 16 Feb 2023 15:07:50 -0800
Subject: [PATCH v9] Iteration count argument to psql \watch command

If the argument is not provided - continue to \watch forever.

Authour: Andrey Borodin
Reviewed-by: Kyotaro Horiguchi, Nathan Bossart, Michael Paquier
Thread: https://postgr.es/m/CAAhFRxiZ2-n_L1ErMm9AZjgmUK%3DqS6VHb%2B0SaMn8sqqbhF7How%40mail.gmail.com
---
 doc/src/sgml/ref/psql-ref.sgml     |  6 +-
 src/bin/psql/command.c             | 95 +++++++++++++++++++++++++-----
 src/bin/psql/help.c                |  2 +-
 src/bin/psql/t/001_basic.pl        |  7 +++
 src/test/regress/expected/psql.out |  2 +-
 src/test/regress/sql/psql.sql      |  2 +-
 6 files changed, 95 insertions(+), 19 deletions(-)

diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7b8ae9fac3..658fa064d2 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -3551,7 +3551,7 @@ testdb=&gt; <userinput>\setenv LESS -imx4F</userinput>
 
 
       <varlistentry id="app-psql-meta-command-watch">
-        <term><literal>\watch [ <replaceable class="parameter">seconds</replaceable> ]</literal></term>
+        <term><literal>\watch [ <replaceable class="parameter">i[nterval]</replaceable>=<replaceable class="parameter">seconds</replaceable> ] [ <replaceable class="parameter">c[ount]</replaceable>=<replaceable class="parameter">times</replaceable> ] [ <replaceable class="parameter">seconds</replaceable> ]</literal></term>
         <listitem>
         <para>
         Repeatedly execute the current query buffer (as <literal>\g</literal> does)
@@ -3564,6 +3564,10 @@ testdb=&gt; <userinput>\setenv LESS -imx4F</userinput>
         If the current query buffer is empty, the most recently sent query
         is re-executed instead.
         </para>
+        <para>
+        If number of iterations is specified - query will be executed only
+        given number of times.
+        </para>
         </listitem>
       </varlistentry>
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 61ec049f05..357ef6b60e 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -162,7 +162,7 @@ static bool do_connect(enum trivalue reuse_previous_specification,
 static bool do_edit(const char *filename_arg, PQExpBuffer query_buf,
 					int lineno, bool discard_on_quit, bool *edited);
 static bool do_shell(const char *command);
-static bool do_watch(PQExpBuffer query_buf, double sleep);
+static bool do_watch(PQExpBuffer query_buf, double sleep, int iter);
 static bool lookup_object_oid(EditableObjectType obj_type, const char *desc,
 							  Oid *obj_oid);
 static bool get_create_object_cmd(EditableObjectType obj_type, Oid oid,
@@ -2759,7 +2759,8 @@ exec_command_write(PsqlScanState scan_state, bool active_branch,
 }
 
 /*
- * \watch -- execute a query every N seconds
+ * \watch -- execute a query every N seconds.
+ * Optionally for M iteration.
  */
 static backslashResult
 exec_command_watch(PsqlScanState scan_state, bool active_branch,
@@ -2771,30 +2772,87 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 	{
 		char	   *opt = psql_scan_slash_option(scan_state,
 												 OT_NORMAL, NULL, true);
+		bool 		have_sleep = false;
 		double		sleep = 2;
+		int			iter = 0;
 
 		/* Convert optional sleep-length argument */
-		if (opt)
+
+		while (opt)
 		{
-			char	   *opt_end;
+			/*
+			 * We can have either sleep interval or "name=value", where name is
+			 * from the set ('i','interval','c','count')
+			 */
+			char *valptr = strchr(opt, '=');
+			char *opt_end;
 
-			errno = 0;
-			sleep = strtod(opt, &opt_end);
-			if (sleep < 0 || *opt_end || errno == ERANGE)
+			if (valptr)
 			{
-				pg_log_error("\\watch: incorrect interval value '%s'", opt);
-				free(opt);
-				resetPQExpBuffer(query_buf);
-				psql_scan_reset(scan_state);
-				return PSQL_CMD_ERROR;
+				valptr++;
+				if (strncmp("i", opt, strlen("i")) == 0 ||
+					strncmp("interval", opt, strlen("interval")) == 0)
+				{
+					errno = 0;
+					sleep = strtod(valptr, &opt_end);
+					if (sleep < 0 || *opt_end || errno == ERANGE)
+					{
+						pg_log_error("\\watch: incorrect interval value '%s'", valptr);
+						free(opt);
+						resetPQExpBuffer(query_buf);
+						psql_scan_reset(scan_state);
+						return PSQL_CMD_ERROR;
+					}
+					/* we do not prevent numerous names iterations like i=1 i=1 i=1 */
+					have_sleep = true;
+				}
+				else if (strncmp("c", opt, strlen("c")) == 0 ||
+					strncmp("count", opt, strlen("count")) == 0)
+				{
+					errno = 0;
+					iter = strtol(valptr, &opt_end, 10);
+					if (iter <= 0 || *opt_end || errno == ERANGE)
+					{
+						pg_log_error("\\watch: incorrect iteration count '%s'", valptr);
+						free(opt);
+						resetPQExpBuffer(query_buf);
+						psql_scan_reset(scan_state);
+						return PSQL_CMD_ERROR;
+					}
+				}
+				else
+				{
+					pg_log_error("Unknown \\watch argument '%s'", opt);
+					free(opt);
+					resetPQExpBuffer(query_buf);
+					psql_scan_reset(scan_state);
+					return PSQL_CMD_ERROR;
+				}
+			}
+			else
+			{
+				errno = 0;
+				sleep = strtod(opt, &opt_end);
+				if (sleep < 0 || *opt_end || errno == ERANGE || have_sleep)
+				{
+					pg_log_error("\\watch: incorrect interval value '%s'", opt);
+					free(opt);
+					resetPQExpBuffer(query_buf);
+					psql_scan_reset(scan_state);
+					return PSQL_CMD_ERROR;
+				}
+				have_sleep = true;
 			}
+
 			free(opt);
+			opt = psql_scan_slash_option(scan_state,
+												 OT_NORMAL, NULL, true);
 		}
 
 		/* If query_buf is empty, recall and execute previous query */
 		(void) copy_previous_query(query_buf, previous_buf);
 
-		success = do_watch(query_buf, sleep);
+		success = do_watch(query_buf, sleep, iter);
 
 		/* Reset the query buffer as though for \r */
 		resetPQExpBuffer(query_buf);
@@ -5056,7 +5114,7 @@ do_shell(const char *command)
  * onto a bunch of exec_command's variables to silence stupider compilers.
  */
 static bool
-do_watch(PQExpBuffer query_buf, double sleep)
+do_watch(PQExpBuffer query_buf, double sleep, int iter)
 {
 	long		sleep_ms = (long) (sleep * 1000);
 	printQueryOpt myopt = pset.popt;
@@ -5158,11 +5216,18 @@ do_watch(PQExpBuffer query_buf, double sleep)
 	title_len = (user_title ? strlen(user_title) : 0) + 256;
 	title = pg_malloc(title_len);
 
-	for (;;)
+	for (int i = 1;;)
 	{
 		time_t		timer;
 		char		timebuf[128];
 
+		/* If we have iteration count - check that it's not exceeded yet */
+		/* Keep in mind that first iteration was performed before do_watch() */
+		if (iter && (i++ == iter))
+		{
+			break;
+		}
+
 		/*
 		 * Prepare title for output.  Note that we intentionally include a
 		 * newline at the end of the title; this is somewhat historical but it
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..1062d0ed7b 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -200,7 +200,7 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\gset [PREFIX]         execute query and store result in psql variables\n");
 	HELP0("  \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n");
 	HELP0("  \\q                     quit psql\n");
-	HELP0("  \\watch [SEC]           execute query every SEC seconds\n");
+	HELP0("  \\watch [c=N] [SEC]     execute query every SEC seconds N times\n");
 	HELP0("\n");
 
 	HELP0("Help\n");
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 64ce012062..6defd636ea 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -350,6 +350,13 @@ psql_like(
 	'\copy from with DEFAULT'
 );
 
+# Check \watch
+psql_like(
+	$node,
+	'SELECT 1;\watch c=3 i=0',
+	qr/1\n1\n1/,
+	'\watch with 3 iterations');
+
 # Check \watch errors
 psql_fails_like(
 	$node,
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index c00e28361c..956e475447 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -4536,7 +4536,7 @@ invalid command \lo
 	\timing arg1
 	\unset arg1
 	\w arg1
-	\watch arg1
+	\watch arg1 arg2
 	\x arg1
 	-- \else here is eaten as part of OT_FILEPIPE argument
 	\w |/no/such/file \else
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index 961783d6ea..630f638f02 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1022,7 +1022,7 @@ select \if false \\ (bogus \else \\ 42 \endif \\ forty_two;
 	\timing arg1
 	\unset arg1
 	\w arg1
-	\watch arg1
+	\watch arg1 arg2
 	\x arg1
 	-- \else here is eaten as part of OT_FILEPIPE argument
 	\w |/no/such/file \else
-- 
2.32.0 (Apple Git-132)

