/* vi: set sw=4 ts=4: */ /* * Termios command line History and Editting, originally * intended for NetBSD sh (ash) * Copyright (c) 1999 * Main code: Adam Rogoyski * Etc: Dave Cinege * Majorly adjusted/re-written for busybox: * Erik Andersen * * You may use this code as you wish, so long as the original author(s) * are attributed in any redistributions of the source code. * This code is 'as is' with no warranty. * This code may safely be consumed by a BSD or GPL license. * * v 0.5 19990328 Initial release * * Future plans: Simple file and path name completion. (like BASH) * */ /* Usage and Known bugs: Terminal key codes are not extensive, and more will probably need to be added. This version was created on Debian GNU/Linux 2.x. Delete, Backspace, Home, End, and the arrow keys were tested to work in an Xterm and console. Ctrl-A also works as Home. Ctrl-E also works as End. Editor with vertical scrolling and completion by Vladimir Oleynik. vodz@usa.net (c) 2001 Small bug: not true work if terminal size (x*y symbols) less size (prompt + editor`s line + 2 symbols) */ #include "busybox.h" #ifdef BB_FEATURE_SH_COMMAND_EDITING #include #include #include #include #include #include #include #include #ifdef BB_FEATURE_SH_TAB_COMPLETION #include #endif #include "pwd_grp/pwd.h" static const int MAX_HISTORY = 15; /* Maximum length of the linked list for the command line history */ enum { ESC = 27, DEL = 127, }; #define member(c, s) ((c) ? ((char *)strchr ((s), (c)) != (char *)NULL) : 0) #define whitespace(c) (((c) == ' ') || ((c) == '\t')) static struct history *his_front = NULL; /* First element in command line list */ static struct history *his_end = NULL; /* Last element in command line list */ /* ED: sparc termios is broken: revert back to old termio handling. */ #if #cpu(sparc) # include # define termios termio # define setTermSettings(fd,argp) ioctl(fd,TCSETAF,argp) # define getTermSettings(fd,argp) ioctl(fd,TCGETA,argp) #else # include # define setTermSettings(fd,argp) tcsetattr(fd,TCSANOW,argp) # define getTermSettings(fd,argp) tcgetattr(fd, argp); #endif /* Current termio and the previous termio before starting sh */ static struct termios initial_settings, new_settings; #ifndef _POSIX_VDISABLE #define _POSIX_VDISABLE '\0' #endif static volatile int cmdedit_termw; /* actual terminal width */ static int history_counter = 0; /* Number of commands in history list */ static volatile int handlers_sets = 0; /* Set next bites when atexit() has been called and set many "terminates" signal handlers and winchg signal handler and if the terminal needs to be reset upon exit */ enum { SET_ATEXIT = 1, SET_TERM_HANDLERS = 2, SET_WCHG_HANDLERS = 4, SET_RESET_TERM = 8, }; static int cmdedit_x; /* real x terminal position, require put prompt in start x position */ static int cmdedit_y; /* pseudoreal y terminal position */ static int cmdedit_prmt_len; /* for fast running, without duplicate calculate */ static int cursor; /* required global for signal handler */ static int len; /* --- "" - - "" - -"- --""-- --""--- */ static char *command_ps; /* --- "" - - "" - -"- --""-- --""--- */ static const char *cmdedit_prompt;/* --- "" - - "" - -"- --""-- --""--- */ /* Link into lash to reset context to 0 * on ^C and such */ extern unsigned int shell_context; struct history { char *s; struct history *p; struct history *n; }; static void cmdedit_setwidth(int w, int redraw_flg); static void win_changed(int nsig) { struct winsize win = { 0, 0, 0, 0 }; static __sighandler_t previous_SIGWINCH_handler; /* for reset */ /* emulate signal call if not called as a sig handler */ if(nsig == -SIGWINCH || nsig == SIGWINCH) { ioctl(0, TIOCGWINSZ, &win); if (win.ws_col > 0) { cmdedit_setwidth( win.ws_col, nsig == SIGWINCH ); } else { /* Default to 79 if their console doesn't want to share */ cmdedit_setwidth( 79, nsig == SIGWINCH ); } } /* Unix not all standart in recall signal */ if(nsig == -SIGWINCH) /* save previous handler */ previous_SIGWINCH_handler = signal(SIGWINCH, win_changed); else if(nsig == SIGWINCH) /* signaled called handler */ signal(SIGWINCH, win_changed); /* set for next call */ else /* set previous handler */ signal(SIGWINCH, previous_SIGWINCH_handler); /* reset */ } static void cmdedit_reset_term(void) { if((handlers_sets & SET_RESET_TERM)!=0) { /* sparc and other have broken termios support: use old termio handling. */ setTermSettings(fileno(stdin), (void*) &initial_settings); handlers_sets &= ~SET_RESET_TERM; } if((handlers_sets & SET_WCHG_HANDLERS)!=0) { /* reset SIGWINCH handler to previous (default) */ win_changed(0); handlers_sets &= ~SET_WCHG_HANDLERS; } fflush(stdout); #ifdef BB_FEATURE_CLEAN_UP if (his_front) { struct history *n; //while(his_front!=his_end) { while(his_front!=his_end) { n = his_front->n; free(his_front->s); free(his_front); his_front=n; } } #endif } /* special for recount position for scroll and remove terminal margin effect */ static void cmdedit_set_out_char(int c, int next_char) { putchar(c); if(++cmdedit_x>=cmdedit_termw) { /* terminal is scrolled down */ cmdedit_y++; cmdedit_x=0; if(!next_char) next_char = ' '; /* destroy "(auto)margin" */ putchar(next_char); putchar('\b'); } cursor++; } /* Move to end line. Bonus: rewrite line from cursor without use special control terminal strings, also saved size and speed! */ static void input_end (void) { while(cursor < len) cmdedit_set_out_char(command_ps[cursor], 0); } /* Go to the next line */ static void goto_new_line(void) { input_end(); cmdedit_set_out_char('\n', 0); } static inline void out1str(const char *s) { fputs (s, stdout); } static inline void beep (void) { putchar('\007'); } /* Go to HOME position */ static void input_home(void) { while(cmdedit_y>0) { /* up to start y */ out1str("\033[A"); cmdedit_y--; } putchar('\r'); cursor = 0; out1str(cmdedit_prompt); cmdedit_x = cmdedit_prmt_len; } /* Move back one charactor */ static void input_backward(void) { if (cursor > 0) { cursor--; if(cmdedit_x!=0) { /* no first position in terminal line */ putchar('\b'); cmdedit_x--; } else { out1str("\033[A"); /* up */ cmdedit_y--; /* to end in current terminal line */ while(cmdedit_x<(cmdedit_termw-1)) { out1str("\033[C"); cmdedit_x++; } } } } /* Delete the char in front of the cursor */ static void input_delete(void) { int j = cursor; if (j == len) return; memmove (command_ps + j, command_ps + j + 1, BUFSIZ - j - 1); len--; input_end(); /* rewtite new line */ cmdedit_set_out_char(' ', 0); /* destroy end char */ while (j < cursor) input_backward(); /* back to old pos cursor */ } /* Delete the char in back of the cursor */ static void input_backspace(void) { if (cursor > 0) { input_backward(); input_delete (); } } /* Move forward one charactor */ static void input_forward(void) { if (cursor < len) cmdedit_set_out_char(command_ps[cursor], command_ps[cursor + 1]); } static void clean_up_and_die(int sig) { goto_new_line(); if (sig!=SIGINT) exit(EXIT_SUCCESS); /* cmdedit_reset_term() called in atexit */ cmdedit_reset_term(); } static void cmdedit_setwidth(int w, int redraw_flg) { cmdedit_termw = cmdedit_prmt_len+2; if (w > cmdedit_termw) { cmdedit_termw = w; if(redraw_flg) { int sav_cursor = cursor; /* set variables for new terminal size */ cmdedit_y = sav_cursor/w; cmdedit_x = sav_cursor-cmdedit_y*w; /* redraw */ input_home(); input_end(); while(sav_cursorpw_name, userlen)) { temp = xmalloc (3 + strlen (entry->pw_name)); sprintf(temp, "~%s/", entry->pw_name); matches = xrealloc(matches, (nm+1)*sizeof(char *)); matches[nm++] = temp; } } endpwent (); (*num_matches) = nm; return (matches); } #endif enum { FIND_EXE_ONLY = 0, FIND_DIR_ONLY = 1, FIND_FILE_ONLY = 2, }; #include static int path_parse(char ***p, int flags) { int npth; char *tmp; char *pth; if(flags!=FIND_EXE_ONLY || (pth=getenv("PATH"))==0) { /* if not setenv PATH variable, to search cur dir "." */ (*p) = xmalloc(sizeof(char *)); (*p)[0] = xstrdup("."); return 1; } tmp = pth; npth=0; for(;;) { npth++; /* count words is + 1 count ':' */ tmp = strchr(tmp, ':'); if(tmp) tmp++; else break; } *p = xmalloc(npth*sizeof(char *)); tmp = pth; (*p)[0] = xstrdup(tmp); npth=1; /* count words is + 1 count ':' */ for(;;) { tmp = strchr(tmp, ':'); if(tmp) { (*p)[0][(tmp-pth)]=0; /* ':' -> '\0'*/ tmp++; } else break; (*p)[npth++] = &(*p)[0][(tmp-pth)]; /* p[next]=p[0][&'\0'+1] */ } return npth; } static char** exe_n_cwd_tab_completion(char* command, int *num_matches, int type) { char *dirName; char **matches = 0; DIR *dir; struct dirent *next; char cmd [BUFSIZ+4]; char *dirbuf; char found [BUFSIZ+4]; int nm = *num_matches; struct stat st; char **paths; int npaths; int i; char full_pth[BUFSIZ+4+PATH_MAX]; strcpy(cmd, command); /* save for change (last '/' to '\0') */ dirName = strrchr(cmd, '/'); if(dirName==NULL) { /* no dir, if flags==EXE_ONLY - get paths, else "." */ npaths = path_parse(&paths, type); if(npaths==0) return 0; } else { /* with dir */ /* save dir */ dirbuf = xstrdup(cmd); /* set only dirname */ dirbuf[(dirName-cmd)+1]=0; /* strip dirname in cmd */ strcpy(cmd, dirName+1); paths = xmalloc(sizeof(char*)); paths[0] = dirbuf; npaths = 1; /* only 1 dir */ } for(i=0; i < npaths; i++) { dir = opendir(paths[i]); if (!dir) { /* Don't print an error, just shut up and return */ return (matches); } while ((next = readdir(dir)) != NULL) { /* matched ? */ if(strncmp(next->d_name, cmd, strlen(cmd))) continue; /* not see .name without .match */ if(*next->d_name == '.' && *cmd != '.') continue; sprintf(full_pth, "%s/%s", paths[i], next->d_name); /* hmm, remover in progress? */ if(stat(full_pth, &st)<0) continue; /* Cool, found a match. */ if (S_ISDIR(st.st_mode)) { /* name is directory */ strcpy(found, next->d_name); strcat(found, "/"); if(type==FIND_DIR_ONLY) strcat(found, " "); } else { /* not put found file if search only dirs for cd */ if(type==FIND_DIR_ONLY) continue; strcpy(found, next->d_name); strcat(found, " "); } /* Add it to the list */ matches = xrealloc(matches, (nm+1)*sizeof(char *)); matches[nm++] = xstrdup(found); } } free(paths[0]); /* allocate memory only in first member */ free(paths); *num_matches = nm; return (matches); } static void input_tab(int lastWasTab) { /* Do TAB completion */ static int num_matches; static char **matches; char matchBuf[BUFSIZ]; int pos = cursor; int find_type=FIND_FILE_ONLY; if (lastWasTab == FALSE) { char *tmp, *tmp1; int len_found; /* For now, we will not bother with trying to distinguish * whether the cursor is in/at a command extression -- we * will always try all possible matches. If you don't like * that then feel free to fix it. */ /* Make a local copy of the string -- up * to the position of the cursor */ memset(matchBuf, 0, BUFSIZ); tmp = strncpy(matchBuf, command_ps, cursor); /* skip past any command seperator tokens */ while ( (tmp1=strpbrk(tmp, ";|&{(`")) != NULL) { tmp = ++tmp1; } /* skip any leading white space */ while (*tmp == ' ') tmp++; if(strncmp(tmp, "cd ", 3)==0) find_type = FIND_DIR_ONLY; else if(strchr(tmp, ' ')==NULL) find_type = FIND_EXE_ONLY; /* find begin curent word */ if( (tmp1=strrchr(tmp, ' ')) != NULL) { tmp = ++tmp1; } strcpy(matchBuf, tmp); /* Free up any memory already allocated */ if (matches) { while(num_matches>0) free(matches[--num_matches]); free(matches); matches = (char **) NULL; } #ifdef BB_FEATURE_USERNAME_COMPLETION /* If the word starts with `~' and there is no slash in the word, * then try completing this word as a username. */ if (matchBuf[0]=='~' && strchr(matchBuf, '/')==0) { matches = username_tab_completion(matchBuf, &num_matches); } #endif /* Try to match any executable in our path and everything * in the current working directory that matches. */ if (!matches) matches = exe_n_cwd_tab_completion(matchBuf, &num_matches, find_type); /* Did we find exactly one match? */ if(!matches || num_matches>1) { beep(); return; } len_found = strlen(matches[0]); /* have space to placed match? */ if ( (len_found-strlen(matchBuf)+len) < BUFSIZ ) { int recalc_pos = len; /* before word for match */ command_ps[pos-strlen(matchBuf)]=0; /* tail line */ strcpy(matchBuf, command_ps+pos); /* add match */ strcat(command_ps, matches[0]); /* add tail */ strcat(command_ps, matchBuf); /* write out the matched command */ len=strlen(command_ps); recalc_pos = len-recalc_pos+pos; input_end(); /* write */ while(recalc_pos0 ) { int i, col; int sav_cursor = cursor; /* Go to the next line */ goto_new_line(); for (i=0,col=0; i 60 && matches[i+1] != NULL) { putchar('\n'); col = 0; } } /* Go to the next line and rewrite the prompt */ printf("\n%s", cmdedit_prompt); cmdedit_x = cmdedit_prmt_len; cmdedit_y = 0; cursor = 0; input_end(); /* Rewrite the command */ /* Put the cursor back to where it used to be */ while (sav_cursor < cursor) input_backward(); } } } #endif static void get_previous_history(struct history **hp, char* command) { if ((*hp)->s) free((*hp)->s); (*hp)->s = strdup(command); *hp = (*hp)->p; } static void get_next_history(struct history **hp, char* command) { if ((*hp)->s) free((*hp)->s); (*hp)->s = strdup(command); *hp = (*hp)->n; } /* * This function is used to grab a character buffer * from the input file descriptor and allows you to * a string with full command editing (sortof like * a mini readline). * * The following standard commands are not implemented: * ESC-b -- Move back one word * ESC-f -- Move forward one word * ESC-d -- Delete back one word * ESC-h -- Delete forward one word * CTL-t -- Transpose two characters * * Furthermore, the "vi" command editing keys are not implemented. * * TODO: implement TAB command completion. :) */ extern void cmdedit_read_input(char* prompt, char command[BUFSIZ]) { int inputFd=fileno(stdin); int j = 0; int break_out = 0; int ret = 0; int lastWasTab = FALSE; char c = 0; struct history *hp = his_end; len = 0; cursor = 0; command_ps = command; if (new_settings.c_cc[VMIN]==0) { getTermSettings(inputFd, (void*) &initial_settings); memcpy(&new_settings, &initial_settings, sizeof(struct termios)); new_settings.c_cc[VMIN] = 1; new_settings.c_cc[VTIME] = 0; new_settings.c_cc[VINTR] = _POSIX_VDISABLE; /* Turn off CTRL-C, so we can trap it */ new_settings.c_lflag &= ~ICANON; /* unbuffered input */ new_settings.c_lflag &= ~(ECHO|ECHOCTL|ECHONL); /* Turn off echoing */ } setTermSettings(inputFd, (void*) &new_settings); handlers_sets |= SET_RESET_TERM; memset(command, 0, BUFSIZ); cmdedit_init(); /* Print out the command prompt */ cmdedit_prompt = prompt; cmdedit_prmt_len = strlen(prompt); printf("%s", prompt); cmdedit_x = cmdedit_prmt_len; /* count real x terminal position */ cmdedit_y = 0; /* quasireal y, not true work if line > xt*yt */ while (1) { fflush(stdout); /* buffered out to fast */ if ((ret = read(inputFd, &c, 1)) < 1) return; //fprintf(stderr, "got a '%c' (%d)\n", c, c); switch (c) { case '\n': case '\r': /* Enter */ *(command + len) = c; len++; input_end (); break_out = 1; break; case 1: /* Control-a -- Beginning of line */ input_home(); break; case 2: /* Control-b -- Move back one character */ input_backward(); break; case 3: /* Control-c -- stop gathering input */ /* Link into lash to reset context to 0 on ^C and such */ shell_context = 0; /* Go to the next line */ goto_new_line(); #if 0 /* Rewrite the prompt */ printf("%s", prompt); /* Reset the command string */ memset(command, 0, BUFSIZ); len = cursor = 0; #endif return; case 4: /* Control-d -- Delete one character, or exit * if the len=0 and no chars to delete */ if (len == 0) { printf("exit"); clean_up_and_die(0); } else { input_delete(); } break; case 5: /* Control-e -- End of line */ input_end(); break; case 6: /* Control-f -- Move forward one character */ input_forward(); break; case '\b': case DEL: /* Control-h and DEL */ input_backspace(); break; case '\t': #ifdef BB_FEATURE_SH_TAB_COMPLETION input_tab(lastWasTab); #endif break; case 14: /* Control-n -- Get next command in history */ if (hp && hp->n && hp->n->s) { get_next_history(&hp, command); goto rewrite_line; } else { beep(); } break; case 16: /* Control-p -- Get previous command from history */ if (hp && hp->p) { get_previous_history(&hp, command); goto rewrite_line; } else { beep(); } break; case ESC:{ /* escape sequence follows */ if ((ret = read(inputFd, &c, 1)) < 1) return; if (c == '[') { /* 91 */ if ((ret = read(inputFd, &c, 1)) < 1) return; switch (c) { case 'A': /* Up Arrow -- Get previous command from history */ if (hp && hp->p) { get_previous_history(&hp, command); goto rewrite_line; } else { beep(); } break; case 'B': /* Down Arrow -- Get next command in history */ if (hp && hp->n && hp->n->s) { get_next_history(&hp, command); goto rewrite_line; } else { beep(); } break; /* Rewrite the line with the selected history item */ rewrite_line: /* return to begin of line */ input_home (); /* for next memmoves without set '\0' */ memset (command, 0, BUFSIZ); /* change command */ strcpy (command, hp->s); /* write new command */ for (j=0; command[j]; j++) cmdedit_set_out_char(command[j], 0); ret = cursor; /* erase tail if required */ for (j = ret; j < len; j++) cmdedit_set_out_char(' ', 0); /* and backward cursor */ for (j = ret; j < len; j++) input_backward(); len = cursor; /* set new len */ break; case 'C': /* Right Arrow -- Move forward one character */ input_forward(); break; case 'D': /* Left Arrow -- Move back one character */ input_backward(); break; case '3': /* Delete */ input_delete(); break; case '1': /* Home (Ctrl-A) */ input_home(); break; case '4': /* End (Ctrl-E) */ input_end(); break; default: beep(); } if (c == '1' || c == '3' || c == '4') if ((ret = read(inputFd, &c, 1)) < 1) return; /* read 126 (~) */ } if (c == 'O') { /* 79 */ if ((ret = read(inputFd, &c, 1)) < 1) return; switch (c) { case 'H': /* Home (xterm) */ input_home(); break; case 'F': /* End (xterm) */ input_end(); break; default: beep(); } } c = 0; break; } default: /* If it's regular input, do the normal thing */ if (!isprint(c)) { /* Skip non-printable characters */ break; } if (len >= (BUFSIZ - 2)) /* Need to leave space for enter */ break; len++; if (cursor == (len - 1)) { /* Append if at the end of the line */ *(command + cursor) = c; cmdedit_set_out_char(c, command[cursor+1]); } else { /* Insert otherwise */ memmove(command + cursor + 1, command + cursor, len - cursor - 1); *(command + cursor) = c; j = cursor+1; /* rewrite from cursor */ input_end (); /* to prev x pos + 1 */ while(cursor > j) input_backward(); } break; } if (c == '\t') lastWasTab = TRUE; else lastWasTab = FALSE; if (break_out) /* Enter is the command terminator, no more input. */ break; } setTermSettings (inputFd, (void *) &initial_settings); handlers_sets &= ~SET_RESET_TERM; /* Handle command history log */ if (len>1) { /* no put empty line (only '\n') */ struct history *h = his_end; char *ss; command[len-1] = 0; /* destroy end '\n' */ ss = strdup(command); /* duplicate without '\n' */ command[len-1] = '\n'; /* restore '\n' */ if (!h) { /* No previous history -- this memory is never freed */ h = his_front = xmalloc(sizeof(struct history)); h->n = xmalloc(sizeof(struct history)); h->p = NULL; h->s = ss; h->n->p = h; h->n->n = NULL; h->n->s = NULL; his_end = h->n; history_counter++; } else { /* Add a new history command -- this memory is never freed */ h->n = xmalloc(sizeof(struct history)); h->n->p = h; h->n->n = NULL; h->n->s = NULL; h->s = ss; his_end = h->n; /* After max history, remove the oldest command */ if (history_counter >= MAX_HISTORY) { struct history *p = his_front->n; p->p = NULL; free(his_front->s); free(his_front); his_front = p; } else { history_counter++; } } } return; } /* Undo the effects of cmdedit_init(). */ extern void cmdedit_terminate(void) { cmdedit_reset_term(); if((handlers_sets & SET_TERM_HANDLERS)!=0) { signal(SIGKILL, SIG_DFL); signal(SIGINT, SIG_DFL); signal(SIGQUIT, SIG_DFL); signal(SIGTERM, SIG_DFL); signal(SIGWINCH, SIG_DFL); handlers_sets &= ~SET_TERM_HANDLERS; } } #endif /* BB_FEATURE_SH_COMMAND_EDITING */