[BusyBox] [PATCH] vi-editing mode for ash

Paul Fox pgf at brightstareng.com
Sat Nov 13 21:48:22 UTC 2004


hi --

as a long time vi user, and an editor curmudgeon in general, i
missed having a vi-mode in ash when doing development on or when
using a busybox-based system, so i finally implemented it.

it's not perfect, of course:  no undo, no searching, the 'w' and
'b' commands are actually 'W' and 'B' (vi users will know what i
mean), but it does have 'p', so you can put back what you've
deleted (which was necessary to be able to enter "xp", something
my typing demands that i do pretty often :-).

it builds on top of emacs-mode -- so most (all?) of the emacs
commands (e.g. ^A, ^F, ^B, ^N, ^P) still work while you're
"inserting" in vi-mode.  this is either a bug or a feature, but
mostly it's just a fallout of the implementation.

turning on CONFIG_FEATURE_COMMAND_EDITING_VI adds another couple
of kbytes on an x86 system (which is roughly 25% more than
emacs-mode by itself).

with luck, i'm not the only person to think this is useful or
worthwhile, and hopefully someone else can try it and offer a
comment.

anyway, hope it can be included...  just trying to give back to
an excellent project!

(the patch is against current CVS)

paul, pgf at brightstareng.com, pgf at foxharp.boston.ma.us
------------------------------

Index: shell/Config.in
===================================================================
RCS file: /var/cvs/busybox/shell/Config.in,v
retrieving revision 1.18
diff -u -r1.18 Config.in
--- shell/Config.in	24 Sep 2004 09:09:44 -0000	1.18
+++ shell/Config.in	13 Nov 2004 18:56:51 -0000
@@ -190,6 +190,14 @@
 	help
 	  Enable command editing in shell.
 
+config CONFIG_FEATURE_COMMAND_EDITING_VI
+	bool "vi-style line editing commands"
+	default n
+	depends on CONFIG_FEATURE_COMMAND_EDITING
+	help
+	  Enable vi-style line editing in the shell.  This mode can be
+	  turned on and off with "set -o vi" and "set +o vi".
+
 config CONFIG_FEATURE_COMMAND_HISTORY
 	int "history size"
 	default 15
Index: shell/ash.c
===================================================================
RCS file: /var/cvs/busybox/shell/ash.c,v
retrieving revision 1.108
diff -u -r1.108 ash.c
--- shell/ash.c	8 Oct 2004 09:43:34 -0000	1.108
+++ shell/ash.c	13 Nov 2004 18:56:53 -0000
@@ -1948,19 +1948,21 @@
 #define bflag optlist[11]
 #define uflag optlist[12]
 #define qflag optlist[13]
+#define viflag optlist[14]
 
 #ifdef DEBUG
-#define nolog optlist[14]
-#define debug optlist[15]
-#define NOPTS   16
-#else
-#define NOPTS   14
+#define nolog optlist[15]
+#define debug optlist[16]
+#endif
+
+#ifndef CONFIG_FEATURE_COMMAND_EDITING_VI
+#define setvimode(on) viflag = 0   /* forcibly keep the option off */
 #endif
 
 /*      $NetBSD: options.c,v 1.33 2003/01/22 20:36:04 dsl Exp $ */
 
 
-static const char *const optletters_optnames[NOPTS] = {
+static const char *const optletters_optnames[] = {
 	"e"   "errexit",
 	"f"   "noglob",
 	"I"   "ignoreeof",
@@ -1975,6 +1977,7 @@
 	"b"   "notify",
 	"u"   "nounset",
 	"q"   "quietprofile",
+	"\0"  "vi",
 #ifdef DEBUG
 	"\0"  "nolog",
 	"\0"  "debug",
@@ -1984,6 +1987,7 @@
 #define optletters(n) optletters_optnames[(n)][0]
 #define optnames(n) (&optletters_optnames[(n)][1])
 
+#define NOPTS (sizeof(optletters_optnames)/sizeof(optletters_optnames[0]))
 
 static char optlist[NOPTS];
 
@@ -8856,6 +8860,7 @@
 #endif
 	setinteractive(iflag);
 	setjobctl(mflag);
+	setvimode(viflag);
 }
 
 static inline void
Index: shell/cmdedit.c
===================================================================
RCS file: /var/cvs/busybox/shell/cmdedit.c,v
retrieving revision 1.93
diff -u -r1.93 cmdedit.c
--- shell/cmdedit.c	19 Aug 2004 18:22:13 -0000	1.93
+++ shell/cmdedit.c	13 Nov 2004 18:56:54 -0000
@@ -441,27 +441,61 @@
 	input_backward(back_cursor);
 }
 
-/* Delete the char in front of the cursor */
-static void input_delete(void)
+#ifdef CONFIG_FEATURE_COMMAND_EDITING_VI
+static char delbuf[BUFSIZ];  /* a place to store deleted characters */
+static char *delp = delbuf;
+static int newdelflag;	    /* whether delbuf should be reused yet */
+#endif
+
+/* Delete the char in front of the cursor, optionally saving it
+ * for later putback */
+static void input_delete(int save)
 {
 	int j = cursor;
 
 	if (j == len)
 		return;
 
+#ifdef CONFIG_FEATURE_COMMAND_EDITING_VI
+	if (save) {
+		if (newdelflag) {
+			delp = delbuf;
+			newdelflag = 0;
+		}
+		if (delp - delbuf < BUFSIZ)
+			*delp++ = command_ps[j];
+	}
+#endif
+
 	strcpy(command_ps + j, command_ps + j + 1);
 	len--;
-	input_end();                    /* rewtite new line */
+	input_end();                    /* rewrite new line */
 	cmdedit_set_out_char(0);        /* destroy end char */
 	input_backward(cursor - j);     /* back to old pos cursor */
 }
 
+#ifdef CONFIG_FEATURE_COMMAND_EDITING_VI
+static void put(void)
+{
+	int ocursor, j = delp - delbuf;
+	if (j == 0)
+		return;
+	ocursor = cursor;
+	/* open hole and then fill it */
+	memmove(command_ps + cursor + j, command_ps + cursor, len - cursor + 1);
+	strncpy(command_ps + cursor, delbuf, j);
+	len += j;
+	input_end();                    /* rewrite new line */
+	input_backward(cursor-ocursor-j+1); /* at end of new text */
+}
+#endif
+
 /* Delete the char in back of the cursor */
 static void input_backspace(void)
 {
 	if (cursor > 0) {
 		input_backward(1);
-		input_delete();
+		input_delete(0);
 	}
 }
 
@@ -473,7 +507,6 @@
 		cmdedit_set_out_char(command_ps[cursor + 1]);
 }
 
-
 static void cmdedit_setwidth(int w, int redraw_flg)
 {
 	cmdedit_termw = cmdedit_prmt_len + 2;
@@ -1217,18 +1250,54 @@
  * ESC-h -- Delete forward one word
  * CTL-t -- Transpose two characters
  *
- * Furthermore, the "vi" command editing keys are not implemented.
+ * Minimalist vi-style command line editing available if configured.
+ *  vi mode implemented by Paul Fox <pgf at foxharp.boston.ma.us>
  *
  */
 
+#ifdef CONFIG_FEATURE_COMMAND_EDITING_VI
+static int vi_mode;
+
+void setvimode ( int viflag )
+{
+	vi_mode = viflag;
+}
+
+#endif
+
+/* 
+ * the normal emacs mode and vi's insert mode are the same.
+ * commands entered when in vi command mode ("escape mode") get
+ * an extra bit added to distinguish them.  this lets them share
+ * much of the code in the big switch and while loop.  i
+ * experimented with an ugly macro to make the case labels for
+ * these cases go away entirely when vi mode isn't configured, in
+ * hopes of letting the jump tables get smaller:
+ *  #define vcase(caselabel) caselabel
+ * and then
+ *	case CNTRL('A'):
+ *	case vcase(VICMD('0'):)
+ * but it didn't seem to make any difference in code size,
+ * and the macro-ized code was too ugly.
+ */
+
+#define VI_cmdbit 0x100
+#define VICMD(somecmd) ((somecmd)|VI_cmdbit)
+
+/* convert uppercase ascii to equivalent control char, for readability */
+#define CNTRL(uc_char) ((uc_char) - 0x40)
+
 
 int cmdedit_read_input(char *prompt, char command[BUFSIZ])
 {
 
 	int break_out = 0;
 	int lastWasTab = FALSE;
-	unsigned char c = 0;
-
+	unsigned char c;
+#ifdef CONFIG_FEATURE_COMMAND_EDITING_VI
+	unsigned int ic, prevc;
+	int vi_cmdmode = 0;
+#endif
 	/* prepare before init handlers */
 	cmdedit_y = 0;  /* quasireal y, not true work if line > xt*yt */
 	len = 0;
@@ -1265,22 +1334,38 @@
 			/* if we can't read input then exit */
 			goto prepare_to_die;
 
-		switch (c) {
+#ifdef CONFIG_FEATURE_COMMAND_EDITING_VI
+		newdelflag = 1;
+		ic = c;
+		if (vi_cmdmode)
+		    	ic |= VI_cmdbit;
+		switch (ic)
+#else
+		switch (c)
+#endif
+		{
 		case '\n':
 		case '\r':
+		case VICMD('\n'):
+		case VICMD('\r'):
 			/* Enter */
 			goto_new_line();
 			break_out = 1;
 			break;
-		case 1:
+		case CNTRL('A'):
+		case VICMD('0'):
 			/* Control-a -- Beginning of line */
 			input_backward(cursor);
 			break;
-		case 2:
+		case CNTRL('B'):
+		case VICMD('h'):
+		case VICMD('\b'):
+		case VICMD(DEL):
 			/* Control-b -- Move back one character */
 			input_backward(1);
 			break;
-		case 3:
+		case CNTRL('C'):
+		case VICMD(CNTRL('C')):
 			/* Control-c -- stop gathering input */
 			goto_new_line();
 #ifndef CONFIG_ASH
@@ -1293,7 +1378,7 @@
 			break_out = -1; /* to control traps */
 #endif
 			break;
-		case 4:
+		case CNTRL('D'):
 			/* Control-d -- Delete one character, or exit
 			 * if the len=0 and no chars to delete */
 			if (len == 0) {
@@ -1310,14 +1395,17 @@
 				break;
 #endif
 			} else {
-				input_delete();
+				input_delete(0);
 			}
 			break;
-		case 5:
+		case CNTRL('E'):
+		case VICMD('$'):
 			/* Control-e -- End of line */
 			input_end();
 			break;
-		case 6:
+		case CNTRL('F'):
+		case VICMD('l'):
+		case VICMD(' '):
 			/* Control-f -- Move forward one character */
 			input_forward();
 			break;
@@ -1331,24 +1419,29 @@
 			input_tab(&lastWasTab);
 #endif
 			break;
-		case 11:
+		case CNTRL('K'):
 			/* Control-k -- clear to end of line */
 			*(command + cursor) = 0;
 			len = cursor;
 			printf("\033[J");
 			break;
-		case 12:
+		case CNTRL('L'):
+		case VICMD(CNTRL('L')):
 			/* Control-l -- clear screen */
 			printf("\033[H");
 			redraw(0, len-cursor);
 			break;
 #if MAX_HISTORY >= 1
-		case 14:
+		case CNTRL('N'):
+		case VICMD(CNTRL('N')):
+		case VICMD('j'):
 			/* Control-n -- Get next command in history */
 			if (get_next_history())
 				goto rewrite_line;
 			break;
-		case 16:
+		case CNTRL('P'):
+		case VICMD(CNTRL('P')):
+		case VICMD('k'):
 			/* Control-p -- Get previous command from history */
 			if (cur_history > 0) {
 				get_previous_history();
@@ -1358,26 +1451,160 @@
 			}
 			break;
 #endif
-		case 21:
+		case CNTRL('U'):
+		case VICMD(CNTRL('U')):
 			/* Control-U -- Clear line before cursor */
 			if (cursor) {
 				strcpy(command, command + cursor);
 				redraw(cmdedit_y, len -= cursor);
 			}
 			break;
-		case 23:
+		case CNTRL('W'):
+		case VICMD(CNTRL('W')):
 			/* Control-W -- Remove the last word */
 			while (cursor > 0 && isspace(command[cursor-1]))
 				input_backspace();
 			while (cursor > 0 &&!isspace(command[cursor-1]))
 				input_backspace();
 			break;
+#if CONFIG_FEATURE_COMMAND_EDITING_VI
+		case VICMD('i'):
+			vi_cmdmode = 0;
+			break;
+		case VICMD('I'):
+			input_backward(cursor);
+			vi_cmdmode = 0;
+			break;
+		case VICMD('a'):
+			input_forward();
+			vi_cmdmode = 0;
+			break;
+		case VICMD('A'):
+			input_end();
+			vi_cmdmode = 0;
+			break;
+		case VICMD('x'):
+			input_delete(1);
+			break;
+		case VICMD('X'):
+			if (cursor > 0) {
+				input_backward(1);
+				input_delete(1);
+			}
+			break;
+		case VICMD('w'):  /* implemented as W for now */
+		case VICMD('W'):
+			while (cursor < len && !isspace(command[cursor]))
+				input_forward();
+			while (cursor < len && isspace(command[cursor]))
+				input_forward();
+			break;
+		case VICMD('e'):  /* implemented as E for now */
+		case VICMD('E'):
+			input_forward();
+			while (cursor < len && isspace(command[cursor]))
+				input_forward();
+			while (cursor < len-1 && !isspace(command[cursor+1]))
+				input_forward();
+			break;
+		case VICMD('b'):  /* implemented as B for now */
+		case VICMD('B'):
+			while (cursor > 0 && isspace(command[cursor-1]))
+				input_backward(1);
+			while (cursor > 0 && !isspace(command[cursor-1]))
+				input_backward(1);
+			break;
+		case VICMD('C'):
+			vi_cmdmode = 0;
+			/* fall through */
+		case VICMD('D'):
+			goto clear_to_eol;
+
+		case VICMD('c'):
+			vi_cmdmode = 0;
+			/* fall through */
+		case VICMD('d'):
+			prevc = ic;
+			if (safe_read(0, &c, 1) < 1)
+				goto prepare_to_die;
+			if (c == (prevc & 0xff)) {
+			    /* "cc", "dd" */
+			    input_backward(cursor);
+			    goto clear_to_eol;
+			    break;
+			}
+			switch(c) {
+			case 'w':  /* "dw", "cw" */
+			case 'W':  /* implemented as W */
+			    while (cursor < len && !isspace(command[cursor]))
+				    input_delete(1);
+			    while (cursor < len && isspace(command[cursor]))
+				    input_delete(1);
+			    break;
+			case 'e':  /* "de", "ce" */
+			case 'E':  /* implemented as E */
+				input_delete(1);
+				while (cursor < len && isspace(command[cursor]))
+					input_delete(1);
+				while (cursor < len-1 && !isspace(command[cursor+1]))
+					input_delete(1);
+				input_delete(1);
+				break;
+			case 'b':  /* "db", "cb" */
+			case 'B':  /* implemented as B */
+			    { int sc = cursor;
+			    while (cursor > 0 && isspace(command[cursor-1]))
+				    input_backward(1);
+			    while (cursor > 0 && !isspace(command[cursor-1]))
+				    input_backward(1);
+			    while (sc-- > cursor)
+				    input_delete(1);
+			    }
+			    break;
+			case ' ':  /* "d ", "c " */
+			    input_delete(1);
+			    break;
+			case '$':  /* "d$", "c$" */
+			clear_to_eol:
+			    while (cursor < len)
+				    input_delete(1);
+			    break;
+			}
+			break;
+		case VICMD('p'):
+			input_forward();
+			/* fallthrough */
+		case VICMD('P'):
+			put();
+			break;
+		case VICMD('r'):
+			if (safe_read(0, &c, 1) < 1)
+				goto prepare_to_die;
+			if (c == 0)
+				beep();
+			else {
+				*(command + cursor) = c;
+				putchar(c);
+				putchar('\b');
+			}
+			break;
+#endif /* CONFIG_FEATURE_COMMAND_EDITING_VI */
 		case ESC:{
+#if CONFIG_FEATURE_COMMAND_EDITING_VI
+			if (vi_mode) {
+				/* ESC: insert mode --> command mode */
+				vi_cmdmode = 1;
+				input_backward(1);
+				break;
+			}
+#endif
 			/* escape sequence follows */
 			if (safe_read(0, &c, 1) < 1)
 				goto prepare_to_die;
 			/* different vt100 emulations */
 			if (c == '[' || c == 'O') {
+		case VICMD('['):
+		case VICMD('O'):
 				if (safe_read(0, &c, 1) < 1)
 					goto prepare_to_die;
 			}
@@ -1409,13 +1636,17 @@
 			case 'B':
 				/* Down Arrow -- Get next command in history */
 				if (!get_next_history())
-				break;
+					break;
 				/* Rewrite the line with the selected history item */
 rewrite_line:
 				/* change command */
 				len = strlen(strcpy(command, history[cur_history]));
-				/* redraw and go to end line */
+				/* redraw and go to eol (bol, in vi */
+#if CONFIG_FEATURE_COMMAND_EDITING_VI
+				redraw(cmdedit_y, vi_mode ? 9999:0);
+#else
 				redraw(cmdedit_y, 0);
+#endif
 				break;
 #endif
 			case 'C':
@@ -1428,7 +1659,7 @@
 				break;
 			case '3':
 				/* Delete */
-				input_delete();
+				input_delete(0);
 				break;
 			case '1':
 			case 'H':
@@ -1450,7 +1681,7 @@
 		default:        /* If it's regular input, do the normal thing */
 #ifdef CONFIG_FEATURE_NONPRINTABLE_INVERSE_PUT
 			/* Control-V -- Add non-printable symbol */
-			if (c == 22) {
+			if (c == CNTRL('V')) {
 				if (safe_read(0, &c, 1) < 1)
 					goto prepare_to_die;
 				if (c == 0) {
@@ -1459,8 +1690,14 @@
 				}
 			} else
 #endif
-			if (!Isprint(c))        /* Skip non-printable characters */
-				break;
+			{
+#if CONFIG_FEATURE_COMMAND_EDITING_VI
+				if (vi_cmdmode)  /* don't self-insert */
+					break;
+#endif
+				if (!Isprint(c)) /* Skip non-printable characters */
+					break;
+			}
 
 			if (len >= (BUFSIZ - 2))        /* Need to leave space for enter */
 				break;
Index: shell/cmdedit.h
===================================================================
RCS file: /var/cvs/busybox/shell/cmdedit.h,v
retrieving revision 1.15
diff -u -r1.15 cmdedit.h
--- shell/cmdedit.h	14 Jan 2004 09:34:51 -0000	1.15
+++ shell/cmdedit.h	13 Nov 2004 18:56:54 -0000
@@ -12,4 +12,8 @@
 void    save_history ( const char *tofile );
 #endif
 
+#if CONFIG_FEATURE_COMMAND_EDITING_VI
+void	setvimode ( int viflag );
+#endif
+
 #endif /* CMDEDIT_H */

-----------end of patch--------

=---------------------
 paul fox, pgf at foxharp.boston.ma.us




More information about the busybox mailing list