1 /*- 2 * Copyright (c) 1991, 1993 3 * The Regents of the University of California. All rights reserved. 4 * 5 * This code is derived from software contributed to Berkeley by 6 * Kenneth Almquist. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 2. Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in the 15 * documentation and/or other materials provided with the distribution. 16 * 3. Neither the name of the University nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 21 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 24 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 26 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 27 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 28 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 29 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 30 * SUCH DAMAGE. 31 */ 32 33 #include <sys/types.h> 34 #include <sys/stat.h> 35 #include <unistd.h> 36 #include <fcntl.h> 37 #include <errno.h> 38 #include <paths.h> 39 #include <stdbool.h> 40 #include <stdlib.h> 41 42 /* 43 * When commands are first encountered, they are entered in a hash table. 44 * This ensures that a full path search will not have to be done for them 45 * on each invocation. 46 * 47 * We should investigate converting to a linear search, even though that 48 * would make the command name "hash" a misnomer. 49 */ 50 51 #include "shell.h" 52 #include "main.h" 53 #include "nodes.h" 54 #include "parser.h" 55 #include "redir.h" 56 #include "eval.h" 57 #include "exec.h" 58 #include "builtins.h" 59 #include "var.h" 60 #include "options.h" 61 #include "input.h" 62 #include "output.h" 63 #include "syntax.h" 64 #include "memalloc.h" 65 #include "error.h" 66 #include "mystring.h" 67 #include "show.h" 68 #include "jobs.h" 69 #include "alias.h" 70 71 72 #define CMDTABLESIZE 31 /* should be prime */ 73 74 75 76 struct tblentry { 77 struct tblentry *next; /* next entry in hash chain */ 78 union param param; /* definition of builtin function */ 79 int special; /* flag for special builtin commands */ 80 signed char cmdtype; /* index identifying command */ 81 char cmdname[]; /* name of command */ 82 }; 83 84 85 static struct tblentry *cmdtable[CMDTABLESIZE]; 86 static int cmdtable_cd = 0; /* cmdtable contains cd-dependent entries */ 87 88 89 static void tryexec(char *, char **, char **); 90 static void printentry(struct tblentry *, int); 91 static struct tblentry *cmdlookup(const char *, int); 92 static void delete_cmd_entry(void); 93 static void addcmdentry(const char *, struct cmdentry *); 94 95 96 97 /* 98 * Exec a program. Never returns. If you change this routine, you may 99 * have to change the find_command routine as well. 100 * 101 * The argv array may be changed and element argv[-1] should be writable. 102 */ 103 104 void 105 shellexec(char **argv, char **envp, const char *path, int idx) 106 { 107 char *cmdname; 108 const char *opt; 109 int e; 110 111 if (strchr(argv[0], '/') != NULL) { 112 tryexec(argv[0], argv, envp); 113 e = errno; 114 } else { 115 e = ENOENT; 116 while ((cmdname = padvance(&path, &opt, argv[0])) != NULL) { 117 if (--idx < 0 && opt == NULL) { 118 tryexec(cmdname, argv, envp); 119 if (errno != ENOENT && errno != ENOTDIR) 120 e = errno; 121 if (e == ENOEXEC) 122 break; 123 } 124 stunalloc(cmdname); 125 } 126 } 127 128 /* Map to POSIX errors */ 129 if (e == ENOENT || e == ENOTDIR) 130 errorwithstatus(127, "%s: not found", argv[0]); 131 else 132 errorwithstatus(126, "%s: %s", argv[0], strerror(e)); 133 } 134 135 136 static bool 137 isbinary(const char *data, size_t len) 138 { 139 const char *nul, *p; 140 bool hasletter; 141 142 nul = memchr(data, '\0', len); 143 if (nul == NULL) 144 return false; 145 /* 146 * POSIX says we shall allow execution if the initial part intended 147 * to be parsed by the shell consists of characters and does not 148 * contain the NUL character. This allows concatenating a shell 149 * script (ending with exec or exit) and a binary payload. 150 * 151 * In order to reject common binary files such as PNG images, check 152 * that there is a lowercase letter or expansion before the last 153 * newline before the NUL character, in addition to the check for 154 * the newline character suggested by POSIX. 155 */ 156 hasletter = false; 157 for (p = data; *p != '\0'; p++) { 158 if ((*p >= 'a' && *p <= 'z') || *p == '$' || *p == '`') 159 hasletter = true; 160 if (hasletter && *p == '\n') 161 return false; 162 } 163 return true; 164 } 165 166 167 static void 168 tryexec(char *cmd, char **argv, char **envp) 169 { 170 int e, in; 171 ssize_t n; 172 char buf[256]; 173 174 execve(cmd, argv, envp); 175 e = errno; 176 if (e == ENOEXEC) { 177 INTOFF; 178 in = open(cmd, O_RDONLY | O_NONBLOCK); 179 if (in != -1) { 180 n = pread(in, buf, sizeof buf, 0); 181 close(in); 182 if (n > 0 && isbinary(buf, n)) { 183 errno = ENOEXEC; 184 return; 185 } 186 } 187 *argv = cmd; 188 *--argv = __DECONST(char *, _PATH_BSHELL); 189 execve(_PATH_BSHELL, argv, envp); 190 } 191 errno = e; 192 } 193 194 /* 195 * Do a path search. The variable path (passed by reference) should be 196 * set to the start of the path before the first call; padvance will update 197 * this value as it proceeds. Successive calls to padvance will return 198 * the possible path expansions in sequence. If popt is not NULL, options 199 * are processed: if an option (indicated by a percent sign) appears in 200 * the path entry then *popt will be set to point to it; else *popt will be 201 * set to NULL. If popt is NULL, percent signs are not special. 202 */ 203 204 char * 205 padvance(const char **path, const char **popt, const char *name) 206 { 207 const char *p, *start; 208 char *q; 209 size_t len, namelen; 210 211 if (*path == NULL) 212 return NULL; 213 start = *path; 214 if (popt != NULL) 215 for (p = start; *p && *p != ':' && *p != '%'; p++) 216 ; /* nothing */ 217 else 218 for (p = start; *p && *p != ':'; p++) 219 ; /* nothing */ 220 namelen = strlen(name); 221 len = p - start + namelen + 2; /* "2" is for '/' and '\0' */ 222 STARTSTACKSTR(q); 223 CHECKSTRSPACE(len, q); 224 if (p != start) { 225 memcpy(q, start, p - start); 226 q += p - start; 227 *q++ = '/'; 228 } 229 memcpy(q, name, namelen + 1); 230 if (popt != NULL) { 231 if (*p == '%') { 232 *popt = ++p; 233 while (*p && *p != ':') p++; 234 } else 235 *popt = NULL; 236 } 237 if (*p == ':') 238 *path = p + 1; 239 else 240 *path = NULL; 241 return stalloc(len); 242 } 243 244 245 246 /*** Command hashing code ***/ 247 248 249 int 250 hashcmd(int argc __unused, char **argv __unused) 251 { 252 struct tblentry **pp; 253 struct tblentry *cmdp; 254 int c; 255 int verbose; 256 struct cmdentry entry; 257 char *name; 258 int errors; 259 260 errors = 0; 261 verbose = 0; 262 while ((c = nextopt("rv")) != '\0') { 263 if (c == 'r') { 264 clearcmdentry(); 265 } else if (c == 'v') { 266 verbose++; 267 } 268 } 269 if (*argptr == NULL) { 270 for (pp = cmdtable ; pp < &cmdtable[CMDTABLESIZE] ; pp++) { 271 for (cmdp = *pp ; cmdp ; cmdp = cmdp->next) { 272 if (cmdp->cmdtype == CMDNORMAL) 273 printentry(cmdp, verbose); 274 } 275 } 276 return 0; 277 } 278 while ((name = *argptr) != NULL) { 279 if ((cmdp = cmdlookup(name, 0)) != NULL 280 && cmdp->cmdtype == CMDNORMAL) 281 delete_cmd_entry(); 282 find_command(name, &entry, DO_ERR, pathval()); 283 if (entry.cmdtype == CMDUNKNOWN) 284 errors = 1; 285 else if (verbose) { 286 cmdp = cmdlookup(name, 0); 287 if (cmdp != NULL) 288 printentry(cmdp, verbose); 289 else { 290 outfmt(out2, "%s: not found\n", name); 291 errors = 1; 292 } 293 flushall(); 294 } 295 argptr++; 296 } 297 return errors; 298 } 299 300 301 static void 302 printentry(struct tblentry *cmdp, int verbose) 303 { 304 int idx; 305 const char *path, *opt; 306 char *name; 307 308 if (cmdp->cmdtype == CMDNORMAL) { 309 idx = cmdp->param.index; 310 path = pathval(); 311 do { 312 name = padvance(&path, &opt, cmdp->cmdname); 313 stunalloc(name); 314 } while (--idx >= 0); 315 out1str(name); 316 } else if (cmdp->cmdtype == CMDBUILTIN) { 317 out1fmt("builtin %s", cmdp->cmdname); 318 } else if (cmdp->cmdtype == CMDFUNCTION) { 319 out1fmt("function %s", cmdp->cmdname); 320 if (verbose) { 321 INTOFF; 322 name = commandtext(getfuncnode(cmdp->param.func)); 323 out1c(' '); 324 out1str(name); 325 ckfree(name); 326 INTON; 327 } 328 #ifdef DEBUG 329 } else { 330 error("internal error: cmdtype %d", cmdp->cmdtype); 331 #endif 332 } 333 out1c('\n'); 334 } 335 336 337 338 /* 339 * Resolve a command name. If you change this routine, you may have to 340 * change the shellexec routine as well. 341 */ 342 343 void 344 find_command(const char *name, struct cmdentry *entry, int act, 345 const char *path) 346 { 347 struct tblentry *cmdp, loc_cmd; 348 int idx; 349 const char *opt; 350 char *fullname; 351 struct stat statb; 352 int e; 353 int i; 354 int spec; 355 int cd; 356 357 /* If name contains a slash, don't use the hash table */ 358 if (strchr(name, '/') != NULL) { 359 entry->cmdtype = CMDNORMAL; 360 entry->u.index = 0; 361 entry->special = 0; 362 return; 363 } 364 365 cd = 0; 366 367 /* If name is in the table, we're done */ 368 if ((cmdp = cmdlookup(name, 0)) != NULL) { 369 if (cmdp->cmdtype == CMDFUNCTION && act & DO_NOFUNC) 370 cmdp = NULL; 371 else 372 goto success; 373 } 374 375 /* Check for builtin next */ 376 if ((i = find_builtin(name, &spec)) >= 0) { 377 INTOFF; 378 cmdp = cmdlookup(name, 1); 379 if (cmdp->cmdtype == CMDFUNCTION) 380 cmdp = &loc_cmd; 381 cmdp->cmdtype = CMDBUILTIN; 382 cmdp->param.index = i; 383 cmdp->special = spec; 384 INTON; 385 goto success; 386 } 387 388 /* We have to search path. */ 389 390 e = ENOENT; 391 idx = -1; 392 for (;(fullname = padvance(&path, &opt, name)) != NULL; 393 stunalloc(fullname)) { 394 idx++; 395 if (opt) { 396 if (strncmp(opt, "func", 4) == 0) { 397 /* handled below */ 398 } else { 399 continue; /* ignore unimplemented options */ 400 } 401 } 402 if (fullname[0] != '/') 403 cd = 1; 404 if (stat(fullname, &statb) < 0) { 405 if (errno != ENOENT && errno != ENOTDIR) 406 e = errno; 407 continue; 408 } 409 e = EACCES; /* if we fail, this will be the error */ 410 if (!S_ISREG(statb.st_mode)) 411 continue; 412 if (opt) { /* this is a %func directory */ 413 readcmdfile(fullname, -1 /* verify */); 414 if ((cmdp = cmdlookup(name, 0)) == NULL || cmdp->cmdtype != CMDFUNCTION) 415 error("%s not defined in %s", name, fullname); 416 stunalloc(fullname); 417 goto success; 418 } 419 #ifdef notdef 420 if (statb.st_uid == geteuid()) { 421 if ((statb.st_mode & 0100) == 0) 422 goto loop; 423 } else if (statb.st_gid == getegid()) { 424 if ((statb.st_mode & 010) == 0) 425 goto loop; 426 } else { 427 if ((statb.st_mode & 01) == 0) 428 goto loop; 429 } 430 #endif 431 TRACE(("searchexec \"%s\" returns \"%s\"\n", name, fullname)); 432 INTOFF; 433 stunalloc(fullname); 434 cmdp = cmdlookup(name, 1); 435 if (cmdp->cmdtype == CMDFUNCTION) 436 cmdp = &loc_cmd; 437 cmdp->cmdtype = CMDNORMAL; 438 cmdp->param.index = idx; 439 cmdp->special = 0; 440 INTON; 441 goto success; 442 } 443 444 if (act & DO_ERR) { 445 if (e == ENOENT || e == ENOTDIR) 446 outfmt(out2, "%s: not found\n", name); 447 else 448 outfmt(out2, "%s: %s\n", name, strerror(e)); 449 } 450 entry->cmdtype = CMDUNKNOWN; 451 entry->u.index = 0; 452 entry->special = 0; 453 return; 454 455 success: 456 if (cd) 457 cmdtable_cd = 1; 458 entry->cmdtype = cmdp->cmdtype; 459 entry->u = cmdp->param; 460 entry->special = cmdp->special; 461 } 462 463 464 465 /* 466 * Search the table of builtin commands. 467 */ 468 469 int 470 find_builtin(const char *name, int *special) 471 { 472 const unsigned char *bp; 473 size_t len; 474 475 len = strlen(name); 476 for (bp = builtincmd ; *bp ; bp += 2 + bp[0]) { 477 if (bp[0] == len && memcmp(bp + 2, name, len) == 0) { 478 *special = (bp[1] & BUILTIN_SPECIAL) != 0; 479 return bp[1] & ~BUILTIN_SPECIAL; 480 } 481 } 482 return -1; 483 } 484 485 486 487 /* 488 * Called when a cd is done. If any entry in cmdtable depends on the current 489 * directory, simply clear cmdtable completely. 490 */ 491 492 void 493 hashcd(void) 494 { 495 if (cmdtable_cd) 496 clearcmdentry(); 497 } 498 499 500 501 /* 502 * Called before PATH is changed. The argument is the new value of PATH; 503 * pathval() still returns the old value at this point. Called with 504 * interrupts off. 505 */ 506 507 void 508 changepath(const char *newval __unused) 509 { 510 clearcmdentry(); 511 } 512 513 514 /* 515 * Clear out cached utility locations. 516 */ 517 518 void 519 clearcmdentry(void) 520 { 521 struct tblentry **tblp; 522 struct tblentry **pp; 523 struct tblentry *cmdp; 524 525 INTOFF; 526 for (tblp = cmdtable ; tblp < &cmdtable[CMDTABLESIZE] ; tblp++) { 527 pp = tblp; 528 while ((cmdp = *pp) != NULL) { 529 if (cmdp->cmdtype == CMDNORMAL) { 530 *pp = cmdp->next; 531 ckfree(cmdp); 532 } else { 533 pp = &cmdp->next; 534 } 535 } 536 } 537 cmdtable_cd = 0; 538 INTON; 539 } 540 541 542 static unsigned int 543 hashname(const char *p) 544 { 545 unsigned int hashval; 546 547 hashval = (unsigned char)*p << 4; 548 while (*p) 549 hashval += *p++; 550 551 return (hashval % CMDTABLESIZE); 552 } 553 554 555 /* 556 * Locate a command in the command hash table. If "add" is nonzero, 557 * add the command to the table if it is not already present. The 558 * variable "lastcmdentry" is set to point to the address of the link 559 * pointing to the entry, so that delete_cmd_entry can delete the 560 * entry. 561 */ 562 563 static struct tblentry **lastcmdentry; 564 565 566 static struct tblentry * 567 cmdlookup(const char *name, int add) 568 { 569 struct tblentry *cmdp; 570 struct tblentry **pp; 571 size_t len; 572 573 pp = &cmdtable[hashname(name)]; 574 for (cmdp = *pp ; cmdp ; cmdp = cmdp->next) { 575 if (equal(cmdp->cmdname, name)) 576 break; 577 pp = &cmdp->next; 578 } 579 if (add && cmdp == NULL) { 580 INTOFF; 581 len = strlen(name); 582 cmdp = *pp = ckmalloc(sizeof (struct tblentry) + len + 1); 583 cmdp->next = NULL; 584 cmdp->cmdtype = CMDUNKNOWN; 585 memcpy(cmdp->cmdname, name, len + 1); 586 INTON; 587 } 588 lastcmdentry = pp; 589 return cmdp; 590 } 591 592 const void * 593 itercmd(const void *entry, struct cmdentry *result) 594 { 595 const struct tblentry *e = entry; 596 size_t i = 0; 597 598 if (e != NULL) { 599 if (e->next != NULL) { 600 e = e->next; 601 goto success; 602 } 603 i = hashname(e->cmdname) + 1; 604 } 605 for (; i < CMDTABLESIZE; i++) 606 if ((e = cmdtable[i]) != NULL) 607 goto success; 608 609 return (NULL); 610 success: 611 result->cmdtype = e->cmdtype; 612 result->cmdname = e->cmdname; 613 614 return (e); 615 } 616 617 /* 618 * Delete the command entry returned on the last lookup. 619 */ 620 621 static void 622 delete_cmd_entry(void) 623 { 624 struct tblentry *cmdp; 625 626 INTOFF; 627 cmdp = *lastcmdentry; 628 *lastcmdentry = cmdp->next; 629 ckfree(cmdp); 630 INTON; 631 } 632 633 634 635 /* 636 * Add a new command entry, replacing any existing command entry for 637 * the same name. 638 */ 639 640 static void 641 addcmdentry(const char *name, struct cmdentry *entry) 642 { 643 struct tblentry *cmdp; 644 645 INTOFF; 646 cmdp = cmdlookup(name, 1); 647 if (cmdp->cmdtype == CMDFUNCTION) { 648 unreffunc(cmdp->param.func); 649 } 650 cmdp->cmdtype = entry->cmdtype; 651 cmdp->param = entry->u; 652 cmdp->special = entry->special; 653 INTON; 654 } 655 656 657 /* 658 * Define a shell function. 659 */ 660 661 void 662 defun(const char *name, union node *func) 663 { 664 struct cmdentry entry; 665 666 INTOFF; 667 entry.cmdtype = CMDFUNCTION; 668 entry.u.func = copyfunc(func); 669 entry.special = 0; 670 addcmdentry(name, &entry); 671 INTON; 672 } 673 674 675 /* 676 * Delete a function if it exists. 677 * Called with interrupts off. 678 */ 679 680 int 681 unsetfunc(const char *name) 682 { 683 struct tblentry *cmdp; 684 685 if ((cmdp = cmdlookup(name, 0)) != NULL && cmdp->cmdtype == CMDFUNCTION) { 686 unreffunc(cmdp->param.func); 687 delete_cmd_entry(); 688 return (0); 689 } 690 return (0); 691 } 692 693 694 /* 695 * Check if a function by a certain name exists. 696 */ 697 int 698 isfunc(const char *name) 699 { 700 struct tblentry *cmdp; 701 cmdp = cmdlookup(name, 0); 702 return (cmdp != NULL && cmdp->cmdtype == CMDFUNCTION); 703 } 704 705 706 static void 707 print_absolute_path(const char *name) 708 { 709 const char *pwd; 710 711 if (*name != '/' && (pwd = lookupvar("PWD")) != NULL && *pwd != '\0') { 712 out1str(pwd); 713 if (strcmp(pwd, "/") != 0) 714 outcslow('/', out1); 715 } 716 out1str(name); 717 outcslow('\n', out1); 718 } 719 720 721 /* 722 * Shared code for the following builtin commands: 723 * type, command -v, command -V 724 */ 725 726 int 727 typecmd_impl(int argc, char **argv, int cmd, const char *path) 728 { 729 struct cmdentry entry; 730 struct tblentry *cmdp; 731 const char *const *pp; 732 struct alias *ap; 733 int i; 734 int error1 = 0; 735 736 if (path != pathval()) 737 clearcmdentry(); 738 739 for (i = 1; i < argc; i++) { 740 /* First look at the keywords */ 741 for (pp = parsekwd; *pp; pp++) 742 if (**pp == *argv[i] && equal(*pp, argv[i])) 743 break; 744 745 if (*pp) { 746 if (cmd == TYPECMD_SMALLV) 747 out1fmt("%s\n", argv[i]); 748 else 749 out1fmt("%s is a shell keyword\n", argv[i]); 750 continue; 751 } 752 753 /* Then look at the aliases */ 754 if ((ap = lookupalias(argv[i], 1)) != NULL) { 755 if (cmd == TYPECMD_SMALLV) { 756 out1fmt("alias %s=", argv[i]); 757 out1qstr(ap->val); 758 outcslow('\n', out1); 759 } else 760 out1fmt("%s is an alias for %s\n", argv[i], 761 ap->val); 762 continue; 763 } 764 765 /* Then check if it is a tracked alias */ 766 if ((cmdp = cmdlookup(argv[i], 0)) != NULL) { 767 entry.cmdtype = cmdp->cmdtype; 768 entry.u = cmdp->param; 769 entry.special = cmdp->special; 770 } 771 else { 772 /* Finally use brute force */ 773 find_command(argv[i], &entry, 0, path); 774 } 775 776 switch (entry.cmdtype) { 777 case CMDNORMAL: { 778 if (strchr(argv[i], '/') == NULL) { 779 const char *path2 = path; 780 const char *opt2; 781 char *name; 782 int j = entry.u.index; 783 do { 784 name = padvance(&path2, &opt2, argv[i]); 785 stunalloc(name); 786 } while (--j >= 0); 787 if (cmd != TYPECMD_SMALLV) 788 out1fmt("%s is%s ", argv[i], 789 (cmdp && cmd == TYPECMD_TYPE) ? 790 " a tracked alias for" : ""); 791 print_absolute_path(name); 792 } else { 793 if (eaccess(argv[i], X_OK) == 0) { 794 if (cmd != TYPECMD_SMALLV) 795 out1fmt("%s is ", argv[i]); 796 print_absolute_path(argv[i]); 797 } else { 798 if (cmd != TYPECMD_SMALLV) 799 outfmt(out2, "%s: %s\n", 800 argv[i], strerror(errno)); 801 error1 |= 127; 802 } 803 } 804 break; 805 } 806 case CMDFUNCTION: 807 if (cmd == TYPECMD_SMALLV) 808 out1fmt("%s\n", argv[i]); 809 else 810 out1fmt("%s is a shell function\n", argv[i]); 811 break; 812 813 case CMDBUILTIN: 814 if (cmd == TYPECMD_SMALLV) 815 out1fmt("%s\n", argv[i]); 816 else if (entry.special) 817 out1fmt("%s is a special shell builtin\n", 818 argv[i]); 819 else 820 out1fmt("%s is a shell builtin\n", argv[i]); 821 break; 822 823 default: 824 if (cmd != TYPECMD_SMALLV) 825 outfmt(out2, "%s: not found\n", argv[i]); 826 error1 |= 127; 827 break; 828 } 829 } 830 831 if (path != pathval()) 832 clearcmdentry(); 833 834 return error1; 835 } 836 837 /* 838 * Locate and print what a word is... 839 */ 840 841 int 842 typecmd(int argc, char **argv) 843 { 844 if (argc > 2 && strcmp(argv[1], "--") == 0) 845 argc--, argv++; 846 return typecmd_impl(argc, argv, TYPECMD_TYPE, bltinlookup("PATH", 1)); 847 } 848