Index | Thread | Search

From:
jerry <mail@jerryfletcher.org>
Subject:
vi(1) bug executing buffer with range fixed
To:
tech@openbsd.org
Date:
Sat, 6 Sep 2025 23:15:34 +0200

Download raw body.

Thread
Hello,

while using vi(1) I found a bug related to executing buffers that I
managed to fix.
The bug happens also in nvi(1) from ports.
This bug is related to executing the content of a buffer specifying a
range, that is a feature documented in the code, but not in the man
page nor in the reference manual, so I also added some documentations.

The steps that cause the bug:
* copy to a buffer some ex commands, for example to buffer a: s/^/# /
* execute the buffer specifying a range -> :.,+10@a
* try to undo the changes -> u

Undoing the changes will result in just the last line being undone,
and in order to undo all the lines the . will have to be pressed
many times.
The correct behaviour should be as if executing directly :.,+10s/^/# /
then doing u, so restoring all the lines changed.

The execution of a buffer with a range was documented in ex/ex_at.c:
/*
 * !!!
 * Historically the @ command took a range of lines, and the @ buffer
 * was executed once per line.  The historic vi could be trashed by
 * this because it didn't notice if the underlying file changed, or,
 * for that matter, if there were no more lines on which to operate.
 * For example, take a 10 line file, load "%delete" into a buffer,
 * and enter :8,10@<buffer>.
 *
 * The solution is a bit tricky.  If the user specifies a range, take
 * the same approach as for global commands, and discard the command
 * if exit or switch to a new file/screen.  If the user doesn't specify
 * the range, continue to execute after a file/screen switch, which
 * means @ buffers are still useful in a multi-screen environment.
 */

Bye,
jerry

diff --git common/screen.h common/screen.h
index 4fdf33bdc26..447cdcfcaa8 100644
--- common/screen.h
+++ common/screen.h
@@ -187,16 +187,17 @@ struct _scr {
 #define	SC_AT_SET	0x00008000	/* Last at buffer set. */
 #define	SC_COMEDIT	0x00010000	/* Colon command-line edit window. */
 #define	SC_EX_GLOBAL	0x00020000	/* Ex: executing a global command. */
-#define	SC_EX_SILENT	0x00040000	/* Ex: batch script. */
-#define	SC_EX_WAIT_NO	0x00080000	/* Ex: don't wait for the user. */
-#define	SC_EX_WAIT_YES	0x00100000	/* Ex:    do wait for the user. */
-#define	SC_READONLY	0x00200000	/* Persistent readonly state. */
-#define	SC_RE_SEARCH	0x00400000	/* Search RE has been compiled. */
-#define	SC_RE_SUBST	0x00800000	/* Substitute RE has been compiled. */
-#define	SC_SCRIPT	0x01000000	/* Shell script window. */
-#define	SC_STATUS	0x02000000	/* Welcome message. */
-#define	SC_STATUS_CNT	0x04000000	/* Welcome message plus file count. */
-#define	SC_TINPUT	0x08000000	/* Doing text input. */
-#define	SC_TINPUT_INFO	0x10000000	/* Doing text input on info line. */
+#define	SC_EX_AT	0x00040000	/* Ex: executing a buffer with range. */
+#define	SC_EX_SILENT	0x00080000	/* Ex: batch script. */
+#define	SC_EX_WAIT_NO	0x00100000	/* Ex: don't wait for the user. */
+#define	SC_EX_WAIT_YES	0x00200000	/* Ex:    do wait for the user. */
+#define	SC_READONLY	0x00400000	/* Persistent readonly state. */
+#define	SC_RE_SEARCH	0x00800000	/* Search RE has been compiled. */
+#define	SC_RE_SUBST	0x01000000	/* Substitute RE has been compiled. */
+#define	SC_SCRIPT	0x02000000	/* Shell script window. */
+#define	SC_STATUS	0x04000000	/* Welcome message. */
+#define	SC_STATUS_CNT	0x08000000	/* Welcome message plus file count. */
+#define	SC_TINPUT	0x10000000	/* Doing text input. */
+#define	SC_TINPUT_INFO	0x20000000	/* Doing text input on info line. */
 	u_int32_t flags;
 };
diff --git docs/USD.doc/vi.man/vi.1 docs/USD.doc/vi.man/vi.1
index 585e033bce6..9f737185315 100644
--- docs/USD.doc/vi.man/vi.1
+++ docs/USD.doc/vi.man/vi.1
@@ -1689,8 +1689,16 @@ A comment.
 .Xc
 Display the selected lines, each preceded with its line number.
 .Pp
-.It Cm @ Ar buffer
-.It Cm * Ar buffer
+.It Xo
+.Op Ar range
+.Cm @
+.Ar buffer
+.Xc
+.It Xo
+.Op Ar range
+.Cm *
+.Ar buffer
+.Xc
 Execute a buffer.
 .Pp
 .It Xo
diff --git docs/USD.doc/vi.ref/ex.cmd.roff docs/USD.doc/vi.ref/ex.cmd.roff
index 196340e91fa..b0943e872e1 100644
--- docs/USD.doc/vi.ref/ex.cmd.roff
+++ docs/USD.doc/vi.ref/ex.cmd.roff
@@ -314,9 +314,9 @@ Affected by the
 option.
 .SE
 .KY "@"
-.IP "@ buffer"
+.IP "[range] @ buffer"
 .KY "*"
-.Ip "* buffer"
+.Ip "[range] * buffer"
 Execute a buffer.
 Each line in the named buffer is executed as an
 .CO ex
@@ -326,6 +326,8 @@ If no buffer is specified, or if the specified buffer is
 or
 .QT * ,
 the last buffer executed is used.
+If the range is specified, discard the command if switch to a new file/screen.
+Without the range continue to execute after a file/screen switch.
 .KY <
 .IP "[range] <[< ...] [count] [flags]"
 Shift lines left or right.
diff --git ex/ex.c ex/ex.c
index 9aa31ad29fb..82d88f9b525 100644
--- ex/ex.c
+++ ex/ex.c
@@ -1336,7 +1336,7 @@ addr_verify:
 	 * If file state available, and not doing a global command,
 	 * log the start of an action.
 	 */
-	if (sp->ep != NULL && !F_ISSET(sp, SC_EX_GLOBAL))
+	if (sp->ep != NULL && !F_ISSET(sp, SC_EX_GLOBAL | SC_EX_AT))
 		(void)log_cursor(sp);
 
 	/*
@@ -1600,8 +1600,9 @@ rsuccess:	tmp = 0;
 	/* Turn off any file name error information. */
 	gp->if_name = NULL;
 
-	/* Turn off the global bit. */
+	/* Turn off the global and at bits. */
 	F_CLR(sp, SC_EX_GLOBAL);
+	F_CLR(sp, SC_EX_AT);
 
 	return (tmp);
 }
@@ -2056,6 +2057,7 @@ ex_load(SCR *sp)
 	RANGE *rp;
 
 	F_CLR(sp, SC_EX_GLOBAL);
+	F_CLR(sp, SC_EX_AT);
 
 	/*
 	 * Lose any exhausted commands.  We know that the first command
@@ -2136,6 +2138,8 @@ ex_load(SCR *sp)
 
 	if (FL_ISSET(ecp->agv_flags, AGV_GLOBAL | AGV_V))
 		F_SET(sp, SC_EX_GLOBAL);
+	if (FL_ISSET(ecp->agv_flags, AGV_AT))
+		F_SET(sp, SC_EX_AT);
 	return (0);
 }
 
diff --git ex/ex_at.c ex/ex_at.c
index dd1b2b90813..87ac4e76c57 100644
--- ex/ex_at.c
+++ ex/ex_at.c
@@ -76,7 +76,7 @@ ex_at(SCR *sp, EXCMD *cmdp)
 	 * The solution is a bit tricky.  If the user specifies a range, take
 	 * the same approach as for global commands, and discard the command
 	 * if exit or switch to a new file/screen.  If the user doesn't specify
-	 * the  range, continue to execute after a file/screen switch, which
+	 * the range, continue to execute after a file/screen switch, which
 	 * means @ buffers are still useful in a multi-screen environment.
 	 */
 	CALLOC_RET(sp, ecp, 1, sizeof(EXCMD));