psql \watch 2nd argument: iteration count

Started by Andrey Borodinalmost 3 years ago33 messages
#1Andrey Borodin
amborodin86@gmail.com
1 attachment(s)

Hi hackers!

From time to time I want to collect some stats from locks, activity
and other stat views into one table from different time points. In
this case the \watch psql command is very handy. However, it's not
currently possible to specify the number of times a query is
performed.
Also, if we do not provide a timespan, 2 seconds are selected. But if
we provide an incorrect argument - 1 second is selected.
PFA the patch that adds iteration count argument and makes timespan
argument more consistent.
What do you think?

Best regards, Andrey Borodin.

Attachments:

0001-Add-iteration-count-argument-to-psql-watch-command.patchapplication/octet-stream; name=0001-Add-iteration-count-argument-to-psql-watch-command.patchDownload
From a07e911f89bc72992edd228fa3c9326a58060cb6 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] Add iteration count argument to psql \watch command

If the argument is not provided - continue to \watch forever.
---
 src/bin/psql/command.c             | 39 +++++++++++++++++++++++++-----
 src/bin/psql/help.c                |  2 +-
 src/test/regress/expected/psql.out |  2 +-
 src/test/regress/sql/psql.sql      |  2 +-
 4 files changed, 36 insertions(+), 9 deletions(-)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index b5201edf55..482ff557f0 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,
@@ -2772,20 +2773,41 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		char	   *opt = psql_scan_slash_option(scan_state,
 												 OT_NORMAL, NULL, true);
 		double		sleep = 2;
+		int			iter = 0;
 
 		/* Convert optional sleep-length argument */
 		if (opt)
 		{
 			sleep = strtod(opt, NULL);
 			if (sleep <= 0)
-				sleep = 1;
+			{
+				pg_log_error("Invalid watch argument %s", opt);
+				resetPQExpBuffer(query_buf);
+				return PSQL_CMD_ERROR;
+			}
+			
 			free(opt);
+
+			/* Check if iteration count is given */
+			opt = psql_scan_slash_option(scan_state,
+												 OT_NORMAL, NULL, true);
+			if (opt)
+			{
+				iter = strtol(opt, NULL, 10);
+				if (iter <= 0)
+				{
+					pg_log_error("Invalid iteration count %s", opt);
+					resetPQExpBuffer(query_buf);
+					return PSQL_CMD_ERROR;
+				}
+				free(opt);
+			}
 		}
 
 		/* 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);
@@ -5047,7 +5069,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;
@@ -5149,7 +5171,7 @@ 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];
@@ -5244,6 +5266,11 @@ do_watch(PQExpBuffer query_buf, double sleep)
 		if (done)
 			break;
 #endif
+		/* If we have iteration count - check that it's not exceeded yet */
+		if (iter && (i++ == iter))
+		{
+			break;
+		}
 	}
 
 	if (pagerpipe)
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..9561f15a71 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 [SEC [N]]       execute query every SEC seconds N times\n");
 	HELP0("\n");
 
 	HELP0("Help\n");
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 8fc62cebd2..e5d7e4c4f5 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 2da9665a19..661847a2f1 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)

#2Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andrey Borodin (#1)
Re: psql \watch 2nd argument: iteration count

On 17.02.23 00:33, Andrey Borodin wrote:

From time to time I want to collect some stats from locks, activity
and other stat views into one table from different time points. In
this case the \watch psql command is very handy. However, it's not
currently possible to specify the number of times a query is
performed.

The watch command on my OS has a lot of options, but this is not one of
them. So probably no one has really needed it so far.

Also, if we do not provide a timespan, 2 seconds are selected. But if
we provide an incorrect argument - 1 second is selected.
PFA the patch that adds iteration count argument and makes timespan
argument more consistent.

That should probably be fixed.

#3Stephen Frost
sfrost@snowman.net
In reply to: Peter Eisentraut (#2)
Re: psql \watch 2nd argument: iteration count

Greetings,

* Peter Eisentraut (peter.eisentraut@enterprisedb.com) wrote:

On 17.02.23 00:33, Andrey Borodin wrote:

From time to time I want to collect some stats from locks, activity
and other stat views into one table from different time points. In
this case the \watch psql command is very handy. However, it's not
currently possible to specify the number of times a query is
performed.

The watch command on my OS has a lot of options, but this is not one of
them. So probably no one has really needed it so far.

watch doesn't ... but top does, and I can certainly see how our watch
having an iterations count could be helpful in much the same way as
top's batch mode does.

Also, if we do not provide a timespan, 2 seconds are selected. But if
we provide an incorrect argument - 1 second is selected.
PFA the patch that adds iteration count argument and makes timespan
argument more consistent.

That should probably be fixed.

And should probably be independent patches.

Thanks,

Stephen

#4Andrey Borodin
amborodin86@gmail.com
In reply to: Stephen Frost (#3)
2 attachment(s)
Re: psql \watch 2nd argument: iteration count

Also, if we do not provide a timespan, 2 seconds are selected. But if
we provide an incorrect argument - 1 second is selected.
PFA the patch that adds iteration count argument and makes timespan
argument more consistent.

That should probably be fixed.

And should probably be independent patches.

PFA 2 independent patches.

Also, I've fixed a place to break after an iteration. Now if we have
e.g. 2 iterations - there will be only 1 sleep time.

Thanks!

Best regards, Andrey Borodin.

Attachments:

v2-0001-Fix-incorrect-argument-handling-in-psql-watch.patchapplication/octet-stream; name=v2-0001-Fix-incorrect-argument-handling-in-psql-watch.patchDownload
From 843d13ec0b94a3385816680a85b7b736803f3154 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <x4mmm@flight.local>
Date: Mon, 20 Feb 2023 10:28:02 -0800
Subject: [PATCH v2] Fix incorrect argument handling in psql \watch

Incorrectly parsed argument was silently substituted with 1 second.
This is changed to proper error message.

Authour: Andrey Borodin <amborodin@acm.org>
Thread: https://postgr.es/m/CAAhFRxiZ2-n_L1ErMm9AZjgmUK%3DqS6VHb%2B0SaMn8sqqbhF7How%40mail.gmail.com
---
 src/bin/psql/command.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index b5201edf55..15f922e6e6 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -2778,7 +2778,12 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		{
 			sleep = strtod(opt, NULL);
 			if (sleep <= 0)
-				sleep = 1;
+			{
+				pg_log_error("Invalid watch argument %s", opt);
+				free(opt);
+				resetPQExpBuffer(query_buf);
+				return PSQL_CMD_ERROR;
+			}
 			free(opt);
 		}
 
-- 
2.32.0 (Apple Git-132)

v2-0001-Add-iteration-count-argument-to-psql-watch-comman.patchapplication/octet-stream; name=v2-0001-Add-iteration-count-argument-to-psql-watch-comman.patchDownload
From 2eb92e1d6a4a39fe220ca16c63f0f7365d3649df 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 v2] Add iteration count argument to psql \watch command

If the argument is not provided - continue to \watch forever.
---
 src/bin/psql/command.c             | 33 +++++++++++++++++++++++++-----
 src/bin/psql/help.c                |  2 +-
 src/test/regress/expected/psql.out |  2 +-
 src/test/regress/sql/psql.sql      |  2 +-
 4 files changed, 31 insertions(+), 8 deletions(-)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index b5201edf55..9d5723e1c7 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,
@@ -2772,6 +2773,7 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		char	   *opt = psql_scan_slash_option(scan_state,
 												 OT_NORMAL, NULL, true);
 		double		sleep = 2;
+		int			iter = 0;
 
 		/* Convert optional sleep-length argument */
 		if (opt)
@@ -2780,12 +2782,27 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 			if (sleep <= 0)
 				sleep = 1;
 			free(opt);
+
+			/* Check if iteration count is given */
+			opt = psql_scan_slash_option(scan_state,
+												 OT_NORMAL, NULL, true);
+			if (opt)
+			{
+				iter = strtol(opt, NULL, 10);
+				if (iter <= 0)
+				{
+					pg_log_error("Invalid iteration count %s", opt);
+					resetPQExpBuffer(query_buf);
+					return PSQL_CMD_ERROR;
+				}
+				free(opt);
+			}
 		}
 
 		/* 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);
@@ -5047,7 +5064,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;
@@ -5149,7 +5166,7 @@ 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];
@@ -5183,6 +5200,12 @@ do_watch(PQExpBuffer query_buf, double sleep)
 		if (pagerpipe && ferror(pagerpipe))
 			break;
 
+		/* If we have iteration count - check that it's not exceeded yet */
+		if (iter && (i++ == iter))
+		{
+			break;
+		}
+
 #ifdef WIN32
 
 		/*
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..9561f15a71 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 [SEC [N]]       execute query every SEC seconds N times\n");
 	HELP0("\n");
 
 	HELP0("Help\n");
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 8fc62cebd2..e5d7e4c4f5 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 2da9665a19..661847a2f1 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)

#5Kyotaro Horiguchi
horikyota.ntt@gmail.com
In reply to: Andrey Borodin (#4)
Re: psql \watch 2nd argument: iteration count

At Mon, 20 Feb 2023 10:45:53 -0800, Andrey Borodin <amborodin86@gmail.com> wrote in

Also, if we do not provide a timespan, 2 seconds are selected. But if
we provide an incorrect argument - 1 second is selected.
PFA the patch that adds iteration count argument and makes timespan
argument more consistent.

That should probably be fixed.

And should probably be independent patches.

PFA 2 independent patches.

Also, I've fixed a place to break after an iteration. Now if we have
e.g. 2 iterations - there will be only 1 sleep time.

IMHO the current behavior for digit inputs looks fine to me. I feel
that the command should selently fix the input to the default in the
case of digits inputs like '-1'. But that may not be the case for
everyone. FWIW the patch still accepts an incorrect parameter '1abc'
by ignoring any trailing garbage.

In any case, I reckon the error message should be more specific. In
other words, it would be better if it suggests the expected input
format and range.

Regarding the second patch, if we want \watch to throw an error
message for the garbage trailing to sleep times, I think we should do
the same for iteration counts. Additionally, we need to update the
documentation.

By the way, when I looked this patch, I noticed that
exec_command_bind() doesn't free the malloc'ed return strings from
psql_scan_slash_option(). The same mistake is seen in some other
places. I'll take a closer look and get back in another thread.

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#6Andrey Borodin
amborodin86@gmail.com
In reply to: Kyotaro Horiguchi (#5)
2 attachment(s)
Re: psql \watch 2nd argument: iteration count

Thanks for looking into this!

On Mon, Feb 20, 2023 at 6:15 PM Kyotaro Horiguchi
<horikyota.ntt@gmail.com> wrote:

FWIW the patch still accepts an incorrect parameter '1abc'
by ignoring any trailing garbage.

Indeed, fixed.

In any case, I reckon the error message should be more specific. In
other words, it would be better if it suggests the expected input
format and range.

+1.
Not a range, actually, because upper limits have no sense for a user.

Regarding the second patch, if we want \watch to throw an error
message for the garbage trailing to sleep times, I think we should do
the same for iteration counts.

+1, done.

Additionally, we need to update the
documentation.

Done.

Thanks for the review!

Best regards, Andrey Borodin.

Attachments:

v3-0002-Add-iteration-count-argument-to-psql-watch-comman.patchapplication/octet-stream; name=v3-0002-Add-iteration-count-argument-to-psql-watch-comman.patchDownload
From 02d250779d0f9fab118e435b7f206efa543fba42 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 v3 2/2] Add iteration count argument to psql \watch command

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

Authour: Andrey Borodin <amborodin@acm.org>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
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             | 37 +++++++++++++++++++++++++-----
 src/bin/psql/help.c                |  2 +-
 src/test/regress/expected/psql.out |  2 +-
 src/test/regress/sql/psql.sql      |  2 +-
 5 files changed, 39 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index dc6528dc11..546697c78c 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -3548,7 +3548,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">seconds</replaceable> [ <replaceable class="parameter">iterations</replaceable> ] ]</literal></term>
         <listitem>
         <para>
         Repeatedly execute the current query buffer (as <literal>\g</literal> does)
@@ -3561,6 +3561,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 e008c5217b..6d94fd1490 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,
@@ -2772,6 +2773,7 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		char	   *opt = psql_scan_slash_option(scan_state,
 												 OT_NORMAL, NULL, true);
 		double		sleep = 2;
+		int			iter = 0;
 
 		/* Convert optional sleep-length argument */
 		if (opt)
@@ -2780,18 +2782,35 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 			sleep = strtod(opt, &opt_end);
 			if (sleep <= 0 || *opt_end)
 			{
-				pg_log_error("Watch period must be positive number, but argument is '%s'", opt);
+				pg_log_error("Watch period must be positive number, but "
+							 "argument is '%s'", opt);
 				free(opt);
 				resetPQExpBuffer(query_buf);
 				return PSQL_CMD_ERROR;
 			}
 			free(opt);
+
+			/* Check if iteration count is given */
+			opt = psql_scan_slash_option(scan_state,
+												 OT_NORMAL, NULL, true);
+			if (opt)
+			{
+				iter = strtol(opt, &opt_end, 10);
+				if (iter <= 0 || *opt_end)
+				{
+					pg_log_error("Watch iteration count must be positive "
+								 "integer, but argument is '%s'", opt);
+					resetPQExpBuffer(query_buf);
+					return PSQL_CMD_ERROR;
+				}
+				free(opt);
+			}
 		}
 
 		/* 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);
@@ -5053,7 +5072,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;
@@ -5155,7 +5174,7 @@ 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];
@@ -5189,6 +5208,12 @@ do_watch(PQExpBuffer query_buf, double sleep)
 		if (pagerpipe && ferror(pagerpipe))
 			break;
 
+		/* If we have iteration count - check that it's not exceeded yet */
+		if (iter && (i++ == iter))
+		{
+			break;
+		}
+
 #ifdef WIN32
 
 		/*
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..9561f15a71 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 [SEC [N]]       execute query every SEC seconds N times\n");
 	HELP0("\n");
 
 	HELP0("Help\n");
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 8fc62cebd2..e5d7e4c4f5 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 2da9665a19..661847a2f1 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)

v3-0001-Fix-incorrect-argument-handling-in-psql-watch.patchapplication/octet-stream; name=v3-0001-Fix-incorrect-argument-handling-in-psql-watch.patchDownload
From eea8351ea540f639dc5f29cbed8a0f38587ebb2d Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <x4mmm@flight.local>
Date: Mon, 20 Feb 2023 10:28:02 -0800
Subject: [PATCH v3 1/2] Fix incorrect argument handling in psql \watch

Incorrectly parsed argument was silently substituted with 1 second.
This is changed to proper error message.

Authour: Andrey Borodin <amborodin@acm.org>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Thread: https://postgr.es/m/CAAhFRxiZ2-n_L1ErMm9AZjgmUK%3DqS6VHb%2B0SaMn8sqqbhF7How%40mail.gmail.com
---
 src/bin/psql/command.c | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..e008c5217b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -2776,9 +2776,15 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		/* Convert optional sleep-length argument */
 		if (opt)
 		{
-			sleep = strtod(opt, NULL);
-			if (sleep <= 0)
-				sleep = 1;
+			char *opt_end;
+			sleep = strtod(opt, &opt_end);
+			if (sleep <= 0 || *opt_end)
+			{
+				pg_log_error("Watch period must be positive number, but argument is '%s'", opt);
+				free(opt);
+				resetPQExpBuffer(query_buf);
+				return PSQL_CMD_ERROR;
+			}
 			free(opt);
 		}
 
-- 
2.32.0 (Apple Git-132)

#7Nathan Bossart
nathandbossart@gmail.com
In reply to: Andrey Borodin (#6)
Re: psql \watch 2nd argument: iteration count

+1 for adding an iteration count argument to \watch.

+			char *opt_end;
+			sleep = strtod(opt, &opt_end);
+			if (sleep <= 0 || *opt_end)
+			{
+				pg_log_error("Watch period must be positive number, but argument is '%s'", opt);
+				free(opt);
+				resetPQExpBuffer(query_buf);
+				return PSQL_CMD_ERROR;
+			}

Is there any reason to disallow 0 for the sleep argument? I often use
commands like "\watch .1" to run statements repeatedly with very little
time in between, and I'd use "\watch 0" instead if it was available.

--
Nathan Bossart
Amazon Web Services: https://aws.amazon.com

#8Andrey Borodin
amborodin86@gmail.com
In reply to: Nathan Bossart (#7)
2 attachment(s)
Re: psql \watch 2nd argument: iteration count

On Wed, Mar 8, 2023 at 10:49 AM Nathan Bossart <nathandbossart@gmail.com> wrote:

Is there any reason to disallow 0 for the sleep argument? I often use
commands like "\watch .1" to run statements repeatedly with very little
time in between, and I'd use "\watch 0" instead if it was available.

Yes, that makes sense! Thanks!

Best regards, Andrey Borodin.

Attachments:

v4-0002-Add-iteration-count-argument-to-psql-watch-comman.patchapplication/octet-stream; name=v4-0002-Add-iteration-count-argument-to-psql-watch-comman.patchDownload
From e39adb92a9a51dde47520d40ff75fd2c7621b9c8 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 v4 2/2] Add iteration count argument to psql \watch command

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

Authour: Andrey Borodin <amborodin@acm.org>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
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             | 37 +++++++++++++++++++++++++-----
 src/bin/psql/help.c                |  2 +-
 src/test/regress/expected/psql.out |  2 +-
 src/test/regress/sql/psql.sql      |  2 +-
 5 files changed, 39 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7b8ae9fac3..94323fdf94 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">seconds</replaceable> [ <replaceable class="parameter">iterations</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 b1707eb972..05acf61873 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,
@@ -2772,6 +2773,7 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		char	   *opt = psql_scan_slash_option(scan_state,
 												 OT_NORMAL, NULL, true);
 		double		sleep = 2;
+		int			iter = 0;
 
 		/* Convert optional sleep-length argument */
 		if (opt)
@@ -2780,18 +2782,35 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 			sleep = strtod(opt, &opt_end);
 			if (sleep < 0 || *opt_end)
 			{
-				pg_log_error("Watch period must be non-negative number, but argument is '%s'", opt);
+				pg_log_error("Watch period must be non-negative number, but "
+							 "argument is '%s'", opt);
 				free(opt);
 				resetPQExpBuffer(query_buf);
 				return PSQL_CMD_ERROR;
 			}
 			free(opt);
+
+			/* Check if iteration count is given */
+			opt = psql_scan_slash_option(scan_state,
+												 OT_NORMAL, NULL, true);
+			if (opt)
+			{
+				iter = strtol(opt, &opt_end, 10);
+				if (iter <= 0 || *opt_end)
+				{
+					pg_log_error("Watch iteration count must be positive "
+								 "integer, but argument is '%s'", opt);
+					resetPQExpBuffer(query_buf);
+					return PSQL_CMD_ERROR;
+				}
+				free(opt);
+			}
 		}
 
 		/* 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);
@@ -5053,7 +5072,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;
@@ -5155,7 +5174,7 @@ 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];
@@ -5189,6 +5208,12 @@ do_watch(PQExpBuffer query_buf, double sleep)
 		if (pagerpipe && ferror(pagerpipe))
 			break;
 
+		/* If we have iteration count - check that it's not exceeded yet */
+		if (iter && (i++ == iter))
+		{
+			break;
+		}
+
 		if (sleep == 0)
 			continue;
 
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..9561f15a71 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 [SEC [N]]       execute query every SEC seconds N times\n");
 	HELP0("\n");
 
 	HELP0("Help\n");
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)

v4-0001-Fix-incorrect-argument-handling-in-psql-watch.patchapplication/octet-stream; name=v4-0001-Fix-incorrect-argument-handling-in-psql-watch.patchDownload
From 75e6daae0fe119f222d4495869e43112b68c8736 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <x4mmm@flight.local>
Date: Mon, 20 Feb 2023 10:28:02 -0800
Subject: [PATCH v4 1/2] Fix incorrect argument handling in psql \watch

Incorrectly parsed argument was silently substituted with 1 second.
This is changed to proper error message.

Authour: Andrey Borodin <amborodin@acm.org>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
Thread: https://postgr.es/m/CAAhFRxiZ2-n_L1ErMm9AZjgmUK%3DqS6VHb%2B0SaMn8sqqbhF7How%40mail.gmail.com
---
 src/bin/psql/command.c | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..b1707eb972 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -2776,9 +2776,15 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		/* Convert optional sleep-length argument */
 		if (opt)
 		{
-			sleep = strtod(opt, NULL);
-			if (sleep <= 0)
-				sleep = 1;
+			char *opt_end;
+			sleep = strtod(opt, &opt_end);
+			if (sleep < 0 || *opt_end)
+			{
+				pg_log_error("Watch period must be non-negative number, but argument is '%s'", opt);
+				free(opt);
+				resetPQExpBuffer(query_buf);
+				return PSQL_CMD_ERROR;
+			}
 			free(opt);
 		}
 
@@ -5183,6 +5189,9 @@ do_watch(PQExpBuffer query_buf, double sleep)
 		if (pagerpipe && ferror(pagerpipe))
 			break;
 
+		if (sleep == 0)
+			continue;
+
 #ifdef WIN32
 
 		/*
-- 
2.32.0 (Apple Git-132)

#9Nathan Bossart
nathandbossart@gmail.com
In reply to: Andrey Borodin (#8)
Re: psql \watch 2nd argument: iteration count

+ pg_log_error("Watch period must be non-negative number, but argument is '%s'", opt);

After looking around at the other error messages in this file, I think we
should make this more concise. Maybe something like

pg_log_error("\\watch: invalid delay interval: %s", opt);

+				free(opt);
+				resetPQExpBuffer(query_buf);
+				return PSQL_CMD_ERROR;

Is this missing psql_scan_reset(scan_state)?

I haven't had a chance to look closely at 0002 yet.

--
Nathan Bossart
Amazon Web Services: https://aws.amazon.com

#10Andrey Borodin
amborodin86@gmail.com
In reply to: Nathan Bossart (#9)
2 attachment(s)
Re: psql \watch 2nd argument: iteration count

On Thu, Mar 9, 2023 at 11:25 AM Nathan Bossart <nathandbossart@gmail.com> wrote:

+ pg_log_error("Watch period must be non-negative number, but argument is '%s'", opt);

After looking around at the other error messages in this file, I think we
should make this more concise. Maybe something like

pg_log_error("\\watch: invalid delay interval: %s", opt);

In the review above Kyotaro-san suggested that message should contain
information on what it expects... So, maybe then
pg_log_error("\\watch interval must be non-negative number, but
argument is '%s'", opt); ?
Or perhaps with articles? pg_log_error("\\watch interval must be a
non-negative number, but the argument is '%s'", opt);

+                               free(opt);
+                               resetPQExpBuffer(query_buf);
+                               return PSQL_CMD_ERROR;

Is this missing psql_scan_reset(scan_state)?

Yes, fixed.

Best regards, Andrey Borodin.

Attachments:

v5-0002-Add-iteration-count-argument-to-psql-watch-comman.patchapplication/octet-stream; name=v5-0002-Add-iteration-count-argument-to-psql-watch-comman.patchDownload
From 582abfdc3cfc5802c4c655643cc3af374a798aa2 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 v5 2/2] Add iteration count argument to psql \watch command

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

Authour: Andrey Borodin <amborodin@acm.org>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
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             | 36 +++++++++++++++++++++++++-----
 src/bin/psql/help.c                |  2 +-
 src/test/regress/expected/psql.out |  2 +-
 src/test/regress/sql/psql.sql      |  2 +-
 5 files changed, 39 insertions(+), 9 deletions(-)

diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7b8ae9fac3..94323fdf94 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">seconds</replaceable> [ <replaceable class="parameter">iterations</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 433db96a0e..20e4b86284 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,
@@ -2772,6 +2773,7 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		char	   *opt = psql_scan_slash_option(scan_state,
 												 OT_NORMAL, NULL, true);
 		double		sleep = 2;
+		int			iter = 0;
 
 		/* Convert optional sleep-length argument */
 		if (opt)
@@ -2788,12 +2790,30 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 				return PSQL_CMD_ERROR;
 			}
 			free(opt);
+
+			/* Check if iteration count is given */
+			opt = psql_scan_slash_option(scan_state,
+												 OT_NORMAL, NULL, true);
+			if (opt)
+			{
+				iter = strtol(opt, &opt_end, 10);
+				if (iter <= 0 || *opt_end)
+				{
+					pg_log_error("Watch iteration count must be positive "
+								 "integer, but argument is '%s'", opt);
+					free(opt);
+					resetPQExpBuffer(query_buf);
+					psql_scan_reset(scan_state);
+					return PSQL_CMD_ERROR;
+				}
+				free(opt);
+			}
 		}
 
 		/* 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);
@@ -5055,7 +5075,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;
@@ -5157,7 +5177,7 @@ 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];
@@ -5191,6 +5211,12 @@ do_watch(PQExpBuffer query_buf, double sleep)
 		if (pagerpipe && ferror(pagerpipe))
 			break;
 
+		/* If we have iteration count - check that it's not exceeded yet */
+		if (iter && (i++ == iter))
+		{
+			break;
+		}
+
 		if (sleep == 0)
 			continue;
 
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..9561f15a71 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 [SEC [N]]       execute query every SEC seconds N times\n");
 	HELP0("\n");
 
 	HELP0("Help\n");
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)

v5-0001-Fix-incorrect-argument-handling-in-psql-watch.patchapplication/octet-stream; name=v5-0001-Fix-incorrect-argument-handling-in-psql-watch.patchDownload
From 11efa9e140baac4074b99577711c0a310fe81dfb Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <x4mmm@flight.local>
Date: Mon, 20 Feb 2023 10:28:02 -0800
Subject: [PATCH v5 1/2] Fix incorrect argument handling in psql \watch

Incorrectly parsed argument was silently substituted with 1 second.
This is changed to proper error message.

Authour: Andrey Borodin <amborodin@acm.org>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
Thread: https://postgr.es/m/CAAhFRxiZ2-n_L1ErMm9AZjgmUK%3DqS6VHb%2B0SaMn8sqqbhF7How%40mail.gmail.com
---
 src/bin/psql/command.c | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..433db96a0e 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -2776,9 +2776,17 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		/* Convert optional sleep-length argument */
 		if (opt)
 		{
-			sleep = strtod(opt, NULL);
-			if (sleep <= 0)
-				sleep = 1;
+			char *opt_end;
+			sleep = strtod(opt, &opt_end);
+			if (sleep < 0 || *opt_end)
+			{
+				pg_log_error("\\watch interval must be non-negative number, "
+							 "but argument is '%s'", opt);
+				free(opt);
+				resetPQExpBuffer(query_buf);
+				psql_scan_reset(scan_state);
+				return PSQL_CMD_ERROR;
+			}
 			free(opt);
 		}
 
@@ -5183,6 +5191,9 @@ do_watch(PQExpBuffer query_buf, double sleep)
 		if (pagerpipe && ferror(pagerpipe))
 			break;
 
+		if (sleep == 0)
+			continue;
+
 #ifdef WIN32
 
 		/*
-- 
2.32.0 (Apple Git-132)

#11Michael Paquier
michael@paquier.xyz
In reply to: Andrey Borodin (#10)
Re: psql \watch 2nd argument: iteration count

On Sun, Mar 12, 2023 at 01:05:39PM -0700, Andrey Borodin wrote:

In the review above Kyotaro-san suggested that message should contain
information on what it expects... So, maybe then
pg_log_error("\\watch interval must be non-negative number, but
argument is '%s'", opt); ?
Or perhaps with articles? pg_log_error("\\watch interval must be a
non-negative number, but the argument is '%s'", opt);

-       HELP0("  \\watch [SEC]           execute query every SEC seconds\n");
+       HELP0("  \\watch [SEC [N]]       execute query every SEC seconds N times\n");

Is that really the interface we'd want to work with in the long-term?
For one, this does not give the option to specify only an interval
while relying on the default number of seconds. This may be fine, but
it does not strike me as the best choice. How about doing something
more extensible, for example:
\watch [ (option=value [, option=value] .. ) ] [SEC]

I am not sure that this will be the last option we'll ever add to
\watch, so I'd rather have us choose a design more flexible than
what's proposed here, in a way similar to \g or \gx.
--
Michael

#12Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#11)
Re: psql \watch 2nd argument: iteration count

On Mon, Mar 13, 2023 at 10:17:12AM +0900, Michael Paquier wrote:

I am not sure that this will be the last option we'll ever add to
\watch, so I'd rather have us choose a design more flexible than
what's proposed here, in a way similar to \g or \gx.

While on it, I have some comments about 0001.

-    sleep = strtod(opt, NULL);
-    if (sleep <= 0)
-            sleep = 1;
+    char *opt_end;
+    sleep = strtod(opt, &opt_end);
+    if (sleep < 0 || *opt_end)
+    {
+            pg_log_error("\\watch interval must be non-negative number, "
+                                     "but argument is '%s'", opt);
+            free(opt);
+            resetPQExpBuffer(query_buf);
+            psql_scan_reset(scan_state);
+            return PSQL_CMD_ERROR;
+    }

Okay by me to make this behavior a bit better, though it is not
something I would backpatch as it can influence existing workflows,
even if they worked in an inappropriate way.

Anyway, are you sure that this is actually OK? It seems to me that
this needs to check for three things:
- If sleep is a negative value.
- errno should be non-zero.
- *opt_end == opt.

So this needs three different error messages to show the exact error
to the user? Wouldn't it be better to have a couple of regression
tests, as well?
--
Michael

#13Andrey Borodin
amborodin86@gmail.com
In reply to: Michael Paquier (#12)
2 attachment(s)
Re: psql \watch 2nd argument: iteration count

Michael, thanks for reviewing this!

On Sun, Mar 12, 2023 at 6:17 PM Michael Paquier <michael@paquier.xyz> wrote:

On Sun, Mar 12, 2023 at 01:05:39PM -0700, Andrey Borodin wrote:

In the review above Kyotaro-san suggested that message should contain
information on what it expects... So, maybe then
pg_log_error("\\watch interval must be non-negative number, but
argument is '%s'", opt); ?
Or perhaps with articles? pg_log_error("\\watch interval must be a
non-negative number, but the argument is '%s'", opt);

-       HELP0("  \\watch [SEC]           execute query every SEC seconds\n");
+       HELP0("  \\watch [SEC [N]]       execute query every SEC seconds N times\n");

Is that really the interface we'd want to work with in the long-term?
For one, this does not give the option to specify only an interval
while relying on the default number of seconds. This may be fine, but
it does not strike me as the best choice. How about doing something
more extensible, for example:
\watch [ (option=value [, option=value] .. ) ] [SEC]

I am not sure that this will be the last option we'll ever add to
\watch, so I'd rather have us choose a design more flexible than
what's proposed here, in a way similar to \g or \gx.

I've attached an implementation of this proposed interface (no tests
and help message yet, though, sorry).
I tried it a little bit, and it works for me.
fire query 3 times
SELECT 1;\watch c=3 0
or with 200ms interval
SELECT 1;\watch i=.2 c=3
nonsense, but correct
SELECT 1;\watch i=1e-100 c=1

Actually Nik was asking for the feature. Nik, what do you think?

On Sun, Mar 12, 2023 at 6:26 PM Michael Paquier <michael@paquier.xyz> wrote:

While on it, I have some comments about 0001.

-    sleep = strtod(opt, NULL);
-    if (sleep <= 0)
-            sleep = 1;
+    char *opt_end;
+    sleep = strtod(opt, &opt_end);
+    if (sleep < 0 || *opt_end)
+    {
+            pg_log_error("\\watch interval must be non-negative number, "
+                                     "but argument is '%s'", opt);
+            free(opt);
+            resetPQExpBuffer(query_buf);
+            psql_scan_reset(scan_state);
+            return PSQL_CMD_ERROR;
+    }

Okay by me to make this behavior a bit better, though it is not
something I would backpatch as it can influence existing workflows,
even if they worked in an inappropriate way.

+1

Anyway, are you sure that this is actually OK? It seems to me that
this needs to check for three things:
- If sleep is a negative value.
- errno should be non-zero.

I think we can treat errno and negative values equally.

- *opt_end == opt.

So this needs three different error messages to show the exact error
to the user?

I've tried this approach, but could not come up with sufficiently
different error messages...

Wouldn't it be better to have a couple of regression
tests, as well?

Added two tests.

Thanks!

Best regards, Andrey Borodin.

Attachments:

v6-0002-Add-iteration-count-argument-to-psql-watch-comman.patchapplication/octet-stream; name=v6-0002-Add-iteration-count-argument-to-psql-watch-comman.patchDownload
From cf00bef1bd4585d3145852a89a945d9083654ad6 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 v6 2/2] Add iteration count argument to psql \watch command

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

Authour: Andrey Borodin <amborodin@acm.org>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
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             | 101 ++++++++++++++++++++++++-----
 src/bin/psql/help.c                |   2 +-
 src/test/regress/expected/psql.out |   2 +-
 src/test/regress/sql/psql.sql      |   2 +-
 5 files changed, 93 insertions(+), 20 deletions(-)

diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7b8ae9fac3..94323fdf94 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">seconds</replaceable> [ <replaceable class="parameter">iterations</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 ae295dcfb2..34bea4e304 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,32 +2772,94 @@ 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)
 		{
+			/*
+			 * 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;
-			sleep = strtod(opt, &opt_end);
-			if (sleep < 0 || *opt_end || errno == ERANGE)
+
+			if (valptr)
 			{
-				if (*opt_end)
-					pg_log_error("\\watch interval must be non-negative number, "
-								 "but argument is '%s'", opt);
+				valptr++;
+				if (strncmp("i", opt, strlen("i")) == 0 ||
+					strncmp("interval", opt, strlen("interval")) == 0)
+				{
+					sleep = strtod(valptr, &opt_end);
+					if (sleep < 0 || *opt_end || errno == ERANGE)
+					{
+						if (*opt_end)
+							pg_log_error("\\watch interval must be non-negative number, "
+										"but argument is '%s'", valptr);
+						else
+							pg_log_error("\\watch interval must be non-negative number");
+						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)
+				{
+					iter = strtol(valptr, &opt_end, 10);
+					if (iter <= 0 || *opt_end || errno == ERANGE)
+					{
+						pg_log_error("\\watch iteration count must be positive "
+									"integer, but argument is '%s'", valptr);
+						free(opt);
+						resetPQExpBuffer(query_buf);
+						psql_scan_reset(scan_state);
+						return PSQL_CMD_ERROR;
+					}
+				}
 				else
-					pg_log_error("\\watch interval must be non-negative number");
-				free(opt);
-				resetPQExpBuffer(query_buf);
-				psql_scan_reset(scan_state);
-				return PSQL_CMD_ERROR;
+				{
+					pg_log_error("Unknown \\watch argument '%s'", opt);
+					free(opt);
+					resetPQExpBuffer(query_buf);
+					psql_scan_reset(scan_state);
+				}
+			}
+			else
+			{
+				sleep = strtod(opt, &opt_end);
+				if (sleep < 0 || *opt_end || errno == ERANGE || have_sleep)
+				{
+					if (*opt_end)
+						pg_log_error("\\watch interval must be non-negative number, "
+									"but argument is '%s'", opt);
+					else if (have_sleep)
+						pg_log_error("\\watch interval must be specified only once");
+					else
+						pg_log_error("\\watch interval must be non-negative number");
+					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);
@@ -5058,7 +5121,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;
@@ -5160,7 +5223,7 @@ 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];
@@ -5194,6 +5257,12 @@ do_watch(PQExpBuffer query_buf, double sleep)
 		if (pagerpipe && ferror(pagerpipe))
 			break;
 
+		/* If we have iteration count - check that it's not exceeded yet */
+		if (iter && (i++ == iter))
+		{
+			break;
+		}
+
 		if (sleep == 0)
 			continue;
 
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..9561f15a71 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 [SEC [N]]       execute query every SEC seconds N times\n");
 	HELP0("\n");
 
 	HELP0("Help\n");
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)

v6-0001-Fix-incorrect-argument-handling-in-psql-watch.patchapplication/octet-stream; name=v6-0001-Fix-incorrect-argument-handling-in-psql-watch.patchDownload
From b1a780642fca335d8db4220f2c6391fcdc2d1815 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <x4mmm@flight.local>
Date: Mon, 20 Feb 2023 10:28:02 -0800
Subject: [PATCH v6 1/2] Fix incorrect argument handling in psql \watch

Incorrectly parsed argument was silently substituted with 1 second.
This is changed to proper error message.

Authour: Andrey Borodin <amborodin@acm.org>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Thread: https://postgr.es/m/CAAhFRxiZ2-n_L1ErMm9AZjgmUK%3DqS6VHb%2B0SaMn8sqqbhF7How%40mail.gmail.com
---
 src/bin/psql/command.c      | 20 +++++++++++++++++---
 src/bin/psql/t/001_basic.pl |  3 +++
 2 files changed, 20 insertions(+), 3 deletions(-)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..ae295dcfb2 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -2776,9 +2776,20 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		/* Convert optional sleep-length argument */
 		if (opt)
 		{
-			sleep = strtod(opt, NULL);
-			if (sleep <= 0)
-				sleep = 1;
+			char *opt_end;
+			sleep = strtod(opt, &opt_end);
+			if (sleep < 0 || *opt_end || errno == ERANGE)
+			{
+				if (*opt_end)
+					pg_log_error("\\watch interval must be non-negative number, "
+								 "but argument is '%s'", opt);
+				else
+					pg_log_error("\\watch interval must be non-negative number");
+				free(opt);
+				resetPQExpBuffer(query_buf);
+				psql_scan_reset(scan_state);
+				return PSQL_CMD_ERROR;
+			}
 			free(opt);
 		}
 
@@ -5183,6 +5194,9 @@ do_watch(PQExpBuffer query_buf, double sleep)
 		if (pagerpipe && ferror(pagerpipe))
 			break;
 
+		if (sleep == 0)
+			continue;
+
 #ifdef WIN32
 
 		/*
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 0167cb58a2..bd0fbf09df 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -325,4 +325,7 @@ is($row_count, '10',
 	'client-side error commits transaction, no ON_ERROR_STOP and multiple -c switches'
 );
 
+psql_fails_like($node, 'SELECT 1;\watch -10', qr/watch interval must be non-negative number/, '\watch negative interval');
+psql_fails_like($node, 'SELECT 1;\watch 10ab', qr/watch interval must be non-negative number, but argument is '10ab'/, '\watch incorrect interval');
+
 done_testing();
-- 
2.32.0 (Apple Git-132)

#14Michael Paquier
michael@paquier.xyz
In reply to: Andrey Borodin (#13)
1 attachment(s)
Re: psql \watch 2nd argument: iteration count

On Sun, Mar 12, 2023 at 08:59:44PM -0700, Andrey Borodin wrote:

I've tried this approach, but could not come up with sufficiently
different error messages...

Wouldn't it be better to have a couple of regression
tests, as well?

Added two tests.

It should have three tests with one for ERANGE on top of the other
two. Passing down a value like "10e400" should be enough to cause
strtod() to fail, as far as I know.

+ if (sleep == 0)
+ continue;

While on it, forgot to comment on this one.. Indeed, this choice to
authorize 0 and not wait between two commands is more natural.

I have tweaked things as bit as of the attached, and ran pgindent.
What do you think?
--
Michael

Attachments:

v7-0001-Fix-incorrect-argument-handling-in-psql-watch.patchtext/x-diff; charset=us-asciiDownload
From f6dad7551c04c3e7b280c310a744f8d4dfc10d19 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 14 Mar 2023 09:21:32 +0900
Subject: [PATCH v7] Fix incorrect argument handling in psql \watch

Incorrectly parsed argument was silently substituted with 1 second.
This is changed to proper error message.

Authour: Andrey Borodin <amborodin@acm.org>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Thread: https://postgr.es/m/CAAhFRxiZ2-n_L1ErMm9AZjgmUK%3DqS6VHb%2B0SaMn8sqqbhF7How%40mail.gmail.com
---
 src/bin/psql/command.c      | 21 ++++++++++++++++++---
 src/bin/psql/t/001_basic.pl | 17 +++++++++++++++++
 2 files changed, 35 insertions(+), 3 deletions(-)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..60cabfd2c8 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -2776,9 +2776,21 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		/* Convert optional sleep-length argument */
 		if (opt)
 		{
-			sleep = strtod(opt, NULL);
-			if (sleep <= 0)
-				sleep = 1;
+			char *opt_end;
+			sleep = strtod(opt, &opt_end);
+			if (sleep < 0 || *opt_end || errno == ERANGE)
+			{
+				if (*opt_end)
+					pg_log_error("\\watch: incorrect interval value '%s'", opt);
+				else if (errno == ERANGE)
+					pg_log_error("\\watch: out-of-range interval value '%s'", opt);
+				else
+					pg_log_error("\\watch: interval value '%s' less than zero", opt);
+				free(opt);
+				resetPQExpBuffer(query_buf);
+				psql_scan_reset(scan_state);
+				return PSQL_CMD_ERROR;
+			}
 			free(opt);
 		}
 
@@ -5183,6 +5195,9 @@ do_watch(PQExpBuffer query_buf, double sleep)
 		if (pagerpipe && ferror(pagerpipe))
 			break;
 
+		if (sleep == 0)
+			continue;
+
 #ifdef WIN32
 
 		/*
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 0f394420b2..b86ff99a63 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -350,4 +350,21 @@ psql_like(
 	'\copy from with DEFAULT'
 );
 
+# Check \watch errors
+psql_fails_like(
+	$node,
+	'SELECT 1;\watch -10',
+	qr/interval value '-10' less than zero/,
+	'\watch, negative interval');
+psql_fails_like(
+	$node,
+	'SELECT 1;\watch 10ab',
+	qr/incorrect interval value '10ab'/,
+	'\watch incorrect interval');
+psql_fails_like(
+	$node,
+	'SELECT 1;\watch 10e400',
+	qr/out-of-range interval value '10e400'/,
+	'\watch out-of-range interval');
+
 done_testing();
-- 
2.39.2

#15Andrey Borodin
amborodin86@gmail.com
In reply to: Michael Paquier (#14)
Re: psql \watch 2nd argument: iteration count

On Mon, Mar 13, 2023 at 5:26 PM Michael Paquier <michael@paquier.xyz> wrote:

I have tweaked things as bit as of the attached, and ran pgindent.
What do you think?

Looks good to me.
Thanks!

Best regards, Andrey Borodin.

#16Michael Paquier
michael@paquier.xyz
In reply to: Andrey Borodin (#15)
Re: psql \watch 2nd argument: iteration count

On Mon, Mar 13, 2023 at 06:14:18PM -0700, Andrey Borodin wrote:

Looks good to me.

Ok, thanks for looking. Let's wait a bit and see if others have an
opinion to offer. At least, the CI is green.
--
Michael

#17Kyotaro Horiguchi
horikyota.ntt@gmail.com
In reply to: Michael Paquier (#12)
Re: psql \watch 2nd argument: iteration count

At Tue, 14 Mar 2023 11:36:17 +0900, Michael Paquier <michael@paquier.xyz> wrote in

Ok, thanks for looking. Let's wait a bit and see if others have an
opinion to offer. At least, the CI is green.

+				if (*opt_end)
+					pg_log_error("\\watch: incorrect interval value '%s'", opt);
+				else if (errno == ERANGE)
+					pg_log_error("\\watch: out-of-range interval value '%s'", opt);
+				else
+					pg_log_error("\\watch: interval value '%s' less than zero", opt);

I'm not sure if we need error messages for that resolution and I'm a
bit happier to have fewer messages to translate:p. Merging the cases
of ERANGE and negative values might be better. And I think we usually
refer to unparsable input as "invalid".

if (*opt_end)
pg_log_error("\\watch: invalid interval value '%s'", opt);
else
pg_log_error("\\watch: interval value '%s' out of range", opt);

It looks good other than that.

By the way, I noticed that \watch erases the query buffer. That
behavior differs from other commands, such as \g. And the difference
is not documented. Why do we erase the query buffer only in the case
of \watch?

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#18Nathan Bossart
nathandbossart@gmail.com
In reply to: Kyotaro Horiguchi (#17)
Re: psql \watch 2nd argument: iteration count

On Tue, Mar 14, 2023 at 01:58:59PM +0900, Kyotaro Horiguchi wrote:

+				if (*opt_end)
+					pg_log_error("\\watch: incorrect interval value '%s'", opt);
+				else if (errno == ERANGE)
+					pg_log_error("\\watch: out-of-range interval value '%s'", opt);
+				else
+					pg_log_error("\\watch: interval value '%s' less than zero", opt);

I'm not sure if we need error messages for that resolution and I'm a
bit happier to have fewer messages to translate:p. Merging the cases
of ERANGE and negative values might be better. And I think we usually
refer to unparsable input as "invalid".

if (*opt_end)
pg_log_error("\\watch: invalid interval value '%s'", opt);
else
pg_log_error("\\watch: interval value '%s' out of range", opt);

+1, I don't think it's necessary to complicate these error messages too
much. This code hasn't reported errors for nearly 10 years, and I'm not
aware of any complaints. I ѕtill think we could simplify this to "\watch:
invalid delay interval: %s" and call it a day.

--
Nathan Bossart
Amazon Web Services: https://aws.amazon.com

#19Kyotaro Horiguchi
horikyota.ntt@gmail.com
In reply to: Nathan Bossart (#18)
Re: psql \watch 2nd argument: iteration count

At Tue, 14 Mar 2023 12:03:00 -0700, Nathan Bossart <nathandbossart@gmail.com> wrote in

On Tue, Mar 14, 2023 at 01:58:59PM +0900, Kyotaro Horiguchi wrote:

+				if (*opt_end)
+					pg_log_error("\\watch: incorrect interval value '%s'", opt);
+				else if (errno == ERANGE)
+					pg_log_error("\\watch: out-of-range interval value '%s'", opt);
+				else
+					pg_log_error("\\watch: interval value '%s' less than zero", opt);

I'm not sure if we need error messages for that resolution and I'm a
bit happier to have fewer messages to translate:p. Merging the cases
of ERANGE and negative values might be better. And I think we usually
refer to unparsable input as "invalid".

if (*opt_end)
pg_log_error("\\watch: invalid interval value '%s'", opt);
else
pg_log_error("\\watch: interval value '%s' out of range", opt);

+1, I don't think it's necessary to complicate these error messages too
much. This code hasn't reported errors for nearly 10 years, and I'm not
aware of any complaints. I till think we could simplify this to "\watch:
invalid delay interval: %s" and call it a day.

I hesitated to propose such a level of simplification, but basically I
was alsothinking the same thing.

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#20Michael Paquier
michael@paquier.xyz
In reply to: Kyotaro Horiguchi (#19)
Re: psql \watch 2nd argument: iteration count

On Wed, Mar 15, 2023 at 10:19:28AM +0900, Kyotaro Horiguchi wrote:

I hesitated to propose such a level of simplification, but basically I
was alsothinking the same thing.

Okay, fine by me to use one single message. I'd rather still keep the
three tests, though, as they check the three conditions upon which the
error would be triggered.
--
Michael

#21Andrey Borodin
amborodin86@gmail.com
In reply to: Michael Paquier (#20)
2 attachment(s)
Re: psql \watch 2nd argument: iteration count

On Tue, Mar 14, 2023 at 6:25 PM Michael Paquier <michael@paquier.xyz> wrote:

On Wed, Mar 15, 2023 at 10:19:28AM +0900, Kyotaro Horiguchi wrote:

I hesitated to propose such a level of simplification, but basically I
was alsothinking the same thing.

+1

Okay, fine by me to use one single message. I'd rather still keep the
three tests, though, as they check the three conditions upon which the
error would be triggered.

PFA v8. Thanks!

Best regards, Andrey Borodin.

Attachments:

v8-0001-Fix-incorrect-argument-handling-in-psql-watch.patchapplication/octet-stream; name=v8-0001-Fix-incorrect-argument-handling-in-psql-watch.patchDownload
From a01f2a2edbd17a1853da29d0a9dc2ea969446ec4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 14 Mar 2023 09:21:32 +0900
Subject: [PATCH v8 1/2] Fix incorrect argument handling in psql \watch

Incorrectly parsed argument was silently substituted with 1 second.
This is changed to proper error message.

Authour: Andrey Borodin <amborodin@acm.org>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Thread: https://postgr.es/m/CAAhFRxiZ2-n_L1ErMm9AZjgmUK%3DqS6VHb%2B0SaMn8sqqbhF7How%40mail.gmail.com
---
 src/bin/psql/command.c      | 16 +++++++++++++---
 src/bin/psql/t/001_basic.pl | 17 +++++++++++++++++
 2 files changed, 30 insertions(+), 3 deletions(-)

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..cd4c9bfd07 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -2776,9 +2776,16 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		/* Convert optional sleep-length argument */
 		if (opt)
 		{
-			sleep = strtod(opt, NULL);
-			if (sleep <= 0)
-				sleep = 1;
+			char *opt_end;
+			sleep = strtod(opt, &opt_end);
+			if (sleep < 0 || *opt_end || errno == ERANGE)
+			{
+				pg_log_error("\\watch: incorrect interval value '%s'", opt);
+				free(opt);
+				resetPQExpBuffer(query_buf);
+				psql_scan_reset(scan_state);
+				return PSQL_CMD_ERROR;
+			}
 			free(opt);
 		}
 
@@ -5183,6 +5190,9 @@ do_watch(PQExpBuffer query_buf, double sleep)
 		if (pagerpipe && ferror(pagerpipe))
 			break;
 
+		if (sleep == 0)
+			continue;
+
 #ifdef WIN32
 
 		/*
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index 0f394420b2..64ce012062 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -350,4 +350,21 @@ psql_like(
 	'\copy from with DEFAULT'
 );
 
+# Check \watch errors
+psql_fails_like(
+	$node,
+	'SELECT 1;\watch -10',
+	qr/incorrect interval value '-10'/,
+	'\watch, negative interval');
+psql_fails_like(
+	$node,
+	'SELECT 1;\watch 10ab',
+	qr/incorrect interval value '10ab'/,
+	'\watch incorrect interval');
+psql_fails_like(
+	$node,
+	'SELECT 1;\watch 10e400',
+	qr/incorrect interval value '10e400'/,
+	'\watch out-of-range interval');
+
 done_testing();
-- 
2.32.0 (Apple Git-132)

v8-0002-Add-iteration-count-argument-to-psql-watch-comman.patchapplication/octet-stream; name=v8-0002-Add-iteration-count-argument-to-psql-watch-comman.patchDownload
From 10ddab98ffbe26eb1d4686f66b5aef3777fc9b5b 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 v8 2/2] Add iteration count argument to psql \watch command

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

Authour: Andrey Borodin <amborodin@acm.org>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
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             | 88 +++++++++++++++++++++++++-----
 src/bin/psql/help.c                |  2 +-
 src/bin/psql/t/001_basic.pl        | 10 ++++
 src/test/regress/expected/psql.out |  2 +-
 src/test/regress/sql/psql.sql      |  2 +-
 6 files changed, 93 insertions(+), 17 deletions(-)

diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7b8ae9fac3..94323fdf94 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">seconds</replaceable> [ <replaceable class="parameter">iterations</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 cd4c9bfd07..6b452f6c49 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,28 +2772,83 @@ 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)
 		{
+			/*
+			 * 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;
-			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)
+				{
+					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)
+				{
+					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);
+				}
+			}
+			else
+			{
+				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);
@@ -5054,7 +5110,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;
@@ -5156,7 +5212,7 @@ 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];
@@ -5190,6 +5246,12 @@ do_watch(PQExpBuffer query_buf, double sleep)
 		if (pagerpipe && ferror(pagerpipe))
 			break;
 
+		/* If we have iteration count - check that it's not exceeded yet */
+		if (iter && (i++ == iter))
+		{
+			break;
+		}
+
 		if (sleep == 0)
 			continue;
 
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..9561f15a71 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 [SEC [N]]       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..ece67d8746 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -366,5 +366,15 @@ psql_fails_like(
 	'SELECT 1;\watch 10e400',
 	qr/incorrect interval value '10e400'/,
 	'\watch out-of-range interval');
+psql_fails_like(
+	$node,
+	'SELECT 1;\watch i=-10',
+	qr/incorrect interval value '-10'/,
+	'\watch, negative interval');
+psql_fails_like(
+	$node,
+	'SELECT 1;\watch c=-10',
+	qr/incorrect iteration count '-10'/,
+	'\watch, negative count');
 
 done_testing();
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)

#22Michael Paquier
michael@paquier.xyz
In reply to: Andrey Borodin (#21)
Re: psql \watch 2nd argument: iteration count

On Tue, Mar 14, 2023 at 08:20:23PM -0700, Andrey Borodin wrote:

PFA v8. Thanks!

Looks OK to me. I've looked as well at resetting query_buffer on
failure, which I guess is better this way because this is an
accumulation of the previous results, right?
--
Michael

#23Nathan Bossart
nathandbossart@gmail.com
In reply to: Michael Paquier (#22)
Re: psql \watch 2nd argument: iteration count
+			sleep = strtod(opt, &opt_end);
+			if (sleep < 0 || *opt_end || errno == ERANGE)

Should we set errno to 0 before calling strtod()?

--
Nathan Bossart
Amazon Web Services: https://aws.amazon.com

#24Michael Paquier
michael@paquier.xyz
In reply to: Nathan Bossart (#23)
Re: psql \watch 2nd argument: iteration count

On Tue, Mar 14, 2023 at 09:23:48PM -0700, Nathan Bossart wrote:

+			sleep = strtod(opt, &opt_end);
+			if (sleep < 0 || *opt_end || errno == ERANGE)

Should we set errno to 0 before calling strtod()?

Yep. You are right.
--
Michael

#25Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#24)
Re: psql \watch 2nd argument: iteration count

On Wed, Mar 15, 2023 at 04:58:49PM +0900, Michael Paquier wrote:

Yep. You are right.

Fixed that and applied 0001.

+    valptr++;
+    if (strncmp("i", opt, strlen("i")) == 0 ||
+            strncmp("interval", opt, strlen("interval")) == 0)
+    {

Did you look at process_command_g_options() and if some consolidation
was possible? It would be nice to have APIs shaped so as more
sub-commands could rely on the same facility in the future.

-        <term><literal>\watch [ <replaceable class="parameter">seconds</replaceable> ]</literal></term>
+        <term><literal>\watch [ <replaceable class="parameter">seconds</replaceable> [ <replaceable class="parameter">iterations</replaceable> ] ]</literal></term>

This set of changes is not reflected in the documentation.

With an interval in place, we could now automate some tests with
\watch where it does not fail. What do you think about adding a test
with a simple query, an interval of 0s and one iteration?
--
Michael

#26Andrey Borodin
amborodin86@gmail.com
In reply to: Michael Paquier (#25)
1 attachment(s)
Re: psql \watch 2nd argument: iteration count

On Wed, Mar 15, 2023 at 5:54 PM Michael Paquier <michael@paquier.xyz> wrote:

On Wed, Mar 15, 2023 at 04:58:49PM +0900, Michael Paquier wrote:

Yep. You are right.

Fixed that and applied 0001.

Great, thanks!

+    valptr++;
+    if (strncmp("i", opt, strlen("i")) == 0 ||
+            strncmp("interval", opt, strlen("interval")) == 0)
+    {

Did you look at process_command_g_options() and if some consolidation
was possible? It would be nice to have APIs shaped so as more
sub-commands could rely on the same facility in the future.

I've tried, but they behave so differently. I could reuse only the
"char *valptr = strchr(opt, '=');" thing from there :)
And process_command_g_options() changes data in-place...
Actually, I'm not sure having "i" == "interval" and "c"=="count" is a
good idea here too. I mean I like it, but is it coherent?
Also I do not like repeating 4 times this 5 lines
+ pg_log_error("\\watch: incorrect interval value '%s'", valptr);
+ free(opt);
+ resetPQExpBuffer(query_buf);
+ psql_scan_reset(scan_state);
+ return PSQL_CMD_ERROR;
But I hesitate defining a new function for this...
-        <term><literal>\watch [ <replaceable class="parameter">seconds</replaceable> ]</literal></term>
+        <term><literal>\watch [ <replaceable class="parameter">seconds</replaceable> [ <replaceable class="parameter">iterations</replaceable> ] ]</literal></term>

This set of changes is not reflected in the documentation.

Done.

With an interval in place, we could now automate some tests with
\watch where it does not fail. What do you think about adding a test
with a simple query, an interval of 0s and one iteration?

Done. Also found a bug that we actually were doing N+1 iterations.

Thank you for working on this!

Best regards, Andrey Borodin.

Attachments:

v9-0001-Iteration-count-argument-to-psql-watch-command.patchapplication/octet-stream; name=v9-0001-Iteration-count-argument-to-psql-watch-command.patchDownload
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)

#27Yugo NAGATA
nagata@sraoss.co.jp
In reply to: Andrey Borodin (#26)
Re: psql \watch 2nd argument: iteration count

Hello,

On Thu, 16 Mar 2023 21:15:30 -0700
Andrey Borodin <amborodin86@gmail.com> wrote:

On Wed, Mar 15, 2023 at 5:54 PM Michael Paquier <michael@paquier.xyz> wrote:

On Wed, Mar 15, 2023 at 04:58:49PM +0900, Michael Paquier wrote:

Yep. You are right.

Fixed that and applied 0001.

Great, thanks!

+    valptr++;
+    if (strncmp("i", opt, strlen("i")) == 0 ||
+            strncmp("interval", opt, strlen("interval")) == 0)
+    {

Did you look at process_command_g_options() and if some consolidation
was possible? It would be nice to have APIs shaped so as more
sub-commands could rely on the same facility in the future.

I've tried, but they behave so differently. I could reuse only the
"char *valptr = strchr(opt, '=');" thing from there :)
And process_command_g_options() changes data in-place...
Actually, I'm not sure having "i" == "interval" and "c"=="count" is a
good idea here too. I mean I like it, but is it coherent?
Also I do not like repeating 4 times this 5 lines
+ pg_log_error("\\watch: incorrect interval value '%s'", valptr);
+ free(opt);
+ resetPQExpBuffer(query_buf);
+ psql_scan_reset(scan_state);
+ return PSQL_CMD_ERROR;
But I hesitate defining a new function for this...
-        <term><literal>\watch [ <replaceable class="parameter">seconds</replaceable> ]</literal></term>
+        <term><literal>\watch [ <replaceable class="parameter">seconds</replaceable> [ <replaceable class="parameter">iterations</replaceable> ] ]</literal></term>

This set of changes is not reflected in the documentation.

Done.

With an interval in place, we could now automate some tests with
\watch where it does not fail. What do you think about adding a test
with a simple query, an interval of 0s and one iteration?

Done. Also found a bug that we actually were doing N+1 iterations.

Here is my review on the v9 patch.

+                   /* we do not prevent numerous names iterations like i=1 i=1 i=1 */
+                   have_sleep = true;

Why this is allowed here? I am not sure there is any reason to allow to specify
multiple "interval" options. (I would apologize it if I missed past discussion.)

+               if (sleep < 0 || *opt_end || errno == ERANGE || have_sleep)
+               {
+                   pg_log_error("\\watch: incorrect interval value '%s'", 

Here, specifying an explicit "interval" option before an interval second without
option is prohibited.

postgres=# select 1 \watch interval=3 4
\watch: incorrect interval value '4'

I think it is ok, but this error message seems not user-friendly because,
in the above example, interval values itself is correct, but it seems just
a syntax error. I wonder it is better to use "watch interval must be specified
only once" or such here, as the past patch.

+        <para>
+        If number of iterations is specified - query will be executed only
+        given number of times.
+        </para>

Is it common to use "-" here? I think using comma like
"If number of iterations is specified, "
is natural.

Regards,
Yugo Nagata

--
Yugo NAGATA <nagata@sraoss.co.jp>

#28Andrey Borodin
amborodin86@gmail.com
In reply to: Yugo NAGATA (#27)
1 attachment(s)
Re: psql \watch 2nd argument: iteration count

On Thu, Mar 23, 2023 at 10:15 PM Yugo NAGATA <nagata@sraoss.co.jp> wrote:

Here is my review on the v9 patch.

+                   /* we do not prevent numerous names iterations like i=1 i=1 i=1 */
+                   have_sleep = true;

Why this is allowed here? I am not sure there is any reason to allow to specify
multiple "interval" options. (I would apologize it if I missed past discussion.)

I do not know, it just seems normal to me. I've fixed this.

postgres=# select 1 \watch interval=3 4
\watch: incorrect interval value '4'

I think it is ok, but this error message seems not user-friendly because,
in the above example, interval values itself is correct, but it seems just
a syntax error. I wonder it is better to use "watch interval must be specified
only once" or such here, as the past patch.

Done.

+        <para>
+        If number of iterations is specified - query will be executed only
+        given number of times.
+        </para>

Is it common to use "-" here? I think using comma like
"If number of iterations is specified, "
is natural.

Done.

Thank for the review!

Best regards, Andrey Borodin.

Attachments:

v10-0001-Iteration-count-argument-to-psql-watch-command.patchapplication/octet-stream; name=v10-0001-Iteration-count-argument-to-psql-watch-command.patchDownload
From a55836c699abcc657e0a85ca2f6dd4e03ecf9f4d 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 v10] 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, Yugo Nagata
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             | 105 ++++++++++++++++++++++++-----
 src/bin/psql/help.c                |   2 +-
 src/bin/psql/t/001_basic.pl        |  21 +++++-
 src/test/regress/expected/psql.out |   2 +-
 src/test/regress/sql/psql.sql      |   2 +-
 6 files changed, 117 insertions(+), 21 deletions(-)

diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7b8ae9fac3..35944af97f 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..cdfafac7a9 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,97 @@ 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;
+		bool 		have_iter = 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 || have_sleep)
+					{
+						if (have_sleep)
+							pg_log_error("\\watch: interval value is specified more than once");
+						else
+							pg_log_error("\\watch: incorrect interval value '%s'", valptr);
+						free(opt);
+						resetPQExpBuffer(query_buf);
+						psql_scan_reset(scan_state);
+						return PSQL_CMD_ERROR;
+					}
+					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 || have_iter)
+					{
+						if (have_iter)
+							pg_log_error("\\watch: iteration count is specified more than once");
+						else
+							pg_log_error("\\watch: incorrect iteration count '%s'", valptr);
+						free(opt);
+						resetPQExpBuffer(query_buf);
+						psql_scan_reset(scan_state);
+						return PSQL_CMD_ERROR;
+					}
+					have_iter = true;
+				}
+				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)
+				{
+					if (have_sleep)
+						pg_log_error("\\watch: interval value is specified more than once");
+					else
+						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 +5124,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 +5226,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..b5cf7b1b43 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,
@@ -360,11 +367,21 @@ psql_fails_like(
 	$node,
 	'SELECT 1;\watch 10ab',
 	qr/incorrect interval value '10ab'/,
-	'\watch incorrect interval');
+	'\watch, incorrect interval');
 psql_fails_like(
 	$node,
 	'SELECT 1;\watch 10e400',
 	qr/incorrect interval value '10e400'/,
-	'\watch out-of-range interval');
+	'\watch, out-of-range interval');
+psql_fails_like(
+	$node,
+	'SELECT 1;\watch 1 1',
+	qr/nterval value is specified more than once/,
+	'\watch, interval value is specified more than once');
+psql_fails_like(
+	$node,
+	'SELECT 1;\watch c=1 c=1',
+	qr/iteration count is specified more than once/,
+	'\watch, iteration count is specified more than once');
 
 done_testing();
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.37.1 (Apple Git-137.1)

#29Kirk Wolak
wolakk@gmail.com
In reply to: Andrey Borodin (#28)
Re: psql \watch 2nd argument: iteration count

On Fri, Mar 24, 2023 at 10:32 PM Andrey Borodin <amborodin86@gmail.com>
wrote:

On Thu, Mar 23, 2023 at 10:15 PM Yugo NAGATA <nagata@sraoss.co.jp> wrote:

Here is my review on the v9 patch.

+ /* we do not prevent numerous names iterations like

i=1 i=1 i=1 */

+ have_sleep = true;

Why this is allowed here? I am not sure there is any reason to allow to

specify

multiple "interval" options. (I would apologize it if I missed past

discussion.)
I do not know, it just seems normal to me. I've fixed this.

postgres=# select 1 \watch interval=3 4
\watch: incorrect interval value '4'

I think it is ok, but this error message seems not user-friendly because,
in the above example, interval values itself is correct, but it seems

just

a syntax error. I wonder it is better to use "watch interval must be

specified

only once" or such here, as the past patch.

Done.

+        <para>
+        If number of iterations is specified - query will be executed

only

+ given number of times.
+ </para>

Is it common to use "-" here? I think using comma like
"If number of iterations is specified, "
is natural.

Done.

Thank for the review!

Best regards, Andrey Borodin.

Okay, I tested this. It handles bad param names, values correctly. Nice
Feature, especially if you leave a 1hr task running and you need to step
away...
Built/Reviewed the Docs. They are correct.
Reviewed \? command. It has the parameters updated, shown as optional

Marked as Ready for Committer.

#30Tom Lane
tgl@sss.pgh.pa.us
In reply to: Kirk Wolak (#29)
Re: psql \watch 2nd argument: iteration count

Kirk Wolak <wolakk@gmail.com> writes:

Marked as Ready for Committer.

Pushed with a pretty fair number of cosmetic changes.

One non-cosmetic change I made is that I didn't agree with your
interpretation of the execution count. IMO this ought to produce
three executions:

regression=# select 1 \watch c=3
Thu Apr 6 13:17:50 2023 (every 2s)

?column?
----------
1
(1 row)

Thu Apr 6 13:17:52 2023 (every 2s)

?column?
----------
1
(1 row)

Thu Apr 6 13:17:54 2023 (every 2s)

?column?
----------
1
(1 row)

regression=#

If you write a semicolon first, you get four, but it's the semicolon
producing the first result not \watch.

regards, tom lane

#31Andrey Borodin
amborodin86@gmail.com
In reply to: Tom Lane (#30)
Re: psql \watch 2nd argument: iteration count

On Thu, Apr 6, 2023 at 10:23 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Kirk Wolak <wolakk@gmail.com> writes:

Marked as Ready for Committer.

Pushed with a pretty fair number of cosmetic changes.

Great, thank you!

If you write a semicolon first, you get four, but it's the semicolon
producing the first result not \watch.

I did not know that. Well, I knew it in parts, but did not understand
as a whole. Thanks!

Best regards, Andrey Borodin.

#32Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Michael Paquier (#11)
Re: psql \watch 2nd argument: iteration count

On 13.03.23 02:17, Michael Paquier wrote:

On Sun, Mar 12, 2023 at 01:05:39PM -0700, Andrey Borodin wrote:

In the review above Kyotaro-san suggested that message should contain
information on what it expects... So, maybe then
pg_log_error("\\watch interval must be non-negative number, but
argument is '%s'", opt); ?
Or perhaps with articles? pg_log_error("\\watch interval must be a
non-negative number, but the argument is '%s'", opt);

-       HELP0("  \\watch [SEC]           execute query every SEC seconds\n");
+       HELP0("  \\watch [SEC [N]]       execute query every SEC seconds N times\n");

Is that really the interface we'd want to work with in the long-term?
For one, this does not give the option to specify only an interval
while relying on the default number of seconds. This may be fine, but
it does not strike me as the best choice. How about doing something
more extensible, for example:
\watch [ (option=value [, option=value] .. ) ] [SEC]

I am not sure that this will be the last option we'll ever add to
\watch, so I'd rather have us choose a design more flexible than
what's proposed here, in a way similar to \g or \gx.

On the other hand, we also have option syntax in \connect that is like
-foo. Would that be a better match here? We should maybe decide before
we diverge and propagate two different option syntaxes in backslash
commands.

#33Tom Lane
tgl@sss.pgh.pa.us
In reply to: Peter Eisentraut (#32)
Re: psql \watch 2nd argument: iteration count

Peter Eisentraut <peter.eisentraut@enterprisedb.com> writes:

On 13.03.23 02:17, Michael Paquier wrote:

I am not sure that this will be the last option we'll ever add to
\watch, so I'd rather have us choose a design more flexible than
what's proposed here, in a way similar to \g or \gx.

On the other hand, we also have option syntax in \connect that is like
-foo. Would that be a better match here? We should maybe decide before
we diverge and propagate two different option syntaxes in backslash
commands.

Reasonable point to raise, but I think \connect's -reuse-previous
is in the minority. \connect itself can use option=value syntax
in the conninfo string (in fact, I guess -reuse-previous was spelled
that way in hopes of not being confusable with a conninfo option).
We also have option=value in the \g and \gx commands. I don't see
any other psql metacommands that use options spelled like -foo.

In short, I'm satisfied with the current answer. There's still
time to debate it though.

regards, tom lane