From: jerry Subject: vi(1) bug executing buffer with range fixed To: tech@openbsd.org Date: Sat, 6 Sep 2025 23:15:34 +0200 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@. * * 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));