1/*
2 * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
3 * Use is subject to license terms.
4 */
5
6/*	Copyright (c) 1983, 1984, 1985, 1986, 1987, 1988, 1989 AT&T	*/
7/*	  All Rights Reserved  	*/
8
9/*
10 * Copyright (c) 1980 Regents of the University of California.
11 * All rights reserved.  The Berkeley Software License Agreement
12 * specifies the terms and conditions for redistribution.
13 */
14
15#pragma ident	"%Z%%M%	%I%	%E% SMI"
16
17#ifdef FILEC
18/*
19 * Tenex style file name recognition, .. and more.
20 * History:
21 *	Author: Ken Greer, Sept. 1975, CMU.
22 *	Finally got around to adding to the Cshell., Ken Greer, Dec. 1981.
23 */
24
25#include "sh.h"
26#include <sys/types.h>
27#include <dirent.h>
28#include <pwd.h>
29#include "sh.tconst.h"
30
31#define	TRUE	1
32#define	FALSE	0
33#define	ON	1
34#define	OFF	0
35
36#define	ESC	'\033'
37
38extern DIR *opendir_(tchar *);
39
40static char *BELL = "\07";
41static char *CTRLR = "^R\n";
42
43typedef enum {LIST, RECOGNIZE} COMMAND;
44
45static jmp_buf osetexit;		/* saved setexit() state */
46static struct termios  tty_save;	/* saved terminal state */
47static struct termios  tty_new;		/* new terminal state */
48
49static int	is_prefix(tchar *, tchar *);
50static int	is_suffix(tchar *, tchar *);
51static int	ignored(tchar *);
52
53/*
54 * Put this here so the binary can be patched with adb to enable file
55 * completion by default.  Filec controls completion, nobeep controls
56 * ringing the terminal bell on incomplete expansions.
57 */
58bool filec = 0;
59
60static void
61setup_tty(int on)
62{
63	int omask;
64#ifdef TRACE
65	tprintf("TRACE- setup_tty()\n");
66#endif
67
68	omask = sigblock(sigmask(SIGINT));
69	if (on) {
70		/*
71		 * The shell makes sure that the tty is not in some weird state
72		 * and fixes it if it is.  But it should be noted that the
73		 * tenex routine will not work correctly in CBREAK or RAW mode
74		 * so this code below is, therefore, mandatory.
75		 *
76		 * Also, in order to recognize the ESC (filename-completion)
77		 * character, set EOL to ESC.  This way, ESC will terminate
78		 * the line, but still be in the input stream.
79		 * EOT (filename list) will also terminate the line,
80		 * but will not appear in the input stream.
81		 *
82		 * The getexit/setexit contortions ensure that the
83		 * tty state will be restored if the user types ^C.
84		 */
85		(void) ioctl(SHIN, TCGETS,  (char *)&tty_save);
86		getexit(osetexit);
87		if (setjmp(reslab)) {
88			(void) ioctl(SHIN, TCSETSW,  (char *)&tty_save);
89			resexit(osetexit);
90			reset();
91		}
92		tty_new = tty_save;
93		tty_new.c_cc[VEOL] = ESC;
94		tty_new.c_iflag |= IMAXBEL | BRKINT | IGNPAR;
95		tty_new.c_lflag |= ICANON;
96		tty_new.c_lflag |= ECHOCTL;
97		tty_new.c_oflag &= ~OCRNL;
98		(void) ioctl(SHIN, TCSETSW,  (char *)&tty_new);
99	} else {
100		/*
101		 * Reset terminal state to what user had when invoked
102		 */
103		(void) ioctl(SHIN, TCSETSW,  (char *)&tty_save);
104		resexit(osetexit);
105	}
106	(void) sigsetmask(omask);
107}
108
109static void
110termchars(void)
111{
112	extern char *tgetstr();
113	char bp[1024];
114	static char area[256];
115	static int been_here = 0;
116	char *ap = area;
117	char *s;
118	char *term;
119
120#ifdef TRACE
121	tprintf("TRACE- termchars()\n");
122#endif
123	if (been_here)
124		return;
125	been_here = TRUE;
126
127	if ((term = getenv("TERM")) == NULL)
128		return;
129	if (tgetent(bp, term) != 1)
130		return;
131	if (s = tgetstr("vb", &ap))		/* Visible Bell */
132		BELL = s;
133}
134
135/*
136 * Move back to beginning of current line
137 */
138static void
139back_to_col_1(void)
140{
141	int omask;
142
143#ifdef TRACE
144	tprintf("TRACE- back_to_col_1()\n");
145#endif
146	omask = sigblock(sigmask(SIGINT));
147	(void) write(SHOUT, "\r", 1);
148	(void) sigsetmask(omask);
149}
150
151/*
152 * Push string contents back into tty queue
153 */
154static void
155pushback(tchar *string, int echoflag)
156{
157	tchar *p;
158	struct termios tty;
159	int omask, retry = 0;
160
161#ifdef TRACE
162	tprintf("TRACE- pushback()\n");
163#endif
164	omask = sigblock(sigmask(SIGINT));
165	tty = tty_new;
166	if (!echoflag)
167		tty.c_lflag &= ~ECHO;
168
169again:
170	(void) ioctl(SHIN, TCSETSF, (char *)&tty);
171
172	for (p = string; *p; p++) {
173		char	mbc[MB_LEN_MAX];
174		int	i, j = wctomb(mbc, (wchar_t)*p);
175
176		if (j < 0) {
177			/* Error! But else what can we do? */
178			continue;
179		}
180		for (i = 0; i < j; ++i) {
181			if (ioctl(SHIN, TIOCSTI, mbc + i) != 0 &&
182			    errno == EAGAIN) {
183				if (retry++ < 5)
184					goto again;
185				/* probably no worth retrying any more */
186			}
187		}
188	}
189
190	if (tty.c_lflag != tty_new.c_lflag)
191		(void) ioctl(SHIN, TCSETS,  (char *)&tty_new);
192	(void) sigsetmask(omask);
193}
194
195/*
196 * Concatenate src onto tail of des.
197 * Des is a string whose maximum length is count.
198 * Always null terminate.
199 */
200void
201catn(tchar *des, tchar *src, int count)
202{
203#ifdef TRACE
204	tprintf("TRACE- catn()\n");
205#endif
206
207	while (--count >= 0 && *des)
208		des++;
209	while (--count >= 0)
210		if ((*des++ = *src++) == '\0')
211			return;
212	*des = '\0';
213}
214
215static int
216max(a, b)
217{
218
219	return (a > b ? a : b);
220}
221
222/*
223 * Like strncpy but always leave room for trailing \0
224 * and always null terminate.
225 */
226void
227copyn(tchar *des, tchar *src, int count)
228{
229
230#ifdef TRACE
231	tprintf("TRACE- copyn()\n");
232#endif
233	while (--count >= 0)
234		if ((*des++ = *src++) == '\0')
235			return;
236	*des = '\0';
237}
238
239/*
240 * For qsort()
241 */
242static int
243fcompare(tchar **file1, tchar **file2)
244{
245
246#ifdef TRACE
247	tprintf("TRACE- fcompare()\n");
248#endif
249	return (strcoll_(*file1, *file2));
250}
251
252static char
253filetype(tchar *dir, tchar *file, int nosym)
254{
255	tchar path[MAXPATHLEN + 1];
256	struct stat statb;
257
258#ifdef TRACE
259	tprintf("TRACE- filetype()\n");
260#endif
261	if (dir) {
262		catn(strcpy_(path, dir), file, MAXPATHLEN);
263		if (nosym) {
264			if (stat_(path, &statb) < 0)
265				return (' ');
266		} else {
267			if (lstat_(path, &statb) < 0)
268				return (' ');
269		}
270		if ((statb.st_mode & S_IFMT) == S_IFLNK)
271			return ('@');
272		if ((statb.st_mode & S_IFMT) == S_IFDIR)
273			return ('/');
274		if (((statb.st_mode & S_IFMT) == S_IFREG) &&
275		    (statb.st_mode & 011))
276			return ('*');
277	}
278	return (' ');
279}
280
281/*
282 * Print sorted down columns
283 */
284static void
285print_by_column(tchar *dir, tchar *items[], int count, int looking_for_command)
286{
287	int i, rows, r, c, maxwidth = 0, columns;
288
289#ifdef TRACE
290	tprintf("TRACE- print_by_column()\n");
291#endif
292	for (i = 0; i < count; i++)
293		maxwidth = max(maxwidth, tswidth(items[i]));
294
295	/* for the file tag and space */
296	maxwidth += looking_for_command ? 1 : 2;
297	columns = max(78 / maxwidth, 1);
298	rows = (count + (columns - 1)) / columns;
299
300	for (r = 0; r < rows; r++) {
301		for (c = 0; c < columns; c++) {
302			i = c * rows + r;
303			if (i < count) {
304				int w;
305
306				/*
307				 * Print filename followed by
308				 * '@' or '/' or '*' or ' '
309				 */
310				printf("%t", items[i]);
311				w = tswidth(items[i]);
312				if (!looking_for_command) {
313					printf("%c",
314					    (tchar) filetype(dir, items[i], 0));
315					w++;
316				}
317				if (c < columns - 1)	/* last column? */
318					for (; w < maxwidth; w++)
319						printf(" ");
320			}
321		}
322		printf("\n");
323	}
324}
325
326/*
327 * Expand file name with possible tilde usage
328 *	~person/mumble
329 * expands to
330 *	home_directory_of_person/mumble
331 */
332tchar *
333tilde(tchar *new, tchar *old)
334{
335	tchar *o, *p;
336	struct passwd *pw;
337	static tchar person[40];
338	char person_[40];		/* work */
339	tchar *pw_dir;			/* work */
340
341#ifdef TRACE
342	tprintf("TRACE- tilde()\n");
343#endif
344	if (old[0] != '~')
345		return (strcpy_(new, old));
346
347	for (p = person, o = &old[1]; *o && *o != '/'; *p++ = *o++)
348		;
349	*p = '\0';
350	if (person[0] == '\0')
351		(void) strcpy_(new, value(S_home /* "home" */));
352	else {
353		pw = getpwnam(tstostr(person_, person));
354		if (pw == NULL)
355			return (NULL);
356		pw_dir = strtots((tchar *)NULL, pw->pw_dir);	/* allocate */
357		(void) strcpy_(new, pw_dir);
358		xfree(pw_dir);					/* free it */
359	}
360	(void) strcat_(new, o);
361	return (new);
362}
363
364/*
365 * Cause pending line to be printed
366 */
367static void
368sim_retype(void)
369{
370#ifdef notdef
371	struct termios tty_pending;
372
373#ifdef TRACE
374	tprintf("TRACE- sim_retypr()\n");
375#endif
376	tty_pending = tty_new;
377	tty_pending.c_lflag |= PENDIN;
378
379	(void) ioctl(SHIN, TCSETS,  (char *)&tty_pending);
380#else
381#ifdef TRACE
382	tprintf("TRACE- sim_retype()\n");
383#endif
384	(void) write(SHOUT, CTRLR, strlen(CTRLR));
385	printprompt();
386#endif
387}
388
389static int
390beep_outc(int c)
391{
392	char	buf[1];
393
394	buf[0] = c;
395
396	(void) write(SHOUT, buf, 1);
397
398	return 0;
399}
400
401static void
402beep(void)
403{
404
405#ifdef TRACE
406	tprintf("TRACE- beep()\n");
407#endif
408	if (adrof(S_nobeep /* "nobeep" */) == 0)
409		(void) tputs(BELL, 0, beep_outc);
410}
411
412/*
413 * Erase that silly ^[ and print the recognized part of the string.
414 */
415static void
416print_recognized_stuff(tchar *recognized_part)
417{
418	int unit =  didfds ? 1 : SHOUT;
419
420#ifdef TRACE
421	tprintf("TRACE- print_recognized_stuff()\n");
422#endif
423
424	/*
425	 * An optimized erasing of that silly ^[
426	 *
427	 * One would think that line speeds have become fast enough that this
428	 * isn't necessary, but it turns out that the visual difference is
429	 * quite noticeable.
430	 */
431	flush();
432	switch (tswidth(recognized_part)) {
433	case 0:
434		/* erase two characters: ^[ */
435		write(unit, "\b\b  \b\b", sizeof "\b\b  \b\b" - 1);
436		break;
437
438	case 1:
439		/* overstrike the ^, erase the [ */
440		write(unit, "\b\b", 2);
441		printf("%t", recognized_part);
442		write(unit, "  \b\b", 4);
443		break;
444
445	default:
446		/* overstrike both characters ^[ */
447		write(unit, "\b\b", 2);
448		printf("%t", recognized_part);
449		break;
450	}
451	flush();
452}
453
454/*
455 * Parse full path in file into 2 parts: directory and file names
456 * Should leave final slash (/) at end of dir.
457 */
458static void
459extract_dir_and_name(tchar *path, tchar *dir, tchar *name)
460{
461	tchar  *p;
462
463#ifdef TRACE
464	tprintf("TRACE- extract_dir_and_name()\n");
465#endif
466	p = rindex_(path, '/');
467	if (p == NOSTR) {
468		copyn(name, path, MAXNAMLEN);
469		dir[0] = '\0';
470	} else {
471		copyn(name, ++p, MAXNAMLEN);
472		copyn(dir, path, p - path);
473	}
474}
475
476tchar *
477getentry(DIR *dir_fd, int looking_for_lognames)
478{
479	struct passwd *pw;
480	struct dirent *dirp;
481	/*
482	 * For char * -> tchar * Conversion
483	 */
484	static tchar strbuf[MAXNAMLEN+1];
485
486#ifdef TRACE
487	tprintf("TRACE- getentry()\n");
488#endif
489	if (looking_for_lognames) {
490		if ((pw = getpwent()) == NULL)
491			return (NULL);
492		return (strtots(strbuf, pw->pw_name));
493	}
494	if (dirp = readdir(dir_fd))
495		return (strtots(strbuf, dirp->d_name));
496	return (NULL);
497}
498
499static void
500free_items(tchar **items)
501{
502	int i;
503
504#ifdef TRACE
505	tprintf("TRACE- free_items()\n");
506#endif
507	for (i = 0; items[i]; i++)
508		xfree(items[i]);
509	xfree((char *)items);
510}
511
512#define	FREE_ITEMS(items) { \
513	int omask;\
514\
515	omask = sigblock(sigmask(SIGINT));\
516	free_items(items);\
517	items = NULL;\
518	(void) sigsetmask(omask);\
519}
520
521/*
522 * Perform a RECOGNIZE or LIST command on string "word".
523 */
524static int
525search2(tchar *word, COMMAND command, int max_word_length)
526{
527	static tchar **items = NULL;
528	DIR *dir_fd;
529	int numitems = 0, ignoring = TRUE, nignored = 0;
530	int name_length, looking_for_lognames;
531	tchar tilded_dir[MAXPATHLEN + 1], dir[MAXPATHLEN + 1];
532	tchar name[MAXNAMLEN + 1], extended_name[MAXNAMLEN+1];
533	tchar *entry;
534#define	MAXITEMS 1024
535#ifdef TRACE
536	tprintf("TRACE- search2()\n");
537#endif
538
539	if (items != NULL)
540		FREE_ITEMS(items);
541
542	looking_for_lognames = (*word == '~') && (index_(word, '/') == NULL);
543	if (looking_for_lognames) {
544		(void) setpwent();
545		copyn(name, &word[1], MAXNAMLEN);	/* name sans ~ */
546	} else {
547		extract_dir_and_name(word, dir, name);
548		if (tilde(tilded_dir, dir) == 0)
549			return (0);
550		dir_fd = opendir_(*tilded_dir ? tilded_dir : S_DOT /* "." */);
551		if (dir_fd == NULL)
552			return (0);
553	}
554
555again:	/* search for matches */
556	name_length = strlen_(name);
557	for (numitems = 0; entry = getentry(dir_fd, looking_for_lognames); ) {
558		if (!is_prefix(name, entry))
559			continue;
560		/* Don't match . files on null prefix match */
561		if (name_length == 0 && entry[0] == '.' &&
562		    !looking_for_lognames)
563			continue;
564		if (command == LIST) {
565			if (numitems >= MAXITEMS) {
566				printf("\nYikes!! Too many %s!!\n",
567				    looking_for_lognames ?
568					"names in password file":"files");
569				break;
570			}
571			if (items == NULL)
572				items =  (tchar **)xcalloc(sizeof (items[1]),
573				    MAXITEMS+1);
574			items[numitems] = (tchar *)xalloc((unsigned)(strlen_(entry) + 1) * sizeof (tchar));
575			copyn(items[numitems], entry, MAXNAMLEN);
576			numitems++;
577		} else {			/* RECOGNIZE command */
578			if (ignoring && ignored(entry))
579				nignored++;
580			else if (recognize(extended_name,
581			    entry, name_length, ++numitems))
582				break;
583		}
584	}
585	if (ignoring && numitems == 0 && nignored > 0) {
586		ignoring = FALSE;
587		nignored = 0;
588		if (looking_for_lognames)
589			(void) setpwent();
590		else
591			rewinddir(dir_fd);
592		goto again;
593	}
594
595	if (looking_for_lognames)
596		(void) endpwent();
597	else {
598		unsetfd(dir_fd->dd_fd);
599		closedir_(dir_fd);
600	}
601	if (command == RECOGNIZE && numitems > 0) {
602		if (looking_for_lognames)
603			copyn(word, S_TIL /* "~" */, 1);
604		else
605			/* put back dir part */
606			copyn(word, dir, max_word_length);
607		/* add extended name */
608		catn(word, extended_name, max_word_length);
609		return (numitems);
610	}
611	if (command == LIST) {
612		qsort((char *)items, numitems, sizeof (items[1]),
613		    (int (*)(const void *, const void *))fcompare);
614		/*
615		 * Never looking for commands in this version, so final
616		 * argument forced to 0.  If command name completion is
617		 * reinstated, this must change.
618		 */
619		print_by_column(looking_for_lognames ? NULL : tilded_dir,
620		    items, numitems, 0);
621		if (items != NULL)
622			FREE_ITEMS(items);
623	}
624	return (0);
625}
626
627/*
628 * Object: extend what user typed up to an ambiguity.
629 * Algorithm:
630 * On first match, copy full entry (assume it'll be the only match)
631 * On subsequent matches, shorten extended_name to the first
632 * character mismatch between extended_name and entry.
633 * If we shorten it back to the prefix length, stop searching.
634 */
635int
636recognize(tchar *extended_name, tchar *entry, int name_length, int numitems)
637{
638
639#ifdef TRACE
640	tprintf("TRACE- recognize()\n");
641#endif
642	if (numitems == 1)				/* 1st match */
643		copyn(extended_name, entry, MAXNAMLEN);
644	else {					/* 2nd and subsequent matches */
645		tchar *x, *ent;
646		int len = 0;
647
648		x = extended_name;
649		for (ent = entry; *x && *x == *ent++; x++, len++)
650			;
651		*x = '\0';			/* Shorten at 1st char diff */
652		if (len == name_length)		/* Ambiguous to prefix? */
653			return (-1);		/* So stop now and save time */
654	}
655	return (0);
656}
657
658/*
659 * Return true if check items initial chars in template
660 * This differs from PWB imatch in that if check is null
661 * it items anything
662 */
663static int
664is_prefix(tchar *check, tchar *template)
665{
666#ifdef TRACE
667	tprintf("TRACE- is_prefix()\n");
668#endif
669
670	do
671		if (*check == 0)
672			return (TRUE);
673	while (*check++ == *template++);
674	return (FALSE);
675}
676
677/*
678 *  Return true if the chars in template appear at the
679 *  end of check, i.e., are its suffix.
680 */
681static int
682is_suffix(tchar *check, tchar *template)
683{
684	tchar *c, *t;
685
686#ifdef TRACE
687	tprintf("TRACE- is_suffix()\n");
688#endif
689	for (c = check; *c++; )
690		;
691	for (t = template; *t++; )
692		;
693	for (;;) {
694		if (t == template)
695			return (TRUE);
696		if (c == check || *--t != *--c)
697			return (FALSE);
698	}
699}
700
701int
702tenex(tchar *inputline, int inputline_size)
703{
704	int numitems, num_read, should_retype;
705	int i;
706
707#ifdef TRACE
708	tprintf("TRACE- tenex()\n");
709#endif
710	setup_tty(ON);
711	termchars();
712	num_read = 0;
713	should_retype = FALSE;
714	while ((i = read_(SHIN, inputline+num_read, inputline_size-num_read))
715	    > 0) {
716		static tchar *delims = S_DELIM /* " '\"\t;&<>()|`" */;
717		tchar *str_end, *word_start, last_char;
718		int space_left;
719		struct termios tty;
720		COMMAND command;
721
722		num_read += i;
723		inputline[num_read] = '\0';
724		last_char = inputline[num_read - 1] & TRIM;
725
726		/*
727		 * read_() can return more than requested size if there
728		 * is multibyte character at the end.
729		 */
730		if ((num_read >= inputline_size) || (last_char == '\n'))
731			break;
732
733		str_end = &inputline[num_read];
734		if (last_char == ESC) {
735			command = RECOGNIZE;
736			*--str_end = '\0';	/* wipe out trailing ESC */
737		} else
738			command = LIST;
739
740		tty = tty_new;
741		tty.c_lflag &= ~ECHO;
742		(void) ioctl(SHIN, TCSETSF, (char *)&tty);
743
744		if (command == LIST)
745			printf("\n");
746		/*
747		 * Find LAST occurence of a delimiter in the inputline.
748		 * The word start is one character past it.
749		 */
750		for (word_start = str_end; word_start > inputline;
751		    --word_start) {
752			if (index_(delims, word_start[-1]) ||
753			    isauxsp(word_start[-1]))
754				break;
755		}
756		space_left = inputline_size - (word_start - inputline) - 1;
757		numitems = search2(word_start, command, space_left);
758
759		/*
760		 * Tabs in the input line cause trouble after a pushback.
761		 * tty driver won't backspace over them because column
762		 * positions are now incorrect. This is solved by retyping
763		 * over current line.
764		 */
765		if (index_(inputline, '\t')) {	/* tab tchar in input line? */
766			back_to_col_1();
767			should_retype = TRUE;
768		}
769		if (command == LIST)		/* Always retype after a LIST */
770			should_retype = TRUE;
771		if (should_retype)
772			printprompt();
773		pushback(inputline, should_retype);
774		num_read = 0;			/* chars will be reread */
775		should_retype = FALSE;
776
777		/*
778		 * Avoid a race condition by echoing what we're recognized
779		 * _after_ pushing back the command line.  This way, if the
780		 * user waits until seeing this output before typing more
781		 * stuff, the resulting keystrokes won't race with the STIed
782		 * input we've pushed back.  (Of course, if the user types
783		 * ahead, the race still exists and it's quite possible that
784		 * the pushed back input line will interleave with the
785		 * keystrokes in unexpected ways.)
786		 */
787		if (command == RECOGNIZE) {
788			/* print from str_end on */
789			print_recognized_stuff(str_end);
790			if (numitems != 1)	/* Beep = No match/ambiguous */
791				beep();
792		}
793	}
794	setup_tty(OFF);
795	return (num_read);
796}
797
798static int
799ignored(tchar *entry)
800{
801	struct varent *vp;
802	tchar **cp;
803
804#ifdef TRACE
805	tprintf("TRACE- ignored()\n");
806#endif
807	if ((vp = adrof(S_fignore /* "fignore" */)) == NULL ||
808	    (cp = vp->vec) == NULL)
809		return (FALSE);
810	for (; *cp != NULL; cp++)
811		if (is_suffix(entry, *cp))
812			return (TRUE);
813	return (FALSE);
814}
815#endif /* FILEC */
816