NetBSD Problem Report #48918

From jarmo.jaakkola@roskakori.fi  Tue Jun 17 12:41:13 2014
Return-Path: <jarmo.jaakkola@roskakori.fi>
Received: from mail.netbsd.org (mail.netbsd.org [149.20.53.66])
	(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
	(Client CN "mail.netbsd.org", Issuer "Postmaster NetBSD.org" (verified OK))
	by mollari.NetBSD.org (Postfix) with ESMTPS id 40D94A653F
	for <gnats-bugs@gnats.NetBSD.org>; Tue, 17 Jun 2014 12:41:13 +0000 (UTC)
Message-Id: <20140617124103.08EB4530F@roskakori.fi>
Date: Tue, 17 Jun 2014 15:41:03 +0300 (EEST)
From: Jarmo Jaakkola <jarmo.jaakkola@roskakori.fi>
Reply-To: Jarmo Jaakkola <jarmo.jaakkola@roskakori.fi>
To: gnats-bugs@gnats.NetBSD.org
Subject: sh(1): entries with '%' handled incorrectly in *PATH
X-Send-Pr-Version: 3.95

>Number:         48918
>Category:       bin
>Synopsis:       Undocumented feature causes problems with '%' in *PATH variables
>Confidential:   no
>Severity:       non-critical
>Priority:       medium
>Responsible:    bin-bug-people
>State:          open
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Tue Jun 17 12:45:00 +0000 2014
>Originator:     Jarmo Jaakkola
>Release:        NetBSD 6.1.2_PATCH
>Organization:
>Environment:
System: NetBSD kotoisa.roskakori.fi 6.1.2_PATCH NetBSD 6.1.2_PATCH (KOTOISA) #5: Mon Jan 20 17:01:44 EET 2014 jammuli@kotoisa.roskakori.fi:/usr/src/sys/arch/amd64/compile/KOTOISA amd64
Architecture: x86_64
Machine: amd64
>Description:
Any entry that contains a percent sign ('%') in any of the variables
CDPATH, MAIL, MAILPATH, or PATH does not work as one would expect: any
entry that does not contain a colon (':') should be ok.  This is because of
an undocumented non-standard extension, where each entry is treated as
"path%option" instead of "path".  The "%option" part modifies the behavior
of said entry in the list.

CDPATH is actually collateral damage, because the "%option" part has
no special meaning.  It is affected because it uses the same padvance()
function that MAIL(PATH) and PATH processing does and that function
always processes the path list for options.  For a "path%something" entry,
the "%something" part is ignored and the entry is treated as if it only
contained "path", which can cause a cd command to change to an incorrect
directory.

MAIL and MAILPATH use the "%option" to specify an alternate message to
print instead of "You have new mail".  An entry of form "path%text"
is treated as a mailbox "path" that should have "text" printed when
it receives mail.

This alternate message feature is also incorrectly implemented, as
the message is not terminated at the next colon but instead runs to
the end of the variable's value.  That is, with
    MAILPATH="mail1%message:mail2"
the printed message is not "message" but "message:mail2".

For PATH the "%option" is used to modify the meaning of the entry, so
an entry of the form "path%option" gets treated as follows depending on
the option part:

    "builtin":
        Do not look for builtins before path search, but instead
        at this point in the path search.  The path part is ignored.
    "func":
        Look for a file named after the command in "path" and if found,
        process the file in the current environment and then try
        to execute a shell function with the same name.
    anything else:
        the path entry is completely ignored.

This feature is also incorrectly implemented, because the match for
"builtin" or "func" needs not to be exact, but are accepted as prefixes.
For example "%builtinfoobar" is accepted as well.

To fix this this, I suggest the following:
    1) Document this extension!
    2) Make the extension toggleable by the user (set -o ...) and disabled
        by default.  Even if a percent sign in a directory name used in
        any path is extremely rare, I'd say it should work by default.
    3) Change the option character from '%' to '?'.  This is for
        interoperability, because other shells (bash, ksh, zsh) use
        '?text' in MAILPATH instead of '%text'.  I haven't seen PATH
        extensions elsewhere, but the other cases should be consistent within
        our implementation.  It also should be safe to change the character
        because the feature has been undocumented up to now.
    4) Disable the feature for CDPATH.
    5) When the feature is enabled, offer a way to escape the option character.
    [6) In MAILPATH, stop at the first colon instead of '\0'.]
    [7) Use exact match for "builtin" and "func"]

>How-To-Repeat:
$ mkdir -p some%dir/subdir
$ cat >some%dir/my_script <<EOF
#!/bin/sh
echo foo
EOF
$ chmod a+x some%dir/my_script

$ PATH="${PATH}:some%dir"
$ my_script
my_script: not found            # Expected: "foo"

$ MAIL='some%dir/mail'
$ echo 'pretend mail' >>some%dir/mail
$ # Expected: "You have new mail" before this prompt
$ MAILPATH='some%dir/mail:other/mail'
$ echo 'pretend mail' >>some%dir/mail
$ # Expected: "You have new mail" before this prompt
$ echo 'pretend mail' >>some
dir/mail:other/mail             # Unexpected (and should be "dir/mail")

$ CDPATH='some%dir'
$ cd subdir
cd: can't cd to subdir          # Expected: succesful cd, ".../some%dir/subdir"
$ rm some; mkdir -p some/subdir2
$ cd subdir2
.../some/subdir2                # Unexpected

>Fix:
A patch that implements my suggested fixes follows.  Please note that
the files
    tests/bin/sh/paths/out/cdpath_rare.out
    tests/bin/sh/paths/out/mailpath_rare.out
contain non-printable characters and probably should be treated as binary
files.  If they don't come through cleanly, I can send the patch by some
other means.

--8<--8<--

From ce41a9b4d9d8b5df2d6ed6dd02ad6254ad7f2979 Mon Sep 17 00:00:00 2001
From: Jarmo Jaakkola <jarmo.jaakkola@roskakori.fi>
Date: Sun, 1 Jun 2014 18:47:06 +0300
Subject: [PATCH] Document and improve "?option" *PATH feature.

Make the "?option" feature toggleable as "pathoptions" shell option
with "set -o" (default off).  This only enables processing for MAIL,
MAILPATH, and PATH.  CDPATH is no longer affected just because it
happens to use the same padvance() function as the others.

Other shells also implement the MAILPATH extension, so change the option
character from '%' to '?' for interoperability.  The character is
changed for PATH too to keep the usage consistent in this shell.
Impact should be minimal, because the feature has been undocumented so
far.  Allow the path part to contain '?' by using "??" as an escape.

Document the feature in the manual page.

Smaller improvements and fixes:
  - Remove the global pathopt variable and pass it as a reference parameter
    instead.
  - Replace mystring.c:prefix() with str(n)cmp because
     1) why not use standard functions, and
     2) prefix(pfx, str) would happily read past the end of str...
  - '%text' in MAILPATH no longer reads past the colon that should
    terminate the message until the end of the variable's value.
  - Shell options now have a callback to call when the value changes.
    (TODO: replace optschanged() with callbacks)
  - Use exact matching for "builtin" and "func" in favor of prefix
    matching with PATH.
  - MAILCHECK is not implemented, say so in manual page.
---
 bin/sh/cd.c                                 |  22 +-
 bin/sh/eval.c                               |   2 +-
 bin/sh/exec.c                               | 357 +++++++++++++++++++---------
 bin/sh/exec.h                               |   9 +-
 bin/sh/jobs.c                               |   3 +-
 bin/sh/mail.c                               |  55 +++--
 bin/sh/mystring.c                           |  26 +-
 bin/sh/mystring.h                           |   9 +-
 bin/sh/options.c                            |  34 ++-
 bin/sh/options.h                            |  24 +-
 bin/sh/sh.1                                 |  72 +++++-
 bin/sh/var.c                                |   2 +-
 bin/sh/var.h                                |  11 +
 etc/mtree/NetBSD.dist.tests                 |   2 +
 tests/bin/sh/Makefile                       |   2 +-
 tests/bin/sh/paths/Makefile                 |  38 +++
 tests/bin/sh/paths/README                   |  24 ++
 tests/bin/sh/paths/cdpath.sh                |  22 ++
 tests/bin/sh/paths/mailpath.sh              |  78 ++++++
 tests/bin/sh/paths/out/cdpath_options.out   |   4 +
 tests/bin/sh/paths/out/cdpath_rare.out      |   3 +
 tests/bin/sh/paths/out/mailpath_options.err |  11 +
 tests/bin/sh/paths/out/mailpath_rare.out    |   6 +
 tests/bin/sh/paths/out/path_options.err     |   3 +
 tests/bin/sh/paths/out/path_options.out     |   8 +
 tests/bin/sh/paths/out/path_rare.out        |   2 +
 tests/bin/sh/paths/path.sh                  |  38 +++
 tests/bin/sh/paths/script1                  |   3 +
 tests/bin/sh/paths/script2                  |   8 +
 tests/bin/sh/paths/t_path.sh                |  83 +++++++
 tests/bin/sh/paths/t_pathoptions.sh         |  80 +++++++
 31 files changed, 860 insertions(+), 181 deletions(-)
 create mode 100644 tests/bin/sh/paths/Makefile
 create mode 100644 tests/bin/sh/paths/README
 create mode 100644 tests/bin/sh/paths/cdpath.sh
 create mode 100644 tests/bin/sh/paths/mailpath.sh
 create mode 100644 tests/bin/sh/paths/out/cdpath_options.out
 create mode 100644 tests/bin/sh/paths/out/cdpath_rare.out
 create mode 100644 tests/bin/sh/paths/out/mailpath_options.err
 create mode 100644 tests/bin/sh/paths/out/mailpath_rare.out
 create mode 100644 tests/bin/sh/paths/out/path_options.err
 create mode 100644 tests/bin/sh/paths/out/path_options.out
 create mode 100644 tests/bin/sh/paths/out/path_rare.out
 create mode 100644 tests/bin/sh/paths/path.sh
 create mode 100644 tests/bin/sh/paths/script1
 create mode 100644 tests/bin/sh/paths/script2
 create mode 100644 tests/bin/sh/paths/t_path.sh
 create mode 100644 tests/bin/sh/paths/t_pathoptions.sh

diff --git bin/sh/cd.c bin/sh/cd.c
index f81540d..616ae0a 100644
--- bin/sh/cd.c
+++ bin/sh/cd.c
@@ -126,21 +126,22 @@ cdcmd(int argc, char **argv)
 	    p++;
 	if (*p == 0 || *p == '/' || (path = bltinlookup("CDPATH", 1)) == NULL)
 		path = nullstr;
-	while ((p = padvance(&path, dest)) != NULL) {
-		if (stat(p, &statb) >= 0 && S_ISDIR(statb.st_mode)) {
+	while ((d = padvance(&path, dest, NULL)) != NULL) {
+		if (stat(d, &statb) >= 0 && S_ISDIR(statb.st_mode)) {
 			if (!print) {
 				/*
 				 * XXX - rethink
 				 */
-				if (p[0] == '.' && p[1] == '/' && p[2] != '\0')
-					print = strcmp(p + 2, dest);
+				if (d[0] == '.' && d[1] == '/' && d[2] != '\0')
+					print = strcmp(d + 2, dest);
 				else
-					print = strcmp(p, dest);
+					print = strcmp(d, dest);
 			}
-			if (docd(p, print) >= 0)
+			if (docd(d, print) >= 0)
+				/* XXX: stunalloc(d)? */
 				return 0;
-
-		}
+		} else
+			stunalloc(d);
 	}
 	error("can't cd to %s", dest);
 	/* NOTREACHED */
@@ -274,12 +275,14 @@ updatepwd(const char *dir)
 	}
 	cdcomppath = stalloc(strlen(dir) + 1);
 	scopy(dir, cdcomppath);
+
+	/* No trailing slashes.  Skip "" and ".", resolve "..". */
 	STARTSTACKSTR(new);
 	if (*dir != '/') {
 		p = curdir;
 		while (*p)
 			STPUTC(*p++, new);
-		if (p[-1] == '/')
+		while (p[-1] == '/')
 			STUNPUTC(new);
 	}
 	while ((p = getcomponent()) != NULL) {
@@ -294,6 +297,7 @@ updatepwd(const char *dir)
 	if (new == stackblock())
 		STPUTC('/', new);
 	STACKSTRNUL(new);
+
 	INTOFF;
 	if (prevdir)
 		ckfree(prevdir);
diff --git bin/sh/eval.c bin/sh/eval.c
index 9541a0c..7ffb18b 100644
--- bin/sh/eval.c
+++ bin/sh/eval.c
@@ -1227,7 +1227,7 @@ find_dot_file(char *basename)
 	if (strchr(basename, '/'))
 		return basename;

-	while ((fullname = padvance(&path, basename)) != NULL) {
+	while ((fullname = padvance(&path, basename, NULL)) != NULL) {
 		if ((stat(fullname, &statb) == 0) && S_ISREG(statb.st_mode)) {
 			/*
 			 * Don't bother freeing here, since it will
diff --git bin/sh/exec.c bin/sh/exec.c
index 3c04e44..d32c5c9 100644
--- bin/sh/exec.c
+++ bin/sh/exec.c
@@ -84,6 +84,15 @@ __RCSID("$NetBSD: exec.c,v 1.42 2008/10/16 15:31:05 dholland Exp $");
 #define CMDTABLESIZE 31		/* should be prime */
 #define ARB 1			/* actual size determined at run time */

+#define DIR_SEP '/'
+#define OPT_SEP '?'
+#define PATH_SEP ':'
+
+STATIC const char *OPT_BUILTIN = "?builtin";
+STATIC const size_t OPT_BUILTIN_LEN =
+	sizeof OPT_BUILTIN / sizeof *OPT_BUILTIN - 1;
+STATIC const char *OPT_FUNC = "?func";
+STATIC const size_t OPT_FUNC_LEN = sizeof OPT_FUNC / sizeof *OPT_FUNC - 1;


 struct tblentry {
@@ -96,7 +105,7 @@ struct tblentry {


 STATIC struct tblentry *cmdtable[CMDTABLESIZE];
-STATIC int builtinloc = -1;		/* index in path of %builtin, or -1 */
+STATIC int builtinloc = -1;		/* index in path of ?builtin, or -1 */
 int exerrno = 0;			/* Last exec error */


@@ -124,13 +133,16 @@ shellexec(char **argv, char **envp, const char *path, int idx, int vforked)
 	char *cmdname;
 	int e;

-	if (strchr(argv[0], '/') != NULL) {
+	if (strchr(argv[0], DIR_SEP) != NULL) {
 		tryexec(argv[0], argv, envp, vforked);
 		e = errno;
 	} else {
+		char *opt, **opt_p;
+		opt = NULL;
+		opt_p = (pathoptions ? &opt : NULL);
 		e = ENOENT;
-		while ((cmdname = padvance(&path, argv[0])) != NULL) {
-			if (--idx < 0 && pathopt == NULL) {
+		while ((cmdname = padvance(&path, argv[0], opt_p)) != NULL) {
+			if (--idx < 0 && opt == NULL) {
 				tryexec(cmdname, argv, envp, vforked);
 				if (errno != ENOENT && errno != ENOTDIR)
 					e = errno;
@@ -256,7 +268,7 @@ bad:		  error("Bad #! line");
 			if (equal(p, "sh") || equal(p, "ash")) {
 				return;
 			}
-			while (*p != '/') {
+			while (*p != DIR_SEP) {
 				if (*p == '\0')
 					goto break2;
 				p++;
@@ -281,51 +293,129 @@ break2:;
 #endif


-
 /*
- * Do a path search.  The variable path (passed by reference) should be
- * set to the start of the path before the first call; padvance will update
- * this value as it proceeds.  Successive calls to padvance will return
- * the possible path expansions in sequence.  If an option (indicated by
- * a percent sign) appears in the path entry then the global variable
- * pathopt will be set to point to it; otherwise pathopt will be set to
- * NULL.
+ * Get the next path expansion for name from the colon separated pathlist.
+ *
+ * The function takes the next path from the path list and returns
+ * "path/name" in a stalloc()'ed string.  The slash is omitted if either
+ * the path or the name is "" or the path already ends with one.  Multiple
+ * trailing slashes in the path are collapsed to one.  If either of pathlist
+ * or name is NULL, there are no expansions and NULL is returned.
+ *
+ * The format of the path list is the same as the $PATH environment variable:
+ *   item[:item ...]
+ * Any item may have zero length.
+ *
+ * The pathlist reference argument should point to the start of the next
+ * item to process in the path list.  After the function returns, pathlist
+ * points to the start of the next item in the list, or is NULL if
+ * the returned expansion was the last one.  This allows the last item of
+ * the list to have zero length ("" or "...:").
+ *
+ * If option != NULL, the path item may contain an optional option suffix
+ * that is separated from the actual list item with a question mark:
+ *   item?option
+ * To include a literal question mark in the item, use '??'.  If an option
+ * suffix is found (even ""), the longest possible one is copied
+ * to stalloc()'ed string whose address is stored into *option.  Otherwise
+ * *option is set to NULL.  The option is allocated after the return value
+ * and they are quaranteed to be in the same block to allow them to be
+ * stunalloced()'ed, if needed.
+ *
+ * Example:
+ *   char *path=pathval(), *exp, *opt=NULL;
+ *   while ((exp = padvance(&path, "foo", &opt)) != NULL) {
+ *     ...
+ *     if (opt != NULL) stunalloc(opt);
+ *     stunalloc(exp);
+ *   }
  */

-const char *pathopt;
-
 char *
-padvance(const char **path, const char *name)
+padvance(const char **pathlist, const char *name, char **option)
 {
-	const char *p;
-	char *q;
-	const char *start;
-	int len;
-
-	if (*path == NULL)
+	static const char seps[] = { OPT_SEP, PATH_SEP, '\0' };
+	/*
+	 * path: [*pathlist, path_end), option: [path_end + 1, opt_end)
+	 * When there's no option, path_end == opt_end.
+	 */
+	const char *opt_end, *path_end;
+	char *result;
+	int name_len, opt_len, path_len, result_size; /* XXX: use size_t */
+	int escape_found;	/* There was an escaped OPT_SEP in the path */
+
+	if (*pathlist == NULL || name == NULL)
 		return NULL;
-	start = *path;
-	for (p = start ; *p && *p != ':' && *p != '%' ; p++);
-	len = p - start + strlen(name) + 2;	/* "2" is for '/' and '\0' */
-	while (stackblocksize() < len)
-		growstackblock();
-	q = stackblock();
-	if (p != start) {
-		memcpy(q, start, p - start);
-		q += p - start;
-		*q++ = '/';
+
+	name_len = strlen(name);
+
+	/* Find the current path.  */
+	escape_found = 0;
+	path_end = *pathlist;
+find_end:
+	path_end = strpbrknul(path_end, (option == NULL ? seps + 1 : seps));
+	if (*path_end == OPT_SEP && *(path_end + 1) == OPT_SEP) {
+		escape_found = 1;
+		path_end += 2;
+		goto find_end;
 	}
-	strcpy(q, name);
-	pathopt = NULL;
-	if (*p == '%') {
-		pathopt = ++p;
-		while (*p && *p != ':')  p++;
+	path_len = path_end - *pathlist;
+
+	/* Find the option. */
+	opt_end = path_end;
+	opt_len = 0;
+	if (*opt_end == OPT_SEP) {
+		opt_end = strpbrknul(opt_end, seps + 1);
+		opt_len = opt_end - (path_end + 1);
 	}
-	if (*p == ':')
-		*path = p + 1;
-	else
-		*path = NULL;
-	return stalloc(len);
+
+	/*
+	 * Ensure that result and option will be in the same block:
+	 *   "path/name\0option\0".
+	 */
+	result_size = path_len + 1 + name_len + 1 + opt_len + 1;
+	while (stackblocksize() < result_size)
+		growstackblock();
+
+	/* Get result. */
+	result = stpncpy(stackblock(), *pathlist, path_len);
+	while (result != stackblock() && *(result - 1) == DIR_SEP)
+		--result;	/* Path of only DIR_SEPs is fixed below. */
+	if ((result != stackblock() && name_len > 0)
+	    || (result == stackblock() && *(path_end - 1) == DIR_SEP))
+		*result++ = DIR_SEP;
+	result = stpncpy(result, name, name_len + 1);
+	/* NUL was copied from name.  Add one to include it. */
+	result = stalloc((result + 1) - stackblock());
+
+	/*
+	 * Replace escaped OPT_SEPs with OPT_SEPs.  The first unescaped one
+	 * terminates the path, so there are only escaped ones.
+	 */
+	if (escape_found) {
+		char *p, *q;
+		p = q = result;
+		while ((*p++ = *q) != '\0') {
+			if (*q == OPT_SEP)
+				q += 2;
+			else
+				q += 1;
+		}
+	}
+
+	/* Get option. */
+	if (*path_end == OPT_SEP) {
+		char *p;
+		p = stpncpy(stackblock(), path_end + 1, opt_len);
+		*p++ = '\0';
+		*option = stalloc(p - stackblock());
+	} else if (option != NULL)
+		*option = NULL;
+
+	/* Advance to next item or terminate iteration. */
+	*pathlist = (*opt_end == PATH_SEP ? opt_end + 1 : NULL);
+
+	return result;
 }


@@ -392,7 +482,7 @@ printentry(struct tblentry *cmdp, int verbose)
 		idx = cmdp->param.index;
 		path = pathval();
 		do {
-			name = padvance(&path, cmdp->cmdname);
+			name = padvance(&path, cmdp->cmdname, NULL);
 			stunalloc(name);
 		} while (--idx >= 0);
 		out1str(name);
@@ -436,13 +526,13 @@ find_command(char *name, struct cmdentry *entry, int act, const char *path)
 	struct tblentry *cmdp, loc_cmd;
 	int idx;
 	int prev;
-	char *fullname;
+	char *fullname, *opt, **opt_p;
 	struct stat statb;
 	int e;
 	int (*bltin)(int,char **);

 	/* If name contains a slash, don't use PATH or hash table */
-	if (strchr(name, '/') != NULL) {
+	if (strchr(name, DIR_SEP) != NULL) {
 		if (act & DO_ABS) {
 			while (stat(name, &statb) < 0) {
 #ifdef SYSV
@@ -464,49 +554,70 @@ find_command(char *name, struct cmdentry *entry, int act, const char *path)
 		return;
 	}

+	/*
+	 * Check if the command is already hashed.  There are some special
+	 * cases when an entry in the hash table cannot be used.
+	 *
+	 * If an alternative $PATH, i.e. "PATH=... command", is being used,
+	 * hashed results could be wrong for the substitute path.  CMDNORMALs
+	 * are always out and CMDBUILTINs are out when the alternative path
+	 * contains '?builtin'.
+	 *
+	 * Functions should be ignored when DO_NOFUNC flag is set (executing
+	 * commands using the "command" utility).
+	 *
+	 * First check for an alternative path and if it contains '?builtin'.
+	 */
 	if (path != pathval())
 		act |= DO_ALTPATH;
-
-	if (act & DO_ALTPATH && strstr(path, "%builtin") != NULL)
-		act |= DO_ALTBLTIN;
+	if ((act & DO_ALTPATH) && pathoptions) {
+		const char *p = path;
+		opt = NULL;
+		while ((fullname = padvance(&p, "", &opt)) != NULL) {
+			if (opt != NULL) {
+				if (strcmp(opt, OPT_BUILTIN) == 0) {
+					act |= DO_ALTBLTIN;
+					p = NULL;
+				}
+				stunalloc(opt);
+			}
+			stunalloc(fullname);
+		}
+	}

 	/* If name is in the table, check answer will be ok */
 	if ((cmdp = cmdlookup(name, 0)) != NULL) {
-		do {
-			switch (cmdp->cmdtype) {
-			case CMDNORMAL:
-				if (act & DO_ALTPATH) {
-					cmdp = NULL;
-					continue;
-				}
-				break;
-			case CMDFUNCTION:
-				if (act & DO_NOFUNC) {
-					cmdp = NULL;
-					continue;
-				}
-				break;
-			case CMDBUILTIN:
-				if ((act & DO_ALTBLTIN) || builtinloc >= 0) {
-					cmdp = NULL;
-					continue;
-				}
-				break;
-			}
-			/* if not invalidated by cd, we're done */
-			if (cmdp->rehash == 0)
-				goto success;
-		} while (0);
+		switch (cmdp->cmdtype) {
+		case CMDNORMAL:
+			if (act & DO_ALTPATH)
+				cmdp = NULL;
+			break;
+		case CMDFUNCTION:
+			if (act & DO_NOFUNC)
+				cmdp = NULL;
+			break;
+		case CMDBUILTIN:
+			/*
+			 * XXX: why check builtinloc?  Don't they get removed
+			 * when $PATH contains '?builtin'?
+			 */
+			if ((act & DO_ALTBLTIN) || builtinloc >= 0)
+				cmdp = NULL;
+			break;
+		}
+		/* if not invalidated by cd, we're done */
+		if (cmdp != NULL && cmdp->rehash == 0)
+			goto success;
 	}

-	/* If %builtin not in path, check for builtin next */
-	if ((act & DO_ALTPATH ? !(act & DO_ALTBLTIN) : builtinloc < 0) &&
-	    (bltin = find_builtin(name)) != 0)
+	/* If '?builtin' is not in path, check for builtin next */
+	if ((act & DO_ALTPATH ? !(act & DO_ALTBLTIN) : builtinloc < 0)
+	    && (bltin = find_builtin(name)) != 0)
 		goto builtin_success;

 	/* We have to search path. */
 	prev = -1;		/* where to start */
-	if (cmdp) {		/* doing a rehash */
+	if (cmdp != NULL) {	/* doing a rehash */
 		if (cmdp->cmdtype == CMDBUILTIN)
 			prev = builtinloc;
 		else
@@ -515,24 +626,39 @@ find_command(char *name, struct cmdentry *entry, int act, const char *path)

 	e = ENOENT;
 	idx = -1;
+	opt = NULL;
+	opt_p = (pathoptions ? &opt : NULL);
 loop:
-	while ((fullname = padvance(&path, name)) != NULL) {
+	while ((fullname = padvance(&path, name, opt_p)) != NULL) {
+		/*
+		 * XXX: So we just stunalloc() and trust that nothing uses
+		 * the stack while we use its previous contents?  Sounds great.
+		 */
+		if (opt != NULL)
+			stunalloc(opt);
 		stunalloc(fullname);
 		idx++;
-		if (pathopt) {
-			if (prefix("builtin", pathopt)) {
+		if (opt != NULL) {
+			if (strcmp(opt, OPT_BUILTIN + 1) == 0) {
 				if ((bltin = find_builtin(name)) == 0)
 					goto loop;
 				goto builtin_success;
-			} else if (prefix("func", pathopt)) {
+			} else if (strcmp(opt, OPT_FUNC + 1) == 0) {
 				/* handled below */
 			} else {
-				/* ignore unimplemented options */
+				/*
+				 * Ignore unimplemented options. This is very
+				 * noisy on purpose: presumably the user forgot
+				 * to escape the OPT_SEP.
+				 */
+				outfmt(out2,
+				    "Unrecognized path option \"%c%s\"\n",
+				    OPT_SEP, opt);
 				goto loop;
 			}
 		}
 		/* if rehash, don't redo absolute path names */
-		if (fullname[0] == '/' && idx <= prev) {
+		if (fullname[0] == DIR_SEP && idx <= prev) {
 			if (idx < prev)
 				goto loop;
 			TRACE(("searchexec \"%s\": no change\n", name));
@@ -550,7 +676,7 @@ loop:
 		e = EACCES;	/* if we fail, this will be the error */
 		if (!S_ISREG(statb.st_mode))
 			goto loop;
-		if (pathopt) {		/* this is a %func directory */
+		if (opt != NULL) {	/* this is a ?func directory */
 			if (act & DO_NOFUNC)
 				goto loop;
 			stalloc(strlen(fullname) + 1);
@@ -692,43 +818,48 @@ hashcd(void)
 }


+void
+changepath_variable(const char* new_val)
+{
+	changepath_options(new_val, pathoptions);
+}

 /*
- * Fix command hash table when PATH changed.
- * Called before PATH is changed.  The argument is the new value of PATH;
- * pathval() still returns the old value at this point.
- * Called with interrupts off.
+ * Fix command hash table when PATH or pathoptions is changed.
+ * Called before the actual change.  The arguments are the new values and
+ * new_val must not be NULL.  The old values are still returned by
+ * pathoptions and pathval() at this point.  Called with interrupts off.
  */

 void
-changepath(const char *newval)
+changepath_options(const char *new_val, int new_pathoptions)
 {
-	const char *old, *new;
-	int idx;
-	int firstchange;
-	int bltin;
+	const char *old, *new, *n;
+	char *o, *o_opt, **o_opt_p, *n_opt, **n_opt_p;
+	int bltin, firstchange, idx;

 	old = pathval();
-	new = newval;
-	firstchange = 9999;	/* assume no change */
-	idx = 0;
+	new = new_val;
+	o_opt = n_opt = NULL;
+	o_opt_p = (pathoptions ? &o_opt : NULL);
+	n_opt_p = (new_pathoptions ? &n_opt :NULL);
+
 	bltin = -1;
-	for (;;) {
-		if (*old != *new) {
+	firstchange = INT_MAX;	/* assume no change */
+	idx = 0;
+	while (new != NULL) {
+		o = padvance(&old, "", o_opt_p);
+		n = padvance(&new, "", n_opt_p);
+
+		if (firstchange == INT_MAX
+		    && !(equal_or_nulls(o, n) && equal_or_nulls(o_opt, n_opt)))
 			firstchange = idx;
-			if ((*old == '\0' && *new == ':')
-			 || (*old == ':' && *new == '\0'))
-				firstchange++;
-			old = new;	/* ignore subsequent differences */
-		}
-		if (*new == '\0')
-			break;
-		if (*new == '%' && bltin < 0 && prefix("builtin", new + 1))
+
+		if (n_opt != NULL && bltin < 0
+		    && strcmp(n_opt, OPT_BUILTIN + 1) == 0)
 			bltin = idx;
-		if (*new == ':') {
-			idx++;
-		}
-		new++, old++;
+
+		++idx;
 	}
 	if (builtinloc < 0 && bltin >= 0)
 		builtinloc = bltin;		/* zap builtins */
@@ -1014,12 +1145,12 @@ typecmd(int argc, char **argv)

 		switch (entry.cmdtype) {
 		case CMDNORMAL: {
-			if (strchr(arg, '/') == NULL) {
+			if (strchr(arg, DIR_SEP) == NULL) {
 				const char *path = pathval();
 				char *name;
 				int j = entry.u.index;
 				do {
-					name = padvance(&path, arg);
+					name = padvance(&path, arg, NULL);
 					stunalloc(name);
 				} while (--j >= 0);
 				if (!v_flag)
@@ -1039,7 +1170,7 @@ typecmd(int argc, char **argv)
 						err = 126;
 				}
 			}
- 			break;
+			break;
 		}
 		case CMDFUNCTION:
 			if (!v_flag)
diff --git bin/sh/exec.h bin/sh/exec.h
index 77af8b8..a045cbf 100644
--- bin/sh/exec.h
+++ bin/sh/exec.h
@@ -57,18 +57,17 @@ struct cmdentry {
 #define DO_ABS		0x02	/* checks absolute paths */
 #define DO_NOFUNC	0x04	/* don't return shell functions, for command */
 #define DO_ALTPATH	0x08	/* using alternate path */
-#define DO_ALTBLTIN	0x20	/* %builtin in alt. path */
-
-extern const char *pathopt;	/* set by padvance */
+#define DO_ALTBLTIN	0x20	/* ?builtin in alt. path */

 void shellexec(char **, char **, const char *, int, int)
     __attribute__((__noreturn__));
-char *padvance(const char **, const char *);
+char *padvance(const char **, const char *, char **);
 void find_command(char *, struct cmdentry *, int, const char *);
 int (*find_builtin(char *))(int, char **);
 int (*find_splbltin(char *))(int, char **);
 void hashcd(void);
-void changepath(const char *);
+void changepath_variable(const char *);
+void changepath_options(const char *, int);
 void deletefuncs(void);
 void getcmdentry(char *, struct cmdentry *);
 void addcmdentry(char *, struct cmdentry *);
diff --git bin/sh/jobs.c bin/sh/jobs.c
index 0b5c999..8ef1c21 100644
--- bin/sh/jobs.c
+++ bin/sh/jobs.c
@@ -730,7 +730,8 @@ getjob(const char *name, int noerror)
 					continue;
 				if ((name[1] == '?'
 					&& strstr(jp->ps[0].cmd, name + 2))
-				    || prefix(name + 1, jp->ps[0].cmd)) {
+				    || strncmp(jp->ps[0].cmd, name + 1,
+						strlen(name + 1)) == 0) {
 					if (found) {
 						err_msg = "%s: ambiguous";
 						found = 0;
diff --git bin/sh/mail.c bin/sh/mail.c
index 4ddd7c0..e039957 100644
--- bin/sh/mail.c
+++ bin/sh/mail.c
@@ -47,6 +47,7 @@ __RCSID("$NetBSD: mail.c,v 1.16 2003/08/07 09:05:33 agc Exp $");
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <stdlib.h>
+#include <string.h>

 #include "shell.h"
 #include "exec.h"	/* defines padvance() */
@@ -55,13 +56,14 @@ __RCSID("$NetBSD: mail.c,v 1.16 2003/08/07 09:05:33 agc Exp $");
 #include "memalloc.h"
 #include "error.h"
 #include "mail.h"
+#include "options.h"


 #define MAXMBOXES 10


 STATIC int nmboxes;			/* number of mailboxes */
-STATIC time_t mailtime[MAXMBOXES];	/* times of mailboxes */
+STATIC off_t mbox_sizes[MAXMBOXES];	/* sizes of mailboxes */



@@ -69,6 +71,16 @@ STATIC time_t mailtime[MAXMBOXES];	/* times of mailboxes */
  * Print appropriate message(s) if mail has arrived.  If the argument is
  * nozero, then the value of MAIL has changed, so we just update the
  * values.
+ * XXX: support $MAILCHECK for setting mail check interval (documentation
+ *      has been promising this, but there's no support in the code).
+ * XXX: Maildir mailboxes.
+ * XXX: Replace $_ with mailbox name (like bash, ksh and zsh) in message.
+ * XXX: Handle $MAIL separately, ':' should be allowed there.
+ * XXX: Recursive mail paths: if path is to a directory, check all files and
+ *      subdirectories for mail.
+ * XXX: Remove arbitrary maximum mailbox count.
+ * XXX: Store the name of the mailbox with its size: checks would not need
+ *      to be reset when $MAILPATH changes.
  */

 void
@@ -77,43 +89,36 @@ chkmail(int silent)
 	int i;
 	const char *mpath;
 	char *p;
-	char *q;
+	char *opt, **opt_adr;
 	struct stackmark smark;
 	struct stat statb;

+	opt = NULL;
+	opt_adr = (pathoptions ? &opt : NULL);
+
 	if (silent)
 		nmboxes = 10;
 	if (nmboxes == 0)
 		return;
 	setstackmark(&smark);
 	mpath = mpathset() ? mpathval() : mailval();
-	for (i = 0 ; i < nmboxes ; i++) {
-		p = padvance(&mpath, nullstr);
-		if (p == NULL)
-			break;
-		if (*p == '\0')
+	for (i = 0 ;
+	    i < nmboxes
+		&& (p = padvance(&mpath, nullstr, opt_adr)) != NULL;
+	    i++) {
+		if (strlen(p) == 0)
 			continue;
-		for (q = p ; *q ; q++);
-		if (q[-1] != '/')
-			abort();
-		q[-1] = '\0';			/* delete trailing '/' */
-#ifdef notdef /* this is what the System V shell claims to do (it lies) */
-		if (stat(p, &statb) < 0)
-			statb.st_mtime = 0;
-		if (statb.st_mtime > mailtime[i] && ! silent) {
-			out2str(pathopt ? pathopt : "you have mail");
-			out2c('\n');
-		}
-		mailtime[i] = statb.st_mtime;
-#else /* this is what it should do */
+
 		if (stat(p, &statb) < 0)
 			statb.st_size = 0;
-		if (statb.st_size > mailtime[i] && ! silent) {
-			out2str(pathopt ? pathopt : "you have mail");
-			out2c('\n');
+		/* Increase: new mail. Decrease: mail read or deleted. */
+		if (mbox_sizes[i] < statb.st_size && !silent) {
+			if (opt != NULL)
+				outfmt(out2, "%s\n", opt);
+			else
+				outfmt(out2, "You have new mail in %s\n", p);
 		}
-		mailtime[i] = statb.st_size;
-#endif
+		mbox_sizes[i] = statb.st_size;
 	}
 	nmboxes = i;
 	popstackmark(&smark);
diff --git bin/sh/mystring.c bin/sh/mystring.c
index aecf83e..566ddd3 100644
--- bin/sh/mystring.c
+++ bin/sh/mystring.c
@@ -88,21 +88,6 @@ scopyn(const char *from, char *to, int size)


 /*
- * prefix -- see if pfx is a prefix of string.
- */
-
-int
-prefix(const char *pfx, const char *string)
-{
-	while (*pfx) {
-		if (*pfx++ != *string++)
-			return 0;
-	}
-	return 1;
-}
-
-
-/*
  * Convert a string of digits to an integer, printing an error message on
  * failure.
  */
@@ -110,7 +95,6 @@ prefix(const char *pfx, const char *string)
 int
 number(const char *s)
 {
-
 	if (! is_number(s))
 		error("Illegal number: %s", s);
 	return atoi(s);
@@ -131,3 +115,13 @@ is_number(const char *p)
 	} while (*++p != '\0');
 	return 1;
 }
+
+const char*
+strpbrknul(const char* s, const char* accept)
+{
+	for ( ; *s != '\0'; ++s) {
+		if (strchr(accept, *s) != NULL)
+			break;
+	}
+	return s;
+}
diff --git bin/sh/mystring.h bin/sh/mystring.h
index 08a73e9..df7256f 100644
--- bin/sh/mystring.h
+++ bin/sh/mystring.h
@@ -37,9 +37,16 @@
 #include <string.h>

 void scopyn(const char *, char *, int);
-int prefix(const char *, const char *);
 int number(const char *);
 int is_number(const char *);

+/*
+ * Like strpbrk(), but return a pointer to the terminating '\0' instead of
+ * NULL when no match is found.
+ */
+const char* strpbrknul(const char*, const char*);
+
 #define equal(s1, s2)	(strcmp(s1, s2) == 0)
+#define equal_or_nulls(s1, s2) ((s1 == NULL && s2 == NULL) \
+	    || (s1 != NULL && s2 != NULL && strcmp(s1, s2) == 0))
 #define scopy(s1, s2)	((void)strcpy(s2, s1))
diff --git bin/sh/options.c bin/sh/options.c
index b21fb5d..cf1e86c 100644
--- bin/sh/options.c
+++ bin/sh/options.c
@@ -64,6 +64,8 @@ __RCSID("$NetBSD: options.c,v 1.42 2011/06/18 21:18:46 christos Exp $");
 #include "myhistedit.h"
 #endif
 #include "show.h"
+#include "exec.h"
+#include "mail.h"

 char *arg0;			/* value of $0 */
 struct shparam shellparam;	/* current positional parameters */
@@ -136,6 +138,7 @@ procargs(int argc, char **argv)
 }


+/* XXX: replace with callbacks */
 void
 optschanged(void)
 {
@@ -198,6 +201,14 @@ options(int cmdline)
 }

 static void
+set_opt_invoke_cb(size_t i, int val)
+{
+	if (optlist[i].cb != NULL && optlist[i].val != val
+	    && optlist[i].val != 2)
+		optlist[i].cb(val);
+}
+
+static void
 set_opt_val(size_t i, int val)
 {
 	size_t j;
@@ -206,9 +217,12 @@ set_opt_val(size_t i, int val)
 	if (val && (flag = optlist[i].opt_set)) {
 		/* some options (eg vi/emacs) are mutually exclusive */
 		for (j = 0; j < NOPTS; j++)
-		    if (optlist[j].opt_set == flag)
-			optlist[j].val = 0;
+			if (optlist[j].opt_set == flag) {
+				set_opt_invoke_cb(j, 0);
+				optlist[j].val = 0;
+			}
 	}
+	set_opt_invoke_cb(i, val);
 	optlist[i].val = val;
 #ifdef DEBUG
 	if (&optlist[i].val == &debug)
@@ -542,3 +556,19 @@ nextopt(const char *optstring)
 	optptr = p;
 	return c;
 }
+
+
+/*
+ * Option callbacks.
+ */
+
+void
+pathoptions_cb(unsigned char new_val)
+{
+	/*
+	 * *PATH is not changed, but the meaning of a path in the list might
+	 * have changed.
+	 */
+	changepath_options(pathval(), new_val);
+	chkmail(1);
+}
diff --git bin/sh/options.h bin/sh/options.h
index 0bf4a2a..6d4fb6f 100644
--- bin/sh/options.h
+++ bin/sh/options.h
@@ -44,22 +44,33 @@ struct shparam {
 };


+/*
+ * Called with the new state of the option before it is changed.
+ * The old value is 2, if we're just initializing the shell.
+ */
+typedef void (*opt_callback)(unsigned char);
 struct optent {
 	const char *name;		/* for set -o <name> */
 	const char letter;		/* set [+/-]<letter> and $- */
 	const char opt_set;		/* mutually exclusive option set */
 	unsigned char val;		/* value of <letter>flag */
+	opt_callback cb;		/* called when val is about to change */
 };

+void pathoptions_cb(unsigned char);
+
 /* Those marked [U] are required by posix, but have no effect! */

 #ifdef DEFINE_OPTIONS
-#define DEF_OPTS(name, letter, opt_set) {name, letter, opt_set, 0},
+#define DEF_OPTS(name, letter, set)         {name, letter, set, 0, NULL},
+#define DEF_OPTS_CB(name, letter, set, cb)  {name, letter, set, 0, cb},
 struct optent optlist[] = {
 #else
-#define DEF_OPTS(name, letter, opt_set)
+#define DEF_OPTS(name, letter, set)
+#define DEF_OPTS_CB(name, letter, set, cb)
 #endif
-#define DEF_OPT(name,letter) DEF_OPTS(name, letter, 0)
+#define DEF_OPT(name,letter)		DEF_OPTS_CB(name, letter, 0, NULL)
+#define DEF_OPT_CB(name, letter, cb)	DEF_OPTS_CB(name, letter, 0, cb)

 DEF_OPT( "errexit",	'e' )	/* exit on error */
 #define eflag optlist[0].val
@@ -99,13 +110,16 @@ DEF_OPT( "cdprint",	0 )	/* always print result of cd */
 #define	cdprint optlist[17].val
 DEF_OPT( "tabcomplete",	0 )	/* <tab> causes filename expansion */
 #define	tabcomplete optlist[18].val
+				/* enable ?options in *PATH variables */
+DEF_OPT_CB("pathoptions", 0,	pathoptions_cb)
+#define	pathoptions optlist[19].val
 #ifdef DEBUG
 DEF_OPT( "debug",	0 )	/* enable debug prints */
-#define	debug optlist[19].val
+#define	debug optlist[20].val
 #endif

 #ifdef DEFINE_OPTIONS
-	{ 0, 0, 0, 0 },
+	{ NULL, 0, 0, 0, NULL },
 };
 #define NOPTS (sizeof optlist / sizeof optlist[0] - 1)
 int sizeof_optlist = sizeof optlist;
diff --git bin/sh/sh.1 bin/sh/sh.1
index 9d7eacb..9a45f50 100644
--- bin/sh/sh.1
+++ bin/sh/sh.1
@@ -304,6 +304,25 @@ Make an interactive shell always print the new directory name when
 changed by the
 .Ic cd
 command.
+.It "\ \ " Em pathoptions
+Process
+.Dq ?option
+suffices attached to entries in some path lists.
+These options modify the meaning of that entry in the path list.
+Currently the affected variables are
+.Ev MAIL ,
+.Ev MAILPATH ,
+and
+.Ev PATH .
+See the description of the individual variable for a discussion about
+the options.
+.Pp
+When this option is unset (the default), question marks are treated as
+regular characters in the affected variables.
+When set, a question mark that is to be a part of the path entry must be
+entered as
+.Dq ??
+to prevent the question mark from signaling the start of an option.
 .It "\ \ " Em tabcomplete
 Enables filename completion in the command line editor.
 Typing a tab character will extend the current input word to match a
@@ -553,6 +572,31 @@ variable should be a series of entries separated by colons.
 Each entry consists of a directory name.
 The current directory may be indicated
 implicitly by an empty directory name, or explicitly by a single period.
+.Pp
+If the
+.Ar pathoptions
+shell option is set, each entry in
+.Ev PATH
+may be followed by
+.Dq Ar ?option .
+This is a non-standard extension.
+The supported options and their meanings are:
+.Bl -tag -width ?builtin:x
+.It Sy ?builtin :
+The path part of the entry is ignored.
+Instead of looking up built-in commands before the path search, the search
+is performed at this point in the path search.
+.It Sy ?func :
+The entry is a directory containing shell scripts, which need not to be
+executable.
+If the name of the command matches a file in the directory, that file is
+read in and is expected to define a function with the same name.
+If such a function is defined, it is called, otherwise an error occurs.
+What other actions the file may take in addition to defining the function,
+is not limited in any way.
+.El
+.Pp
+Entries with unrecognized options are ignored with a diagnostic message.
 .El
 .Ss Command Exit Status
 Each command has an exit status that can influence the behavior
@@ -1910,8 +1954,13 @@ This environment variable also functions as the default argument for the
 built-in.
 .It Ev PATH
 The default search path for executables.
+When the
+.Ar pathoptions
+shell option is set, the entries may be followed by
+.Dq ?option .
 See the above section
-.Sx Path Search .
+.Sx Path Search
+for details.
 .It Ev CDPATH
 The search path used with the
 .Ic cd
@@ -1925,6 +1974,12 @@ See
 The name of a mail file, that will be checked for the arrival of new mail.
 Overridden by
 .Ev MAILPATH .
+When the
+.Ar pathoptions
+shell option is set, the value is processed for
+.Dq Ar ?text
+like
+.Ev MAILPATH .
 .It Ev MAILCHECK
 The frequency in seconds that the shell checks for the arrival of mail
 in the files specified by the
@@ -1933,6 +1988,7 @@ or the
 .Ev MAIL
 file.
 If set to 0, the check will occur at each prompt.
+(Not implemented yet.)
 .It Ev MAILPATH
 A colon
 .Dq \&:
@@ -1941,6 +1997,20 @@ This environment setting overrides the
 .Ev MAIL
 setting.
 There is a maximum of 10 mailboxes that can be monitored at once.
+.Pp
+When the
+.Ar pathoptions
+shell option has been set, each file may be followed by a path option
+.Dq Ar ?text .
+Here
+.Ar text
+is the text that is to be printed upon the arrival of new mail into
+that mailbox instead of the default message.
+.Pp
+Many shells allow the overriding message to contain
+.Dq $_ ,
+which would expand to the name of the mailbox.
+Unfortunately this shell is not yet one of those shells.
 .It Ev PS1
 The primary prompt string, which defaults to
 .Dq $ \  ,
diff --git bin/sh/var.c bin/sh/var.c
index cae5c3f..2664196 100644
--- bin/sh/var.c
+++ bin/sh/var.c
@@ -119,7 +119,7 @@ const struct varinit varinit[] = {
 	{ &vmpath,	VSTRFIXED|VTEXTFIXED|VUNSET,	"MAILPATH=",
 	  NULL },
 	{ &vpath,	VSTRFIXED|VTEXTFIXED,		"PATH=" _PATH_DEFPATH,
-	  changepath },
+	  changepath_variable },
 	/*
 	 * vps1 depends on uid
 	 */
diff --git bin/sh/var.h bin/sh/var.h
index c5b6927..c034314 100644
--- bin/sh/var.h
+++ bin/sh/var.h
@@ -49,6 +49,17 @@
 #define VNOSET		0x80	/* do not set variable - just readonly test */


+/*
+ * XXX: add a field for internal representation / data.
+ * Use case: pathoptions is set, $PATH contains ??, ?builtin, or ?func.
+ * The first one has no special meaning and the others are meaningless
+ * for other programs and their $PATH should not contain these values ->
+ *   text == "PATH=dir1:dir?3"
+ *   internal == { { "dir1", NORM }, { "", BUILTIN }, { "dir??3", NORM },
+ *                 { "dir4", FUNC } }
+ * XXX: if implemented, what if sh is invoked indirectly by a utility that
+ * has a cleaned up PATH?
+ */
 struct var {
 	struct var *next;		/* next entry in hash list */
 	int flags;			/* flags are defined above */
diff --git etc/mtree/NetBSD.dist.tests etc/mtree/NetBSD.dist.tests
index 903010d..5e0e714 100644
--- etc/mtree/NetBSD.dist.tests
+++ etc/mtree/NetBSD.dist.tests
@@ -132,6 +132,8 @@
 ./usr/tests/bin/sh
 ./usr/tests/bin/sh/dotcmd
 ./usr/tests/bin/sh/dotcmd/out
+./usr/tests/bin/sh/paths
+./usr/tests/bin/sh/paths/out
 ./usr/tests/crypto
 ./usr/tests/crypto/libcrypto
 ./usr/tests/dev
diff --git tests/bin/sh/Makefile tests/bin/sh/Makefile
index 022b7c9..60e41cd 100644
--- tests/bin/sh/Makefile
+++ tests/bin/sh/Makefile
@@ -5,6 +5,6 @@

 TESTSDIR = ${TESTSBASE}/bin/sh

-TESTS_SUBDIRS += dotcmd
+TESTS_SUBDIRS += dotcmd paths

 .include <bsd.test.mk>
diff --git tests/bin/sh/paths/Makefile tests/bin/sh/paths/Makefile
new file mode 100644
index 0000000..774032b
--- /dev/null
+++ tests/bin/sh/paths/Makefile
@@ -0,0 +1,38 @@
+# $NetBSD$
+#
+
+.include <bsd.own.mk>
+
+TESTSDIR = ${TESTSBASE}/bin/sh/paths
+
+TESTS_SH = t_path t_pathoptions
+
+FILESDIR = ${TESTSDIR}
+FILESMODE = ${BINMODE}
+FILES += \
+	cdpath.sh \
+	mailpath.sh \
+	path.sh \
+	script1 \
+	script2
+
+outfiles := \
+	cdpath_options \
+	cdpath_rare \
+	mailpath_rare \
+	path_options \
+	path_rare
+
+errfiles := \
+	mailpath_options \
+	path_options
+
+.for output in ${outfiles:%=out/%.out} ${errfiles:%=out/%.err}
+FILES += ${output}
+FILESDIR_${output} = ${TESTSDIR}/out
+FILESMODE_${output:S/^out\///} = ${NONBINMODE}
+.endfor
+outfiles :=
+errfiles :=
+
+.include <bsd.test.mk>
diff --git tests/bin/sh/paths/README tests/bin/sh/paths/README
new file mode 100644
index 0000000..c1edb72
--- /dev/null
+++ tests/bin/sh/paths/README
@@ -0,0 +1,24 @@
+About test scripts
+==================
+
+The scripts cdpath.sh, mailpath.sh, and path.sh all have the same usage
+conventions:
+	script regular rare
+	script regular rare rare_escaped
+
+All of the arguments are directory names, which will be used in a path
+variable of appropriate type.  It is the responsibility of the test driver
+to create and otherwise setup these directories.
+
+The first format is used to verify correct behavior when a directory name
+(the second argument) contains all sorts of rare characters, i.e. anything
+that is not '/', ':' or NUL.
+
+The second format is used to verify correct behavior when pathoptions is
+set/unset.  The second argument is expected to contain a path that contains
+at least one '?' and the third argument is the same with all occurrences of
+'?' properly escaped, so it can be easily used as an argument for commands
+in the script.
+
+The first argument is always a directory name with no funny business,
+i.e. only alphabetic ASCII characters.
diff --git tests/bin/sh/paths/cdpath.sh tests/bin/sh/paths/cdpath.sh
new file mode 100644
index 0000000..0c59081
--- /dev/null
+++ tests/bin/sh/paths/cdpath.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+# Setup:
+# Create directories ${1}/dir1 and ${2}/dir2.
+
+prefix="${PWD}"
+regular="${1}"
+rare="${2}"
+
+CDPATH="${prefix}/${rare}:${prefix}/${regular}"
+
+cd dir1 && echo "${PWD#${prefix}/}"	# "${1}/dir1\n"
+cd dir2 && echo "${PWD#${prefix}/}"	# "${2}/dir2\n"
+
+if [ -n "${3}" ]
+then
+	# CDPATH is not affected by pathoptions.
+	set -o pathoptions
+
+	cd dir1 && echo "${PWD#${prefix}/}"	# "${1}/dir1\n"
+	cd dir2 && echo "${PWD#${prefix}/}"	# "${2}/dir2\n"
+fi
diff --git tests/bin/sh/paths/mailpath.sh tests/bin/sh/paths/mailpath.sh
new file mode 100644
index 0000000..cfec7b5
--- /dev/null
+++ tests/bin/sh/paths/mailpath.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+
+# Setup:
+# Create directories ${1} and ${2}.
+#
+# Note: all output is to stderr, because prompts and mail notifications are
+# printed there.
+
+regular="${1}"
+rare="${2}"
+
+# Let's play it safe: $MAILPATH overrides $MAIL.
+unset MAILPATH
+
+MAIL="${rare}/mail"
+echo 'pretend mail' >mail
+# Mail is only checked for interactive shells.  Enable interactivity only
+# when needed to keep output simpler.
+set -i
+cp mail "${rare}" 	# "$ You have ...\n"
+set +i			# "$ "
+
+MAILPATH="${rare}/mail:${regular}/mail"
+echo 'pretend mail' >>mail
+set -i
+cp mail "${rare}"	# "$ You have ...\n"
+cp mail "${regular}"	# "$ You have ...\n"
+set +i			# "$ "
+
+# Cannot use 'if [ -n "${3}" ]; then ...; fi' because the contents of
+# the then-list are pre-parsed instead of being read line-by-line
+# by cmdloop() -> no chkmail()!
+if [ -z "${3}" ]
+then
+	exit 0
+fi
+
+rare_orig="${rare}"
+rare="${3}"
+set -o pathoptions
+
+echo 'pretend mail' >>mail
+set -i
+cp mail "${rare_orig}"	# "$ " (mbox not recognized because of an unescaped '?')
+cp mail "${regular}"	# "$ You have ...\n""
+set +i			# "$ "
+
+unset MAILPATH		# Use $MAIL again.
+echo 'pretend mail' >>mail
+set -i
+cp mail "${rare_orig}"	# "$ " (mbox not recognized because of an unescaped '?')
+set +i			# "$ "
+
+MAIL="${rare}/mail"
+echo 'pretend mail' >>mail
+set -i
+cp mail "${rare_orig}"	# "$ You have ...\n"
+set +i			# "$ "
+
+MAIL="${rare}/mail?rare mail"
+echo 'pretend mail' >>mail
+set -i
+cp mail "${rare_orig}"	# "$ rare mail\n"
+set +i			# "$ "
+
+MAILPATH="${rare}/mail:${regular}/mail"
+echo 'pretend mail' >>mail
+set -i
+cp mail "${rare_orig}"	# "$ You have ...\n"
+cp mail "${regular}"	# "$ You have ...\n"
+set +i			# "$ "
+
+MAILPATH="${rare}/mail?rare mail:${regular}/mail"
+echo 'pretend mail' >>mail
+set -i
+cp mail "${rare_orig}"	# "$ rare mail\n"
+cp mail "${regular}"	# "$ You have ...\n"
+set +i			# "$ "
diff --git tests/bin/sh/paths/out/cdpath_options.out tests/bin/sh/paths/out/cdpath_options.out
new file mode 100644
index 0000000..195c558
--- /dev/null
+++ tests/bin/sh/paths/out/cdpath_options.out
@@ -0,0 +1,4 @@
+base1/dir1
+base?2/dir2
+base1/dir1
+base?2/dir2
diff --git tests/bin/sh/paths/out/cdpath_rare.out tests/bin/sh/paths/out/cdpath_rare.out
new file mode 100644
index 0000000..cacbf2b
--- /dev/null
+++ tests/bin/sh/paths/out/cdpath_rare.out
@@ -0,0 +1,3 @@
+regular/dir1
+	
+ !"#$%&'()*+,-.0123456789;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
diff --git tests/bin/sh/paths/out/mailpath_options.err tests/bin/sh/paths/out/mailpath_options.err
new file mode 100644
index 0000000..181b8f5
--- /dev/null
+++ tests/bin/sh/paths/out/mailpath_options.err
@@ -0,0 +1,11 @@
+$ You have new mail in dir?2/mail
+$ $ You have new mail in dir?2/mail
+$ You have new mail in dir1/mail
+$ $ $ You have new mail in dir1/mail
+$ $ $ $ You have new mail in dir?2/mail
+$ $ rare mail
+$ $ You have new mail in dir?2/mail
+$ You have new mail in dir1/mail
+$ $ rare mail
+$ You have new mail in dir1/mail
+$ 
\ No newline at end of file
diff --git tests/bin/sh/paths/out/mailpath_rare.out tests/bin/sh/paths/out/mailpath_rare.out
new file mode 100644
index 0000000..6de2b89
--- /dev/null
+++ tests/bin/sh/paths/out/mailpath_rare.out
@@ -0,0 +1,6 @@
+$ You have new mail in 	
+ !"#$%&'()*+,-.0123456789;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
+$ $ You have new mail in 	
+ !"#$%&'()*+,-.0123456789;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
+$ You have new mail in regular/mail
+$ 
\ No newline at end of file
diff --git tests/bin/sh/paths/out/path_options.err tests/bin/sh/paths/out/path_options.err
new file mode 100644
index 0000000..eff07be
--- /dev/null
+++ tests/bin/sh/paths/out/path_options.err
@@ -0,0 +1,3 @@
+Unrecognized path option "?2"
+Unrecognized path option "?2"
+script2: not found
diff --git tests/bin/sh/paths/out/path_options.out tests/bin/sh/paths/out/path_options.out
new file mode 100644
index 0000000..3095839
--- /dev/null
+++ tests/bin/sh/paths/out/path_options.out
@@ -0,0 +1,8 @@
+script1
+script2
+script1
+script1
+script2
+script1
+script2
+script2 function
diff --git tests/bin/sh/paths/out/path_rare.out tests/bin/sh/paths/out/path_rare.out
new file mode 100644
index 0000000..2363aa4
--- /dev/null
+++ tests/bin/sh/paths/out/path_rare.out
@@ -0,0 +1,2 @@
+script1
+script2
diff --git tests/bin/sh/paths/path.sh tests/bin/sh/paths/path.sh
new file mode 100644
index 0000000..46e8a30
--- /dev/null
+++ tests/bin/sh/paths/path.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+# Setup:
+# Copy script1 to ${1} and script2 to ${2}.  For the second form, also
+# link script2 to ${1}/echo.
+
+PATH_orig="${PATH}"
+regular="${1}"
+rare="${2}"
+
+PATH="${PATH_orig}:${rare}:${regular}"
+
+script1	# "script1\n"
+script2 # "script2\n"
+
+if [ -n "${3}" ]
+then
+	rare="${3}"
+
+	set -o pathoptions
+
+	script1 # "script1\n", stderr: "Unrecognized path option \"?2\"\n"
+	script2	# stderr: "Unrecognized path option \"?2\"\n"
+		#         "script2: not found\n"
+
+	PATH="${PATH_orig}:${rare}:${regular}"
+
+	script1 # "script1\n"
+	script2 # "script2\n"
+
+	# Versions of echo: builtin, /bin/echo, ${regular}/echo
+	# We want ${regular}/echo, so ${regular} goes before ${PATH_orig}.
+	PATH="${regular}:${PATH_orig}:${rare}?func:path/ignored?builtin"
+
+	echo not builtin	# "script1"
+	script2			# "script2\n"
+				# "script2 function\n"
+fi
diff --git tests/bin/sh/paths/script1 tests/bin/sh/paths/script1
new file mode 100644
index 0000000..86171cde
--- /dev/null
+++ tests/bin/sh/paths/script1
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+printf "%s\n" script1
diff --git tests/bin/sh/paths/script2 tests/bin/sh/paths/script2
new file mode 100644
index 0000000..e8743f9
--- /dev/null
+++ tests/bin/sh/paths/script2
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+script2()
+{
+	printf "%s\n" 'script2 function'
+}
+
+printf "%s\n" script2
diff --git tests/bin/sh/paths/t_path.sh tests/bin/sh/paths/t_path.sh
new file mode 100644
index 0000000..5a5febd
--- /dev/null
+++ tests/bin/sh/paths/t_path.sh
@@ -0,0 +1,83 @@
+# $NetBSD$
+#
+# Copyright (c) 2014 The NetBSD Foundation, Inc.
+# All rights reserved.
+#
+# This code is derived from software contributed to The NetBSD Foundation
+# by Jarmo Jaakkola.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+rare="$(
+awk 'END { for (i = 1; i < 256; ++i) { if (i != 47 && i != 58) printf "%c", i; } }' </dev/null
+)"
+
+path_rare_head()
+{
+	atf_set 'descr' '$PATH entry with all octets (excl. NUL, / and :)'
+}
+
+path_rare_body()
+{
+	srcdir="$(atf_get_srcdir)"
+	mkdir regular "${rare}"
+	cp -p "${srcdir}/script1" regular
+	cp -p "${srcdir}/script2" "${rare}"
+	atf_check -o file:"${srcdir}/out/path_rare.out" \
+	    "${srcdir}/path.sh" regular "${rare}"
+}
+
+cdpath_rare_head()
+{
+	atf_set 'descr' '$CDPATH entry with all octets (excl. NUL, / and :)'
+}
+
+cdpath_rare_body()
+{
+	srcdir="$(atf_get_srcdir)"
+	mkdir -p regular/dir1 "${rare}/dir2"
+	atf_check -o file:"${srcdir}/out/cdpath_rare.out" \
+	    "${srcdir}/cdpath.sh" regular "${rare}"
+}
+
+mailpath_rare_head()
+{
+	atf_set 'descr' \
+	    '$MAILPATH entry with all octets (excl. NUL, / and :)'
+}
+
+mailpath_rare_body()
+{
+	srcdir="$(atf_get_srcdir)"
+	mkdir regular "${rare}"
+	atf_check -e file:"${srcdir}/out/mailpath_rare.out" \
+	    "${srcdir}/mailpath.sh" regular "${rare}"
+}
+
+
+atf_init_test_cases()
+{
+	atf_add_test_case path_rare
+	atf_add_test_case cdpath_rare
+	atf_add_test_case mailpath_rare
+}
diff --git tests/bin/sh/paths/t_pathoptions.sh tests/bin/sh/paths/t_pathoptions.sh
new file mode 100644
index 0000000..235ae10
--- /dev/null
+++ tests/bin/sh/paths/t_pathoptions.sh
@@ -0,0 +1,80 @@
+# $NetBSD$
+#
+# Copyright (c) 2014 The NetBSD Foundation, Inc.
+# All rights reserved.
+#
+# This code is derived from software contributed to The NetBSD Foundation
+# by Jarmo Jaakkola.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+path_pathoptions_head()
+{
+	atf_set 'descr' '$PATH with pathoptions off and on.'
+}
+
+path_pathoptions_body()
+{
+	srcdir="$(atf_get_srcdir)"
+	mkdir dir1 'dir?2'
+	cp -p "${srcdir}/script1" 'dir1'
+	cp -p "${srcdir}/script2" 'dir?2'
+	ln 'dir1/script1' 'dir1/echo'
+	atf_check -o file:"${srcdir}/out/path_options.out" \
+	    -e file:"${srcdir}/out/path_options.err" \
+	    "${srcdir}/path.sh" dir1 'dir?2' 'dir??2'
+}
+
+cdpath_pathoptions_head()
+{
+	atf_set 'descr' '$CDPATH with pathoptions off and on'
+}
+
+cdpath_pathoptions_body()
+{
+	srcdir="$(atf_get_srcdir)"
+	mkdir -p base1/dir1 'base?2/dir2'
+	atf_check -o file:"${srcdir}/out/cdpath_options.out" \
+	    "${srcdir}/cdpath.sh" base1 'base?2' 'base??2'
+}
+
+mailpath_pathoptions_head()
+{
+	atf_set 'descr' '$MAILPATH entry with pathoptions off and on'
+}
+
+mailpath_pathoptions_body()
+{
+	srcdir="$(atf_get_srcdir)"
+	mkdir -p dir1 'dir?2'
+	atf_check -e file:"${srcdir}/out/mailpath_options.err" \
+	    "${srcdir}/mailpath.sh" dir1 'dir?2' 'dir??2'
+}
+
+
+atf_init_test_cases()
+{
+	atf_add_test_case path_pathoptions
+	atf_add_test_case cdpath_pathoptions
+	atf_add_test_case mailpath_pathoptions
+}
-- 
1.8.5.1

NetBSD Home
NetBSD PR Database Search

(Contact us) $NetBSD: query-full-pr,v 1.39 2013/11/01 18:47:49 spz Exp $
$NetBSD: gnats_config.sh,v 1.8 2006/05/07 09:23:38 tsutsui Exp $
Copyright © 1994-2007 The NetBSD Foundation, Inc. ALL RIGHTS RESERVED.