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