/* 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. The binary size increase is <3K. Editting will not display correctly for lines greater then the terminal width. (more then one line.) However, history will. */ #include "internal.h" #ifdef BB_FEATURE_SH_COMMAND_EDITING #include #include #include #include #include #include #include #include #define MAX_HISTORY 15 /* Maximum length of the linked list for the command line history */ #define ESC 27 #define 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. */ #ifdef BB_FEATURE_USE_TERMIOS #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 */ struct termios initial_settings, new_settings; #ifndef _POSIX_VDISABLE #define _POSIX_VDISABLE '\0' #endif #endif static int cmdedit_termw = 80; /* actual terminal width */ static int cmdedit_scroll = 27; /* width of EOL scrolling region */ static int history_counter = 0; /* Number of commands in history list */ static int reset_term = 0; /* Set to true if the terminal needs to be reset upon exit */ struct history { char *s; struct history *p; struct history *n; }; #define xwrite write void cmdedit_setwidth(int w) { if (w > 20) { cmdedit_termw = w; cmdedit_scroll = w / 3; } else { errorMsg("\n*** Error: minimum screen width is 21\n"); } } void cmdedit_reset_term(void) { if (reset_term) /* sparc and other have broken termios support: use old termio handling. */ setTermSettings(fileno(stdin), (void*) &initial_settings); } void clean_up_and_die(int sig) { cmdedit_reset_term(); fprintf(stdout, "\n"); if (sig!=SIGINT) exit(TRUE); } /* Go to HOME position */ void input_home(int outputFd, int *cursor) { while (*cursor > 0) { xwrite(outputFd, "\b", 1); --*cursor; } } /* Go to END position */ void input_end(int outputFd, int *cursor, int len) { while (*cursor < len) { xwrite(outputFd, "\033[C", 3); ++*cursor; } } /* Delete the char in back of the cursor */ void input_backspace(char* command, int outputFd, int *cursor, int *len) { int j = 0; if (*cursor > 0) { xwrite(outputFd, "\b \b", 3); --*cursor; memmove(command + *cursor, command + *cursor + 1, BUFSIZ - *cursor + 1); for (j = *cursor; j < (BUFSIZ - 1); j++) { if (!*(command + j)) break; else xwrite(outputFd, (command + j), 1); } xwrite(outputFd, " \b", 2); while (j-- > *cursor) xwrite(outputFd, "\b", 1); --*len; } } /* Delete the char in front of the cursor */ void input_delete(char* command, int outputFd, int cursor, int *len) { int j = 0; if (cursor == *len) return; memmove(command + cursor, command + cursor + 1, BUFSIZ - cursor - 1); for (j = cursor; j < (BUFSIZ - 1); j++) { if (!*(command + j)) break; else xwrite(outputFd, (command + j), 1); } xwrite(outputFd, " \b", 2); while (j-- > cursor) xwrite(outputFd, "\b", 1); --*len; } /* Move forward one charactor */ void input_forward(int outputFd, int *cursor, int len) { if (*cursor < len) { xwrite(outputFd, "\033[C", 3); ++*cursor; } } /* Move back one charactor */ void input_backward(int outputFd, int *cursor) { if (*cursor > 0) { xwrite(outputFd, "\033[D", 3); --*cursor; } } #ifdef BB_FEATURE_SH_TAB_COMPLETION char** username_tab_completion(char* command, int *num_matches) { char **matches = (char **) NULL; *num_matches=0; fprintf(stderr, "\nin username_tab_completion\n"); return (matches); } #include char** exe_n_cwd_tab_completion(char* command, int *num_matches) { char *dirName; char **matches = (char **) NULL; DIR *dir; struct dirent *next; matches = malloc( sizeof(char*)*50); /* Stick a wildcard onto the command, for later use */ strcat( command, "*"); /* Now wall the current directory */ dirName = get_current_dir_name(); dir = opendir(dirName); if (!dir) { /* Don't print an error, just shut up and return */ *num_matches=0; return (matches); } while ((next = readdir(dir)) != NULL) { /* Some quick sanity checks */ if ((strcmp(next->d_name, "..") == 0) || (strcmp(next->d_name, ".") == 0)) { continue; } /* See if this matches */ if (check_wildcard_match(next->d_name, command) == TRUE) { /* Cool, found a match. Add it to the list */ matches[*num_matches] = malloc(strlen(next->d_name)+1); strcpy( matches[*num_matches], next->d_name); ++*num_matches; //matches = realloc( matches, sizeof(char*)*(*num_matches)); } } return (matches); } void input_tab(char* command, char* prompt, int outputFd, int *cursor, int *len) { /* Do TAB completion */ static int num_matches=0; static char **matches = (char **) NULL; int pos = cursor; if (lastWasTab == FALSE) { char *tmp, *tmp1, *matchBuf; /* 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 */ matchBuf = (char *) calloc(BUFSIZ, sizeof(char)); strncpy(matchBuf, command, cursor); tmp=matchBuf; /* skip past any command seperator tokens */ while (*tmp && (tmp1=strpbrk(tmp, ";|&{(`")) != NULL) { tmp=++tmp1; /* skip any leading white space */ while (*tmp && isspace(*tmp)) ++tmp; } /* skip any leading white space */ while (*tmp && isspace(*tmp)) ++tmp; /* Free up any memory already allocated */ if (matches) { free(matches); matches = (char **) NULL; } /* If the word starts with `~' and there is no slash in the word, * then try completing this word as a username. */ /* FIXME -- this check is broken! */ if (*tmp == '~' && !strchr(tmp, '/')) matches = username_tab_completion(tmp, &num_matches); /* 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(tmp, &num_matches); /* Don't leak memory */ free( matchBuf); /* Did we find exactly one match? */ if (matches && num_matches==1) { /* write out the matched command */ strncpy(command+pos, matches[0]+pos, strlen(matches[0])-pos); len=strlen(command); cursor=len; xwrite(outputFd, matches[0]+pos, strlen(matches[0])-pos); break; } } else { /* Ok -- the last char was a TAB. Since they * just hit TAB again, print a list of all the * available choices... */ if ( matches && num_matches>0 ) { int i, col; /* Go to the next line */ xwrite(outputFd, "\n", 1); /* Print the list of matches */ for (i=0,col=0; i 60 && matches[i+1] != NULL) { xwrite(outputFd, "\n", 1); col = 0; } } /* Go to the next line */ xwrite(outputFd, "\n", 1); /* Rewrite the prompt */ xwrite(outputFd, prompt, strlen(prompt)); /* Rewrite the command */ xwrite(outputFd, command, len); /* Put the cursor back to where it used to be */ for (cursor=len; cursor > pos; cursor--) xwrite(outputFd, "\b", 1); } } } #endif void get_previous_history(struct history **hp, char* command) { free((*hp)->s); (*hp)->s = strdup(command); *hp = (*hp)->p; } void get_next_history(struct history **hp, char* command) { 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 outputFd=fileno(stdout); int nr = 0; int len = 0; int j = 0; int cursor = 0; int break_out = 0; int ret = 0; int lastWasTab = FALSE; char c = 0; struct history *hp = his_end; memset(command, 0, sizeof(command)); if (!reset_term) { 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 */ reset_term = 1; } setTermSettings(inputFd, (void*) &new_settings); memset(command, 0, BUFSIZ); while (1) { 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++ + 1) = c; xwrite(outputFd, &c, 1); break_out = 1; break; case 1: /* Control-a -- Beginning of line */ input_home(outputFd, &cursor); case 2: /* Control-b -- Move back one character */ input_backward(outputFd, &cursor); break; case 3: /* Control-c -- leave the current line, * and start over on the next line */ /* Go to the next line */ xwrite(outputFd, "\n", 1); /* Rewrite the prompt */ xwrite(outputFd, prompt, strlen(prompt)); /* Reset the command string */ memset(command, 0, sizeof(command)); len = cursor = 0; break; case 4: /* Control-d -- Delete one character, or exit * if the len=0 and no chars to delete */ if (len == 0) { xwrite(outputFd, "exit", 4); clean_up_and_die(0); } else { input_delete(command, outputFd, cursor, &len); } break; case 5: /* Control-e -- End of line */ input_end(outputFd, &cursor, len); break; case 6: /* Control-f -- Move forward one character */ input_forward(outputFd, &cursor, len); break; case '\b': case DEL: /* Control-h and DEL */ input_backspace(command, outputFd, &cursor, &len); break; case '\t': #ifdef BB_FEATURE_SH_TAB_COMPLETION input_tab(command, prompt, outputFd, &cursor, &len); #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 { xwrite(outputFd, "\007", 1); } break; case 16: /* Control-p -- Get previous command from history */ if (hp && hp->p) { get_previous_history(&hp, command); goto rewrite_line; } else { xwrite(outputFd, "\007", 1); } 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 { xwrite(outputFd, "\007", 1); } 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 { xwrite(outputFd, "\007", 1); } break; /* Rewrite the line with the selected history item */ rewrite_line: /* erase old command from command line */ len = strlen(command)-strlen(hp->s); while (len>0) input_backspace(command, outputFd, &cursor, &len); input_home(outputFd, &cursor); /* write new command */ strcpy(command, hp->s); len = strlen(hp->s); xwrite(outputFd, command, len); cursor = len; break; case 'C': /* Right Arrow -- Move forward one character */ input_forward(outputFd, &cursor, len); break; case 'D': /* Left Arrow -- Move back one character */ input_backward(outputFd, &cursor); break; case '3': /* Delete */ input_delete(command, outputFd, cursor, &len); break; case '1': /* Home (Ctrl-A) */ input_home(outputFd, &cursor); break; case '4': /* End (Ctrl-E) */ input_end(outputFd, &cursor, len); break; default: xwrite(outputFd, "\007", 1); } 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(outputFd, &cursor); break; case 'F': /* End (xterm) */ input_end(outputFd, &cursor, len); break; default: xwrite(outputFd, "\007", 1); } } 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; } else { /* Insert otherwise */ memmove(command + cursor + 1, command + cursor, len - cursor - 1); *(command + cursor) = c; for (j = cursor; j < len; j++) xwrite(outputFd, command + j, 1); for (; j > cursor; j--) xwrite(outputFd, "\033[D", 3); } cursor++; xwrite(outputFd, &c, 1); break; } if (c == '\t') lastWasTab = TRUE; else lastWasTab = FALSE; if (break_out) /* Enter is the command terminator, no more input. */ break; } nr = len + 1; setTermSettings(inputFd, (void *) &initial_settings); reset_term = 0; /* Handle command history log */ if (*(command)) { struct history *h = his_end; if (!h) { /* No previous history */ h = his_front = malloc(sizeof(struct history)); h->n = malloc(sizeof(struct history)); h->p = NULL; h->s = strdup(command); h->n->p = h; h->n->n = NULL; h->n->s = NULL; his_end = h->n; history_counter++; } else { /* Add a new history command */ h->n = malloc(sizeof(struct history)); h->n->p = h; h->n->n = NULL; h->n->s = NULL; h->s = strdup(command); 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; } extern void cmdedit_init(void) { atexit(cmdedit_reset_term); signal(SIGKILL, clean_up_and_die); signal(SIGINT, clean_up_and_die); signal(SIGQUIT, clean_up_and_die); signal(SIGTERM, clean_up_and_die); } #endif /* BB_FEATURE_SH_COMMAND_EDITING */