More Doc updates. cmdedit and more termio fixes.
[oweals/busybox.git] / cmdedit.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * Termios command line History and Editting, originally 
4  * intended for NetBSD sh (ash)
5  * Copyright (c) 1999
6  *      Main code:            Adam Rogoyski <rogoyski@cs.utexas.edu> 
7  *      Etc:                  Dave Cinege <dcinege@psychosis.com>
8  *  Majorly adjusted/re-written for busybox:
9  *                            Erik Andersen <andersee@debian.org>
10  *
11  * You may use this code as you wish, so long as the original author(s)
12  * are attributed in any redistributions of the source code.
13  * This code is 'as is' with no warranty.
14  * This code may safely be consumed by a BSD or GPL license.
15  *
16  * v 0.5  19990328      Initial release 
17  *
18  * Future plans: Simple file and path name completion. (like BASH)
19  *
20  */
21
22 /*
23    Usage and Known bugs:
24    Terminal key codes are not extensive, and more will probably
25    need to be added. This version was created on Debian GNU/Linux 2.x.
26    Delete, Backspace, Home, End, and the arrow keys were tested
27    to work in an Xterm and console. Ctrl-A also works as Home.
28    Ctrl-E also works as End. The binary size increase is <3K.
29
30    Editting will not display correctly for lines greater then the 
31    terminal width. (more then one line.) However, history will.
32  */
33
34 #include "internal.h"
35 #ifdef BB_FEATURE_SH_COMMAND_EDITING
36
37 #include <stdio.h>
38 #include <errno.h>
39 #include <unistd.h>
40 #include <stdlib.h>
41 #include <string.h>
42 #include <sys/ioctl.h>
43 #include <ctype.h>
44 #include <signal.h>
45
46
47 #define  MAX_HISTORY   15               /* Maximum length of the linked list for the command line history */
48
49 #define ESC     27
50 #define DEL     127
51 #define member(c, s) ((c) ? ((char *)strchr ((s), (c)) != (char *)NULL) : 0)
52 #define whitespace(c) (((c) == ' ') || ((c) == '\t'))
53
54 static struct history *his_front = NULL;        /* First element in command line list */
55 static struct history *his_end = NULL;  /* Last element in command line list */
56
57 /* ED: sparc termios is broken: revert back to old termio handling. */
58 #ifdef BB_FEATURE_USE_TERMIOS
59
60 #if #cpu(sparc)
61 #      include <termio.h>
62 #      define termios termio
63 #      define setTermSettings(fd,argp) ioctl(fd,TCSETAF,argp)
64 #      define getTermSettings(fd,argp) ioctl(fd,TCGETA,argp)
65 #else
66 #      include <termios.h>
67 #      define setTermSettings(fd,argp) tcsetattr(fd,TCSANOW,argp)
68 #      define getTermSettings(fd,argp) tcgetattr(fd, argp);
69 #endif
70
71 /* Current termio and the previous termio before starting sh */
72 struct termios initial_settings, new_settings;
73 #endif
74
75
76
77 static int cmdedit_termw = 80;  /* actual terminal width */
78 static int cmdedit_scroll = 27; /* width of EOL scrolling region */
79 static int history_counter = 0; /* Number of commands in history list */
80 static int reset_term = 0;              /* Set to true if the terminal needs to be reset upon exit */
81
82 struct history {
83         char *s;
84         struct history *p;
85         struct history *n;
86 };
87
88 #define xwrite write
89
90 void
91 cmdedit_setwidth(int w)
92 {
93         if (w > 20) {
94                 cmdedit_termw = w;
95                 cmdedit_scroll = w / 3;
96         } else {
97                 errorMsg("\n*** Error: minimum screen width is 21\n");
98         }
99 }
100
101
102 void cmdedit_reset_term(void)
103 {
104         if (reset_term)
105                 /* sparc and other have broken termios support: use old termio handling. */
106                 setTermSettings(fileno(stdin), (void*) &initial_settings);
107 }
108
109 void clean_up_and_die(int sig)
110 {
111         cmdedit_reset_term();
112         fprintf(stdout, "\n");
113         if (sig!=SIGINT)
114                 exit(TRUE);
115 }
116
117 /* Go to HOME position */
118 void input_home(int outputFd, int *cursor)
119 {
120         while (*cursor > 0) {
121                 xwrite(outputFd, "\b", 1);
122                 --*cursor;
123         }
124 }
125
126 /* Go to END position */
127 void input_end(int outputFd, int *cursor, int len)
128 {
129         while (*cursor < len) {
130                 xwrite(outputFd, "\033[C", 3);
131                 ++*cursor;
132         }
133 }
134
135 /* Delete the char in back of the cursor */
136 void input_backspace(char* command, int outputFd, int *cursor, int *len)
137 {
138         int j = 0;
139
140         if (*cursor > 0) {
141                 xwrite(outputFd, "\b \b", 3);
142                 --*cursor;
143                 memmove(command + *cursor, command + *cursor + 1,
144                                 BUFSIZ - *cursor + 1);
145
146                 for (j = *cursor; j < (BUFSIZ - 1); j++) {
147                         if (!*(command + j))
148                                 break;
149                         else
150                                 xwrite(outputFd, (command + j), 1);
151                 }
152
153                 xwrite(outputFd, " \b", 2);
154
155                 while (j-- > *cursor)
156                         xwrite(outputFd, "\b", 1);
157
158                 --*len;
159         }
160 }
161
162 /* Delete the char in front of the cursor */
163 void input_delete(char* command, int outputFd, int cursor, int *len)
164 {
165         int j = 0;
166
167         if (cursor == *len)
168                 return;
169         
170         memmove(command + cursor, command + cursor + 1,
171                         BUFSIZ - cursor - 1);
172         for (j = cursor; j < (BUFSIZ - 1); j++) {
173                 if (!*(command + j))
174                         break;
175                 else
176                         xwrite(outputFd, (command + j), 1);
177         }
178
179         xwrite(outputFd, " \b", 2);
180
181         while (j-- > cursor)
182                 xwrite(outputFd, "\b", 1);
183         --*len;
184 }
185
186 /* Move forward one charactor */
187 void input_forward(int outputFd, int *cursor, int len)
188 {
189         if (*cursor < len) {
190                 xwrite(outputFd, "\033[C", 3);
191                 ++*cursor;
192         }
193 }
194
195 /* Move back one charactor */
196 void input_backward(int outputFd, int *cursor)
197 {
198         if (*cursor > 0) {
199                 xwrite(outputFd, "\033[D", 3);
200                 --*cursor;
201         }
202 }
203
204
205
206 #ifdef BB_FEATURE_SH_TAB_COMPLETION
207 char** username_tab_completion(char* command, int *num_matches)
208 {
209         char **matches = (char **) NULL;
210         *num_matches=0;
211         fprintf(stderr, "\nin username_tab_completion\n");
212         return (matches);
213 }
214
215 #include <dirent.h>
216 char** exe_n_cwd_tab_completion(char* command, int *num_matches)
217 {
218         char *dirName;
219         char **matches = (char **) NULL;
220         DIR *dir;
221         struct dirent *next;
222                         
223         matches = malloc( sizeof(char*)*50);
224
225         /* Stick a wildcard onto the command, for later use */
226         strcat( command, "*");
227
228         /* Now wall the current directory */
229         dirName = get_current_dir_name();
230         dir = opendir(dirName);
231         if (!dir) {
232                 /* Don't print an error, just shut up and return */
233                 *num_matches=0;
234                 return (matches);
235         }
236         while ((next = readdir(dir)) != NULL) {
237
238                 /* Some quick sanity checks */
239                 if ((strcmp(next->d_name, "..") == 0)
240                         || (strcmp(next->d_name, ".") == 0)) {
241                         continue;
242                 } 
243                 /* See if this matches */
244                 if (check_wildcard_match(next->d_name, command) == TRUE) {
245                         /* Cool, found a match.  Add it to the list */
246                         matches[*num_matches] = malloc(strlen(next->d_name)+1);
247                         strcpy( matches[*num_matches], next->d_name);
248                         ++*num_matches;
249                         //matches = realloc( matches, sizeof(char*)*(*num_matches));
250                 }
251         }
252
253         return (matches);
254 }
255
256 void input_tab(char* command, char* prompt, int outputFd, int *cursor, int *len)
257 {
258         /* Do TAB completion */
259         static int num_matches=0;
260         static char **matches = (char **) NULL;
261         int pos = cursor;
262
263
264         if (lastWasTab == FALSE) {
265                 char *tmp, *tmp1, *matchBuf;
266
267                 /* For now, we will not bother with trying to distinguish
268                  * whether the cursor is in/at a command extression -- we
269                  * will always try all possable matches.  If you don't like
270                  * that then feel free to fix it.
271                  */
272
273                 /* Make a local copy of the string -- up 
274                  * to the position of the cursor */
275                 matchBuf = (char *) calloc(BUFSIZ, sizeof(char));
276                 strncpy(matchBuf, command, cursor);
277                 tmp=matchBuf;
278
279                 /* skip past any command seperator tokens */
280                 while (*tmp && (tmp1=strpbrk(tmp, ";|&{(`")) != NULL) {
281                         tmp=++tmp1;
282                         /* skip any leading white space */
283                         while (*tmp && isspace(*tmp)) 
284                                 ++tmp;
285                 }
286
287                 /* skip any leading white space */
288                 while (*tmp && isspace(*tmp)) 
289                         ++tmp;
290
291                 /* Free up any memory already allocated */
292                 if (matches) {
293                         free(matches);
294                         matches = (char **) NULL;
295                 }
296
297                 /* If the word starts with `~' and there is no slash in the word, 
298                  * then try completing this word as a username. */
299
300                 /* FIXME -- this check is broken! */
301                 if (*tmp == '~' && !strchr(tmp, '/'))
302                         matches = username_tab_completion(tmp, &num_matches);
303
304                 /* Try to match any executable in our path and everything 
305                  * in the current working directory that matches.  */
306                 if (!matches)
307                         matches = exe_n_cwd_tab_completion(tmp, &num_matches);
308
309                 /* Don't leak memory */
310                 free( matchBuf);
311
312                 /* Did we find exactly one match? */
313                 if (matches && num_matches==1) {
314                         /* write out the matched command */
315                         strncpy(command+pos, matches[0]+pos, strlen(matches[0])-pos);
316                         len=strlen(command);
317                         cursor=len;
318                         xwrite(outputFd, matches[0]+pos, strlen(matches[0])-pos);
319                         break;
320                 }
321         } else {
322                 /* Ok -- the last char was a TAB.  Since they
323                  * just hit TAB again, print a list of all the
324                  * available choices... */
325                 if ( matches && num_matches>0 ) {
326                         int i, col;
327
328                         /* Go to the next line */
329                         xwrite(outputFd, "\n", 1);
330                         /* Print the list of matches */
331                         for (i=0,col=0; i<num_matches; i++) {
332                                 char foo[17];
333                                 sprintf(foo, "%-14s  ", matches[i]);
334                                 col += xwrite(outputFd, foo, strlen(foo));
335                                 if (col > 60 && matches[i+1] != NULL) {
336                                         xwrite(outputFd, "\n", 1);
337                                         col = 0;
338                                 }
339                         }
340                         /* Go to the next line */
341                         xwrite(outputFd, "\n", 1);
342                         /* Rewrite the prompt */
343                         xwrite(outputFd, prompt, strlen(prompt));
344                         /* Rewrite the command */
345                         xwrite(outputFd, command, len);
346                         /* Put the cursor back to where it used to be */
347                         for (cursor=len; cursor > pos; cursor--)
348                                 xwrite(outputFd, "\b", 1);
349                 }
350         }
351 }
352 #endif
353
354 void get_previous_history(struct history **hp, char* command)
355 {
356         free((*hp)->s);
357         (*hp)->s = strdup(command);
358         *hp = (*hp)->p;
359 }
360
361 void get_next_history(struct history **hp, char* command)
362 {
363         free((*hp)->s);
364         (*hp)->s = strdup(command);
365         *hp = (*hp)->n;
366 }
367
368 /*
369  * This function is used to grab a character buffer
370  * from the input file descriptor and allows you to
371  * a string with full command editing (sortof like
372  * a mini readline).
373  *
374  * The following standard commands are not implemented:
375  * ESC-b -- Move back one word
376  * ESC-f -- Move forward one word
377  * ESC-d -- Delete back one word
378  * ESC-h -- Delete forward one word
379  * CTL-t -- Transpose two characters
380  *
381  * Furthermore, the "vi" command editing keys are not implemented.
382  *
383  * TODO: implement TAB command completion. :)
384  */
385 extern void cmdedit_read_input(char* prompt, char command[BUFSIZ])
386 {
387
388         int inputFd=fileno(stdin);
389         int outputFd=fileno(stdout);
390         int nr = 0;
391         int len = 0;
392         int j = 0;
393         int cursor = 0;
394         int break_out = 0;
395         int ret = 0;
396         int lastWasTab = FALSE;
397         char c = 0;
398         struct history *hp = his_end;
399
400         memset(command, 0, sizeof(command));
401         if (!reset_term) {
402                 
403                 getTermSettings(inputFd, (void*) &initial_settings);
404                 memcpy(&new_settings, &initial_settings, sizeof(struct termios));
405                 new_settings.c_cc[VMIN] = 1;
406                 new_settings.c_cc[VTIME] = 0;
407                 new_settings.c_cc[VINTR] = _POSIX_VDISABLE; /* Turn off CTRL-C, so we can trap it */
408                 new_settings.c_lflag &= ~ICANON;        /* unbuffered input */
409                 new_settings.c_lflag &= ~(ECHO|ECHOCTL|ECHONL); /* Turn off echoing */
410                 reset_term = 1;
411         }
412         setTermSettings(inputFd, (void*) &new_settings);
413
414         memset(command, 0, BUFSIZ);
415
416         while (1) {
417
418                 if ((ret = read(inputFd, &c, 1)) < 1)
419                         return;
420                 //fprintf(stderr, "got a '%c' (%d)\n", c, c);
421
422                 switch (c) {
423                 case '\n':
424                 case '\r':
425                         /* Enter */
426                         *(command + len++ + 1) = c;
427                         xwrite(outputFd, &c, 1);
428                         break_out = 1;
429                         break;
430                 case 1:
431                         /* Control-a -- Beginning of line */
432                         input_home(outputFd, &cursor);
433                 case 2:
434                         /* Control-b -- Move back one character */
435                         input_backward(outputFd, &cursor);
436                         break;
437                 case 3:
438                         /* Control-c -- leave the current line, 
439                          * and start over on the next line */ 
440
441                         /* Go to the next line */
442                         xwrite(outputFd, "\n", 1);
443
444                         /* Rewrite the prompt */
445                         xwrite(outputFd, prompt, strlen(prompt));
446
447                         /* Reset the command string */
448                         memset(command, 0, sizeof(command));
449                         len = cursor = 0;
450
451                         break;
452                 case 4:
453                         /* Control-d -- Delete one character, or exit 
454                          * if the len=0 and no chars to delete */
455                         if (len == 0) {
456                                 xwrite(outputFd, "exit", 4);
457                                 clean_up_and_die(0);
458                         } else {
459                                 input_delete(command, outputFd, cursor, &len);
460                         }
461                         break;
462                 case 5:
463                         /* Control-e -- End of line */
464                         input_end(outputFd, &cursor, len);
465                         break;
466                 case 6:
467                         /* Control-f -- Move forward one character */
468                         input_forward(outputFd, &cursor, len);
469                         break;
470                 case '\b':
471                 case DEL:
472                         /* Control-h and DEL */
473                         input_backspace(command, outputFd, &cursor, &len);
474                         break;
475                 case '\t':
476 #ifdef BB_FEATURE_SH_TAB_COMPLETION
477                         input_tab(command, prompt, outputFd, &cursor, &len);
478 #endif
479                         break;
480                 case 14:
481                         /* Control-n -- Get next command in history */
482                         if (hp && hp->n && hp->n->s) {
483                                 get_next_history(&hp, command);
484                                 goto rewrite_line;
485                         } else {
486                                 xwrite(outputFd, "\007", 1);
487                         }
488                         break;
489                 case 16:
490                         /* Control-p -- Get previous command from history */
491                         if (hp && hp->p) {
492                                 get_previous_history(&hp, command);
493                                 goto rewrite_line;
494                         } else {
495                                 xwrite(outputFd, "\007", 1);
496                         }
497                         break;
498                 case ESC:{
499                                 /* escape sequence follows */
500                                 if ((ret = read(inputFd, &c, 1)) < 1)
501                                         return;
502
503                                 if (c == '[') { /* 91 */
504                                         if ((ret = read(inputFd, &c, 1)) < 1)
505                                                 return;
506
507                                         switch (c) {
508                                         case 'A':
509                                                 /* Up Arrow -- Get previous command from history */
510                                                 if (hp && hp->p) {
511                                                         get_previous_history(&hp, command);
512                                                         goto rewrite_line;
513                                                 } else {
514                                                         xwrite(outputFd, "\007", 1);
515                                                 }
516                                                 break;
517                                         case 'B':
518                                                 /* Down Arrow -- Get next command in history */
519                                                 if (hp && hp->n && hp->n->s) {
520                                                         get_next_history(&hp, command);
521                                                         goto rewrite_line;
522                                                 } else {
523                                                         xwrite(outputFd, "\007", 1);
524                                                 }
525                                                 break;
526
527                                                 /* Rewrite the line with the selected history item */
528                                           rewrite_line:
529                                                 /* erase old command from command line */
530                                                 len = strlen(command)-strlen(hp->s);
531                                                 while (len>0)
532                                                         input_backspace(command, outputFd, &cursor, &len);
533                                                 input_home(outputFd, &cursor);
534                                                 
535                                                 /* write new command */
536                                                 strcpy(command, hp->s);
537                                                 len = strlen(hp->s);
538                                                 xwrite(outputFd, command, len);
539                                                 cursor = len;
540                                                 break;
541                                         case 'C':
542                                                 /* Right Arrow -- Move forward one character */
543                                                 input_forward(outputFd, &cursor, len);
544                                                 break;
545                                         case 'D':
546                                                 /* Left Arrow -- Move back one character */
547                                                 input_backward(outputFd, &cursor);
548                                                 break;
549                                         case '3':
550                                                 /* Delete */
551                                                 input_delete(command, outputFd, cursor, &len);
552                                                 break;
553                                         case '1':
554                                                 /* Home (Ctrl-A) */
555                                                 input_home(outputFd, &cursor);
556                                                 break;
557                                         case '4':
558                                                 /* End (Ctrl-E) */
559                                                 input_end(outputFd, &cursor, len);
560                                                 break;
561                                         default:
562                                                 xwrite(outputFd, "\007", 1);
563                                         }
564                                         if (c == '1' || c == '3' || c == '4')
565                                                 if ((ret = read(inputFd, &c, 1)) < 1)
566                                                         return; /* read 126 (~) */
567                                 }
568                                 if (c == 'O') {
569                                         /* 79 */
570                                         if ((ret = read(inputFd, &c, 1)) < 1)
571                                                 return;
572                                         switch (c) {
573                                         case 'H':
574                                                 /* Home (xterm) */
575                                                 input_home(outputFd, &cursor);
576                                                 break;
577                                         case 'F':
578                                                 /* End (xterm) */
579                                                 input_end(outputFd, &cursor, len);
580                                                 break;
581                                         default:
582                                                 xwrite(outputFd, "\007", 1);
583                                         }
584                                 }
585                                 c = 0;
586                                 break;
587                         }
588
589                 default:                                /* If it's regular input, do the normal thing */
590
591                         if (!isprint(c)) {      /* Skip non-printable characters */
592                                 break;
593                         }
594
595                         if (len >= (BUFSIZ - 2))        /* Need to leave space for enter */
596                                 break;
597
598                         len++;
599
600                         if (cursor == (len - 1)) {      /* Append if at the end of the line */
601                                 *(command + cursor) = c;
602                         } else {                        /* Insert otherwise */
603                                 memmove(command + cursor + 1, command + cursor,
604                                                 len - cursor - 1);
605
606                                 *(command + cursor) = c;
607
608                                 for (j = cursor; j < len; j++)
609                                         xwrite(outputFd, command + j, 1);
610                                 for (; j > cursor; j--)
611                                         xwrite(outputFd, "\033[D", 3);
612                         }
613
614                         cursor++;
615                         xwrite(outputFd, &c, 1);
616                         break;
617                 }
618                 if (c == '\t')
619                         lastWasTab = TRUE;
620                 else
621                         lastWasTab = FALSE;
622
623                 if (break_out)                  /* Enter is the command terminator, no more input. */
624                         break;
625         }
626
627         nr = len + 1;
628         setTermSettings(inputFd, (void *) &initial_settings);
629         reset_term = 0;
630
631
632         /* Handle command history log */
633         if (*(command)) {
634
635                 struct history *h = his_end;
636
637                 if (!h) {
638                         /* No previous history */
639                         h = his_front = malloc(sizeof(struct history));
640                         h->n = malloc(sizeof(struct history));
641
642                         h->p = NULL;
643                         h->s = strdup(command);
644                         h->n->p = h;
645                         h->n->n = NULL;
646                         h->n->s = NULL;
647                         his_end = h->n;
648                         history_counter++;
649                 } else {
650                         /* Add a new history command */
651                         h->n = malloc(sizeof(struct history));
652
653                         h->n->p = h;
654                         h->n->n = NULL;
655                         h->n->s = NULL;
656                         h->s = strdup(command);
657                         his_end = h->n;
658
659                         /* After max history, remove the oldest command */
660                         if (history_counter >= MAX_HISTORY) {
661
662                                 struct history *p = his_front->n;
663
664                                 p->p = NULL;
665                                 free(his_front->s);
666                                 free(his_front);
667                                 his_front = p;
668                         } else {
669                                 history_counter++;
670                         }
671                 }
672         }
673
674         return;
675 }
676
677 extern void cmdedit_init(void)
678 {
679         atexit(cmdedit_reset_term);
680         signal(SIGKILL, clean_up_and_die);
681         signal(SIGINT, clean_up_and_die);
682         signal(SIGQUIT, clean_up_and_die);
683         signal(SIGTERM, clean_up_and_die);
684 }
685 #endif                                                  /* BB_FEATURE_SH_COMMAND_EDITING */