X-Git-Url: https://git.librecmc.org/?a=blobdiff_plain;f=libbb%2Flineedit.c;h=0563e6d011599e6dbf08f4e13579e3b1cee3cdac;hb=0bfb9c2cf0e8b711634014e06d39b5f47c5a95c2;hp=6de66ba83e3b151fa2d894e4c11bd336da3a2945;hpb=5d89fbaa2e00a8a26e530306d76b78bf91d12ec8;p=oweals%2Fbusybox.git diff --git a/libbb/lineedit.c b/libbb/lineedit.c index 6de66ba83..0563e6d01 100644 --- a/libbb/lineedit.c +++ b/libbb/lineedit.c @@ -1,6 +1,6 @@ /* vi: set sw=4 ts=4: */ /* - * Termios command line History and Editing. + * Command line editing. * * Copyright (c) 1986-2003 may safely be consumed by a BSD or GPL license. * Written by: Vladimir Oleynik @@ -15,61 +15,98 @@ */ /* - 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. - - Small bugs (simple effect): - - not true viewing if terminal size (x*y symbols) less - size (prompt + editor's line + 2 symbols) - - not true viewing if length prompt less terminal width + * Usage and known bugs: + * Terminal key codes are not extensive, more needs 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 following readline-like commands are not implemented: + * ESC-b -- Move back one word + * ESC-f -- Move forward one word + * ESC-d -- Delete forward one word + * CTL-t -- Transpose two characters + * + * lineedit does not know that the terminal escape sequences do not + * take up space on the screen. The redisplay code assumes, unless + * told otherwise, that each character in the prompt is a printable + * character that takes up one character position on the screen. + * You need to tell lineedit that some sequences of characters + * in the prompt take up no screen space. Compatibly with readline, + * use the \[ escape to begin a sequence of non-printing characters, + * and the \] escape to signal the end of such a sequence. Example: + * + * PS1='\[\033[01;32m\]\u@\h\[\033[01;34m\] \w \$\[\033[00m\] ' */ - #include "libbb.h" - - -/* FIXME: obsolete CONFIG item? */ -#define ENABLE_FEATURE_NONPRINTABLE_INVERSE_PUT 0 - +#include "unicode.h" #ifdef TEST - -#define ENABLE_FEATURE_EDITING 0 -#define ENABLE_FEATURE_TAB_COMPLETION 0 -#define ENABLE_FEATURE_USERNAME_COMPLETION 0 -#define ENABLE_FEATURE_NONPRINTABLE_INVERSE_PUT 0 - -#endif /* TEST */ +# define ENABLE_FEATURE_EDITING 0 +# define ENABLE_FEATURE_TAB_COMPLETION 0 +# define ENABLE_FEATURE_USERNAME_COMPLETION 0 +#endif /* Entire file (except TESTing part) sits inside this #if */ #if ENABLE_FEATURE_EDITING -#if ENABLE_LOCALE_SUPPORT -#define Isprint(c) isprint(c) -#else -#define Isprint(c) ((c) >= ' ' && (c) != ((unsigned char)'\233')) -#endif -#define ENABLE_FEATURE_GETUSERNAME_AND_HOMEDIR \ +#define ENABLE_USERNAME_OR_HOMEDIR \ (ENABLE_FEATURE_USERNAME_COMPLETION || ENABLE_FEATURE_EDITING_FANCY_PROMPT) -#define USE_FEATURE_GETUSERNAME_AND_HOMEDIR(...) -#if ENABLE_FEATURE_GETUSERNAME_AND_HOMEDIR -#undef USE_FEATURE_GETUSERNAME_AND_HOMEDIR -#define USE_FEATURE_GETUSERNAME_AND_HOMEDIR(...) __VA_ARGS__ +#define IF_USERNAME_OR_HOMEDIR(...) +#if ENABLE_USERNAME_OR_HOMEDIR +# undef IF_USERNAME_OR_HOMEDIR +# define IF_USERNAME_OR_HOMEDIR(...) __VA_ARGS__ +#endif + + +#undef CHAR_T +#if ENABLE_UNICODE_SUPPORT +# define BB_NUL ((wchar_t)0) +# define CHAR_T wchar_t +static bool BB_isspace(CHAR_T c) { return ((unsigned)c < 256 && isspace(c)); } +# if ENABLE_FEATURE_EDITING_VI +static bool BB_isalnum(CHAR_T c) { return ((unsigned)c < 256 && isalnum(c)); } +# endif +static bool BB_ispunct(CHAR_T c) { return ((unsigned)c < 256 && ispunct(c)); } +# undef isspace +# undef isalnum +# undef ispunct +# undef isprint +# define isspace isspace_must_not_be_used +# define isalnum isalnum_must_not_be_used +# define ispunct ispunct_must_not_be_used +# define isprint isprint_must_not_be_used +#else +# define BB_NUL '\0' +# define CHAR_T char +# define BB_isspace(c) isspace(c) +# define BB_isalnum(c) isalnum(c) +# define BB_ispunct(c) ispunct(c) +#endif +#if ENABLE_UNICODE_PRESERVE_BROKEN +# define unicode_mark_raw_byte(wc) ((wc) | 0x20000000) +# define unicode_is_raw_byte(wc) ((wc) & 0x20000000) +#else +# define unicode_is_raw_byte(wc) 0 #endif + +#define ESC "\033" + +#define SEQ_CLEAR_TILL_END_OF_SCREEN ESC"[J" +//#define SEQ_CLEAR_TILL_END_OF_LINE ESC"[K" + + enum { - /* We use int16_t for positions, need to limit line len */ MAX_LINELEN = CONFIG_FEATURE_EDITING_MAX_LEN < 0x7ff0 ? CONFIG_FEATURE_EDITING_MAX_LEN : 0x7ff0 }; -#if ENABLE_FEATURE_GETUSERNAME_AND_HOMEDIR +#if ENABLE_USERNAME_OR_HOMEDIR static const char null_str[] ALIGN1 = ""; #endif @@ -80,21 +117,23 @@ struct lineedit_statics { volatile unsigned cmdedit_termw; /* = 80; */ /* actual terminal width */ sighandler_t previous_SIGWINCH_handler; - - int cmdedit_x; /* real x terminal position */ - int cmdedit_y; /* pseudoreal y terminal position */ - int cmdedit_prmt_len; /* length of prompt (without colors etc) */ + unsigned cmdedit_x; /* real x (col) terminal position */ + unsigned cmdedit_y; /* pseudoreal y (row) terminal position */ + unsigned cmdedit_prmt_len; /* length of prompt (without colors etc) */ unsigned cursor; - unsigned command_len; - char *command_ps; + int command_len; /* must be signed */ + /* signed maxsize: we want x in "if (x > S.maxsize)" + * to _not_ be promoted to unsigned */ + int maxsize; + CHAR_T *command_ps; const char *cmdedit_prompt; #if ENABLE_FEATURE_EDITING_FANCY_PROMPT int num_ok_lines; /* = 1; */ #endif -#if ENABLE_FEATURE_GETUSERNAME_AND_HOMEDIR +#if ENABLE_USERNAME_OR_HOMEDIR char *user_buf; char *home_pwd_buf; /* = (char*)null_str; */ #endif @@ -105,18 +144,13 @@ struct lineedit_statics { #endif #if ENABLE_FEATURE_EDITING_VI -#define DELBUFSIZ 128 - char *delptr; +# define DELBUFSIZ 128 + CHAR_T *delptr; smallint newdelflag; /* whether delbuf should be reused yet */ - char delbuf[DELBUFSIZ]; /* a place to store deleted characters */ + CHAR_T delbuf[DELBUFSIZ]; /* a place to store deleted characters */ #endif - - /* Formerly these were big buffers on stack: */ -#if ENABLE_FEATURE_TAB_COMPLETION - char exe_n_cwd_tab_completion__dirbuf[MAX_LINELEN]; - char input_tab__matchBuf[MAX_LINELEN]; - int16_t find_match__int_buf[MAX_LINELEN + 1]; /* need to have 9 bits at least */ - int16_t find_match__pos_buf[MAX_LINELEN + 1]; +#if ENABLE_FEATURE_EDITING_ASK_TERMINAL + smallint sent_ESC_br6n; #endif }; @@ -147,17 +181,17 @@ extern struct lineedit_statics *const lineedit_ptr_to_statics; (*(struct lineedit_statics**)&lineedit_ptr_to_statics) = xzalloc(sizeof(S)); \ barrier(); \ cmdedit_termw = 80; \ - USE_FEATURE_EDITING_FANCY_PROMPT(num_ok_lines = 1;) \ - USE_FEATURE_GETUSERNAME_AND_HOMEDIR(home_pwd_buf = (char*)null_str;) \ + IF_FEATURE_EDITING_FANCY_PROMPT(num_ok_lines = 1;) \ + IF_USERNAME_OR_HOMEDIR(home_pwd_buf = (char*)null_str;) \ } while (0) static void deinit_S(void) { #if ENABLE_FEATURE_EDITING_FANCY_PROMPT /* This one is allocated only if FANCY_PROMPT is on - * (otherwise it points to verbatim prompt (NOT malloced) */ + * (otherwise it points to verbatim prompt (NOT malloced)) */ free((char*)cmdedit_prompt); #endif -#if ENABLE_FEATURE_GETUSERNAME_AND_HOMEDIR +#if ENABLE_USERNAME_OR_HOMEDIR free(user_buf); if (home_pwd_buf != null_str) free(home_pwd_buf); @@ -166,89 +200,263 @@ static void deinit_S(void) } #define DEINIT_S() deinit_S() + +#if ENABLE_UNICODE_SUPPORT +static size_t load_string(const char *src, int maxsize) +{ + if (unicode_status == UNICODE_ON) { + ssize_t len = mbstowcs(command_ps, src, maxsize - 1); + if (len < 0) + len = 0; + command_ps[len] = BB_NUL; + return len; + } else { + unsigned i = 0; + while ((command_ps[i] = src[i]) != 0) + i++; + return i; + } +} +static unsigned save_string(char *dst, unsigned maxsize) +{ + if (unicode_status == UNICODE_ON) { +# if !ENABLE_UNICODE_PRESERVE_BROKEN + ssize_t len = wcstombs(dst, command_ps, maxsize - 1); + if (len < 0) + len = 0; + dst[len] = '\0'; + return len; +# else + unsigned dstpos = 0; + unsigned srcpos = 0; + + maxsize--; + while (dstpos < maxsize) { + wchar_t wc; + int n = srcpos; + + /* Convert up to 1st invalid byte (or up to end) */ + while ((wc = command_ps[srcpos]) != BB_NUL + && !unicode_is_raw_byte(wc) + ) { + srcpos++; + } + command_ps[srcpos] = BB_NUL; + n = wcstombs(dst + dstpos, command_ps + n, maxsize - dstpos); + if (n < 0) /* should not happen */ + break; + dstpos += n; + if (wc == BB_NUL) /* usually is */ + break; + + /* We do have invalid byte here! */ + command_ps[srcpos] = wc; /* restore it */ + srcpos++; + if (dstpos == maxsize) + break; + dst[dstpos++] = (char) wc; + } + dst[dstpos] = '\0'; + return dstpos; +# endif + } else { + unsigned i = 0; + while ((dst[i] = command_ps[i]) != 0) + i++; + return i; + } +} +/* I thought just fputwc(c, stdout) would work. But no... */ +static void BB_PUTCHAR(wchar_t c) +{ + if (unicode_status == UNICODE_ON) { + char buf[MB_CUR_MAX + 1]; + mbstate_t mbst = { 0 }; + ssize_t len = wcrtomb(buf, c, &mbst); + if (len > 0) { + buf[len] = '\0'; + fputs(buf, stdout); + } + } else { + /* In this case, c is always one byte */ + putchar(c); + } +} +# if ENABLE_UNICODE_COMBINING_WCHARS || ENABLE_UNICODE_WIDE_WCHARS +static wchar_t adjust_width_and_validate_wc(unsigned *width_adj, wchar_t wc) +# else +static wchar_t adjust_width_and_validate_wc(wchar_t wc) +# define adjust_width_and_validate_wc(width_adj, wc) \ + ((*(width_adj))++, adjust_width_and_validate_wc(wc)) +# endif +{ + int w = 1; + + if (unicode_status == UNICODE_ON) { + if (wc > CONFIG_LAST_SUPPORTED_WCHAR) { + /* note: also true for unicode_is_raw_byte(wc) */ + goto subst; + } + w = wcwidth(wc); + if ((ENABLE_UNICODE_COMBINING_WCHARS && w < 0) + || (!ENABLE_UNICODE_COMBINING_WCHARS && w <= 0) + || (!ENABLE_UNICODE_WIDE_WCHARS && w > 1) + ) { + subst: + w = 1; + wc = CONFIG_SUBST_WCHAR; + } + } + +# if ENABLE_UNICODE_COMBINING_WCHARS || ENABLE_UNICODE_WIDE_WCHARS + *width_adj += w; +#endif + return wc; +} +#else /* !UNICODE */ +static size_t load_string(const char *src, int maxsize) +{ + safe_strncpy(command_ps, src, maxsize); + return strlen(command_ps); +} +# if ENABLE_FEATURE_TAB_COMPLETION +static void save_string(char *dst, unsigned maxsize) +{ + safe_strncpy(dst, command_ps, maxsize); +} +# endif +# define BB_PUTCHAR(c) bb_putchar(c) +/* Should never be called: */ +int adjust_width_and_validate_wc(unsigned *width_adj, int wc); +#endif + + /* Put 'command_ps[cursor]', cursor++. * Advance cursor on screen. If we reached right margin, scroll text up * and remove terminal margin effect by printing 'next_char' */ -static void cmdedit_set_out_char(int next_char) +#define HACK_FOR_WRONG_WIDTH 1 +static void put_cur_glyph_and_inc_cursor(void) { - int c = (unsigned char)command_ps[cursor]; + CHAR_T c = command_ps[cursor]; + unsigned width = 0; + int ofs_to_right; - if (c == '\0') { + if (c == BB_NUL) { /* erase character after end of input string */ c = ' '; + } else { + /* advance cursor only if we aren't at the end yet */ + cursor++; + if (unicode_status == UNICODE_ON) { + IF_UNICODE_WIDE_WCHARS(width = cmdedit_x;) + c = adjust_width_and_validate_wc(&cmdedit_x, c); + IF_UNICODE_WIDE_WCHARS(width = cmdedit_x - width;) + } else { + cmdedit_x++; + } } -#if ENABLE_FEATURE_NONPRINTABLE_INVERSE_PUT - /* Display non-printable characters in reverse */ - if (!Isprint(c)) { - if (c >= 128) - c -= 128; - if (c < ' ') - c += '@'; - if (c == 127) - c = '?'; - printf("\033[7m%c\033[0m", c); - } else -#endif - { - bb_putchar(c); + + ofs_to_right = cmdedit_x - cmdedit_termw; + if (!ENABLE_UNICODE_WIDE_WCHARS || ofs_to_right <= 0) { + /* c fits on this line */ + BB_PUTCHAR(c); } - if (++cmdedit_x >= cmdedit_termw) { - /* terminal is scrolled down */ - cmdedit_y++; - cmdedit_x = 0; - /* destroy "(auto)margin" */ - bb_putchar(next_char); + + if (ofs_to_right >= 0) { + /* we go to the next line */ +#if HACK_FOR_WRONG_WIDTH + /* This works better if our idea of term width is wrong + * and it is actually wider (often happens on serial lines). + * Printing CR,LF *forces* cursor to next line. + * OTOH if terminal width is correct AND terminal does NOT + * have automargin (IOW: it is moving cursor to next line + * by itself (which is wrong for VT-10x terminals)), + * this will break things: there will be one extra empty line */ + puts("\r"); /* + implicit '\n' */ +#else + /* VT-10x terminals don't wrap cursor to next line when last char + * on the line is printed - cursor stays "over" this char. + * Need to print _next_ char too (first one to appear on next line) + * to make cursor move down to next line. + */ + /* Works ok only if cmdedit_termw is correct. */ + c = command_ps[cursor]; + if (c == BB_NUL) + c = ' '; + BB_PUTCHAR(c); bb_putchar('\b'); +#endif + cmdedit_y++; + if (!ENABLE_UNICODE_WIDE_WCHARS || ofs_to_right == 0) { + width = 0; + } else { /* ofs_to_right > 0 */ + /* wide char c didn't fit on prev line */ + BB_PUTCHAR(c); + } + cmdedit_x = width; } -// Huh? What if command_ps[cursor] == '\0' (we are at the end already?) - cursor++; } /* Move to end of line (by printing all chars till the end) */ -static void input_end(void) +static void put_till_end_and_adv_cursor(void) { while (cursor < command_len) - cmdedit_set_out_char(' '); + put_cur_glyph_and_inc_cursor(); } /* Go to the next line */ static void goto_new_line(void) { - input_end(); - if (cmdedit_x) + put_till_end_and_adv_cursor(); + if (cmdedit_x != 0) bb_putchar('\n'); } - -static void out1str(const char *s) +static void beep(void) { - if (s) - fputs(s, stdout); + bb_putchar('\007'); } -static void beep(void) +static void put_prompt(void) { - bb_putchar('\007'); + unsigned w; + + fputs(cmdedit_prompt, stdout); + fflush_all(); + cursor = 0; + w = cmdedit_termw; /* read volatile var once */ + cmdedit_y = cmdedit_prmt_len / w; /* new quasireal y */ + cmdedit_x = cmdedit_prmt_len % w; } /* Move back one character */ /* (optimized for slow terminals) */ static void input_backward(unsigned num) { - int count_y; - if (num > cursor) num = cursor; - if (!num) + if (num == 0) return; cursor -= num; + if ((ENABLE_UNICODE_COMBINING_WCHARS || ENABLE_UNICODE_WIDE_WCHARS) + && unicode_status == UNICODE_ON + ) { + /* correct NUM to be equal to _screen_ width */ + int n = num; + num = 0; + while (--n >= 0) + adjust_width_and_validate_wc(&num, command_ps[cursor + n]); + if (num == 0) + return; + } + if (cmdedit_x >= num) { cmdedit_x -= num; if (num <= 4) { /* This is longer by 5 bytes on x86. - * Also gets mysteriously - * miscompiled for some ARM users. + * Also gets miscompiled for ARM users + * (busybox.net/bugs/view.php?id=2274). * printf(("\b\b\b\b" + 4) - num); * return; */ @@ -257,37 +465,66 @@ static void input_backward(unsigned num) } while (--num); return; } - printf("\033[%uD", num); + printf(ESC"[%uD", num); return; } /* Need to go one or more lines up */ - num -= cmdedit_x; - count_y = 1 + (num / cmdedit_termw); - cmdedit_y -= count_y; - cmdedit_x = cmdedit_termw * count_y - num; - /* go to 1st column; go up; go to correct column */ - printf("\r" "\033[%dA" "\033[%dC", count_y, cmdedit_x); -} - -static void put_prompt(void) -{ - out1str(cmdedit_prompt); - cmdedit_x = cmdedit_prmt_len; - cursor = 0; -// Huh? what if cmdedit_prmt_len >= width? - cmdedit_y = 0; /* new quasireal y */ + if (ENABLE_UNICODE_WIDE_WCHARS) { + /* With wide chars, it is hard to "backtrack" + * and reliably figure out where to put cursor. + * Example (<> is a wide char; # is an ordinary char, _ cursor): + * |prompt: <><> | + * |<><><><><><> | + * |_ | + * and user presses left arrow. num = 1, cmdedit_x = 0, + * We need to go up one line, and then - how do we know that + * we need to go *10* positions to the right? Because + * |prompt: <>#<>| + * |<><><>#<><><>| + * |_ | + * in this situation we need to go *11* positions to the right. + * + * A simpler thing to do is to redraw everything from the start + * up to new cursor position (which is already known): + */ + unsigned sv_cursor; + /* go to 1st column; go up to first line */ + printf("\r" ESC"[%uA", cmdedit_y); + cmdedit_y = 0; + sv_cursor = cursor; + put_prompt(); /* sets cursor to 0 */ + while (cursor < sv_cursor) + put_cur_glyph_and_inc_cursor(); + } else { + int lines_up; + unsigned width; + /* num = chars to go back from the beginning of current line: */ + num -= cmdedit_x; + width = cmdedit_termw; /* read volatile var once */ + /* num=1...w: one line up, w+1...2w: two, etc: */ + lines_up = 1 + (num - 1) / width; + cmdedit_x = (width * cmdedit_y - num) % width; + cmdedit_y -= lines_up; + /* go to 1st column; go up */ + printf("\r" ESC"[%uA", lines_up); + /* go to correct column. + * xterm, konsole, Linux VT interpret 0 as 1 below! wow. + * need to *make sure* we skip it if cmdedit_x == 0 */ + if (cmdedit_x) + printf(ESC"[%uC", cmdedit_x); + } } /* draw prompt, editor line, and clear tail */ static void redraw(int y, int back_cursor) { - if (y > 0) /* up to start y */ - printf("\033[%dA", y); + if (y > 0) /* up y lines */ + printf(ESC"[%uA", y); bb_putchar('\r'); put_prompt(); - input_end(); /* rewrite */ - printf("\033[J"); /* erase after cursor */ + put_till_end_and_adv_cursor(); + printf(SEQ_CLEAR_TILL_END_OF_SCREEN); input_backward(back_cursor); } @@ -302,7 +539,7 @@ static void input_delete(int save) { int j = cursor; - if (j == command_len) + if (j == (int)command_len) return; #if ENABLE_FEATURE_EDITING_VI @@ -316,10 +553,14 @@ static void input_delete(int save) } #endif - strcpy(command_ps + j, command_ps + j + 1); + memmove(command_ps + j, command_ps + j + 1, + /* (command_len + 1 [because of NUL]) - (j + 1) + * simplified into (command_len - j) */ + (command_len - j) * sizeof(command_ps[0])); command_len--; - input_end(); /* rewrite new line */ - cmdedit_set_out_char(' '); /* erase char */ + put_till_end_and_adv_cursor(); + /* Last char is still visible, erase it (and more) */ + printf(SEQ_CLEAR_TILL_END_OF_SCREEN); input_backward(cursor - j); /* back to old pos cursor */ } @@ -333,10 +574,11 @@ static void put(void) return; ocursor = cursor; /* open hole and then fill it */ - memmove(command_ps + cursor + j, command_ps + cursor, command_len - cursor + 1); - strncpy(command_ps + cursor, delbuf, j); + memmove(command_ps + cursor + j, command_ps + cursor, + (command_len - cursor + 1) * sizeof(command_ps[0])); + memcpy(command_ps + cursor, delbuf, j * sizeof(command_ps[0])); command_len += j; - input_end(); /* rewrite new line */ + put_till_end_and_adv_cursor(); input_backward(cursor - ocursor - j + 1); /* at end of new text */ } #endif @@ -354,11 +596,17 @@ static void input_backspace(void) static void input_forward(void) { if (cursor < command_len) - cmdedit_set_out_char(command_ps[cursor + 1]); + put_cur_glyph_and_inc_cursor(); } #if ENABLE_FEATURE_TAB_COMPLETION +//FIXME: +//needs to be more clever: currently it thinks that "foo\ b +//matches the file named "foo bar", which is untrue. +//Also, perhaps "foo b needs to complete to "foo bar" , +//not "foo bar ... + static void free_tab_completion_data(void) { if (matches) { @@ -371,64 +619,68 @@ static void free_tab_completion_data(void) static void add_match(char *matched) { - int nm = num_matches; - int nm1 = nm + 1; - - matches = xrealloc(matches, nm1 * sizeof(char *)); - matches[nm] = matched; + matches = xrealloc_vector(matches, 4, num_matches); + matches[num_matches] = matched; num_matches++; } -#if ENABLE_FEATURE_USERNAME_COMPLETION -static void username_tab_completion(char *ud, char *with_shash_flg) +# if ENABLE_FEATURE_USERNAME_COMPLETION +/* Replace "~user/..." with "/homedir/...". + * The parameter is malloced, free it or return it + * unchanged if no user is matched. + */ +static char *username_path_completion(char *ud) { struct passwd *entry; - int userlen; + char *tilde_name = ud; + char *home = NULL; - ud++; /* ~user/... to user/... */ - userlen = strlen(ud); + ud++; /* skip ~ */ + if (*ud == '/') { /* "~/..." */ + home = home_pwd_buf; + } else { + /* "~user/..." */ + ud = strchr(ud, '/'); + *ud = '\0'; /* "~user" */ + entry = getpwnam(tilde_name + 1); + *ud = '/'; /* restore "~user/..." */ + if (entry) + home = entry->pw_dir; + } + if (home) { + ud = concat_path_file(home, ud); + free(tilde_name); + tilde_name = ud; + } + return tilde_name; +} - if (with_shash_flg) { /* "~/..." or "~user/..." */ - char *sav_ud = ud - 1; - char *home = NULL; +/* ~use - find all users with this prefix. + * Return the length of the prefix used for matching. + */ +static NOINLINE unsigned complete_username(const char *ud) +{ + /* Using _r function to avoid pulling in static buffers */ + char line_buff[256]; + struct passwd pwd; + struct passwd *result; + unsigned userlen; - if (*ud == '/') { /* "~/..." */ - home = home_pwd_buf; - } else { - /* "~user/..." */ - char *temp; - temp = strchr(ud, '/'); - *temp = '\0'; /* ~user\0 */ - entry = getpwnam(ud); - *temp = '/'; /* restore ~user/... */ - ud = temp; - if (entry) - home = entry->pw_dir; - } - if (home) { - if ((userlen + strlen(home) + 1) < MAX_LINELEN) { - /* /home/user/... */ - sprintf(sav_ud, "%s%s", home, ud); - } - } - } else { - /* "~[^/]*" */ - /* Using _r function to avoid pulling in static buffers */ - char line_buff[256]; - struct passwd pwd; - struct passwd *result; - - setpwent(); - while (!getpwent_r(&pwd, line_buff, sizeof(line_buff), &result)) { - /* Null usernames should result in all users as possible completions. */ - if (/*!userlen || */ strncmp(ud, pwd.pw_name, userlen) == 0) { - add_match(xasprintf("~%s/", pwd.pw_name)); - } + ud++; /* skip ~ */ + userlen = strlen(ud); + + setpwent(); + while (!getpwent_r(&pwd, line_buff, sizeof(line_buff), &result)) { + /* Null usernames should result in all users as possible completions. */ + if (/*!userlen || */ strncmp(ud, pwd.pw_name, userlen) == 0) { + add_match(xasprintf("~%s/", pwd.pw_name)); } - endpwent(); } + endpwent(); + + return 1 + userlen; } -#endif /* FEATURE_COMMAND_USERNAME_COMPLETION */ +# endif /* FEATURE_USERNAME_COMPLETION */ enum { FIND_EXE_ONLY = 0, @@ -436,22 +688,19 @@ enum { FIND_FILE_ONLY = 2, }; -static int path_parse(char ***p, int flags) +static int path_parse(char ***p) { int npth; const char *pth; char *tmp; char **res; - /* if not setenv PATH variable, to search cur dir "." */ - if (flags != FIND_EXE_ONLY) - return 1; - if (state->flags & WITH_PATH_LOOKUP) pth = state->path_lookup; else pth = getenv("PATH"); - /* PATH= or PATH=: */ + + /* PATH="" or PATH=":"? */ if (!pth || !pth[0] || LONE_CHAR(pth, ':')) return 1; @@ -461,12 +710,13 @@ static int path_parse(char ***p, int flags) tmp = strchr(tmp, ':'); if (!tmp) break; - if (*++tmp == '\0') + tmp++; + if (*tmp == '\0') break; /* : */ npth++; } - res = xmalloc(npth * sizeof(char*)); + *p = res = xmalloc(npth * sizeof(res[0])); res[0] = tmp = xstrdup(pth); npth = 1; while (1) { @@ -478,232 +728,241 @@ static int path_parse(char ***p, int flags) break; /* : */ res[npth++] = tmp; } - *p = res; return npth; } -static void exe_n_cwd_tab_completion(char *command, int type) +/* Complete command, directory or file name. + * Return the length of the prefix used for matching. + */ +static NOINLINE unsigned complete_cmd_dir_file(const char *command, int type) { - DIR *dir; - struct dirent *next; - struct stat st; char *path1[1]; char **paths = path1; int npaths; int i; - char *found; - char *pfind = strrchr(command, '/'); -/* char dirbuf[MAX_LINELEN]; */ -#define dirbuf (S.exe_n_cwd_tab_completion__dirbuf) + unsigned pf_len; + const char *pfind; + char *dirbuf = NULL; npaths = 1; path1[0] = (char*)"."; - if (pfind == NULL) { - /* no dir, if flags==EXE_ONLY - get paths, else "." */ - npaths = path_parse(&paths, type); + pfind = strrchr(command, '/'); + if (!pfind) { + if (type == FIND_EXE_ONLY) + npaths = path_parse(&paths); pfind = command; } else { - /* dirbuf = ".../.../.../" */ - safe_strncpy(dirbuf, command, (pfind - command) + 2); -#if ENABLE_FEATURE_USERNAME_COMPLETION - if (dirbuf[0] == '~') /* ~/... or ~user/... */ - username_tab_completion(dirbuf, dirbuf); -#endif - paths[0] = dirbuf; /* point to 'l' in "..../last_component" */ pfind++; + /* dirbuf = ".../.../.../" */ + dirbuf = xstrndup(command, pfind - command); +# if ENABLE_FEATURE_USERNAME_COMPLETION + if (dirbuf[0] == '~') /* ~/... or ~user/... */ + dirbuf = username_path_completion(dirbuf); +# endif + path1[0] = dirbuf; } + pf_len = strlen(pfind); for (i = 0; i < npaths; i++) { + DIR *dir; + struct dirent *next; + struct stat st; + char *found; + dir = opendir(paths[i]); - if (!dir) /* Don't print an error */ - continue; + if (!dir) + continue; /* don't print an error */ while ((next = readdir(dir)) != NULL) { - int len1; - const char *str_found = next->d_name; + unsigned len; + const char *name_found = next->d_name; - /* matched? */ - if (strncmp(str_found, pfind, strlen(pfind))) + /* .../: bash 3.2.0 shows dotfiles, but not . and .. */ + if (!pfind[0] && DOT_OR_DOTDOT(name_found)) continue; - /* not see .name without .match */ - if (*str_found == '.' && *pfind == 0) { - if (NOT_LONE_CHAR(paths[i], '/') || str_found[1]) - continue; - str_found = ""; /* only "/" */ - } - found = concat_path_file(paths[i], str_found); - /* hmm, remover in progress? */ - if (lstat(found, &st) < 0) - goto cont; - /* find with dirs? */ - if (paths[i] != dirbuf) - strcpy(found, next->d_name); /* only name */ - - len1 = strlen(found); - found = xrealloc(found, len1 + 2); - found[len1] = '\0'; - found[len1+1] = '\0'; + /* match? */ + if (strncmp(name_found, pfind, pf_len) != 0) + continue; /* no */ + + found = concat_path_file(paths[i], name_found); + /* NB: stat() first so that we see is it a directory; + * but if that fails, use lstat() so that + * we still match dangling links */ + if (stat(found, &st) && lstat(found, &st)) + goto cont; /* hmm, remove in progress? */ + + /* Save only name */ + len = strlen(name_found); + found = xrealloc(found, len + 2); /* +2: for slash and NUL */ + strcpy(found, name_found); if (S_ISDIR(st.st_mode)) { - /* name is directory */ - if (found[len1-1] != '/') { - found[len1] = '/'; - } + /* name is a directory, add slash */ + found[len] = '/'; + found[len + 1] = '\0'; } else { - /* not put found file if search only dirs for cd */ + /* skip files if looking for dirs only (example: cd) */ if (type == FIND_DIR_ONLY) goto cont; } - /* Add it to the list */ + /* add it to the list */ add_match(found); continue; cont: free(found); } closedir(dir); - } + } /* for every path */ + if (paths != path1) { - free(paths[0]); /* allocated memory only in first member */ + free(paths[0]); /* allocated memory is only in first member */ free(paths); } -#undef dirbuf + free(dirbuf); + + return pf_len; } +/* build_match_prefix: + * On entry, match_buf contains everything up to cursor at the moment + * was pressed. This function looks at it, figures out what part of it + * constitutes the command/file/directory prefix to use for completion, + * and rewrites match_buf to contain only that part. + */ +#define dbg_bmp 0 +/* Helpers: */ +/* QUOT is used on elements of int_buf[], which are bytes, + * not Unicode chars. Therefore it works correctly even in Unicode mode. + */ #define QUOT (UCHAR_MAX+1) +static void remove_chunk(int16_t *int_buf, int beg, int end) +{ + /* beg must be <= end */ + if (beg == end) + return; -#define collapse_pos(is, in) do { \ - memmove(int_buf+(is), int_buf+(in), (MAX_LINELEN+1-(is)-(in)) * sizeof(pos_buf[0])); \ - memmove(pos_buf+(is), pos_buf+(in), (MAX_LINELEN+1-(is)-(in)) * sizeof(pos_buf[0])); \ -} while (0) + while ((int_buf[beg] = int_buf[end]) != 0) + beg++, end++; -static int find_match(char *matchBuf, int *len_with_quotes) + if (dbg_bmp) { + int i; + for (i = 0; int_buf[i]; i++) + bb_putchar((unsigned char)int_buf[i]); + bb_putchar('\n'); + } +} +/* Caller ensures that match_buf points to a malloced buffer + * big enough to hold strlen(match_buf)*2 + 2 + */ +static NOINLINE int build_match_prefix(char *match_buf) { int i, j; int command_mode; - int c, c2; -/* int16_t int_buf[MAX_LINELEN + 1]; */ -/* int16_t pos_buf[MAX_LINELEN + 1]; */ -#define int_buf (S.find_match__int_buf) -#define pos_buf (S.find_match__pos_buf) - - /* set to integer dimension characters and own positions */ - for (i = 0;; i++) { - int_buf[i] = (unsigned char)matchBuf[i]; - if (int_buf[i] == 0) { - pos_buf[i] = -1; /* indicator end line */ - break; - } - pos_buf[i] = i; - } + int16_t *int_buf = (int16_t*)match_buf; - /* mask \+symbol and convert '\t' to ' ' */ - for (i = j = 0; matchBuf[i]; i++, j++) - if (matchBuf[i] == '\\') { - collapse_pos(j, j + 1); - int_buf[j] |= QUOT; - i++; -#if ENABLE_FEATURE_NONPRINTABLE_INVERSE_PUT - if (matchBuf[i] == '\t') /* algorithm equivalent */ - int_buf[j] = ' ' | QUOT; -#endif - } -#if ENABLE_FEATURE_NONPRINTABLE_INVERSE_PUT - else if (matchBuf[i] == '\t') - int_buf[j] = ' '; -#endif + if (dbg_bmp) printf("\n%s\n", match_buf); - /* mask "symbols" or 'symbols' */ - c2 = 0; + /* Copy in reverse order, since they overlap */ + i = strlen(match_buf); + do { + int_buf[i] = (unsigned char)match_buf[i]; + i--; + } while (i >= 0); + + /* Mark every \c as "quoted c" */ for (i = 0; int_buf[i]; i++) { - c = int_buf[i]; - if (c == '\'' || c == '"') { - if (c2 == 0) - c2 = c; - else { - if (c == c2) - c2 = 0; - else - int_buf[i] |= QUOT; - } - } else if (c2 != 0 && c != '$') + if (int_buf[i] == '\\') { + remove_chunk(int_buf, i, i + 1); int_buf[i] |= QUOT; + } + } + /* Quote-mark "chars" and 'chars', drop delimiters */ + { + int in_quote = 0; + i = 0; + while (int_buf[i]) { + int cur = int_buf[i]; + if (!cur) + break; + if (cur == '\'' || cur == '"') { + if (!in_quote || (cur == in_quote)) { + in_quote ^= cur; + remove_chunk(int_buf, i, i + 1); + continue; + } + } + if (in_quote) + int_buf[i] = cur | QUOT; + i++; + } } - /* skip commands with arguments if line has commands delimiters */ - /* ';' ';;' '&' '|' '&&' '||' but `>&' `<&' `>|' */ + /* Remove everything up to command delimiters: + * ';' ';;' '&' '|' '&&' '||', + * but careful with '>&' '<&' '>|' + */ for (i = 0; int_buf[i]; i++) { - c = int_buf[i]; - c2 = int_buf[i + 1]; - j = i ? int_buf[i - 1] : -1; - command_mode = 0; - if (c == ';' || c == '&' || c == '|') { - command_mode = 1 + (c == c2); - if (c == '&') { - if (j == '>' || j == '<') - command_mode = 0; - } else if (c == '|' && j == '>') - command_mode = 0; - } - if (command_mode) { - collapse_pos(0, i + command_mode); - i = -1; /* hack incremet */ + int cur = int_buf[i]; + if (cur == ';' || cur == '&' || cur == '|') { + int prev = i ? int_buf[i - 1] : 0; + if (cur == '&' && (prev == '>' || prev == '<')) { + continue; + } else if (cur == '|' && prev == '>') { + continue; + } + remove_chunk(int_buf, 0, i + 1 + (cur == int_buf[i + 1])); + i = -1; /* back to square 1 */ } } - /* collapse `command...` */ - for (i = 0; int_buf[i]; i++) + /* Remove all `cmd` */ + for (i = 0; int_buf[i]; i++) { if (int_buf[i] == '`') { - for (j = i + 1; int_buf[j]; j++) + for (j = i + 1; int_buf[j]; j++) { if (int_buf[j] == '`') { - collapse_pos(i, j + 1); - j = 0; - break; + /* `cmd` should count as a word: + * `cmd` c should search for files c*, + * not commands c*. Therefore we don't drop + * `cmd` entirely, we replace it with single `. + */ + remove_chunk(int_buf, i, j); + goto next; } - if (j) { - /* not found close ` - command mode, collapse all previous */ - collapse_pos(0, i + 1); - break; - } else - i--; /* hack incremet */ + } + /* No closing ` - command mode, remove all up to ` */ + remove_chunk(int_buf, 0, i + 1); + break; + next: ; } + } - /* collapse (command...(command...)...) or {command...{command...}...} */ - c = 0; /* "recursive" level */ - c2 = 0; - for (i = 0; int_buf[i]; i++) + /* Remove "cmd (" and "cmd {" + * Example: "if { c" + * In this example, c should be matched as command pfx. + */ + for (i = 0; int_buf[i]; i++) { if (int_buf[i] == '(' || int_buf[i] == '{') { - if (int_buf[i] == '(') - c++; - else - c2++; - collapse_pos(0, i + 1); - i = -1; /* hack incremet */ - } - for (i = 0; pos_buf[i] >= 0 && (c > 0 || c2 > 0); i++) - if ((int_buf[i] == ')' && c > 0) || (int_buf[i] == '}' && c2 > 0)) { - if (int_buf[i] == ')') - c--; - else - c2--; - collapse_pos(0, i + 1); - i = -1; /* hack incremet */ + remove_chunk(int_buf, 0, i + 1); + i = -1; /* back to square 1 */ } + } - /* skip first not quote space */ + /* Remove leading unquoted spaces */ for (i = 0; int_buf[i]; i++) if (int_buf[i] != ' ') break; - if (i) - collapse_pos(0, i); + remove_chunk(int_buf, 0, i); - /* set find mode for completion */ + /* Determine completion mode */ command_mode = FIND_EXE_ONLY; - for (i = 0; int_buf[i]; i++) + for (i = 0; int_buf[i]; i++) { if (int_buf[i] == ' ' || int_buf[i] == '<' || int_buf[i] == '>') { - if (int_buf[i] == ' ' && command_mode == FIND_EXE_ONLY - && matchBuf[pos_buf[0]] == 'c' - && matchBuf[pos_buf[1]] == 'd' + if (int_buf[i] == ' ' + && command_mode == FIND_EXE_ONLY + && (char)int_buf[0] == 'c' + && (char)int_buf[1] == 'd' + && i == 2 /* -> int_buf[2] == ' ' */ ) { command_mode = FIND_DIR_ONLY; } else { @@ -711,44 +970,33 @@ static int find_match(char *matchBuf, int *len_with_quotes) break; } } - for (i = 0; int_buf[i]; i++) - /* "strlen" */; - /* find last word */ + } + if (dbg_bmp) printf("command_mode(0:exe/1:dir/2:file):%d\n", command_mode); + + /* Remove everything except last word */ + for (i = 0; int_buf[i]; i++) /* quasi-strlen(int_buf) */ + continue; for (--i; i >= 0; i--) { - c = int_buf[i]; - if (c == ' ' || c == '<' || c == '>' || c == '|' || c == '&') { - collapse_pos(0, i + 1); + int cur = int_buf[i]; + if (cur == ' ' || cur == '<' || cur == '>' || cur == '|' || cur == '&') { + remove_chunk(int_buf, 0, i + 1); break; } } - /* skip first not quoted '\'' or '"' */ - for (i = 0; int_buf[i] == '\'' || int_buf[i] == '"'; i++) - /*skip*/; - /* collapse quote or unquote // or /~ */ - while ((int_buf[i] & ~QUOT) == '/' - && ((int_buf[i+1] & ~QUOT) == '/' || (int_buf[i+1] & ~QUOT) == '~') - ) { + + /* Convert back to string of _chars_ */ + i = 0; + while ((match_buf[i] = int_buf[i]) != '\0') i++; - } - /* set only match and destroy quotes */ - j = 0; - for (c = 0; pos_buf[i] >= 0; i++) { - matchBuf[c++] = matchBuf[pos_buf[i]]; - j = pos_buf[i] + 1; - } - matchBuf[c] = '\0'; - /* old length matchBuf with quotes symbols */ - *len_with_quotes = j ? j - pos_buf[0] : 0; + if (dbg_bmp) printf("final match_buf:'%s'\n", match_buf); return command_mode; -#undef int_buf -#undef pos_buf } /* - * display by column (original idea from ls applet, - * very optimized by me :) + * Display by column (original idea from ls applet, + * very optimized by me [Vladimir] :) */ static void showfiles(void) { @@ -758,9 +1006,9 @@ static void showfiles(void) int nrows = nfiles; int l; - /* find the longest file name- use that as the column width */ + /* find the longest file name - use that as the column width */ for (row = 0; row < nrows; row++) { - l = strlen(matches[row]); + l = unicode_strwidth(matches[row]); if (column_width < l) column_width = l; } @@ -780,382 +1028,671 @@ static void showfiles(void) for (nc = 1; nc < ncols && n+nrows < nfiles; n += nrows, nc++) { printf("%s%-*s", matches[n], - (int)(column_width - strlen(matches[n])), ""); + (int)(column_width - unicode_strwidth(matches[n])), "" + ); } - puts(matches[n]); + if (ENABLE_UNICODE_SUPPORT) + puts(printable_string(NULL, matches[n])); + else + puts(matches[n]); } } -static char *add_quote_for_spec_chars(char *found) +static const char *is_special_char(char c) +{ + return strchr(" `\"#$%^&*()=+{}[]:;'|\\<>", c); +} + +static char *quote_special_chars(char *found) { int l = 0; - char *s = xmalloc((strlen(found) + 1) * 2); + char *s = xzalloc((strlen(found) + 1) * 2); while (*found) { - if (strchr(" `\"#$%^&*()=+{}[]:;\'|\\<>", *found)) + if (is_special_char(*found)) s[l++] = '\\'; s[l++] = *found++; } - s[l] = 0; + /* s[l] = '\0'; - already is */ return s; } /* Do TAB completion */ -static void input_tab(smallint *lastWasTab) +static NOINLINE void input_tab(smallint *lastWasTab) { + char *chosen_match; + char *match_buf; + size_t len_found; + /* Length of string used for matching */ + unsigned match_pfx_len = match_pfx_len; + int find_type; +# if ENABLE_UNICODE_SUPPORT + /* cursor pos in command converted to multibyte form */ + int cursor_mb; +# endif if (!(state->flags & TAB_COMPLETION)) return; - if (!*lastWasTab) { - char *tmp, *tmp1; - int len_found; -/* char matchBuf[MAX_LINELEN]; */ -#define matchBuf (S.input_tab__matchBuf) - int find_type; - int recalc_pos; + if (*lastWasTab) { + /* The last char was a TAB too. + * Print a list of all the available choices. + */ + if (num_matches > 0) { + /* cursor will be changed by goto_new_line() */ + int sav_cursor = cursor; + goto_new_line(); + showfiles(); + redraw(0, command_len - sav_cursor); + } + return; + } - *lastWasTab = TRUE; /* flop trigger */ + *lastWasTab = 1; + chosen_match = NULL; + + /* Make a local copy of the string up to the position of the cursor. + * build_match_prefix will expand it into int16_t's, need to allocate + * twice as much as the string_len+1. + * (we then also (ab)use this extra space later - see (**)) + */ + match_buf = xmalloc(MAX_LINELEN * sizeof(int16_t)); +# if !ENABLE_UNICODE_SUPPORT + save_string(match_buf, cursor + 1); /* +1 for NUL */ +# else + { + CHAR_T wc = command_ps[cursor]; + command_ps[cursor] = BB_NUL; + save_string(match_buf, MAX_LINELEN); + command_ps[cursor] = wc; + cursor_mb = strlen(match_buf); + } +# endif + find_type = build_match_prefix(match_buf); - /* Make a local copy of the string -- up - * to the position of the cursor */ - tmp = strncpy(matchBuf, command_ps, cursor); - tmp[cursor] = '\0'; + /* Free up any memory already allocated */ + free_tab_completion_data(); - find_type = find_match(matchBuf, &recalc_pos); +# if ENABLE_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 (state->flags & USERNAME_COMPLETION) + if (match_buf[0] == '~' && strchr(match_buf, '/') == NULL) + match_pfx_len = complete_username(match_buf); +# endif + /* If complete_username() did not match, + * try to match a command in $PATH, or a directory, or a file */ + if (!matches) + match_pfx_len = complete_cmd_dir_file(match_buf, find_type); + + /* Account for backslashes which will be inserted + * by quote_special_chars() later */ + { + const char *e = match_buf + strlen(match_buf); + const char *s = e - match_pfx_len; + while (s < e) + if (is_special_char(*s++)) + match_pfx_len++; + } - /* Free up any memory already allocated */ - free_tab_completion_data(); + /* Remove duplicates */ + if (matches) { + unsigned i, n = 0; + qsort_string_vector(matches, num_matches); + for (i = 0; i < num_matches - 1; ++i) { + //if (matches[i] && matches[i+1]) { /* paranoia */ + if (strcmp(matches[i], matches[i+1]) == 0) { + free(matches[i]); + //matches[i] = NULL; /* paranoia */ + } else { + matches[n++] = matches[i]; + } + //} + } + matches[n++] = matches[i]; + num_matches = n; + } -#if ENABLE_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 (state->flags & USERNAME_COMPLETION) - if (matchBuf[0] == '~' && strchr(matchBuf, '/') == 0) - username_tab_completion(matchBuf, NULL); -#endif - /* Try to match any executable in our path and everything - * in the current working directory */ + /* Did we find exactly one match? */ + if (num_matches != 1) { /* no */ + char *cp; + beep(); if (!matches) - exe_n_cwd_tab_completion(matchBuf, find_type); - /* Sort, then remove any duplicates found */ - if (matches) { - int i, n = 0; - qsort_string_vector(matches, num_matches); - for (i = 0; i < num_matches - 1; ++i) { - if (matches[i] && matches[i+1]) { /* paranoia */ - if (strcmp(matches[i], matches[i+1]) == 0) { - free(matches[i]); - matches[i] = NULL; /* paranoia */ - } else { - matches[n++] = matches[i]; - } + goto ret; /* no matches at all */ + /* Find common prefix */ + chosen_match = xstrdup(matches[0]); + for (cp = chosen_match; *cp; cp++) { + unsigned n; + for (n = 1; n < num_matches; n++) { + if (matches[n][cp - chosen_match] != *cp) { + goto stop; } } - matches[n] = matches[i]; - num_matches = n + 1; } - /* Did we find exactly one match? */ - if (!matches || num_matches > 1) { - beep(); - if (!matches) - return; /* not found */ - /* find minimal match */ - tmp1 = xstrdup(matches[0]); - for (tmp = tmp1; *tmp; tmp++) - for (len_found = 1; len_found < num_matches; len_found++) - if (matches[len_found][(tmp - tmp1)] != *tmp) { - *tmp = '\0'; - break; - } - if (*tmp1 == '\0') { /* have unique */ - free(tmp1); - return; - } - tmp = add_quote_for_spec_chars(tmp1); - free(tmp1); - } else { /* one match */ - tmp = add_quote_for_spec_chars(matches[0]); - /* for next completion current found */ - *lastWasTab = FALSE; - - len_found = strlen(tmp); - if (tmp[len_found-1] != '/') { - tmp[len_found] = ' '; - tmp[len_found+1] = '\0'; - } + stop: + if (cp == chosen_match) { /* have unique prefix? */ + goto ret; /* no */ } - len_found = strlen(tmp); - /* have space to placed match? */ - if ((len_found - strlen(matchBuf) + command_len) < MAX_LINELEN) { - /* before word for match */ - command_ps[cursor - recalc_pos] = '\0'; - /* save tail line */ - strcpy(matchBuf, command_ps + cursor); - /* add match */ - strcat(command_ps, tmp); - /* add tail */ - strcat(command_ps, matchBuf); - /* back to begin word for match */ - input_backward(recalc_pos); - /* new pos */ - recalc_pos = cursor + len_found; - /* new len */ - command_len = strlen(command_ps); - /* write out the matched command */ - redraw(cmdedit_y, command_len - recalc_pos); + *cp = '\0'; + cp = quote_special_chars(chosen_match); + free(chosen_match); + chosen_match = cp; + len_found = strlen(chosen_match); + } else { /* exactly one match */ + /* Next is not a double-tab */ + *lastWasTab = 0; + + chosen_match = quote_special_chars(matches[0]); + len_found = strlen(chosen_match); + if (chosen_match[len_found-1] != '/') { + chosen_match[len_found] = ' '; + chosen_match[++len_found] = '\0'; } - free(tmp); -#undef matchBuf - } 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 sav_cursor = cursor; /* change goto_new_line() */ + } - /* Go to the next line */ - goto_new_line(); - showfiles(); - redraw(0, command_len - sav_cursor); +# if !ENABLE_UNICODE_SUPPORT + /* Have space to place the match? */ + /* The result consists of three parts with these lengths: */ + /* cursor + (len_found - match_pfx_len) + (command_len - cursor) */ + /* it simplifies into: */ + if ((int)(len_found - match_pfx_len + command_len) < S.maxsize) { + int pos; + /* save tail */ + strcpy(match_buf, &command_ps[cursor]); + /* add match and tail */ + sprintf(&command_ps[cursor], "%s%s", chosen_match + match_pfx_len, match_buf); + command_len = strlen(command_ps); + /* new pos */ + pos = cursor + len_found - match_pfx_len; + /* write out the matched command */ + redraw(cmdedit_y, command_len - pos); + } +# else + { + /* Use 2nd half of match_buf as scratch space - see (**) */ + char *command = match_buf + MAX_LINELEN; + int len = save_string(command, MAX_LINELEN); + /* Have space to place the match? */ + /* cursor_mb + (len_found - match_pfx_len) + (len - cursor_mb) */ + if ((int)(len_found - match_pfx_len + len) < MAX_LINELEN) { + int pos; + /* save tail */ + strcpy(match_buf, &command[cursor_mb]); + /* where do we want to have cursor after all? */ + strcpy(&command[cursor_mb], chosen_match + match_pfx_len); + len = load_string(command, S.maxsize); + /* add match and tail */ + sprintf(&command[cursor_mb], "%s%s", chosen_match + match_pfx_len, match_buf); + command_len = load_string(command, S.maxsize); + /* write out the matched command */ + /* paranoia: load_string can return 0 on conv error, + * prevent passing pos = (0 - 12) to redraw */ + pos = command_len - len; + redraw(cmdedit_y, pos >= 0 ? pos : 0); } } +# endif + ret: + free(chosen_match); + free(match_buf); } -#endif /* FEATURE_COMMAND_TAB_COMPLETION */ +#endif /* FEATURE_TAB_COMPLETION */ + + +line_input_t* FAST_FUNC new_line_input_t(int flags) +{ + line_input_t *n = xzalloc(sizeof(*n)); + n->flags = flags; + n->max_history = MAX_HISTORY; + return n; +} #if MAX_HISTORY > 0 +unsigned size_from_HISTFILESIZE(const char *hp) +{ + int size = MAX_HISTORY; + if (hp) { + size = atoi(hp); + if (size <= 0) + return 1; + if (size > MAX_HISTORY) + return MAX_HISTORY; + } + return size; +} + +static void save_command_ps_at_cur_history(void) +{ + if (command_ps[0] != BB_NUL) { + int cur = state->cur_history; + free(state->history[cur]); + +# if ENABLE_UNICODE_SUPPORT + { + char tbuf[MAX_LINELEN]; + save_string(tbuf, sizeof(tbuf)); + state->history[cur] = xstrdup(tbuf); + } +# else + state->history[cur] = xstrdup(command_ps); +# endif + } +} + /* state->flags is already checked to be nonzero */ -static void get_previous_history(void) +static int get_previous_history(void) { - if (command_ps[0] != '\0' || state->history[state->cur_history] == NULL) { - free(state->history[state->cur_history]); - state->history[state->cur_history] = xstrdup(command_ps); + if ((state->flags & DO_HISTORY) && state->cur_history) { + save_command_ps_at_cur_history(); + state->cur_history--; + return 1; } - state->cur_history--; + beep(); + return 0; } static int get_next_history(void) { if (state->flags & DO_HISTORY) { - int ch = state->cur_history; - if (ch < state->cnt_history) { - get_previous_history(); /* save the current history line */ - state->cur_history = ch + 1; - return state->cur_history; + if (state->cur_history < state->cnt_history) { + save_command_ps_at_cur_history(); /* save the current history line */ + return ++state->cur_history; } } beep(); return 0; } -#if ENABLE_FEATURE_EDITING_SAVEHISTORY +# if ENABLE_FEATURE_EDITING_SAVEHISTORY +/* We try to ensure that concurrent additions to the history + * do not overwrite each other. + * Otherwise shell users get unhappy. + * + * History file is trimmed lazily, when it grows several times longer + * than configured MAX_HISTORY lines. + */ + +static void free_line_input_t(line_input_t *n) +{ + int i = n->cnt_history; + while (i > 0) + free(n->history[--i]); + free(n); +} + /* state->flags is already checked to be nonzero */ -static void load_history(const char *fromfile) +static void load_history(line_input_t *st_parm) { + char *temp_h[MAX_HISTORY]; + char *line; FILE *fp; - int hi; + unsigned idx, i, line_len; /* NB: do not trash old history if file can't be opened */ - fp = fopen(fromfile, "r"); + fp = fopen_for_read(st_parm->hist_file); if (fp) { /* clean up old history */ - for (hi = state->cnt_history; hi > 0;) { - hi--; - free(state->history[hi]); + for (idx = st_parm->cnt_history; idx > 0;) { + idx--; + free(st_parm->history[idx]); + st_parm->history[idx] = NULL; } - for (hi = 0; hi < MAX_HISTORY;) { - char *hl = xmalloc_fgetline(fp); - int l; - - if (!hl) - break; - l = strlen(hl); - if (l >= MAX_LINELEN) - hl[MAX_LINELEN-1] = '\0'; - if (l == 0 || hl[0] == ' ') { - free(hl); + /* fill temp_h[], retaining only last MAX_HISTORY lines */ + memset(temp_h, 0, sizeof(temp_h)); + st_parm->cnt_history_in_file = idx = 0; + while ((line = xmalloc_fgetline(fp)) != NULL) { + if (line[0] == '\0') { + free(line); continue; } - state->history[hi++] = hl; + free(temp_h[idx]); + temp_h[idx] = line; + st_parm->cnt_history_in_file++; + idx++; + if (idx == st_parm->max_history) + idx = 0; } fclose(fp); - state->cur_history = state->cnt_history = hi; + + /* find first non-NULL temp_h[], if any */ + if (st_parm->cnt_history_in_file) { + while (temp_h[idx] == NULL) { + idx++; + if (idx == st_parm->max_history) + idx = 0; + } + } + + /* copy temp_h[] to st_parm->history[] */ + for (i = 0; i < st_parm->max_history;) { + line = temp_h[idx]; + if (!line) + break; + idx++; + if (idx == st_parm->max_history) + idx = 0; + line_len = strlen(line); + if (line_len >= MAX_LINELEN) + line[MAX_LINELEN-1] = '\0'; + st_parm->history[i++] = line; + } + st_parm->cnt_history = i; } } /* state->flags is already checked to be nonzero */ -static void save_history(const char *tofile) +static void save_history(char *str) { - FILE *fp; + int fd; + int len, len2; - fp = fopen(tofile, "w"); - if (fp) { - int i; - - for (i = 0; i < state->cnt_history; i++) { - fprintf(fp, "%s\n", state->history[i]); + fd = open(state->hist_file, O_WRONLY | O_CREAT | O_APPEND, 0600); + if (fd < 0) + return; + xlseek(fd, 0, SEEK_END); /* paranoia */ + len = strlen(str); + str[len] = '\n'; /* we (try to) do atomic write */ + len2 = full_write(fd, str, len + 1); + str[len] = '\0'; + close(fd); + if (len2 != len + 1) + return; /* "wtf?" */ + + /* did we write so much that history file needs trimming? */ + state->cnt_history_in_file++; + if (state->cnt_history_in_file > state->max_history * 4) { + char *new_name; + line_input_t *st_temp; + + /* we may have concurrently written entries from others. + * load them */ + st_temp = new_line_input_t(state->flags); + st_temp->hist_file = state->hist_file; + st_temp->max_history = state->max_history; + load_history(st_temp); + + /* write out temp file and replace hist_file atomically */ + new_name = xasprintf("%s.%u.new", state->hist_file, (int) getpid()); + fd = open(state->hist_file, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd >= 0) { + FILE *fp; + int i; + + fp = xfdopen_for_write(fd); + for (i = 0; i < st_temp->cnt_history; i++) + fprintf(fp, "%s\n", st_temp->history[i]); + fclose(fp); + if (rename(new_name, state->hist_file) == 0) + state->cnt_history_in_file = st_temp->cnt_history; } - fclose(fp); + free(new_name); + free_line_input_t(st_temp); } } -#else -#define load_history(a) ((void)0) -#define save_history(a) ((void)0) -#endif /* FEATURE_COMMAND_SAVEHISTORY */ +# else +# define load_history(a) ((void)0) +# define save_history(a) ((void)0) +# endif /* FEATURE_COMMAND_SAVEHISTORY */ -static void remember_in_history(const char *str) +static void remember_in_history(char *str) { int i; if (!(state->flags & DO_HISTORY)) return; - + if (str[0] == '\0') + return; i = state->cnt_history; - free(state->history[MAX_HISTORY]); - state->history[MAX_HISTORY] = NULL; - /* After max history, remove the oldest command */ - if (i >= MAX_HISTORY) { + /* Don't save dupes */ + if (i && strcmp(state->history[i-1], str) == 0) + return; + + free(state->history[state->max_history]); /* redundant, paranoia */ + state->history[state->max_history] = NULL; /* redundant, paranoia */ + + /* If history[] is full, remove the oldest command */ + /* we need to keep history[state->max_history] empty, hence >=, not > */ + if (i >= state->max_history) { free(state->history[0]); - for (i = 0; i < MAX_HISTORY-1; i++) + for (i = 0; i < state->max_history-1; i++) state->history[i] = state->history[i+1]; + /* i == state->max_history-1 */ } -// Maybe "if (!i || strcmp(history[i-1], command) != 0) ..." -// (i.e. do not save dups?) + /* i <= state->max_history-1 */ state->history[i++] = xstrdup(str); + /* i <= state->max_history */ state->cur_history = i; state->cnt_history = i; -#if ENABLE_FEATURE_EDITING_SAVEHISTORY +# if MAX_HISTORY > 0 && ENABLE_FEATURE_EDITING_SAVEHISTORY if ((state->flags & SAVE_HISTORY) && state->hist_file) - save_history(state->hist_file); -#endif - USE_FEATURE_EDITING_FANCY_PROMPT(num_ok_lines++;) + save_history(str); +# endif + IF_FEATURE_EDITING_FANCY_PROMPT(num_ok_lines++;) } #else /* MAX_HISTORY == 0 */ -#define remember_in_history(a) ((void)0) +# define remember_in_history(a) ((void)0) #endif /* MAX_HISTORY */ +#if ENABLE_FEATURE_EDITING_VI /* - * This function is used to grab a character buffer - * from the input file descriptor and allows you to - * a string with full command editing (sort of 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 - * - * Minimalist vi-style command line editing available if configured. * vi mode implemented 2005 by Paul Fox */ - -#if ENABLE_FEATURE_EDITING_VI static void -vi_Word_motion(char *command, int eat) +vi_Word_motion(int eat) { - while (cursor < command_len && !isspace(command[cursor])) + CHAR_T *command = command_ps; + + while (cursor < command_len && !BB_isspace(command[cursor])) input_forward(); - if (eat) while (cursor < command_len && isspace(command[cursor])) + if (eat) while (cursor < command_len && BB_isspace(command[cursor])) input_forward(); } static void -vi_word_motion(char *command, int eat) +vi_word_motion(int eat) { - if (isalnum(command[cursor]) || command[cursor] == '_') { + CHAR_T *command = command_ps; + + if (BB_isalnum(command[cursor]) || command[cursor] == '_') { while (cursor < command_len - && (isalnum(command[cursor+1]) || command[cursor+1] == '_')) + && (BB_isalnum(command[cursor+1]) || command[cursor+1] == '_') + ) { input_forward(); - } else if (ispunct(command[cursor])) { - while (cursor < command_len && ispunct(command[cursor+1])) + } + } else if (BB_ispunct(command[cursor])) { + while (cursor < command_len && BB_ispunct(command[cursor+1])) input_forward(); } if (cursor < command_len) input_forward(); - if (eat && cursor < command_len && isspace(command[cursor])) - while (cursor < command_len && isspace(command[cursor])) + if (eat) { + while (cursor < command_len && BB_isspace(command[cursor])) input_forward(); + } } static void -vi_End_motion(char *command) +vi_End_motion(void) { + CHAR_T *command = command_ps; + input_forward(); - while (cursor < command_len && isspace(command[cursor])) + while (cursor < command_len && BB_isspace(command[cursor])) input_forward(); - while (cursor < command_len-1 && !isspace(command[cursor+1])) + while (cursor < command_len-1 && !BB_isspace(command[cursor+1])) input_forward(); } static void -vi_end_motion(char *command) +vi_end_motion(void) { + CHAR_T *command = command_ps; + if (cursor >= command_len-1) return; input_forward(); - while (cursor < command_len-1 && isspace(command[cursor])) + while (cursor < command_len-1 && BB_isspace(command[cursor])) input_forward(); if (cursor >= command_len-1) return; - if (isalnum(command[cursor]) || command[cursor] == '_') { + if (BB_isalnum(command[cursor]) || command[cursor] == '_') { while (cursor < command_len-1 - && (isalnum(command[cursor+1]) || command[cursor+1] == '_') + && (BB_isalnum(command[cursor+1]) || command[cursor+1] == '_') ) { input_forward(); } - } else if (ispunct(command[cursor])) { - while (cursor < command_len-1 && ispunct(command[cursor+1])) + } else if (BB_ispunct(command[cursor])) { + while (cursor < command_len-1 && BB_ispunct(command[cursor+1])) input_forward(); } } static void -vi_Back_motion(char *command) +vi_Back_motion(void) { - while (cursor > 0 && isspace(command[cursor-1])) + CHAR_T *command = command_ps; + + while (cursor > 0 && BB_isspace(command[cursor-1])) input_backward(1); - while (cursor > 0 && !isspace(command[cursor-1])) + while (cursor > 0 && !BB_isspace(command[cursor-1])) input_backward(1); } static void -vi_back_motion(char *command) +vi_back_motion(void) { + CHAR_T *command = command_ps; + if (cursor <= 0) return; input_backward(1); - while (cursor > 0 && isspace(command[cursor])) + while (cursor > 0 && BB_isspace(command[cursor])) input_backward(1); if (cursor <= 0) return; - if (isalnum(command[cursor]) || command[cursor] == '_') { + if (BB_isalnum(command[cursor]) || command[cursor] == '_') { while (cursor > 0 - && (isalnum(command[cursor-1]) || command[cursor-1] == '_') + && (BB_isalnum(command[cursor-1]) || command[cursor-1] == '_') ) { input_backward(1); } - } else if (ispunct(command[cursor])) { - while (cursor > 0 && ispunct(command[cursor-1])) + } else if (BB_ispunct(command[cursor])) { + while (cursor > 0 && BB_ispunct(command[cursor-1])) input_backward(1); } } #endif +/* Modelled after bash 4.0 behavior of Ctrl- */ +static void ctrl_left(void) +{ + CHAR_T *command = command_ps; + + while (1) { + CHAR_T c; + + input_backward(1); + if (cursor == 0) + break; + c = command[cursor]; + if (c != ' ' && !BB_ispunct(c)) { + /* we reached a "word" delimited by spaces/punct. + * go to its beginning */ + while (1) { + c = command[cursor - 1]; + if (c == ' ' || BB_ispunct(c)) + break; + input_backward(1); + if (cursor == 0) + break; + } + break; + } + } +} +static void ctrl_right(void) +{ + CHAR_T *command = command_ps; + + while (1) { + CHAR_T c; + + c = command[cursor]; + if (c == BB_NUL) + break; + if (c != ' ' && !BB_ispunct(c)) { + /* we reached a "word" delimited by spaces/punct. + * go to its end + 1 */ + while (1) { + input_forward(); + c = command[cursor]; + if (c == BB_NUL || c == ' ' || BB_ispunct(c)) + break; + } + break; + } + input_forward(); + } +} + /* * read_line_input and its helpers */ +#if ENABLE_FEATURE_EDITING_ASK_TERMINAL +static void ask_terminal(void) +{ + /* Ask terminal where is the cursor now. + * lineedit_read_key handles response and corrects + * our idea of current cursor position. + * Testcase: run "echo -n long_line_long_line_long_line", + * then type in a long, wrapping command and try to + * delete it using backspace key. + * Note: we print it _after_ prompt, because + * prompt may contain CR. Example: PS1='\[\r\n\]\w ' + */ + /* Problem: if there is buffered input on stdin, + * the response will be delivered later, + * possibly to an unsuspecting application. + * Testcase: "sleep 1; busybox ash" + press and hold [Enter]. + * Result: + * ~/srcdevel/bbox/fix/busybox.t4 # + * ~/srcdevel/bbox/fix/busybox.t4 # + * ^[[59;34~/srcdevel/bbox/fix/busybox.t4 # <-- garbage + * ~/srcdevel/bbox/fix/busybox.t4 # + * + * Checking for input with poll only makes the race narrower, + * I still can trigger it. Strace: + * + * write(1, "~/srcdevel/bbox/fix/busybox.t4 # ", 33) = 33 + * poll([{fd=0, events=POLLIN}], 1, 0) = 0 (Timeout) <-- no input exists + * write(1, "\33[6n", 4) = 4 <-- send the ESC sequence, quick! + * poll([{fd=0, events=POLLIN}], 1, 4294967295) = 1 ([{fd=0, revents=POLLIN}]) + * read(0, "\n", 1) = 1 <-- oh crap, user's input got in first + */ + struct pollfd pfd; + + pfd.fd = STDIN_FILENO; + pfd.events = POLLIN; + if (safe_poll(&pfd, 1, 0) == 0) { + S.sent_ESC_br6n = 1; + fputs(ESC"[6n", stdout); + fflush_all(); /* make terminal see it ASAP! */ + } +} +#else +#define ask_terminal() ((void)0) +#endif + #if !ENABLE_FEATURE_EDITING_FANCY_PROMPT static void parse_and_put_prompt(const char *prmt_ptr) { @@ -1199,11 +1736,11 @@ static void parse_and_put_prompt(const char *prmt_ptr) c = *prmt_ptr++; switch (c) { -#if ENABLE_FEATURE_GETUSERNAME_AND_HOMEDIR +# if ENABLE_USERNAME_OR_HOMEDIR case 'u': pbuf = user_buf ? user_buf : (char*)""; break; -#endif +# endif case 'h': pbuf = free_me = safe_gethostname(); *strchrnul(pbuf, '.') = '\0'; @@ -1211,7 +1748,7 @@ static void parse_and_put_prompt(const char *prmt_ptr) case '$': c = (geteuid() == 0 ? '#' : '$'); break; -#if ENABLE_FEATURE_GETUSERNAME_AND_HOMEDIR +# if ENABLE_USERNAME_OR_HOMEDIR case 'w': /* /home/user[/something] -> ~[/something] */ pbuf = cwd_buf; @@ -1224,7 +1761,7 @@ static void parse_and_put_prompt(const char *prmt_ptr) pbuf = free_me = xasprintf("~%s", cwd_buf + l); } break; -#endif +# endif case 'W': pbuf = cwd_buf; cp = strrchr(pbuf, '/'); @@ -1289,56 +1826,141 @@ static void cmdedit_setwidth(unsigned w, int redraw_flg) int new_y = (cursor + cmdedit_prmt_len) / w; /* redraw */ redraw((new_y >= cmdedit_y ? new_y : cmdedit_y), command_len - cursor); - fflush(stdout); + fflush_all(); } } static void win_changed(int nsig) { - int width; + int sv_errno = errno; + unsigned width; get_terminal_width_height(0, &width, NULL); cmdedit_setwidth(width, nsig /* - just a yes/no flag */); if (nsig == SIGWINCH) signal(SIGWINCH, win_changed); /* rearm ourself */ + errno = sv_errno; } -/* - * The emacs and vi modes share much of the code in the big - * command loop. Commands entered when in vi's command mode (aka - * "escape mode") get an extra bit added to distinguish them -- - * this keeps them from being self-inserted. This clutters the - * big switch a bit, but keeps all the code in one place. - */ +static int lineedit_read_key(char *read_key_buffer, int timeout) +{ + int64_t ic; +#if ENABLE_UNICODE_SUPPORT + char unicode_buf[MB_CUR_MAX + 1]; + int unicode_idx = 0; +#endif -#define vbit 0x100 + while (1) { + /* Wait for input. TIMEOUT = -1 makes read_key wait even + * on nonblocking stdin, TIMEOUT = 50 makes sure we won't + * insist on full MB_CUR_MAX buffer to declare input like + * "\xff\n",pause,"ls\n" invalid and thus won't lose "ls". + * + * Note: read_key sets errno to 0 on success. + */ + ic = read_key(STDIN_FILENO, read_key_buffer, timeout); + if (errno) { +#if ENABLE_UNICODE_SUPPORT + if (errno == EAGAIN && unicode_idx != 0) + goto pushback; +#endif + break; + } + +#if ENABLE_FEATURE_EDITING_ASK_TERMINAL + if ((int32_t)ic == KEYCODE_CURSOR_POS + && S.sent_ESC_br6n + ) { + S.sent_ESC_br6n = 0; + if (cursor == 0) { /* otherwise it may be bogus */ + int col = ((ic >> 32) & 0x7fff) - 1; + if (col > cmdedit_prmt_len) { + cmdedit_x += (col - cmdedit_prmt_len); + while (cmdedit_x >= cmdedit_termw) { + cmdedit_x -= cmdedit_termw; + cmdedit_y++; + } + } + } + continue; + } +#endif + +#if ENABLE_UNICODE_SUPPORT + if (unicode_status == UNICODE_ON) { + wchar_t wc; + + if ((int32_t)ic < 0) /* KEYCODE_xxx */ + break; + // TODO: imagine sequence like: 0xff,: we are currently losing 0xff... + + unicode_buf[unicode_idx++] = ic; + unicode_buf[unicode_idx] = '\0'; + if (mbstowcs(&wc, unicode_buf, 1) != 1) { + /* Not (yet?) a valid unicode char */ + if (unicode_idx < MB_CUR_MAX) { + timeout = 50; + continue; + } + pushback: + /* Invalid sequence. Save all "bad bytes" except first */ + read_key_ungets(read_key_buffer, unicode_buf + 1, unicode_idx - 1); +# if !ENABLE_UNICODE_PRESERVE_BROKEN + ic = CONFIG_SUBST_WCHAR; +# else + ic = unicode_mark_raw_byte(unicode_buf[0]); +# endif + } else { + /* Valid unicode char, return its code */ + ic = wc; + } + } +#endif + break; + } + + return ic; +} + +#if ENABLE_UNICODE_BIDI_SUPPORT +static int isrtl_str(void) +{ + int idx = cursor; + + while (idx < command_len && unicode_bidi_is_neutral_wchar(command_ps[idx])) + idx++; + return unicode_bidi_isrtl(command_ps[idx]); +} +#else +# define isrtl_str() 0 +#endif /* leave out the "vi-mode"-only case labels if vi editing isn't * configured. */ -#define vi_case(caselabel) USE_FEATURE_EDITING(case caselabel) +#define vi_case(caselabel) IF_FEATURE_EDITING_VI(case caselabel) /* convert uppercase ascii to equivalent control char, for readability */ #undef CTRL #define CTRL(a) ((a) & ~0x40) -/* Returns: +/* maxsize must be >= 2. + * Returns: * -1 on read errors or EOF, or on bare Ctrl-D, * 0 on ctrl-C (the line entered is still returned in 'command'), * >0 length of input string, including terminating '\n' */ -int read_line_input(const char *prompt, char *command, int maxsize, line_input_t *st) +int FAST_FUNC read_line_input(line_input_t *st, const char *prompt, char *command, int maxsize, int timeout) { + int len; #if ENABLE_FEATURE_TAB_COMPLETION - smallint lastWasTab = FALSE; + smallint lastWasTab = 0; #endif - unsigned int ic; - unsigned char c; smallint break_out = 0; #if ENABLE_FEATURE_EDITING_VI smallint vi_cmdmode = 0; - smalluint prevc; #endif struct termios initial_settings; struct termios new_settings; + char read_key_buffer[KEYCODE_BUFFER_SIZE]; INIT_S(); @@ -1346,9 +1968,8 @@ int read_line_input(const char *prompt, char *command, int maxsize, line_input_t || !(initial_settings.c_lflag & ECHO) ) { /* Happens when e.g. stty -echo was run before */ - int len; parse_and_put_prompt(prompt); - fflush(stdout); + /* fflush_all(); - done by parse_and_put_prompt */ if (fgets(command, maxsize, stdin) == NULL) len = -1; /* EOF or error */ else @@ -1357,22 +1978,35 @@ int read_line_input(const char *prompt, char *command, int maxsize, line_input_t return len; } + init_unicode(); + // FIXME: audit & improve this if (maxsize > MAX_LINELEN) maxsize = MAX_LINELEN; + S.maxsize = maxsize; - /* With null flags, no other fields are ever used */ + /* With zero flags, no other fields are ever used */ state = st ? st : (line_input_t*) &const_int_0; -#if ENABLE_FEATURE_EDITING_SAVEHISTORY +#if MAX_HISTORY > 0 +# if ENABLE_FEATURE_EDITING_SAVEHISTORY if ((state->flags & SAVE_HISTORY) && state->hist_file) - load_history(state->hist_file); + if (state->cnt_history == 0) + load_history(state); +# endif + if (state->flags & DO_HISTORY) + state->cur_history = state->cnt_history; #endif /* prepare before init handlers */ cmdedit_y = 0; /* quasireal y, not true if line > xt*yt */ command_len = 0; +#if ENABLE_UNICODE_SUPPORT + command_ps = xzalloc(maxsize * sizeof(command_ps[0])); +#else command_ps = command; command[0] = '\0'; +#endif +#define command command_must_not_be_used new_settings = initial_settings; new_settings.c_lflag &= ~ICANON; /* unbuffered input */ @@ -1383,15 +2017,14 @@ int read_line_input(const char *prompt, char *command, int maxsize, line_input_t new_settings.c_cc[VTIME] = 0; /* Turn off CTRL-C, so we can trap it */ #ifndef _POSIX_VDISABLE -#define _POSIX_VDISABLE '\0' +# define _POSIX_VDISABLE '\0' #endif new_settings.c_cc[VINTR] = _POSIX_VDISABLE; - tcsetattr(STDIN_FILENO, TCSANOW, &new_settings); + tcsetattr_stdin_TCSANOW(&new_settings); - /* Now initialize things */ previous_SIGWINCH_handler = signal(SIGWINCH, win_changed); win_changed(0); /* do initial resizing */ -#if ENABLE_FEATURE_GETUSERNAME_AND_HOMEDIR +#if ENABLE_USERNAME_OR_HOMEDIR { struct passwd *entry; @@ -1402,222 +2035,223 @@ int read_line_input(const char *prompt, char *command, int maxsize, line_input_t } } #endif - /* Print out the command prompt */ - parse_and_put_prompt(prompt); - while (1) { - fflush(NULL); +#if 0 + for (i = 0; i <= state->max_history; i++) + bb_error_msg("history[%d]:'%s'", i, state->history[i]); + bb_error_msg("cur_history:%d cnt_history:%d", state->cur_history, state->cnt_history); +#endif - if (nonblock_safe_read(STDIN_FILENO, &c, 1) < 1) { - /* if we can't read input then exit */ - goto prepare_to_die; - } + /* Print out the command prompt, optionally ask where cursor is */ + parse_and_put_prompt(prompt); + ask_terminal(); - ic = c; + read_key_buffer[0] = 0; + while (1) { + /* + * The emacs and vi modes share much of the code in the big + * command loop. Commands entered when in vi's command mode + * (aka "escape mode") get an extra bit added to distinguish + * them - this keeps them from being self-inserted. This + * clutters the big switch a bit, but keeps all the code + * in one place. + */ + enum { + VI_CMDMODE_BIT = 0x40000000, + /* 0x80000000 bit flags KEYCODE_xxx */ + }; + int32_t ic, ic_raw; + + fflush_all(); + ic = ic_raw = lineedit_read_key(read_key_buffer, timeout); #if ENABLE_FEATURE_EDITING_VI newdelflag = 1; - if (vi_cmdmode) - ic |= vbit; + if (vi_cmdmode) { + /* btw, since KEYCODE_xxx are all < 0, this doesn't + * change ic if it contains one of them: */ + ic |= VI_CMDMODE_BIT; + } #endif + switch (ic) { case '\n': case '\r': - vi_case('\n'|vbit:) - vi_case('\r'|vbit:) + vi_case('\n'|VI_CMDMODE_BIT:) + vi_case('\r'|VI_CMDMODE_BIT:) /* Enter */ goto_new_line(); break_out = 1; break; case CTRL('A'): - vi_case('0'|vbit:) + vi_case('0'|VI_CMDMODE_BIT:) /* Control-a -- Beginning of line */ input_backward(cursor); break; case CTRL('B'): - vi_case('h'|vbit:) - vi_case('\b'|vbit:) - vi_case('\x7f'|vbit:) /* DEL */ - /* Control-b -- Move back one character */ - input_backward(1); + vi_case('h'|VI_CMDMODE_BIT:) + vi_case('\b'|VI_CMDMODE_BIT:) /* ^H */ + vi_case('\x7f'|VI_CMDMODE_BIT:) /* DEL */ + input_backward(1); /* Move back one character */ break; - case CTRL('C'): - vi_case(CTRL('C')|vbit:) - /* Control-c -- stop gathering input */ - goto_new_line(); - command_len = 0; - break_out = -1; /* "do not append '\n'" */ - break; - case CTRL('D'): - /* Control-d -- Delete one character, or exit - * if the len=0 and no chars to delete */ - if (command_len == 0) { - errno = 0; - prepare_to_die: - /* to control stopped jobs */ - break_out = command_len = -1; - break; - } - input_delete(0); - break; - case CTRL('E'): - vi_case('$'|vbit:) + vi_case('$'|VI_CMDMODE_BIT:) /* Control-e -- End of line */ - input_end(); + put_till_end_and_adv_cursor(); break; case CTRL('F'): - vi_case('l'|vbit:) - vi_case(' '|vbit:) - /* Control-f -- Move forward one character */ - input_forward(); + vi_case('l'|VI_CMDMODE_BIT:) + vi_case(' '|VI_CMDMODE_BIT:) + input_forward(); /* Move forward one character */ break; - - case '\b': + case '\b': /* ^H */ case '\x7f': /* DEL */ - /* Control-h and DEL */ - input_backspace(); + if (!isrtl_str()) + input_backspace(); + else + input_delete(0); + break; + case KEYCODE_DELETE: + if (!isrtl_str()) + input_delete(0); + else + input_backspace(); break; - #if ENABLE_FEATURE_TAB_COMPLETION case '\t': input_tab(&lastWasTab); break; #endif - case CTRL('K'): /* Control-k -- clear to end of line */ - command[cursor] = 0; + command_ps[cursor] = BB_NUL; command_len = cursor; - printf("\033[J"); + printf(SEQ_CLEAR_TILL_END_OF_SCREEN); break; case CTRL('L'): - vi_case(CTRL('L')|vbit:) + vi_case(CTRL('L')|VI_CMDMODE_BIT:) /* Control-l -- clear screen */ - printf("\033[H"); + printf(ESC"[H"); /* cursor to top,left */ redraw(0, command_len - cursor); break; - #if MAX_HISTORY > 0 case CTRL('N'): - vi_case(CTRL('N')|vbit:) - vi_case('j'|vbit:) + vi_case(CTRL('N')|VI_CMDMODE_BIT:) + vi_case('j'|VI_CMDMODE_BIT:) /* Control-n -- Get next command in history */ if (get_next_history()) goto rewrite_line; break; case CTRL('P'): - vi_case(CTRL('P')|vbit:) - vi_case('k'|vbit:) + vi_case(CTRL('P')|VI_CMDMODE_BIT:) + vi_case('k'|VI_CMDMODE_BIT:) /* Control-p -- Get previous command from history */ - if ((state->flags & DO_HISTORY) && state->cur_history > 0) { - get_previous_history(); + if (get_previous_history()) goto rewrite_line; - } - beep(); break; #endif - case CTRL('U'): - vi_case(CTRL('U')|vbit:) + vi_case(CTRL('U')|VI_CMDMODE_BIT:) /* Control-U -- Clear line before cursor */ if (cursor) { - strcpy(command, command + cursor); command_len -= cursor; + memmove(command_ps, command_ps + cursor, + (command_len + 1) * sizeof(command_ps[0])); redraw(cmdedit_y, command_len); } break; case CTRL('W'): - vi_case(CTRL('W')|vbit:) + vi_case(CTRL('W')|VI_CMDMODE_BIT:) /* Control-W -- Remove the last word */ - while (cursor > 0 && isspace(command[cursor-1])) + while (cursor > 0 && BB_isspace(command_ps[cursor-1])) input_backspace(); - while (cursor > 0 && !isspace(command[cursor-1])) + while (cursor > 0 && !BB_isspace(command_ps[cursor-1])) input_backspace(); break; #if ENABLE_FEATURE_EDITING_VI - case 'i'|vbit: + case 'i'|VI_CMDMODE_BIT: vi_cmdmode = 0; break; - case 'I'|vbit: + case 'I'|VI_CMDMODE_BIT: input_backward(cursor); vi_cmdmode = 0; break; - case 'a'|vbit: + case 'a'|VI_CMDMODE_BIT: input_forward(); vi_cmdmode = 0; break; - case 'A'|vbit: - input_end(); + case 'A'|VI_CMDMODE_BIT: + put_till_end_and_adv_cursor(); vi_cmdmode = 0; break; - case 'x'|vbit: + case 'x'|VI_CMDMODE_BIT: input_delete(1); break; - case 'X'|vbit: + case 'X'|VI_CMDMODE_BIT: if (cursor > 0) { input_backward(1); input_delete(1); } break; - case 'W'|vbit: - vi_Word_motion(command, 1); + case 'W'|VI_CMDMODE_BIT: + vi_Word_motion(1); break; - case 'w'|vbit: - vi_word_motion(command, 1); + case 'w'|VI_CMDMODE_BIT: + vi_word_motion(1); break; - case 'E'|vbit: - vi_End_motion(command); + case 'E'|VI_CMDMODE_BIT: + vi_End_motion(); break; - case 'e'|vbit: - vi_end_motion(command); + case 'e'|VI_CMDMODE_BIT: + vi_end_motion(); break; - case 'B'|vbit: - vi_Back_motion(command); + case 'B'|VI_CMDMODE_BIT: + vi_Back_motion(); break; - case 'b'|vbit: - vi_back_motion(command); + case 'b'|VI_CMDMODE_BIT: + vi_back_motion(); break; - case 'C'|vbit: + case 'C'|VI_CMDMODE_BIT: vi_cmdmode = 0; /* fall through */ - case 'D'|vbit: + case 'D'|VI_CMDMODE_BIT: goto clear_to_eol; - case 'c'|vbit: + case 'c'|VI_CMDMODE_BIT: vi_cmdmode = 0; /* fall through */ - case 'd'|vbit: { + case 'd'|VI_CMDMODE_BIT: { int nc, sc; - sc = cursor; - prevc = ic; - if (safe_read(STDIN_FILENO, &c, 1) < 1) - goto prepare_to_die; - if (c == (prevc & 0xff)) { - /* "cc", "dd" */ + + ic = lineedit_read_key(read_key_buffer, timeout); + if (errno) /* error */ + goto return_error_indicator; + if (ic == ic_raw) { /* "cc", "dd" */ input_backward(cursor); goto clear_to_eol; break; } - switch (c) { + + sc = cursor; + switch (ic) { case 'w': case 'W': case 'e': case 'E': - switch (c) { + switch (ic) { case 'w': /* "dw", "cw" */ - vi_word_motion(command, vi_cmdmode); + vi_word_motion(vi_cmdmode); break; case 'W': /* 'dW', 'cW' */ - vi_Word_motion(command, vi_cmdmode); + vi_Word_motion(vi_cmdmode); break; case 'e': /* 'de', 'ce' */ - vi_end_motion(command); + vi_end_motion(); input_forward(); break; case 'E': /* 'dE', 'cE' */ - vi_End_motion(command); + vi_End_motion(); input_forward(); break; } @@ -1628,10 +2262,10 @@ int read_line_input(const char *prompt, char *command, int maxsize, line_input_t break; case 'b': /* "db", "cb" */ case 'B': /* implemented as B */ - if (c == 'b') - vi_back_motion(command); + if (ic == 'b') + vi_back_motion(); else - vi_Back_motion(command); + vi_Back_motion(); while (sc-- > cursor) input_delete(1); break; @@ -1639,161 +2273,188 @@ int read_line_input(const char *prompt, char *command, int maxsize, line_input_t input_delete(1); break; case '$': /* "d$", "c$" */ - clear_to_eol: + clear_to_eol: while (cursor < command_len) input_delete(1); break; } break; } - case 'p'|vbit: + case 'p'|VI_CMDMODE_BIT: input_forward(); /* fallthrough */ - case 'P'|vbit: + case 'P'|VI_CMDMODE_BIT: put(); break; - case 'r'|vbit: - if (safe_read(STDIN_FILENO, &c, 1) < 1) - goto prepare_to_die; - if (c == 0) + case 'r'|VI_CMDMODE_BIT: +//FIXME: unicode case? + ic = lineedit_read_key(read_key_buffer, timeout); + if (errno) /* error */ + goto return_error_indicator; + if (ic < ' ' || ic > 255) { beep(); - else { - *(command + cursor) = c; - bb_putchar(c); + } else { + command_ps[cursor] = ic; + bb_putchar(ic); bb_putchar('\b'); } break; -#endif /* FEATURE_COMMAND_EDITING_VI */ - case '\x1b': /* ESC */ - -#if ENABLE_FEATURE_EDITING_VI if (state->flags & VI_MODE) { - /* ESC: insert mode --> command mode */ + /* insert mode --> command mode */ vi_cmdmode = 1; input_backward(1); - break; - } -#endif - /* escape sequence follows */ - if (safe_read(STDIN_FILENO, &c, 1) < 1) - goto prepare_to_die; - /* different vt100 emulations */ - if (c == '[' || c == 'O') { - vi_case('['|vbit:) - vi_case('O'|vbit:) - if (safe_read(STDIN_FILENO, &c, 1) < 1) - goto prepare_to_die; - } - if (c >= '1' && c <= '9') { - unsigned char dummy; - - if (safe_read(STDIN_FILENO, &dummy, 1) < 1) - goto prepare_to_die; - if (dummy != '~') - c = '\0'; } + break; +#endif /* FEATURE_COMMAND_EDITING_VI */ - switch (c) { -#if ENABLE_FEATURE_TAB_COMPLETION - case '\t': /* Alt-Tab */ - input_tab(&lastWasTab); - break; -#endif #if MAX_HISTORY > 0 - case 'A': - /* Up Arrow -- Get previous command from history */ - if ((state->flags & DO_HISTORY) && state->cur_history > 0) { - get_previous_history(); - goto rewrite_line; - } - beep(); + case KEYCODE_UP: + if (get_previous_history()) + goto rewrite_line; + beep(); + break; + case KEYCODE_DOWN: + if (!get_next_history()) break; - case 'B': - /* Down Arrow -- Get next command in history */ - if (!get_next_history()) - break; rewrite_line: - /* Rewrite the line with the selected history item */ - /* change command */ - command_len = strlen(strcpy(command, state->history[state->cur_history])); - /* redraw and go to eol (bol, in vi */ - redraw(cmdedit_y, (state->flags & VI_MODE) ? 9999 : 0); - break; + /* Rewrite the line with the selected history item */ + /* change command */ + command_len = load_string(state->history[state->cur_history] ? + state->history[state->cur_history] : "", maxsize); + /* redraw and go to eol (bol, in vi) */ + redraw(cmdedit_y, (state->flags & VI_MODE) ? 9999 : 0); + break; #endif - case 'C': - /* Right Arrow -- Move forward one character */ - input_forward(); - break; - case 'D': - /* Left Arrow -- Move back one character */ - input_backward(1); - break; - case '3': - /* Delete */ - input_delete(0); - break; - case '1': // vt100? linux vt? or what? - case '7': // vt100? linux vt? or what? - case 'H': /* xterm's */ - input_backward(cursor); - break; - case '4': // vt100? linux vt? or what? - case '8': // vt100? linux vt? or what? - case 'F': /* xterm's */ - input_end(); - break; - default: - c = '\0'; - beep(); - } + case KEYCODE_RIGHT: + input_forward(); + break; + case KEYCODE_LEFT: + input_backward(1); + break; + case KEYCODE_CTRL_LEFT: + ctrl_left(); + break; + case KEYCODE_CTRL_RIGHT: + ctrl_right(); + break; + case KEYCODE_HOME: + input_backward(cursor); + break; + case KEYCODE_END: + put_till_end_and_adv_cursor(); break; - default: /* If it's regular input, do the normal thing */ - - /* Control-V -- force insert of next char */ - if (c == CTRL('V')) { - if (safe_read(STDIN_FILENO, &c, 1) < 1) - goto prepare_to_die; - if (c == 0) { - beep(); + default: + if (initial_settings.c_cc[VINTR] != 0 + && ic_raw == initial_settings.c_cc[VINTR] + ) { + /* Ctrl-C (usually) - stop gathering input */ + goto_new_line(); + command_len = 0; + break_out = -1; /* "do not append '\n'" */ + break; + } + if (initial_settings.c_cc[VEOF] != 0 + && ic_raw == initial_settings.c_cc[VEOF] + ) { + /* Ctrl-D (usually) - delete one character, + * or exit if len=0 and no chars to delete */ + if (command_len == 0) { + errno = 0; + + case -1: /* error (e.g. EIO when tty is destroyed) */ + IF_FEATURE_EDITING_VI(return_error_indicator:) + break_out = command_len = -1; break; } + input_delete(0); + break; } - -#if ENABLE_FEATURE_EDITING_VI - if (vi_cmdmode) /* Don't self-insert */ +// /* Control-V -- force insert of next char */ +// if (c == CTRL('V')) { +// if (safe_read(STDIN_FILENO, &c, 1) < 1) +// goto return_error_indicator; +// if (c == 0) { +// beep(); +// break; +// } +// } + if (ic < ' ' + || (!ENABLE_UNICODE_SUPPORT && ic >= 256) + || (ENABLE_UNICODE_SUPPORT && ic >= VI_CMDMODE_BIT) + ) { + /* If VI_CMDMODE_BIT is set, ic is >= 256 + * and vi mode ignores unexpected chars. + * Otherwise, we are here if ic is a + * control char or an unhandled ESC sequence, + * which is also ignored. + */ break; -#endif - if (command_len >= (maxsize - 2)) /* Need to leave space for enter */ + } + if ((int)command_len >= (maxsize - 2)) { + /* Not enough space for the char and EOL */ break; + } command_len++; - if (cursor == (command_len - 1)) { /* Append if at the end of the line */ - command[cursor] = c; - command[cursor+1] = '\0'; - cmdedit_set_out_char(' '); - } else { /* Insert otherwise */ + if (cursor == (command_len - 1)) { + /* We are at the end, append */ + command_ps[cursor] = ic; + command_ps[cursor + 1] = BB_NUL; + put_cur_glyph_and_inc_cursor(); + if (unicode_bidi_isrtl(ic)) + input_backward(1); + } else { + /* In the middle, insert */ int sc = cursor; - memmove(command + sc + 1, command + sc, command_len - sc); - command[sc] = c; - sc++; - /* rewrite from cursor */ - input_end(); + memmove(command_ps + sc + 1, command_ps + sc, + (command_len - sc) * sizeof(command_ps[0])); + command_ps[sc] = ic; + /* is right-to-left char, or neutral one (e.g. comma) was just added to rtl text? */ + if (!isrtl_str()) + sc++; /* no */ + put_till_end_and_adv_cursor(); /* to prev x pos + 1 */ input_backward(cursor - sc); } break; - } - if (break_out) /* Enter is the command terminator, no more input. */ + } /* switch (ic) */ + + if (break_out) break; #if ENABLE_FEATURE_TAB_COMPLETION - if (c != '\t') - lastWasTab = FALSE; + if (ic_raw != '\t') + lastWasTab = 0; #endif + } /* while (1) */ + +#if ENABLE_FEATURE_EDITING_ASK_TERMINAL + if (S.sent_ESC_br6n) { + /* "sleep 1; busybox ash" + hold [Enter] to trigger. + * We sent "ESC [ 6 n", but got '\n' first, and + * KEYCODE_CURSOR_POS response is now buffered from terminal. + * It's bad already and not much can be done with it + * (it _will_ be visible for the next process to read stdin), + * but without this delay it even shows up on the screen + * as garbage because we restore echo settings with tcsetattr + * before it comes in. UGLY! + */ + usleep(20*1000); } +#endif + +/* End of bug-catching "command_must_not_be_used" trick */ +#undef command + +#if ENABLE_UNICODE_SUPPORT + command[0] = '\0'; + if (command_len > 0) + command_len = save_string(command, maxsize - 1); + free(command_ps); +#endif if (command_len > 0) remember_in_history(command); @@ -1808,35 +2469,29 @@ int read_line_input(const char *prompt, char *command, int maxsize, line_input_t #endif /* restore initial_settings */ - tcsetattr(STDIN_FILENO, TCSANOW, &initial_settings); + tcsetattr_stdin_TCSANOW(&initial_settings); /* restore SIGWINCH handler */ signal(SIGWINCH, previous_SIGWINCH_handler); - fflush(stdout); + fflush_all(); + len = command_len; DEINIT_S(); - return command_len; -} - -line_input_t *new_line_input_t(int flags) -{ - line_input_t *n = xzalloc(sizeof(*n)); - n->flags = flags; - return n; + return len; /* can't return command_len, DEINIT_S() destroys it */ } -#else +#else /* !FEATURE_EDITING */ #undef read_line_input -int read_line_input(const char* prompt, char* command, int maxsize) +int FAST_FUNC read_line_input(const char* prompt, char* command, int maxsize) { fputs(prompt, stdout); - fflush(stdout); + fflush_all(); fgets(command, maxsize, stdin); return strlen(command); } -#endif /* FEATURE_COMMAND_EDITING */ +#endif /* !FEATURE_EDITING */ /* @@ -1861,15 +2516,12 @@ int main(int argc, char **argv) "% "; #endif -#if ENABLE_FEATURE_NONPRINTABLE_INVERSE_PUT - setlocale(LC_ALL, ""); -#endif while (1) { int l; l = read_line_input(prompt, buff); if (l <= 0 || buff[l-1] != '\n') break; - buff[l-1] = 0; + buff[l-1] = '\0'; printf("*** read_line_input() returned line =%s=\n", buff); } printf("*** read_line_input() detect ^D\n");