A patch to add filename completion like in csh to NetBSD's /bin/sh. by Eric Fischer This isn't really ready for release yet, but it's still pretty useful in its current form. Only in .: alias.o Only in .: arith.o Only in .: arith_lex.o Only in ./bltin: bltin.h.orig Only in .: builtins.c Only in .: builtins.h Only in .: builtins.o Only in .: cd.o Only in .: echo.o Only in .: error.o Only in .: eval.o Only in .: exec.o Only in .: expand.o Only in .: histedit.o Only in .: init.c Only in .: init.o diff -rc ../../../../../src/bin/sh/input.c ./input.c *** ../../../../../src/bin/sh/input.c Wed Oct 18 23:14:37 1995 --- ./input.c Mon Dec 30 14:48:30 1996 *************** *** 50,55 **** --- 50,62 ---- #include #include #include + #include + #include + #include + #include + + #include + #undef CEOF /* conflict between and "syntax.h" */ /* * This file implements the input routines used by the parser. *************** *** 175,180 **** --- 182,667 ---- return pgetc_macro(); } + /* + * isolate: + * + * find the last token in null-terminated string "src". dest will + * be the string with quoting removed; the return value is the + * offset into src where this last token begins. + */ + + static char * + isolate (char *src, char *dest) + { + char *c, *o = dest, *lastword = src, *secondtolast = src; + + *o = 0; + + for (c = src; ; c++) { + if (*c == '\'') + for (c++; *c; c++) + if (*c == '\'') + break; + else + *o++ = *c; + else if (*c == '\"') + for (c++; *c; c++) + if (*c == '\"') + break; + else if (*c == '\\' && c[1]) + *o++ = *++c; + else + *o++ = *c; + else if (*c == '\\' && c[1]) + *o++ = *++c; + else if (*c == ' ' || *c == '\t' || *c == '&' || + *c == '(' || *c == ')' || *c == ';' || + *c == '|' || *c == '\n' || *c == '<' || + *c == '>' || *c == 0 || *c == '=') { + if (o != dest) { + *o = 0; + lastword = secondtolast; + } + + o = dest; + secondtolast = c + 1; + + if (*c == 0) break; + } else + *o++ = *c; + + /* in case we fell off the end of an unclosed ' or " */ + + if (*c == 0) + c--; + } + + return lastword; + } + + static int + howmatch (char *one, char *two) + { + int c; + + for (c = 0; one[c] && two[c]; c++) + if (one[c] != two[c]) + break; + return c; + } + + #define FILEC_COLWID 16 + #define TABWID 8 + + static int + filec (char *src, int maxlen, int listonly, int ttywid) + { + int sl, maxmatch = -1; + char *lastc, *step; + struct dirent *de; + DIR *d; + int needbeep = 0; + char *t1, *t3, *t4; + int h = 0; + + /* + * if it begins with a tilde, do home directory substitution + */ + if (*src == '~') { + /* + * if no name following the ~, then expand the user's + * home directory. + */ + if (src[1] == '/' || src[1] == 0) { + char *s = getenv ("HOME"); + + /* + * if they unset their home directory, they just + * don't get tilde expansion. Serves them right. + */ + if (!s) { + t1 = malloc((strlen (src) + 1) * sizeof (char)); + if (t1 == 0) { + out2str ("\a"); + return 0; + } + + strcpy (t1, src); + } else { + t1 = malloc ((strlen (s) + strlen (src + 1) + + 1) * sizeof (char)); + if (t1 == 0) { + out2str ("\a"); + return 0; + } + + strcpy (t1, s); + strcat (t1, src + 1); + } + } else { + struct passwd *pw; + char *cp; + char *t2; + + for (cp = src; *cp && *cp != '/'; cp++) + ; + + t2 = malloc ((strlen (cp) + 1) * sizeof (char)); + if (t2 == 0) { + out2str ("\a"); + return 0; + } + + strcpy (t2, cp); + + *cp = 0; + pw = getpwnam (src + 1); + + if (!pw) { + t1 = malloc ((strlen (src) + strlen (t2) + 1) + * sizeof (char)); + if (t1 == 0) { + out2str ("\a"); + free (t2); + return 0; + } + + strcpy (t1, src); + strcat (t1, t2); + + free (t2); + } else { + t1 = malloc ((strlen (t2) + strlen(pw->pw_dir) + + 1) * sizeof (char)); + if (t1 == 0) { + out2str ("\a"); + free (t2); + return 0; + } + + strcpy (t1, pw->pw_dir); + strcat (t1, t2); + + free (t2); + } + } + } else { + t1 = malloc ((strlen (src) + 1) * sizeof (char)); + + if (t1 == 0) { + out2str ("\a"); + return 0; + } + + strcpy (t1, src); + } + + lastc = t1; + + for (step = t1; *step; step++) { + if (*step == '/') /* && step[1] */ + lastc = step + 1; + } + + sl = strlen (lastc); + t3 = 0; + + if (lastc == t1) { + d = opendir ("."); + } else { + *(lastc - 1) = 0; + + if (lastc == t1 + 1) { + d = opendir ("/"); + } else + d = opendir (t1); + } + + if (d) { + while (de = readdir (d)) { + if (*de->d_name == '.' && *lastc != '.') + continue; + + if (sl == howmatch (de->d_name, lastc)) { + if (listonly) { + if (h + FILEC_COLWID > ttywid) { + out2str ("\n"); + h = 0; + } + + while (h % FILEC_COLWID != 0) { + out2str ("\t"); + h = ((h + TABWID) / TABWID) + * TABWID; + } + + if (strlen (de->d_name) + h > ttywid) { + out2str ("\n"); + h = 0; + } + + out2str (de->d_name); + h += strlen (de->d_name); + + if (h % FILEC_COLWID == 0) { + out2str (" "); + h++; + } + } + + if (sl > maxmatch) { + maxmatch = sl; + + if (t3 != 0) + free (t3); + + t3 = malloc ((strlen (de->d_name) + 1) + * sizeof (char)); + if (t3 == 0) { + free (t1); + out2str ("\a"); + return 0; + } + + strcpy (t3, de->d_name); + } else { + t3 [howmatch (de->d_name, t3)] = 0; + needbeep = 1; + } + } + } + + closedir (d); + } + + if (lastc != t1) + *(lastc - 1) = '/'; + + if (t3) { + *lastc = 0; + + t4 = malloc ((strlen (t3) + strlen (t1) + 1) * sizeof (char)); + if (t4 == 0) { + free (t1); + free (t3); + out2str ("\a"); + return 0; + } + + strcpy (t4, t1); + strcat (t4, t3); + + free (t1); + free (t3); + } else { + t4 = t1; + needbeep = 1; + } + + if (listonly) { + free (t4); + out2str ("\n"); + return 0; + } else { + strncpy (src, t4, maxlen); + src[maxlen] = 0; + + free (t4); + + if (needbeep) { + out2str ("\a"); + return 0; + } else { + return 1; + } + } + } + + #define TABKEY '\t' + #define ESCKEY 27 + #define CTRL_V ('v' & 31) + + void + retype (fd, c, t) + int fd; + char c; + struct termios *t; + { + register int n; + char d = CTRL_V; + + if (c < ' ') + ioctl (fd, TIOCSTI, &d); + else + for (n = 0; n < NCCS; n++) + if (t->c_cc[n] == c) { + ioctl (fd, TIOCSTI, &d); + break; + } + + ioctl (fd, TIOCSTI, &c); + } + + static int + readfc (fd, buf, sz) + int fd; + char *buf; + size_t sz; + { + int nr; + struct termios ti; + + while (1) { + char *expbuf; + int ttywid = 80; + + if (tcgetattr (fd, &ti) != -1) { + ti.c_cc[VEOL] = TABKEY; + ti.c_cc[VEOL2] = ESCKEY; /* for the csh fans */ + tcsetattr (fd, TCSANOW, &ti); + } + + nr = read (fd, buf, sz); + + if (tcgetattr (fd, &ti) != -1) { + /* + * maybe we should be setting the EOL character + * back to its original value. But that seems to + * cause problems after interrupts, because we + * never get to here to set it back, so the next + * time we check what EOL was, it's already set + * to \t and we foolishly assume that the user + * actually set it that way rather than just being + * victimized by the interrupt handling. So assuming + * they didn't want a special EOL character seems + * like the safest way to handle it. And hey, that's + * what csh does so if anyone complains I can say + * I'm just following historical precedent. + */ + ti.c_cc[VEOL] = _POSIX_VDISABLE; + ti.c_cc[VEOL2] = _POSIX_VDISABLE; + tcsetattr (fd, TCSANOW, &ti); + } + + /* + * if nr does equal sz, maybe they typed exactly sz + * characters and hit control-D. But it's more likely + * the buffer filled up and there's more to come that + * would be badly broken by messing with the input stream. + */ + if (nr <= 0 || nr == sz || buf[nr - 1] == '\n') + break; + + /* + * if canonical mode isn't on, we're going to get + * characters returned without a trailing \n and this + * silly thing is going to think that means we want + * a filename completion list. then it'll retype the + * line so far, which will return again, which will + * look like a completion list request again, and so + * on. so if we aren't canonicalizing, then act like + * we've got a full line and return no matter what. + */ + if (! (ti.c_lflag & ICANON)) + break; + + /* + * yeah, it's always going to be BUFSIZ - 1 so it could just + * be a regular array rather than having to malloc it. But + * we aren't supposed to know that here. + */ + expbuf = malloc (sz * sizeof (char)); + + if (expbuf) { + char *lastword; + struct termios t2; + char *c; + int ttworked = 0; + int listonly; + int succ = 0; + struct winsize ws; + + if (ioctl (fd, TIOCGWINSZ, &ws) == 0 && ws.ws_col != 0) + ttywid = ws.ws_col; + + if (buf[nr - 1] == TABKEY || buf [nr - 1] == ESCKEY) { + if (buf[nr - 1] == ESCKEY) + out2str ("\b\b "); + + buf[nr - 1] = 0; + lastword = isolate (buf, expbuf); + succ = filec (expbuf, sz, 0, ttywid); + } else { + out2str ("\n"); + buf[nr] = 0; + lastword = isolate (buf, expbuf); + filec (expbuf, sz, 1, ttywid); + } + + out2str ("\r"); + out2str (getprompt (NULL)); + + if (tcgetattr (fd, &ti) != -1) { + t2 = ti; + ttworked = 1; + + /* + * we can't switch out of canonical mode + * to make sure that control characters, etc + * don't get interpreted, because then when + * we switch back to canonical mode it'll + * reprocess them then, and echo everything + * twice to boot. + * + * so instead, make sure input extensions + * are on and we know what LNEXT is, and + * explicitly LNEXT everything as we type it. + */ + ti.c_lflag |= IEXTEN; + ti.c_cc[VLNEXT] = CTRL_V; + tcsetattr (fd, TCSANOW, &ti); + } + + for (c = buf; c < lastword; c++) { + retype (fd, *c, &ti); + } + + for (c = expbuf; *c; c++) { + char *look = " \t&();|\n<>\"'\\"; + + for (; *look; look++) { + if (*c == *look) { + retype (fd, '\\', &ti); + break; + } + } + + retype (fd, *c, &ti); + } + + if (succ) { + struct stat sb; + + if (stat (expbuf, &sb) == 0) + if (sb.st_mode & S_IFDIR) + retype (fd, '/', &ti); + else + retype (fd, ' ', &ti); + } + + if (ttworked) { + tcsetattr (fd, TCSANOW, &t2); + } + + free (expbuf); + } else { + out2str ("\nSorry, malloc() failed.\n"); + out2str (getprompt (NULL)); + } + } + + return nr; + } static int pread() *************** *** 194,199 **** --- 681,688 ---- /* XXX - BUFSIZE should redesign so not necessary */ (void) strcpy(parsenextc, rl_cp); } + } else if (Fflag && parsefile->fd == 0 && iflag) { + nr = readfc(parsefile->fd, parsenextc, BUFSIZ - 1); } else { nr = read(parsefile->fd, parsenextc, BUFSIZ - 1); } Only in .: input.c.orig Only in .: input.o Only in .: jobs.o Only in .: mail.o Only in .: main.o Only in .: memalloc.o Only in .: miscbltin.o Only in .: mkinit Only in .: mknodes Only in .: mksyntax Only in .: mystring.o Only in .: nodes.c Only in .: nodes.h Only in .: nodes.o diff -rc ../../../../../src/bin/sh/options.h ./options.h *** ../../../../../src/bin/sh/options.h Thu May 11 16:29:48 1995 --- ./options.h Mon Dec 30 14:48:30 1996 *************** *** 63,70 **** #define aflag optlist[12].val #define bflag optlist[13].val #define uflag optlist[14].val ! #define NOPTS 15 struct optent { const char *name; --- 63,71 ---- #define aflag optlist[12].val #define bflag optlist[13].val #define uflag optlist[14].val + #define Fflag optlist[15].val ! #define NOPTS 16 struct optent { const char *name; *************** *** 89,94 **** --- 90,96 ---- { "allexport", 'a', 0 }, { "notify", 'b', 0 }, { "nounset", 'u', 0 }, + { "filec", 'F', 0 }, }; #else extern struct optent optlist[NOPTS]; Only in .: options.h.orig Only in .: options.o Only in .: output.o Only in .: parser.o Only in .: redir.o Only in .: sh diff -rc ../../../../../src/bin/sh/sh.1 ./sh.1 *** ../../../../../src/bin/sh/sh.1 Thu May 11 16:30:18 1995 --- ./sh.1 Mon Dec 30 14:48:31 1996 *************** *** 36,46 **** .\" @(#)sh.1 8.6 (Berkeley) 5/4/95 .\" .na ! .TH SH 1 .SH NAME sh \- command interpreter (shell) .SH SYNOPSIS ! sh [-/+aCefnuvxIimsVEb] [-/+o longname] [arg ...] .SH DESCRIPTION .LP Sh is the standard command interpreter for the system. --- 36,46 ---- .\" @(#)sh.1 8.6 (Berkeley) 5/4/95 .\" .na ! .TH SH 1 "December 7, 1996" .SH NAME sh \- command interpreter (shell) .SH SYNOPSIS ! sh [-/+aCefnuvxIimsVEbF] [-/+o longname] [arg ...] .SH DESCRIPTION .LP Sh is the standard command interpreter for the system. *************** *** 202,207 **** --- 202,211 ---- Enable asynchronous notification of background job completion. (UNIMPLEMENTED for 4.4alpha) + .TP + -F filec + Enable interactive filename completion. See ``Filename + Completion'' below. .LP .sp 2 .B Lexical Structure *************** *** 1308,1310 **** --- 1312,1346 ---- document. It's similar to vi: typing will throw you into command VI command mode. Hitting while in command mode will pass the line to the shell. + .\" + .\" Ick! Is there any good reason to make subsection headers by + .\" hand like this instead of just using .SS ? + .\" + .LP + .sp 2 + .B Filename Completion + .sp + .LP + When sh is being used interactively and the + .B filec + .RB ( -F ) + option is set, pressing the Tab key will make sh search for + files that begin with the last word on the line you were + typing and, if any were found, fill in as much of the rest + of the word as can be done unambiguously. + The filename completion is mostly aware of the quoting rules + for shell commands and will add the appropriate backslashes + when a completed filename contains spaces or other characters + which are interpreted specially by the shell. + .SH BUGS + Filename completion isn't aware that quoted strings can cross + line boundaries, so if you've typed, say, + .LP + .RS + echo 'Hello, + .br + world!'; ls foo + .RE + .LP + and press Tab, sh will try to find filenames that begin with + ``world!'; ls foo'' rather than ones that just begin with ``foo''. Only in .: sh.1.orig Only in .: sh.cat1 Only in .: show.o Only in .: syntax.c Only in .: syntax.h Only in .: syntax.o Only in .: token.def Only in .: trap.o Only in .: var.o Only in .: y.tab.h