1 /* $NetBSD: histedit.c,v 1.69 2024/07/05 04:07:26 kre Exp $ */ 2 3 /*- 4 * Copyright (c) 1993 5 * The Regents of the University of California. All rights reserved. 6 * 7 * This code is derived from software contributed to Berkeley by 8 * Kenneth Almquist. 9 * 10 * Redistribution and use in source and binary forms, with or without 11 * modification, are permitted provided that the following conditions 12 * are met: 13 * 1. Redistributions of source code must retain the above copyright 14 * notice, this list of conditions and the following disclaimer. 15 * 2. Redistributions in binary form must reproduce the above copyright 16 * notice, this list of conditions and the following disclaimer in the 17 * documentation and/or other materials provided with the distribution. 18 * 3. Neither the name of the University nor the names of its contributors 19 * may be used to endorse or promote products derived from this software 20 * without specific prior written permission. 21 * 22 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 23 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 25 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 26 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 28 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 29 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 31 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 32 * SUCH DAMAGE. 33 */ 34 35 #include <sys/cdefs.h> 36 #ifndef lint 37 #if 0 38 static char sccsid[] = "@(#)histedit.c 8.2 (Berkeley) 5/4/95"; 39 #else 40 __RCSID("$NetBSD: histedit.c,v 1.69 2024/07/05 04:07:26 kre Exp $"); 41 #endif 42 #endif /* not lint */ 43 44 #include <sys/param.h> 45 #include <sys/stat.h> 46 #include <dirent.h> 47 #include <paths.h> 48 #include <stdio.h> 49 #include <stdlib.h> 50 #include <unistd.h> 51 /* 52 * Editline and history functions (and glue). 53 */ 54 #include "shell.h" 55 #include "parser.h" 56 #include "var.h" 57 #include "options.h" 58 #include "builtins.h" 59 #include "main.h" 60 #include "output.h" 61 #include "mystring.h" 62 #include "myhistedit.h" 63 #include "error.h" 64 #include "alias.h" 65 66 #ifndef SMALL /* almost all the rest of this file */ 67 68 #include "eval.h" 69 #include "memalloc.h" 70 #include "show.h" 71 72 #define MAXHISTLOOPS 4 /* max recursions through fc */ 73 #define DEFEDITOR "ed" /* default editor *should* be $EDITOR */ 74 75 History *hist; /* history cookie */ 76 EditLine *el; /* editline cookie */ 77 int displayhist; 78 static FILE *el_in, *el_out; 79 static int curpos; 80 81 #ifdef DEBUG 82 extern FILE *tracefile; 83 #endif 84 85 static const char *fc_replace(const char *, char *, char *); 86 static int not_fcnumber(const char *); 87 static int str_to_event(const char *, int); 88 static int comparator(const void *, const void *); 89 static char **sh_matches(const char *, int, int); 90 static unsigned char sh_complete(EditLine *, int); 91 92 /* 93 * Set history and editing status. Called whenever the status may 94 * have changed (figures out what to do). 95 */ 96 void 97 histedit(void) 98 { 99 FILE *el_err; 100 101 #define editing (Eflag || Vflag) 102 103 CTRACE(DBG_HISTORY, ("histedit: %cE%cV %sinteractive\n", 104 Eflag ? '-' : '+', Vflag ? '-' : '+', iflag ? "" : "not ")); 105 106 if (iflag == 1) { 107 if (!hist) { 108 /* 109 * turn history on 110 */ 111 INTOFF; 112 hist = history_init(); 113 INTON; 114 115 if (hist != NULL) 116 sethistsize(histsizeval()); 117 else 118 out2str("sh: can't initialize history\n"); 119 } 120 if (editing && !el && isatty(0)) { /* && isatty(2) ??? */ 121 /* 122 * turn editing on 123 */ 124 char *term; 125 126 INTOFF; 127 if (el_in == NULL) 128 el_in = fdopen(0, "r"); 129 if (el_out == NULL) 130 el_out = fdopen(2, "w"); 131 if (el_in == NULL || el_out == NULL) 132 goto bad; 133 el_err = el_out; 134 #if DEBUG 135 if (tracefile) 136 el_err = tracefile; 137 #endif 138 /* 139 * This odd piece of code doesn't affect the shell 140 * at all, the environment modified here is the 141 * stuff accessed via "environ" (the incoming 142 * environment to the shell) which is only ever 143 * touched at sh startup time (long before we get 144 * here) and ignored thereafter. 145 * 146 * But libedit calls getenv() to discover TERM 147 * and that searches the "environ" environment, 148 * not the shell's internal variable data struct, 149 * so we need to make sure that TERM in there is 150 * correct. 151 * 152 * This sequence copies TERM from the shell into 153 * the old "environ" environment. 154 */ 155 term = lookupvar("TERM"); 156 if (term) 157 setenv("TERM", term, 1); 158 else 159 unsetenv("TERM"); 160 el = el_init("sh", el_in, el_out, el_err); 161 VTRACE(DBG_HISTORY, ("el_init() %sed\n", 162 el != NULL ? "succeed" : "fail")); 163 if (el != NULL) { 164 if (hist) 165 el_set(el, EL_HIST, history, hist); 166 167 set_prompt_lit(lookupvar("PSlit")); 168 el_set(el, EL_SIGNAL, 1); 169 el_set(el, EL_SAFEREAD, 1); 170 el_set(el, EL_ALIAS_TEXT, alias_text, NULL); 171 el_set(el, EL_ADDFN, "rl-complete", 172 "ReadLine compatible completion function", 173 sh_complete); 174 } else { 175 bad:; 176 out2str("sh: can't initialize editing\n"); 177 } 178 INTON; 179 } else if (!editing && el) { 180 INTOFF; 181 el_end(el); 182 el = NULL; 183 VTRACE(DBG_HISTORY, ("line editing disabled\n")); 184 INTON; 185 } 186 if (el) { 187 INTOFF; 188 if (Vflag) 189 el_set(el, EL_EDITOR, "vi"); 190 else if (Eflag) 191 el_set(el, EL_EDITOR, "emacs"); 192 VTRACE(DBG_HISTORY, ("reading $EDITRC\n")); 193 el_source(el, lookupvar("EDITRC")); 194 el_set(el, EL_BIND, "^I", 195 tabcomplete ? "rl-complete" : "ed-insert", NULL); 196 INTON; 197 } 198 } else { 199 INTOFF; 200 if (el) { /* no editing if not interactive */ 201 el_end(el); 202 el = NULL; 203 } 204 if (hist) { 205 history_end(hist); 206 hist = NULL; 207 } 208 INTON; 209 VTRACE(DBG_HISTORY, ("line editing & history disabled\n")); 210 } 211 } 212 213 void 214 set_prompt_lit(const char *lit_ch) 215 { 216 wchar_t wc; 217 218 if (!(iflag && editing && el)) 219 return; 220 221 if (lit_ch == NULL) { 222 el_set(el, EL_PROMPT, getprompt); 223 return; 224 } 225 226 mbtowc(&wc, NULL, 1); /* state init */ 227 228 INTOFF; 229 if (mbtowc(&wc, lit_ch, strlen(lit_ch)) <= 0) 230 el_set(el, EL_PROMPT, getprompt); 231 else 232 el_set(el, EL_PROMPT_ESC, getprompt, (int)wc); 233 INTON; 234 } 235 236 void 237 set_editrc(const char *fname) 238 { 239 INTOFF; 240 if (iflag && editing && el) 241 el_source(el, fname); 242 INTON; 243 } 244 245 void 246 sethistsize(const char *hs) 247 { 248 int histsize; 249 HistEvent he; 250 251 if (hist != NULL) { 252 if (hs == NULL || *hs == '\0' || !is_number(hs) || 253 (histsize = number(hs)) < 0) 254 histsize = 100; 255 INTOFF; 256 history(hist, &he, H_SETSIZE, histsize); 257 history(hist, &he, H_SETUNIQUE, 1); 258 INTON; 259 } 260 } 261 262 void 263 setterm(const char *term) 264 { 265 INTOFF; 266 if (el != NULL && term != NULL) 267 if (el_set(el, EL_TERMINAL, term) != 0) { 268 outfmt(out2, "sh: Can't set terminal type %s\n", term); 269 outfmt(out2, "sh: Using dumb terminal settings.\n"); 270 } 271 INTON; 272 } 273 274 int 275 inputrc(int argc, char **argv) 276 { 277 CTRACE(DBG_HISTORY, ("inputrc (%d arg%s)", argc-1, argc==2?"":"s")); 278 if (argc != 2) { 279 CTRACE(DBG_HISTORY, (" -- bad\n")); 280 out2str("usage: inputrc file\n"); 281 return 1; 282 } 283 CTRACE(DBG_HISTORY, (" file: \"%s\"\n", argv[1])); 284 if (el != NULL) { 285 INTOFF; 286 if (el_source(el, argv[1])) { 287 INTON; 288 out2str("inputrc: failed\n"); 289 return 1; 290 } 291 INTON; 292 return 0; 293 } else { 294 out2str("sh: inputrc ignored, not editing\n"); 295 return 1; 296 } 297 } 298 299 /* 300 * This command is provided since POSIX decided to standardize 301 * the Korn shell fc command. Oh well... 302 */ 303 int 304 histcmd(volatile int argc, char ** volatile argv) 305 { 306 int ch; 307 const char * volatile editor = NULL; 308 HistEvent he; 309 volatile int lflg = 0, nflg = 0, rflg = 0, sflg = 0; 310 int i, retval; 311 const char *firststr, *laststr; 312 int first, last, direction; 313 314 char * volatile pat = NULL; /* ksh "fc old=new" crap */ 315 char * volatile repl; 316 317 static int active = 0; 318 struct jmploc jmploc; 319 struct jmploc *volatile savehandler; 320 char editfile[MAXPATHLEN + 1]; 321 FILE * volatile efp; 322 323 #ifdef __GNUC__ 324 repl = NULL; /* XXX gcc4 */ 325 efp = NULL; /* XXX gcc4 */ 326 #endif 327 328 if (hist == NULL) 329 error("history not active"); 330 331 CTRACE(DBG_HISTORY, ("histcmd (fc) %d arg%s\n", argc, argc==1?"":"s")); 332 if (argc == 1) 333 error("missing history argument"); 334 335 optreset = 1; optind = 1; /* initialize getopt */ 336 while (not_fcnumber(argv[optind]) && 337 (ch = getopt(argc, argv, ":e:lnrs")) != -1) 338 switch ((char)ch) { 339 case 'e': 340 editor = optarg; 341 VTRACE(DBG_HISTORY, ("histcmd -e %s\n", editor)); 342 break; 343 case 'l': 344 lflg = 1; 345 VTRACE(DBG_HISTORY, ("histcmd -l\n")); 346 break; 347 case 'n': 348 nflg = 1; 349 VTRACE(DBG_HISTORY, ("histcmd -n\n")); 350 break; 351 case 'r': 352 rflg = 1; 353 VTRACE(DBG_HISTORY, ("histcmd -r\n")); 354 break; 355 case 's': 356 sflg = 1; 357 VTRACE(DBG_HISTORY, ("histcmd -s\n")); 358 break; 359 case ':': 360 error("option -%c expects argument", optopt); 361 /* NOTREACHED */ 362 case '?': 363 default: 364 error("unknown option: -%c", optopt); 365 /* NOTREACHED */ 366 } 367 argc -= optind, argv += optind; 368 369 /* 370 * If executing... 371 */ 372 if (lflg == 0 || editor || sflg) { 373 lflg = 0; /* ignore */ 374 editfile[0] = '\0'; 375 /* 376 * Catch interrupts to reset active counter and 377 * cleanup temp files. 378 */ 379 savehandler = handler; 380 if (setjmp(jmploc.loc)) { 381 active = 0; 382 if (*editfile) { 383 VTRACE(DBG_HISTORY, 384 ("histcmd err jump unlink temp \"%s\"\n", 385 editfile)); 386 unlink(editfile); 387 } 388 handler = savehandler; 389 longjmp(handler->loc, 1); 390 } 391 handler = &jmploc; 392 VTRACE(DBG_HISTORY, ("histcmd is active %d(++)\n", active)); 393 if (++active > MAXHISTLOOPS) { 394 active = 0; 395 displayhist = 0; 396 error("called recursively too many times"); 397 } 398 /* 399 * Set editor. 400 */ 401 if (sflg == 0) { 402 if (editor == NULL && 403 (editor = bltinlookup("FCEDIT", 1)) == NULL && 404 (editor = bltinlookup("EDITOR", 1)) == NULL) 405 editor = DEFEDITOR; 406 if (editor[0] == '-' && editor[1] == '\0') { 407 sflg = 1; /* no edit */ 408 editor = NULL; 409 } 410 VTRACE(DBG_HISTORY, ("histcmd using %s as editor\n", 411 editor == NULL ? "-nothing-" : editor)); 412 } 413 } 414 415 /* 416 * If executing, parse [old=new] now 417 */ 418 if (lflg == 0 && argc > 0 && 419 ((repl = strchr(argv[0], '=')) != NULL)) { 420 pat = argv[0]; 421 *repl++ = '\0'; 422 argc--, argv++; 423 VTRACE(DBG_HISTORY, ("histcmd replace old=\"%s\" new=\"%s\"" 424 " (%d args)\n", pat, repl, argc)); 425 } 426 427 /* 428 * If -s is specified, accept only one operand 429 */ 430 if (sflg && argc >= 2) 431 error("too many args"); 432 433 /* 434 * determine [first] and [last] 435 */ 436 switch (argc) { 437 case 0: 438 if (lflg) { 439 firststr = "-16"; 440 laststr = "-1"; 441 } else 442 firststr = laststr = "-1"; 443 break; 444 case 1: 445 firststr = argv[0]; 446 laststr = lflg ? "-1" : argv[0]; 447 break; 448 case 2: 449 firststr = argv[0]; 450 laststr = argv[1]; 451 break; 452 default: 453 error("too many args"); 454 /* NOTREACHED */ 455 } 456 /* 457 * Turn into event numbers. 458 */ 459 first = str_to_event(firststr, 0); 460 last = str_to_event(laststr, 1); 461 462 if (first == -1 || last == -1) { 463 if (lflg) /* no history exists, that's OK */ 464 return 0; 465 if (first == -1 && last == -1) { 466 if (firststr != laststr) 467 error("history events %s to %s do not exist", 468 firststr, laststr); 469 else 470 error("history event %s does not exist", 471 firststr); 472 } else { 473 error("history event %s does not exist", 474 first == -1 ? firststr : laststr); 475 } 476 } 477 478 if (rflg) { 479 i = last; 480 last = first; 481 first = i; 482 } 483 VTRACE(DBG_HISTORY, ("histcmd%s first=\"%s\" (#%d) last=\"%s\" (#%d)\n", 484 rflg ? " reversed" : "", rflg ? laststr : firststr, first, 485 rflg ? firststr : laststr, last)); 486 487 /* 488 * XXX - this should not depend on the event numbers 489 * always increasing. Add sequence numbers or offset 490 * to the history element in next (diskbased) release. 491 */ 492 direction = first < last ? H_PREV : H_NEXT; 493 494 /* 495 * If editing, grab a temp file. 496 */ 497 if (editor) { 498 int fd; 499 500 INTOFF; /* easier */ 501 snprintf(editfile, sizeof(editfile), 502 "%s_shXXXXXX", _PATH_TMP); 503 if ((fd = mkstemp(editfile)) < 0) 504 error("can't create temporary file %s", editfile); 505 if ((efp = fdopen(fd, "w")) == NULL) { 506 close(fd); 507 error("can't allocate stdio buffer for temp"); 508 } 509 VTRACE(DBG_HISTORY, ("histcmd created \"%s\" for edit buffer" 510 " fd=%d\n", editfile, fd)); 511 } 512 513 /* 514 * Loop through selected history events. If listing or executing, 515 * do it now. Otherwise, put into temp file and call the editor 516 * after. 517 * 518 * The history interface needs rethinking, as the following 519 * convolutions will demonstrate. 520 */ 521 history(hist, &he, H_FIRST); 522 retval = history(hist, &he, H_NEXT_EVENT, first); 523 for ( ; retval != -1; retval = history(hist, &he, direction)) { 524 if (lflg) { 525 if (!nflg) 526 out1fmt("%5d ", he.num); 527 out1str(he.str); 528 } else { 529 const char *s = pat ? 530 fc_replace(he.str, pat, repl) : he.str; 531 532 if (sflg) { 533 VTRACE(DBG_HISTORY, ("histcmd -s \"%s\"\n", s)); 534 if (displayhist) { 535 out2str(s); 536 } 537 538 evalstring(strcpy(stalloc(strlen(s)+1), s), 0); 539 if (displayhist && hist) { 540 /* 541 * XXX what about recursive and 542 * relative histnums. 543 */ 544 history(hist, &he, H_ENTER, s); 545 } 546 547 break; 548 } else 549 fputs(s, efp); 550 } 551 /* 552 * At end? (if we were to lose last, we'd sure be 553 * messed up). 554 */ 555 if (he.num == last) 556 break; 557 } 558 if (editor) { 559 char *editcmd; 560 size_t cmdlen; 561 562 fclose(efp); 563 cmdlen = strlen(editor) + strlen(editfile) + 2; 564 editcmd = stalloc(cmdlen); 565 snprintf(editcmd, cmdlen, "%s %s", editor, editfile); 566 VTRACE(DBG_HISTORY, ("histcmd editing: \"%s\"\n", editcmd)); 567 evalstring(editcmd, 0); /* XXX - should use no JC command */ 568 stunalloc(editcmd); 569 VTRACE(DBG_HISTORY, ("histcmd read cmds from %s\n", editfile)); 570 readcmdfile(editfile); /* XXX - should read back - quick tst */ 571 VTRACE(DBG_HISTORY, ("histcmd unlink %s\n", editfile)); 572 unlink(editfile); 573 editfile[0] = '\0'; 574 INTON; 575 } 576 577 if (lflg == 0 && active > 0) 578 --active; 579 if (displayhist) 580 displayhist = 0; 581 return 0; 582 } 583 584 static const char * 585 fc_replace(const char *s, char *p, char *r) 586 { 587 char *dest; 588 int plen = strlen(p); 589 590 VTRACE(DBG_HISTORY, ("histcmd s/%s/%s/ in \"%s\" -> ", p, r, s)); 591 STARTSTACKSTR(dest); 592 while (*s) { 593 if (*s == *p && strncmp(s, p, plen) == 0) { 594 while (*r) 595 STPUTC(*r++, dest); 596 s += plen; 597 *p = '\0'; /* so no more matches */ 598 } else 599 STPUTC(*s++, dest); 600 } 601 STACKSTRNUL(dest); 602 dest = grabstackstr(dest); 603 VTRACE(DBG_HISTORY, ("\"%s\"\n", dest)); 604 605 return dest; 606 } 607 608 609 /* 610 * Comparator function for qsort(). The use of curpos here is to skip 611 * characters that we already know to compare equal (common prefix). 612 */ 613 static int 614 comparator(const void *a, const void *b) 615 { 616 return strcmp(*(char *const *)a + curpos, 617 *(char *const *)b + curpos); 618 } 619 620 /* 621 * This function is passed to libedit's fn_complete(). The library will 622 * use it instead of its standard function to find matches, which 623 * searches for files in current directory. If we're at the start of the 624 * line, we want to look for available commands from all paths in $PATH. 625 */ 626 static char ** 627 sh_matches(const char *text, int start, int end) 628 { 629 char *free_path = NULL, *dirname, *path; 630 char **matches = NULL; 631 size_t i = 0, size = 16; 632 633 if (start > 0) 634 return NULL; 635 curpos = end - start; 636 if ((free_path = path = strdup(pathval())) == NULL) 637 goto out; 638 if ((matches = malloc(size * sizeof(matches[0]))) == NULL) 639 goto out; 640 while ((dirname = strsep(&path, ":")) != NULL) { 641 struct dirent *entry; 642 DIR *dir; 643 int dfd; 644 645 if ((dir = opendir(dirname)) == NULL) 646 continue; 647 if ((dfd = dirfd(dir)) == -1) 648 continue; 649 while ((entry = readdir(dir)) != NULL) { 650 struct stat statb; 651 652 if (strncmp(entry->d_name, text, curpos) != 0) 653 continue; 654 if (entry->d_type == DT_UNKNOWN || 655 entry->d_type == DT_LNK) { 656 if (fstatat(dfd, entry->d_name, &statb, 0) 657 == -1) 658 continue; 659 if (!S_ISREG(statb.st_mode)) 660 continue; 661 } else if (entry->d_type != DT_REG) 662 continue; 663 if (++i >= size - 1) { 664 size *= 2; 665 if (reallocarr(&matches, size, 666 sizeof(*matches))) 667 { 668 closedir(dir); 669 goto out; 670 } 671 } 672 matches[i] = strdup(entry->d_name); 673 } 674 closedir(dir); 675 } 676 out:; 677 free(free_path); 678 if (i == 0) { 679 free(matches); 680 return NULL; 681 } 682 if (i == 1) { 683 matches[0] = strdup(matches[1]); 684 matches[i + 1] = NULL; 685 } else { 686 size_t j, k; 687 688 qsort(matches + 1, i, sizeof(matches[0]), comparator); 689 for (j = 1, k = 2; k <= i; k++) 690 if (strcmp(matches[j] + curpos, matches[k] + curpos) 691 == 0) 692 free(matches[k]); 693 else 694 matches[++j] = matches[k]; 695 matches[0] = strdup(text); 696 matches[j + 1] = NULL; 697 } 698 return matches; 699 } 700 701 /* 702 * This is passed to el_set(el, EL_ADDFN, ...) so that it's possible to 703 * bind a key (tab by default) to execute the function. 704 */ 705 unsigned char 706 sh_complete(EditLine *sel, int ch __unused) 707 { 708 return (unsigned char)fn_complete2(sel, NULL, sh_matches, 709 L" \t\n\"\\'`@$><=;|&{(", NULL, NULL, (size_t)100, 710 NULL, &((int) {0}), NULL, NULL, FN_QUOTE_MATCH); 711 } 712 713 static int 714 not_fcnumber(const char *s) 715 { 716 if (s == NULL) 717 return 0; 718 if (*s == '-') 719 s++; 720 return !is_number(s); 721 } 722 723 static int 724 str_to_event(const char *str, int last) 725 { 726 HistEvent he; 727 const char *s = str; 728 int relative = 0; 729 int i, retval; 730 731 retval = history(hist, &he, H_FIRST); 732 switch (*s) { 733 case '-': 734 relative = 1; 735 /*FALLTHROUGH*/ 736 case '+': 737 s++; 738 } 739 if (is_number(s)) { 740 i = number(s); 741 if (relative) { 742 while (retval != -1 && i--) { 743 retval = history(hist, &he, H_NEXT); 744 } 745 if (retval == -1) 746 retval = history(hist, &he, H_LAST); 747 } else { 748 retval = history(hist, &he, H_NEXT_EVENT, i); 749 if (retval == -1) { 750 /* 751 * the notion of first and last is 752 * backwards to that of the history package 753 */ 754 retval = history(hist, &he, 755 last ? H_FIRST : H_LAST); 756 } 757 } 758 if (retval == -1) 759 return -1; 760 } else { 761 /* 762 * pattern 763 */ 764 retval = history(hist, &he, H_PREV_STR, str); 765 if (retval == -1) 766 error("history pattern not found: %s", str); 767 } 768 return he.num; 769 } 770 771 #else /* defined(SMALL) */ 772 773 int 774 histcmd(int argc, char **argv) 775 { 776 error("not compiled with history support"); 777 /* NOTREACHED */ 778 } 779 780 int 781 inputrc(int argc, char **argv) 782 { 783 error("not compiled with history support"); 784 /* NOTREACHED */ 785 } 786 787 #endif /* SMALL */ 788