static char *RCSid = "$Header: /usr/local/src/cmd/tcsh/tenex.c,v 1.9 83/10/05 21:56:27 kg Exp $"; /* * Tenex style file name recognition, .. and more. * History: * Author: Ken Greer, Sept. 1975, CMU. * Finally got around to adding to the Cshell., Ken Greer, Dec. 1981. * * Search and recognition of command names (in addition to file names) * by Mike Ellis, Fairchild A.I. Labs, Sept 1983. * */ #include #include #include #include #include #include /* Don't include stdio.h! Csh doesn't like it!! */ #ifdef TEST #include #include "dir.h" #define flush() fflush(stdout) #endif #define TRUE 1 #define FALSE 0 #define ON 1 #define OFF 0 #define FILSIZ 512 /* Max reasonable file name length */ #define ESC '\033' #define equal(a, b) (strcmp(a, b) == 0) #define is_set(var) adrof(var) #define BUILTINS "/usr/new/lib/builtins/" /* fake builtin bin */ extern short SHIN, SHOUT; extern char *getenv (); extern putchar (); typedef enum {LIST, RECOGNIZE} COMMAND; static char *BELL = "\07"; static setup_tty (on) { static struct tchars tchars; /* INT, QUIT, XON, XOFF, EOF, BRK */ static char save_t_brkc = -1; /* Save user's break character */ sigignore (SIGINT); if (on) { struct sgttyb sgtty; ioctl (SHIN, TIOCGETC, &tchars); /* Get current break character*/ save_t_brkc = tchars.t_brkc; /* Current break char, if any */ if (save_t_brkc != ESC) /* If it's not already ESCAPE */ { tchars.t_brkc = ESC; /* Set break char to ESCAPE */ ioctl (SHIN, TIOCSETC, &tchars); } /* * This is a useful feature in it's own right... * The shell makes sure that the tty is not in some weird state * and fixes it if it is. But it should be noted that the * tenex routine will not work correctly in CBREAK or RAW mode * so this code below is, therefore, mandatory. */ ioctl (SHIN, TIOCGETP, &sgtty); if ((sgtty.sg_flags & (RAW | CBREAK)) || ((sgtty.sg_flags & ECHO) == 0)) /* not manditory, but nice */ { sgtty.sg_flags &= ~(RAW | CBREAK); sgtty.sg_flags |= ECHO; ioctl (SHIN, TIOCSETP, &sgtty); } } else { /* * Reset break character to what user had when invoked * (providing it is different from current one) */ if (save_t_brkc != tchars.t_brkc) { tchars.t_brkc = save_t_brkc; ioctl (SHIN, TIOCSETC, &tchars); } } sigrelse (SIGINT); } static termchars () { extern char *tgetstr (); char bp[1024]; static char area[256]; static int been_here = 0; char *ap = area; register char *s; if (been_here) return; been_here = TRUE; if (tgetent (bp, getenv ("TERM")) != 1) return; if (s = tgetstr ("vb", &ap)) /* Visible Bell */ BELL = s; return; } /* * Move back to beginning of current line */ static back_to_col_1 () { struct sgttyb tty, tty_normal; sigignore (SIGINT); ioctl (SHIN, TIOCGETP, &tty); tty_normal = tty; tty.sg_flags &= ~CRMOD; ioctl (SHIN, TIOCSETN, &tty); (void) write (SHOUT, "\r", 1); ioctl (SHIN, TIOCSETN, &tty_normal); sigrelse (SIGINT); } /* * Push string contents back into tty queue */ static pushback (string) char *string; { register char *p; struct sgttyb tty, tty_normal; sigignore (SIGINT); ioctl (SHOUT, TIOCGETP, &tty); tty_normal = tty; tty.sg_flags &= ~ECHO; ioctl (SHOUT, TIOCSETN, &tty); for (p = string; *p; p++) ioctl (SHOUT, TIOCSTI, p); ioctl (SHOUT, TIOCSETN, &tty_normal); sigrelse (SIGINT); } /* * Concatonate src onto tail of des. * Des is a string whose maximum length is count. * Always null terminate. */ catn (des, src, count) register char *des, *src; register count; { while (--count >= 0 && *des) des++; while (--count >= 0) if ((*des++ = *src++) == 0) return; *des = '\0'; } static max (a, b) { if (a > b) return (a); return (b); } /* * like strncpy but always leave room for trailing \0 * and always null terminate. */ copyn (des, src, count) register char *des, *src; register count; { while (--count >= 0) if ((*des++ = *src++) == 0) return; *des = '\0'; } /* * For qsort() */ static fcompare (file1, file2) char **file1, **file2; { return (strcmp (*file1, *file2)); } static char filetype (dir, file) char *dir, *file; { if (dir) { char path[512]; struct stat statb; strcpy (path, dir); catn (path, file, sizeof path); if (stat (path, &statb) >= 0) { if (statb.st_mode & S_IFDIR) return ('/'); if (statb.st_mode & 0111) return ('*'); } } return (' '); } /* * Print sorted down columns */ static print_by_column (dir, items, count, l_for_command) register char *dir, *items[]; { register int i, rows, r, c, maxwidth = 0, columns; for (i = 0; i < count; i++) maxwidth = max (maxwidth, strlen (items[i])); maxwidth += l_for_command ? 1:2; /* for the file tag and space */ columns = 80 / maxwidth; rows = (count + (columns - 1)) / columns; for (r = 0; r < rows; r++) { for (c = 0; c < columns; c++) { i = c * rows + r; if (i < count) { register int w; printf("%s", items[i]); w = strlen (items[i]); /* Print filename followed by '/' or '*' or ' ' */ if (!l_for_command) putchar (filetype (dir, items[i])), w++; if (c < (columns - 1)) /* Not last column? */ for (; w < maxwidth; w++) putchar (' '); } } printf ("\n"); } } /* * expand "old" file name with possible tilde usage * ~person/mumble * expands to * home_directory_of_person/mumble * into string "new". */ char * tilde (new, old) char *new, *old; { extern struct passwd *getpwuid (), *getpwnam (); register char *o, *p; register struct passwd *pw; static char person[40] = {0}; if (old[0] != '~') { strcpy (new, old); return (new); } for (p = person, o = &old[1]; *o && *o != '/'; *p++ = *o++); *p = '\0'; if (person[0] == '\0') /* then use current uid */ pw = getpwuid (getuid ()); else pw = getpwnam (person); if (pw == NULL) return (NULL); strcpy (new, pw -> pw_dir); (void) strcat (new, o); return (new); } /* * Cause pending line to be printed */ static retype () { int pending_input = LPENDIN; ioctl (SHOUT, TIOCLBIS, &pending_input); } static beep () { (void) write (SHOUT, BELL, strlen(BELL)); } /* * parse full path in file into 2 parts: directory and file names * Should leave final slash (/) at end of dir. */ static dir_name (path, dir, name) char *path, *dir, *name; { extern char *rindex (); register char *p; p = rindex (path, '/'); if (p == NULL) { copyn (name, path, MAXNAMLEN); dir[0] = '\0'; } else { p++; copyn (name, p, MAXNAMLEN); copyn (dir, path, p - path); } } char * getentry (dir_fd, l_for_lognames) DIR *dir_fd; { if (l_for_lognames) /* Is it login names we want? */ { extern struct passwd *getpwent (); register struct passwd *pw; if ((pw = getpwent ()) == NULL) return (NULL); return (pw -> pw_name); } else /* It's a dir entry we want */ { register struct direct *dirp; if (dirp = readdir (dir_fd)) return (dirp -> d_name); return (NULL); } } static free_items (items) register char **items; { register int i; for (i = 0; items[i]; i++) free (items[i]); free (items); } #define FREE_ITEMS(items)\ {\ sighold (SIGINT);\ free_items (items);\ items = NULL;\ sigrelse (SIGINT);\ } #define FREE_DIR(fd)\ {\ sighold (SIGINT);\ closedir (fd);\ fd = NULL;\ sigrelse (SIGINT);\ } static int dirctr; /* -1 0 1 2 ... */ static char dirflag[5]; /* ' nn\0' - dir #s - . 1 2 ... */ /* * Strip next directory from path; return ptr to next unstripped directory. */ char *dir_f_path (path, dir) char *path, dir[]; { register char *d = dir; while (*path && (*path == ' ' || *path == ':')) path++; while (*path && (*path != ' ' && *path != ':')) *(d++) = *(path++); while (*path && (*path == ' ' || *path == ':')) path++; ++dirctr; if (*dir == '.') strcpy (dirflag, " ."); else { dirflag[0] = ' '; if (dirctr <= 9) { dirflag[1] = '0' + dirctr; dirflag[2] = '\0'; } else { dirflag[1] = '0' + dirctr / 10; dirflag[2] = '0' + dirctr % 10; dirflag[3] = '\0'; } } *(d++) = '/'; *d = 0; return path; } /* * Perform a RECOGNIZE or LIST command on string "word". */ static search (word, wp, command, routine, max_word_length, l_for_command) char *word, *wp; /* original end-of-word */ COMMAND command; int (*routine) (); { # define MAXITEMS 2048 register numitems, name_length, /* Length of prefix (file name) */ l_for_lognames; /* True if looking for login names */ int showpathn; /* True if we want path number */ struct stat dot_statb, /* Stat buffer for "." */ curdir_statb; /* Stat buffer for current directory */ int dot_scan, /* True if scanning "." */ dot_got; /* True if have scanned dot already */ char tilded_dir[FILSIZ + 1], /* dir after ~ expansion */ dir[FILSIZ + 1], /* /x/y/z/ part in /x/y/z/f */ name[MAXNAMLEN + 1], /* f part in /d/d/d/f */ extended_name[MAXNAMLEN+1], /* the recognized (extended) name */ *entry, /* single directory entry or logname */ *path; /* hacked PATH environment variable */ static DIR *dir_fd = NULL; static char **items = NULL; /* file names when doing a LIST */ if (items != NULL) FREE_ITEMS (items); if (dir_fd != NULL) FREE_DIR (dir_fd); l_for_lognames = (*word == '~') && (index (word, '/') == NULL); l_for_command &= (*word != '~') && (index (word, '/') == NULL); if (l_for_command) { copyn (name, word, MAXNAMLEN); if ((path = getenv ("PATH")) == NULL) path = ""; /* setup builtins as 1st to search before PATH */ copyn (dir, BUILTINS, sizeof dir); dirctr = -1; /* BUILTINS -1 */ dirflag[0] = 0; } numitems = 0; dot_got = FALSE; stat (".", &dot_statb); cmdloop: /* One loop per directory in PATH, if l_for_command */ if (l_for_lognames) /* Looking for login names? */ { setpwent (); /* Open passwd file */ copyn (name, &word[1], MAXNAMLEN); /* name sans ~ */ } else { /* Open directory */ if (!l_for_command) dir_name (word, dir, name); if ((tilde (tilded_dir, dir) == 0) || /* expand ~user/... stuff */ ((dir_fd = opendir (*tilded_dir ? tilded_dir : ".")) == NULL)) { if (l_for_command) goto try_next_path; else return (0); } dot_scan = FALSE; if (l_for_command) { /* * Are we searching "."? */ fstat (dir_fd->dd_fd, &curdir_statb); if (curdir_statb.st_dev == dot_statb.st_dev && curdir_statb.st_ino == dot_statb.st_ino) { if (dot_got) /* Second time in PATH? */ goto try_next_path; dot_scan = TRUE; dot_got = TRUE; } } } name_length = strlen (name); showpathn = l_for_command && is_set("listpathnum"); while (entry = getentry (dir_fd, l_for_lognames)) { if (!is_prefix (name, entry)) continue; /* * Don't match . files on null prefix match */ if (name_length == 0 && entry[0] == '.' && !l_for_lognames) continue; /* * Skip non-executables if looking for commands: * Only done for directory "." for speed. * (Benchmarked with and without: * With filetype check, a full search took 10 seconds. * Without filetype check, a full search took 1 second.) * -Ken Greer */ if (l_for_command && dot_scan && filetype (dir, entry) != '*') continue; if (command == LIST) /* LIST command */ { extern char *malloc (); register int length; if (numitems >= MAXITEMS) { printf ("\nYikes!! Too many %s!!\n", l_for_lognames ? "names in password file":"files"); break; } if (items == NULL) { items = (char **) calloc (sizeof (items[1]), MAXITEMS + 1); if (items == NULL) break; } length = strlen(entry) + 1; if (showpathn) length += strlen(dirflag); if ((items[numitems] = malloc (length)) == NULL) { printf ("out of mem\n"); break; } copyn (items[numitems], entry, MAXNAMLEN); if (showpathn) catn (items[numitems], dirflag, MAXNAMLEN); numitems++; } else /* RECOGNIZE command */ if (recognize (extended_name, entry, name_length, ++numitems)) break; } if (l_for_lognames) endpwent (); else FREE_DIR (dir_fd); try_next_path: if (l_for_command && *path && (path = dir_f_path (path, dir), dir)) goto cmdloop; if (command == RECOGNIZE && numitems > 0) { if (l_for_lognames) copyn (word, "~", 1); else if (l_for_command) word[0] = 0; else copyn (word, dir, max_word_length); /* put back dir part */ catn (word, extended_name, max_word_length); /* add extended name */ while (*wp) (*routine) (*wp++); return (numitems); } if (command == LIST) { qsort (items, numitems, sizeof (items[1]), fcompare); print_by_column (l_for_lognames ? NULL:tilded_dir, items, numitems, l_for_command); if (items != NULL) FREE_ITEMS (items); } return (0); } /* * Object: extend what user typed up to an ambiguity. * Algorithm: * On first match, copy full entry (assume it'll be the only match) * On subsequent matches, shorten extended_name to the first * character mismatch between extended_name and entry. * If we shorten it back to the prefix length, stop searching. */ recognize (extended_name, entry, name_length, numitems) char *extended_name, *entry; { if (numitems == 1) /* 1st match */ copyn (extended_name, entry, MAXNAMLEN); else /* 2nd and subsequent matches */ { register char *x, *ent; register int len = 0; for (x = extended_name, ent = entry; *x && *x == *ent++; x++, len++); *x = '\0'; /* Shorten at 1st char diff */ if (len == name_length) /* Ambiguous to prefix? */ return (-1); /* So stop now and save time */ } return (0); } /* * return true if check items initial chars in template * This differs from PWB imatch in that if check is null * it items anything */ static is_prefix (check, template) char *check, *template; { register char *check_char, *templ_char; check_char = check; templ_char = template; do if (*check_char == 0) return (TRUE); while (*check_char++ == *templ_char++); return (FALSE); } starting_a_command (wordstart, inputline) register char *wordstart, *inputline; { static char *cmdstart = ";&(|`", *cmdalive = " \t'\""; while (--wordstart >= inputline) { if (index (cmdstart, *wordstart)) break; if (!index (cmdalive, *wordstart)) return (FALSE); } if (wordstart > inputline && *wordstart == '&') /* Look for >& */ { while (wordstart > inputline && (*--wordstart == ' ' || *wordstart == '\t')); if (*wordstart == '>') return (FALSE); } return (TRUE); } tenematch (inputline, inline_size, num_read, command, command_routine) char *inputline; /* match string prefix */ int inline_size; /* max size of string */ int num_read; /* # actually in inputline */ COMMAND command; /* LIST or RECOGNIZE */ int (*command_routine) (); /* either append char or display char */ { static char *delims = " '\"\t;&<>()|^%"; char word [FILSIZ + 1]; register char *str_end, *word_start, *cmd_start, *wp; int space_left; int is_a_cmd; /* UNIX command rather than filename */ str_end = &inputline[num_read]; /* * Find LAST occurence of a delimiter in the inputline. * The word start is one character past it. */ for (word_start = str_end; word_start > inputline; --word_start) if (index (delims, word_start[-1])) break; space_left = inline_size - (word_start - inputline) - 1; is_a_cmd = starting_a_command (word_start, inputline); for (cmd_start = word_start, wp = word; cmd_start < str_end; *wp++ = *cmd_start++); *wp = 0; return search (word, wp, command, command_routine, space_left, is_a_cmd); } char *CharPtr; static CharAppend (c) { putchar (c); *CharPtr++ = c; *CharPtr = 0; } tenex (inputline, inline_size) char *inputline; int inline_size; { register int numitems, num_read; setup_tty (ON); termchars (); while((num_read = read (SHIN, inputline, inline_size)) > 0) { register char *str_end, last_char, should_retype; COMMAND command; int tty_local = 0; /* tty "local mode" bits */ last_char = inputline[num_read - 1] & 0177; if (last_char == '\n' || num_read == inline_size) break; ioctl (SHIN, TIOCLGET, &tty_local); if (last_char == ESC) /* RECOGNIZE */ { if (tty_local & LCTLECH) printf ("\210\210 \210\210"); /* Erase ^[ */ /* if (num_read == 1) { num_read = tenedit (inputline, inline_size, ""); break; } else */ command = RECOGNIZE; num_read--; } else /* LIST */ command = LIST, putchar ('\n'); CharPtr = str_end = &inputline[num_read]; *str_end = '\0'; numitems = tenematch (inputline, inline_size, num_read, command, command == LIST ? putchar : CharAppend); flush (); if (command == RECOGNIZE) if (numitems != 1) /* Beep = No match/ambiguous */ beep (); /* * Tabs in the input line cause trouble after a pushback. * tty driver won't backspace over them because column positions * are now incorrect. This is solved by retyping over current line. */ should_retype = FALSE; if (index (inputline, '\t') /* tab in input line? */ || (tty_local & LCTLECH) == 0) /* Control chars don't echo? */ { back_to_col_1 (); should_retype = TRUE; } if (command == LIST) /* Always retype after LIST */ should_retype = TRUE; if (should_retype) printprompt (); pushback (inputline); if (should_retype) retype (); } setup_tty (OFF); return (num_read); } #ifdef TEST short SHIN = 0, SHOUT = 1; printprompt () { (void) write (SHOUT, "-> ", 3); return (1); } main (argc, argv) char **argv; { char string[128]; int n; while (printprompt () && (n = tenex (string, 127)) > 0) { string[n] = '\0'; printf ("Tenex returns \"%s\"\n", string); } } #endif