Subject: make: :[] implementation
To: None <tech-toolchain@netbsd.org>
From: Simon J. Gerraty <sjg@crufty.net>
List: tech-toolchain
Date: 09/23/2003 12:07:13
The patches below are essentially Alan Barrett's implementation of the
:[] and related modifiers discussed some weeks ago.
FWIW :[] privides similar functionality to GNU make's $(word and $(words.

I've tweaked the man page a bit, and resolved some issues that Alan
found along the way, and made the unit tests fit in with current plan.
Otherwise the work was done by Alan.

I was originally going to remove the :tW and :tw modifiers since :[*]
and :[@] have the same effect, but for the little code they consume I
don't mind leaving them as is.  The W modifier for :C and :S is also
handy.

--sjg

? unit-tests/modword
Index: make.1
===================================================================
RCS file: /cvsroot/src/usr.bin/make/make.1,v
retrieving revision 1.88
diff -u -p -r1.88 make.1
--- make.1	2003/09/10 18:04:23	1.88
+++ make.1	2003/09/23 18:54:35
@@ -471,7 +471,6 @@ i.e.
 .Ql \&$$
 expands to a single dollar
 sign.
-.Pq Va argv[0]
 .It Va .ALLTARGETS
 The list of all targets encountered in the Makefile.
 If evaluated during
@@ -483,7 +482,8 @@ was executed.
 .It Ev MAKE
 The name that
 .Nm
-was executed with.
+was executed with
+.Pq Va argv[0] .
 For compatibily
 .Nm
 also sets
@@ -637,10 +637,20 @@ If
 is omitted, then no separator is used.
 .It Cm tu
 Converts variable to upper-case letters.
+.It Cm tW
+Means the same as
+.Ql \&:[*] .
+It causes the value to be treated as a single word
+(possibly containing embedded white space).
+.It Cm tw
+Means the same as
+.Ql \&:[@] .
+It causes the value to be treated as a sequence of
+words delimited by white space.
 .Sm off
 .It Cm S No \&/ Ar old_string Xo
 .No \&/ Ar new_string
-.No \&/ Op Cm 1g
+.No \&/ Op Cm 1gW
 .Xc
 .Sm on
 Modify the first occurrence of
@@ -655,6 +665,11 @@ If a
 .Ql 1
 is appended to the last slash of the pattern, only the first word
 is affected.
+If a
+.Ql W
+is appended to the last slash of the pattern,
+then the value is treated as a single word
+(possibly containing embedded white space).
 If
 .Ar old_string
 begins with a caret
@@ -693,7 +708,7 @@ not a preceding dollar sign as is usual.
 .Sm off
 .It Cm C No \&/ Ar pattern Xo
 .No \&/ Ar replacement
-.No \&/ Op Cm 1g
+.No \&/ Op Cm 1gW
 .Xc
 .Sm on
 The
@@ -720,7 +735,10 @@ modifier causes the substitution to appl
 modifier causes the substitution to apply to as many instances of the
 search pattern
 .Ar pattern
-as occur in the word or words it is found in.
+as occur in the word or words it is found in; the
+.Ql W
+modifier causes the value to be treated as a single word
+(possibly containing embedded white space).
 Note that
 .Ql 1
 and
@@ -849,6 +867,63 @@ to the variable.
 Assign the output of
 .Ar cmd
 to the variable.
+.It Cm \&[ Ns Ar range Ns Cm \&]
+Selects one or more words from the value,
+or performs other operations related to the way in which the
+value is divided into words.
+.Pp
+Ordinarily, a value is treated as a sequence of words
+delimited by white space.
+Some modifiers suppress this behaviour,
+causing a value to be treated as a single word
+(possibly containing embedded white space).
+An empty value, or a value that consists entirely of white-space,
+is treated as a single word.
+For the purposes of the
+.Ql \&:[]
+modifier, the words are indexed both forwards using positive integers
+(where index 1 represents the first word),
+and backwards using negative integers
+(where index -1 represents the last word).
+.Pp
+The
+.Ar range
+is subjected to variable expansion, and the expanded result is
+then interpreted as follows:
+.Bl -tag -width index
+\" :[n]
+.It Ar index
+Selects a single word from the value.
+\" :[start..end]
+.It Ar start Ns Cm \&.. Ns Ar end
+Selects all words from
+.Ar start
+to
+.Ar end ,
+inclusive.
+For example,
+.Ql \&:[2..-1]
+selects all words from the second word to the last word.
+\" :[*]
+.It Cm \&*
+Causes subsequent modifiers to treat the value as a single word
+(possibly containing embedded white space).  Analogous to the effect of
+\&"$*\&" 
+in Bourne shell.
+\" :[0]
+.It 0
+Means the same as
+.Ql \&:[*] .
+\" :[*]
+.It Cm \&@
+Causes subsequent modifiers to treat the value as a sequence of words
+delimited by white space.  Analogous to the effect of
+\&"$@\&" 
+in Bourne shell.
+\" :[#]
+.It Cm \&#
+Returns the number of words in the value.
+.El \" :[range]
 .El
 .Sh INCLUDE STATEMENTS, CONDITIONALS AND FOR LOOPS
 Makefile inclusion, conditional structures and for loops  reminiscent
Index: str.c
===================================================================
RCS file: /cvsroot/src/usr.bin/make/str.c,v
retrieving revision 1.20
diff -u -p -r1.20 str.c
--- str.c	2003/08/07 11:14:57	1.20
+++ str.c	2003/09/23 18:54:35
@@ -129,8 +129,7 @@ str_concat(const char *s1, const char *s
  *	are ignored.
  *
  * returns --
- *	Pointer to the array of pointers to the words.  To make life easier,
- *	the first word is always the value of the .MAKE variable.
+ *	Pointer to the array of pointers to the words.
  */
 char **
 brk_string(const char *str, int *store_argc, Boolean expand, char **buffer)
Index: var.c
===================================================================
RCS file: /cvsroot/src/usr.bin/make/var.c,v
retrieving revision 1.80
diff -u -p -r1.80 var.c
--- var.c	2003/08/07 11:14:59	1.80
+++ var.c	2003/09/23 18:54:37
@@ -195,12 +195,34 @@ typedef struct Var {
 #define VAR_MATCH_END	0x10	/* Match at end of word */
 #define VAR_NOSUBST	0x20	/* don't expand vars in VarGetPattern */
 
-static Byte varSpace = ' ';	/* word separator in expansions */
-
 /* Var_Set flags */
 #define VAR_NO_EXPORT	0x01	/* do not export */
 
 typedef struct {
+    /*
+     * The following fields are set by Var_Parse() when it
+     * encounters modifiers that need to keep state for use by
+     * subsequent modifiers within the same variable expansion.
+     */
+    Byte	varSpace;	/* Word separator in expansions */
+    Boolean	oneBigWord;	/* TRUE if we will treat the variable as a
+				 * single big word, even if it contains
+				 * embedded spaces (as opposed to the
+				 * usual behaviour of treating it as
+				 * several space-separated words). */
+    /*
+     * The following fields are set by VarModify() to let its
+     * worker functions know which word within the variable
+     * is being processed.
+     */
+    int		argc;		/* Total number of words in the value. */
+    int		argnum;		/* Which word is being worked on now.
+				 * The range is from 0 to (argc-1).  */
+} Var_Parse_State;
+
+/* struct passed as ClientData to VarSubstitute() for ":S/lhs/rhs/",
+ * to VarSYSVMatch() for ":lhs=rhs". */
+typedef struct {
     const char   *lhs;	    /* String to match */
     int	    	  leftLen; /* Length of string */
     const char   *rhs;	    /* Replacement string (w/ &'s removed) */
@@ -208,6 +230,7 @@ typedef struct {
     int	    	  flags;
 } VarPattern;
 
+/* struct passed as ClientData to VarLoopExpand() for ":@tvar@str@" */
 typedef struct {
     GNode	*ctxt;		/* variable context */
     char	*tvar;		/* name of temp var */
@@ -218,6 +241,7 @@ typedef struct {
 } VarLoop_t;
 
 #ifndef NO_REGEX
+/* struct passed as ClientData to VarRESubstitute() for ":C///" */
 typedef struct {
     regex_t	   re;
     int		   nsub;
@@ -227,29 +251,50 @@ typedef struct {
 } VarREPattern;
 #endif
 
+/* struct passed as ClientData to VarSelectWords() for ":[start..end]" */
+typedef struct {
+    int		start;		/* first word to select */
+    int		end;		/* last word to select */
+    Boolean	sanitised;	/* TRUE if start/end have been sanitised */
+} VarSelectWords_t;
+
 static Var *VarFind(const char *, GNode *, int);
 static void VarAdd(const char *, const char *, GNode *);
-static Boolean VarHead(GNode *, char *, Boolean, Buffer, ClientData);
-static Boolean VarTail(GNode *, char *, Boolean, Buffer, ClientData);
-static Boolean VarSuffix(GNode *, char *, Boolean, Buffer, ClientData);
-static Boolean VarRoot(GNode *, char *, Boolean, Buffer, ClientData);
-static Boolean VarMatch(GNode *, char *, Boolean, Buffer, ClientData);
+static Boolean VarHead(GNode *, Var_Parse_State *,
+			char *, Boolean, Buffer, ClientData);
+static Boolean VarTail(GNode *, Var_Parse_State *,
+			char *, Boolean, Buffer, ClientData);
+static Boolean VarSuffix(GNode *, Var_Parse_State *,
+			char *, Boolean, Buffer, ClientData);
+static Boolean VarRoot(GNode *, Var_Parse_State *,
+			char *, Boolean, Buffer, ClientData);
+static Boolean VarMatch(GNode *, Var_Parse_State *,
+			char *, Boolean, Buffer, ClientData);
 #ifdef SYSVVARSUB
-static Boolean VarSYSVMatch(GNode *, char *, Boolean, Buffer, ClientData);
+static Boolean VarSYSVMatch(GNode *, Var_Parse_State *,
+			char *, Boolean, Buffer, ClientData);
 #endif
-static Boolean VarNoMatch(GNode *, char *, Boolean, Buffer, ClientData);
+static Boolean VarNoMatch(GNode *, Var_Parse_State *,
+			char *, Boolean, Buffer, ClientData);
 #ifndef NO_REGEX
 static void VarREError(int, regex_t *, const char *);
-static Boolean VarRESubstitute(GNode *, char *, Boolean, Buffer, ClientData);
+static Boolean VarRESubstitute(GNode *, Var_Parse_State *,
+			char *, Boolean, Buffer, ClientData);
 #endif
-static Boolean VarSubstitute(GNode *, char *, Boolean, Buffer, ClientData);
-static Boolean VarLoopExpand(GNode *, char *, Boolean, Buffer, ClientData);
-static char *VarGetPattern(GNode *, int, const char **, int, int *, int *,
+static Boolean VarSubstitute(GNode *, Var_Parse_State *,
+			char *, Boolean, Buffer, ClientData);
+static Boolean VarLoopExpand(GNode *, Var_Parse_State *,
+			char *, Boolean, Buffer, ClientData);
+static Boolean VarSelectWords(GNode *, Var_Parse_State *,
+			char *, Boolean, Buffer, ClientData);
+static char *VarGetPattern(GNode *, Var_Parse_State *,
+			   int, const char **, int, int *, int *,
 			   VarPattern *);
 static char *VarQuote(char *);
 static char *VarChangeCase(char *, int);
-static char *VarModify(GNode *, const char *,
-    Boolean (*)(GNode *, char *, Boolean, Buffer, ClientData),
+static char *VarModify(GNode *, Var_Parse_State *,
+    const char *,
+    Boolean (*)(GNode *, Var_Parse_State *, char *, Boolean, Buffer, ClientData),
     ClientData);
 static char *VarSort(const char *);
 static char *VarUniq(const char *);
@@ -681,15 +726,16 @@ Var_Value(const char *name, GNode *ctxt,
  *-----------------------------------------------------------------------
  */
 static Boolean
-VarHead(GNode *ctx, char *word, Boolean addSpace, Buffer buf,
+VarHead(GNode *ctx, Var_Parse_State *vpstate,
+	char *word, Boolean addSpace, Buffer buf,
 	ClientData dummy)
 {
     char *slash;
 
     slash = strrchr (word, '/');
     if (slash != (char *)NULL) {
-	if (addSpace && varSpace) {
-	    Buf_AddByte (buf, varSpace);
+	if (addSpace && vpstate->varSpace) {
+	    Buf_AddByte (buf, vpstate->varSpace);
 	}
 	*slash = '\0';
 	Buf_AddBytes (buf, strlen (word), (Byte *)word);
@@ -699,8 +745,8 @@ VarHead(GNode *ctx, char *word, Boolean 
 	/*
 	 * If no directory part, give . (q.v. the POSIX standard)
 	 */
-	if (addSpace && varSpace)
-	    Buf_AddByte(buf, varSpace);
+	if (addSpace && vpstate->varSpace)
+	    Buf_AddByte(buf, vpstate->varSpace);
 	Buf_AddByte(buf, (Byte)'.');
     }
     return(dummy ? TRUE : TRUE);
@@ -728,13 +774,14 @@ VarHead(GNode *ctx, char *word, Boolean 
  *-----------------------------------------------------------------------
  */
 static Boolean
-VarTail(GNode *ctx, char *word, Boolean addSpace, Buffer buf,
+VarTail(GNode *ctx, Var_Parse_State *vpstate,
+	char *word, Boolean addSpace, Buffer buf,
 	ClientData dummy)
 {
     char *slash;
 
-    if (addSpace && varSpace) {
-	Buf_AddByte (buf, varSpace);
+    if (addSpace && vpstate->varSpace) {
+	Buf_AddByte (buf, vpstate->varSpace);
     }
 
     slash = strrchr (word, '/');
@@ -769,15 +816,16 @@ VarTail(GNode *ctx, char *word, Boolean 
  *-----------------------------------------------------------------------
  */
 static Boolean
-VarSuffix(GNode *ctx, char *word, Boolean addSpace, Buffer buf,
+VarSuffix(GNode *ctx, Var_Parse_State *vpstate,
+	  char *word, Boolean addSpace, Buffer buf,
 	  ClientData dummy)
 {
     char *dot;
 
     dot = strrchr (word, '.');
     if (dot != (char *)NULL) {
-	if (addSpace && varSpace) {
-	    Buf_AddByte (buf, varSpace);
+	if (addSpace && vpstate->varSpace) {
+	    Buf_AddByte (buf, vpstate->varSpace);
 	}
 	*dot++ = '\0';
 	Buf_AddBytes (buf, strlen (dot), (Byte *)dot);
@@ -809,13 +857,14 @@ VarSuffix(GNode *ctx, char *word, Boolea
  *-----------------------------------------------------------------------
  */
 static Boolean
-VarRoot(GNode *ctx, char *word, Boolean addSpace, Buffer buf,
+VarRoot(GNode *ctx, Var_Parse_State *vpstate,
+	char *word, Boolean addSpace, Buffer buf,
 	ClientData dummy)
 {
     char *dot;
 
-    if (addSpace && varSpace) {
-	Buf_AddByte (buf, varSpace);
+    if (addSpace && vpstate->varSpace) {
+	Buf_AddByte (buf, vpstate->varSpace);
     }
 
     dot = strrchr (word, '.');
@@ -852,12 +901,13 @@ VarRoot(GNode *ctx, char *word, Boolean 
  *-----------------------------------------------------------------------
  */
 static Boolean
-VarMatch(GNode *ctx, char *word, Boolean addSpace, Buffer buf,
+VarMatch(GNode *ctx, Var_Parse_State *vpstate,
+	 char *word, Boolean addSpace, Buffer buf,
 	 ClientData pattern)
 {
     if (Str_Match(word, (char *) pattern)) {
-	if (addSpace && varSpace) {
-	    Buf_AddByte(buf, varSpace);
+	if (addSpace && vpstate->varSpace) {
+	    Buf_AddByte(buf, vpstate->varSpace);
 	}
 	addSpace = TRUE;
 	Buf_AddBytes(buf, strlen(word), (Byte *)word);
@@ -890,7 +940,8 @@ VarMatch(GNode *ctx, char *word, Boolean
  *-----------------------------------------------------------------------
  */
 static Boolean
-VarSYSVMatch(GNode *ctx, char *word, Boolean addSpace, Buffer buf,
+VarSYSVMatch(GNode *ctx, Var_Parse_State *vpstate,
+	     char *word, Boolean addSpace, Buffer buf,
 	     ClientData patp)
 {
     int len;
@@ -898,8 +949,8 @@ VarSYSVMatch(GNode *ctx, char *word, Boo
     VarPattern 	  *pat = (VarPattern *) patp;
     char *varexp;
 
-    if (addSpace && varSpace)
-	Buf_AddByte(buf, varSpace);
+    if (addSpace && vpstate->varSpace)
+	Buf_AddByte(buf, vpstate->varSpace);
 
     addSpace = TRUE;
 
@@ -939,12 +990,13 @@ VarSYSVMatch(GNode *ctx, char *word, Boo
  *-----------------------------------------------------------------------
  */
 static Boolean
-VarNoMatch(GNode *ctx, char *word, Boolean addSpace, Buffer buf,
+VarNoMatch(GNode *ctx, Var_Parse_State *vpstate,
+	   char *word, Boolean addSpace, Buffer buf,
 	   ClientData pattern)
 {
     if (!Str_Match(word, (char *) pattern)) {
-	if (addSpace && varSpace) {
-	    Buf_AddByte(buf, varSpace);
+	if (addSpace && vpstate->varSpace) {
+	    Buf_AddByte(buf, vpstate->varSpace);
 	}
 	addSpace = TRUE;
 	Buf_AddBytes(buf, strlen(word), (Byte *)word);
@@ -975,7 +1027,8 @@ VarNoMatch(GNode *ctx, char *word, Boole
  *-----------------------------------------------------------------------
  */
 static Boolean
-VarSubstitute(GNode *ctx, char *word, Boolean addSpace, Buffer buf,
+VarSubstitute(GNode *ctx, Var_Parse_State *vpstate,
+	      char *word, Boolean addSpace, Buffer buf,
 	      ClientData patternp)
 {
     int  	wordLen;    /* Length of word */
@@ -1002,8 +1055,8 @@ VarSubstitute(GNode *ctx, char *word, Bo
 			 * if rhs is non-null.
 			 */
 			if (pattern->rightLen != 0) {
-			    if (addSpace && varSpace) {
-				Buf_AddByte(buf, varSpace);
+			    if (addSpace && vpstate->varSpace) {
+				Buf_AddByte(buf, vpstate->varSpace);
 			    }
 			    addSpace = TRUE;
 			    Buf_AddBytes(buf, pattern->rightLen,
@@ -1020,8 +1073,8 @@ VarSubstitute(GNode *ctx, char *word, Bo
 		     * Matches at start but need to copy in trailing characters
 		     */
 		    if ((pattern->rightLen + wordLen - pattern->leftLen) != 0){
-			if (addSpace && varSpace) {
-			    Buf_AddByte(buf, varSpace);
+			if (addSpace && vpstate->varSpace) {
+			    Buf_AddByte(buf, vpstate->varSpace);
 			}
 			addSpace = TRUE;
 		    }
@@ -1053,8 +1106,8 @@ VarSubstitute(GNode *ctx, char *word, Bo
 		 * by the right-hand-side.
 		 */
 		if (((cp - word) + pattern->rightLen) != 0) {
-		    if (addSpace && varSpace) {
-			Buf_AddByte(buf, varSpace);
+		    if (addSpace && vpstate->varSpace) {
+			Buf_AddByte(buf, vpstate->varSpace);
 		    }
 		    addSpace = TRUE;
 		}
@@ -1089,7 +1142,7 @@ VarSubstitute(GNode *ctx, char *word, Bo
 		cp = Str_FindSubstring(word, pattern->lhs);
 		if (cp != (char *)NULL) {
 		    if (addSpace && (((cp - word) + pattern->rightLen) != 0)){
-			Buf_AddByte(buf, varSpace);
+			Buf_AddByte(buf, vpstate->varSpace);
 			addSpace = FALSE;
 		    }
 		    Buf_AddBytes(buf, cp-word, (const Byte *)word);
@@ -1109,8 +1162,8 @@ VarSubstitute(GNode *ctx, char *word, Bo
 		}
 	    }
 	    if (wordLen != 0) {
-		if (addSpace && varSpace) {
-		    Buf_AddByte(buf, varSpace);
+		if (addSpace && vpstate->varSpace) {
+		    Buf_AddByte(buf, vpstate->varSpace);
 		}
 		Buf_AddBytes(buf, wordLen, (Byte *)word);
 	    }
@@ -1124,8 +1177,8 @@ VarSubstitute(GNode *ctx, char *word, Bo
 	return (addSpace);
     }
  nosub:
-    if (addSpace && varSpace) {
-	Buf_AddByte(buf, varSpace);
+    if (addSpace && vpstate->varSpace) {
+	Buf_AddByte(buf, vpstate->varSpace);
     }
     Buf_AddBytes(buf, wordLen, (Byte *)word);
     return(TRUE);
@@ -1174,7 +1227,8 @@ VarREError(int err, regex_t *pat, const 
  *-----------------------------------------------------------------------
  */
 static Boolean
-VarRESubstitute(GNode *ctx, char *word, Boolean addSpace, Buffer buf,
+VarRESubstitute(GNode *ctx, Var_Parse_State *vpstate,
+		char *word, Boolean addSpace, Buffer buf,
 		ClientData patternp)
 {
     VarREPattern *pat;
@@ -1313,7 +1367,8 @@ VarRESubstitute(GNode *ctx, char *word, 
  *-----------------------------------------------------------------------
  */
 static Boolean
-VarLoopExpand(GNode *ctx, char *word, Boolean addSpace, Buffer buf,
+VarLoopExpand(GNode *ctx, Var_Parse_State *vpstate,
+	      char *word, Boolean addSpace, Buffer buf,
 	      ClientData loopp)
 {
     VarLoop_t	*loop = (VarLoop_t *) loopp;
@@ -1336,6 +1391,70 @@ VarLoopExpand(GNode *ctx, char *word, Bo
 
 /*-
  *-----------------------------------------------------------------------
+ * VarSelectWords --
+ *	Implements the :[start..end] modifier.
+ *
+ * Input:
+ *	word		Word to modify
+ *	addSpace	True if space should be added before
+ *			other characters
+ *	buf		Buffer for result
+ *	datap		Information about which words to select.
+ *
+ * Results:
+ *	TRUE if a space is needed before more characters are added.
+ *
+ * Side Effects:
+ *	None.
+ *
+ *-----------------------------------------------------------------------
+ */
+static Boolean
+VarSelectWords(GNode *ctx, Var_Parse_State *vpstate,
+	      char *word, Boolean addSpace, Buffer buf,
+	      ClientData datap)
+{
+    VarSelectWords_t *seldata = (VarSelectWords_t *) datap;
+    /* We want to count starting from 1.  vpstate->argnum starts from 0. */
+    int argnum = (vpstate->argnum + 1);
+
+    /*
+     * If seldata->start or seldata->end are negative, convert them to
+     * the positive equivalents (-1 gets converted to argc, -2 gets
+     * converted to (argc-1), etc.).
+     *
+     * If they are in the wrong order, swap them.
+     *
+     * It's an error for them to be zero, but we don't test for that.
+     */
+    if (! seldata->sanitised) {
+	if (seldata->start < 0)
+	    seldata->start = vpstate->argc + seldata->start + 1;
+	if (seldata->end < 0)
+	    seldata->end = vpstate->argc + seldata->end + 1;
+	if (seldata->start > seldata->end) {
+	    int tmp = seldata->end;
+	    seldata->end = seldata->start;
+	    seldata->start = tmp;
+	}
+	seldata->sanitised = TRUE;
+    }
+
+    if (word && *word) {
+	if (argnum >= seldata->start && argnum <= seldata->end) {
+	    if (addSpace && vpstate->varSpace) {
+		Buf_AddByte(buf, vpstate->varSpace);
+	    }
+	    Buf_AddBytes(buf, strlen(word), (Byte *)word);
+	    return TRUE;
+	}
+    }
+    return addSpace;
+}
+
+
+/*-
+ *-----------------------------------------------------------------------
  * VarModify --
  *	Modify each of the words of the passed string using the given
  *	function. Used to implement all modifiers.
@@ -1354,25 +1473,39 @@ VarLoopExpand(GNode *ctx, char *word, Bo
  *-----------------------------------------------------------------------
  */
 static char *
-VarModify(GNode *ctx, const char *str,
-    Boolean (*modProc)(GNode *, char *, Boolean, Buffer, ClientData),
+VarModify(GNode *ctx, Var_Parse_State *vpstate,
+    const char *str,
+    Boolean (*modProc)(GNode *, Var_Parse_State *, char *,
+		       Boolean, Buffer, ClientData),
     ClientData datum)
 {
     Buffer  	  buf;	    	    /* Buffer for the new string */
     Boolean 	  addSpace; 	    /* TRUE if need to add a space to the
 				     * buffer before adding the trimmed
 				     * word */
-    char **av;			    /* word list [first word does not count] */
+    char **av;			    /* word list */
     char *as;			    /* word list memory */
     int ac, i;
 
     buf = Buf_Init (0);
     addSpace = FALSE;
 
-    av = brk_string(str, &ac, FALSE, &as);
+    if (vpstate->oneBigWord) {
+	/* fake what brk_string() would do if there were only one word */
+	ac = 1;
+    	av = (char **)emalloc((ac + 1) * sizeof(char *));
+	as = strdup(str);
+	av[0] = as;
+	av[1] = NULL;
+    } else {
+	av = brk_string(str, &ac, FALSE, &as);
+    }
 
-    for (i = 0; i < ac; i++)
-	addSpace = (*modProc)(ctx, av[i], addSpace, buf, datum);
+    vpstate->argc = ac;
+    for (i = 0; i < ac; i++) {
+	vpstate->argnum = i;
+	addSpace = (*modProc)(ctx, vpstate, av[i], addSpace, buf, datum);
+    }
 
     free(as);
     free(av);
@@ -1511,7 +1644,8 @@ VarUniq(const char *str)
  *-----------------------------------------------------------------------
  */
 static char *
-VarGetPattern(GNode *ctxt, int err, const char **tstr, int delim, int *flags,
+VarGetPattern(GNode *ctxt, Var_Parse_State *vpstate,
+	      int err, const char **tstr, int delim, int *flags,
 	      int *length, VarPattern *pattern)
 {
     const char *cp;
@@ -1728,13 +1862,14 @@ Var_Parse(const char *str, GNode *ctxt, 
 				 * expanding it in a non-local context. This
 				 * is done to support dynamic sources. The
 				 * result is just the invocation, unaltered */
+    Var_Parse_State parsestate = {0}; /* Flags passed to helper functions */
 
     *freePtr = FALSE;
     dynamic = FALSE;
     start = str;
+    parsestate.oneBigWord = FALSE;
+    parsestate.varSpace = ' ';	/* word separator */
 
-    varSpace = ' ';			/* reset this */
-    
     if (str[1] != '(' && str[1] != '{') {
 	/*
 	 * If it's not bounded by braces of some sort, life is much simpler.
@@ -1864,9 +1999,11 @@ Var_Parse(const char *str, GNode *ctxt, 
 			val = (char *)Buf_GetAll(v->val, (int *)NULL);
 
 			if (str[1] == 'D') {
-			    val = VarModify(ctxt, val, VarHead, (ClientData)0);
+			    val = VarModify(ctxt, &parsestate, val, VarHead,
+					    (ClientData)0);
 			} else {
-			    val = VarModify(ctxt, val, VarTail, (ClientData)0);
+			    val = VarModify(ctxt, &parsestate, val, VarTail,
+					    (ClientData)0);
 			}
 			/*
 			 * Resulting string is dynamically allocated, so
@@ -1986,9 +2123,9 @@ Var_Parse(const char *str, GNode *ctxt, 
      *  	  	    	<pattern> is of the standard file
      *  	  	    	wildcarding form.
      *  	  :N<pattern>	words which do not match the given <pattern>.
-     *  	  :S<d><pat1><d><pat2><d>[g]
+     *  	  :S<d><pat1><d><pat2><d>[1gW]
      *  	  	    	Substitute <pat2> for <pat1> in the value
-     *  	  :C<d><pat1><d><pat2><d>[g]
+     *  	  :C<d><pat1><d><pat2><d>[1gW]
      *  	  	    	Substitute <pat2> for regex <pat1> in the value
      *  	  :H	    	Substitute the head of each word
      *  	  :T	    	Substitute the tail of each word
@@ -2003,6 +2140,20 @@ Var_Parse(const char *str, GNode *ctxt, 
      *		  :ts[c]	Sets varSpace - the char used to
      *				separate words to 'c'. If 'c' is
      *				omitted then no separation is used.
+     *		  :tW		Treat the variable contents as a single
+     *				word, even if it contains spaces.
+     *				(Mnemonic: one big 'W'ord.)
+     *		  :tw		Treat the variable contents as multiple
+     *				space-separated words.
+     *				(Mnemonic: many small 'w'ords.)
+     *		  :[index]	Select a single word from the value.
+     *		  :[start..end]	Select multiple words from the value.
+     *		  :[*] or :[0]	Select the entire value, as a single
+     *				word.  Equivalent to :tW.
+     *		  :[@]		Select the entire value, as multiple
+     *				words.	Undoes the effect of :[*].
+     *				Equivalent to :tw.
+     *		  :[#]		Returns the number of words in the value.
      *
      *		  :?<true-value>:<false-value>
      *				If the variable evaluates to true, return
@@ -2066,10 +2217,13 @@ Var_Parse(const char *str, GNode *ctxt, 
 	    newStr = var_Error;
 	    switch (*tstr) {
 	        case ':':
-		    
+		{
 		if (tstr[1] == '=' ||
 		    (tstr[2] == '=' &&
 		     (tstr[1] == '!' || tstr[1] == '+' || tstr[1] == '?'))) {
+		    /*
+		     * "::=", "::!=", "::+=", or "::?="
+		     */
 		    GNode *v_ctxt;		/* context where v belongs */
 		    const char *emsg;
 		    VarPattern	pattern;
@@ -2104,8 +2258,10 @@ Var_Parse(const char *str, GNode *ctxt, 
 		    delim = '}';
 		    pattern.flags = 0;
 
-		    if ((pattern.rhs = VarGetPattern(ctxt, err, &cp, delim,
-						     NULL, &pattern.rightLen, NULL)) == NULL) {
+		    if ((pattern.rhs = VarGetPattern(ctxt, &parsestate, err,
+						     &cp, delim, NULL,
+						     &pattern.rightLen,
+						     NULL)) == NULL) {
 			if (v->flags & VAR_JUNK) {
 			    free(v->name);
 			    v->name = nstr;
@@ -2143,8 +2299,9 @@ Var_Parse(const char *str, GNode *ctxt, 
 		    free(UNCONST(pattern.rhs));
 		    newStr = var_Error;
 		    break;
+		}
+		goto default_case; /* "::<unrecognised>" */
 		}
-		goto default_case;
 	        case '@':
 		{
 		    VarLoop_t	loop;
@@ -2152,12 +2309,14 @@ Var_Parse(const char *str, GNode *ctxt, 
 
 		    cp = ++tstr;
 		    delim = '@';
-		    if ((loop.tvar = VarGetPattern(ctxt, err, &cp, delim,
+		    if ((loop.tvar = VarGetPattern(ctxt, &parsestate, err,
+						   &cp, delim,
 						   &flags, &loop.tvarLen,
 						   NULL)) == NULL)
 			goto cleanup;
 
-		    if ((loop.str = VarGetPattern(ctxt, err, &cp, delim,
+		    if ((loop.str = VarGetPattern(ctxt, &parsestate, err,
+						  &cp, delim,
 						  &flags, &loop.strLen,
 						  NULL)) == NULL)
 			goto cleanup;
@@ -2167,7 +2326,7 @@ Var_Parse(const char *str, GNode *ctxt, 
 
 		    loop.err = err;
 		    loop.ctxt = ctxt;
-		    newStr = VarModify(ctxt, nstr, VarLoopExpand,
+		    newStr = VarModify(ctxt, &parsestate, nstr, VarLoopExpand,
 				       (ClientData)&loop);
 		    free(loop.tvar);
 		    free(loop.str);
@@ -2268,8 +2427,10 @@ Var_Parse(const char *str, GNode *ctxt, 
 		    delim = '!';
 
 		    cp = ++tstr;
-		    if ((pattern.rhs = VarGetPattern(ctxt, err, &cp, delim,
-						     NULL, &pattern.rightLen, NULL)) == NULL)
+		    if ((pattern.rhs = VarGetPattern(ctxt, &parsestate, err,
+						     &cp, delim,
+						     NULL, &pattern.rightLen,
+						     NULL)) == NULL)
 			goto cleanup;
 		    newStr = Cmd_Exec (pattern.rhs, &emsg);
 		    free(UNCONST(pattern.rhs));
@@ -2282,6 +2443,132 @@ Var_Parse(const char *str, GNode *ctxt, 
 		    }
 		    break;
 		}
+		case '[':
+		{
+		    /*
+		     * Look for the closing ']', recursively
+		     * expanding any embedded variables.
+		     *
+		     * estr is a pointer to the expanded result,
+		     * which we must free().
+		     */
+		    char *estr;
+
+		    cp = tstr+1; /* point to char after '[' */
+		    delim = ']'; /* look for closing ']' */
+		    estr = VarGetPattern(ctxt, &parsestate,
+					 err, &cp, delim,
+					 NULL, NULL, NULL);
+		    if (estr == NULL)
+			goto cleanup; /* report missing ']' */
+		    /* now cp points just after the closing ']' */
+		    delim = '\0';
+		    if (cp[0] != ':' && cp[0] != endc) {
+			/* Found junk after ']' */
+			free(estr);
+			goto bad_modifier;
+		    }
+		    if (estr[0] == '\0') {
+			/* Found empty square brackets in ":[]". */
+			free(estr);
+			goto bad_modifier;
+		    } else if (estr[0] == '#' && estr[1] == '\0') {
+			/* Found ":[#]" */
+			if (parsestate.oneBigWord)
+			    asprintf(&newStr, "1");
+			else {
+			    /* XXX: brk_string() is a rather expensive
+			     * way of counting words. */
+			    char **av;
+			    char *as;
+			    int ac;
+
+			    av = brk_string(nstr, &ac, FALSE, &as);
+			    asprintf(&newStr, "%d", ac);
+			    free(as);
+			    free(av);
+			}
+			termc = *cp;
+			free(estr);
+			break;
+		    } else if (estr[0] == '*' && estr[1] == '\0') {
+			/* Found ":[*]" */
+			parsestate.oneBigWord = TRUE;
+			newStr = nstr;
+			termc = *cp;
+			free(estr);
+			break;
+		    } else if (estr[0] == '@' && estr[1] == '\0') {
+			/* Found ":[@]" */
+			parsestate.oneBigWord = FALSE;
+			newStr = nstr;
+			termc = *cp;
+			free(estr);
+			break;
+		    } else {
+			/*
+			 * We expect estr to contain a single
+			 * integer for :[N], or two integers
+			 * separated by ".." for :[start..end].
+			 */
+			 char *ep;
+			 VarSelectWords_t seldata = {0};
+
+			 seldata.start = strtol(estr, &ep, 0);
+			 if (ep == estr) {
+			    /* Found junk instead of a number */
+			    free(estr);
+			    goto bad_modifier;
+			 } else if (ep[0] == '\0') {
+			     /* Found only one integer in :[N] */
+			     seldata.end = seldata.start;
+			 } else if (ep[0] == '.' && ep[1] == '.' &&
+				    ep[2] != '\0') {
+			     /* Expecting another integer after ".." */
+			     ep += 2;
+			     seldata.end = strtol(ep, &ep, 0);
+			     if (ep[0] != '\0') {
+				 /* Found junk after ".." */
+				 free(estr);
+				 goto bad_modifier;
+			     }
+			 } else {
+			     /* Found junk instead of ".." */
+			     free(estr);
+			     goto bad_modifier;
+			 }
+			 /*
+			  * Now seldata is properly filled in,
+			  * but we still have to check for 0 as
+			  * a special case.
+			  */
+			 if (seldata.start == 0 && seldata.end==0) {
+			     /* ":[0]" or perhaps ":[0..0]" */
+			     parsestate.oneBigWord = TRUE;
+			     newStr = nstr;
+			     termc = *cp;
+			     free(estr);
+			     break;
+			 } else if (seldata.start == 0 ||
+				    seldata.end == 0) {
+			     /* ":[0..N]" or ":[N..0]" */
+			     free(estr);
+			     goto bad_modifier;
+			 }
+			/*
+			 * Normal case: select the words
+			 * described by seldata.
+			 */
+			 newStr = VarModify(ctxt, &parsestate,
+					nstr,
+					VarSelectWords,
+					(ClientData)&seldata);
+			 termc = *cp;
+			 free(estr);
+			 break;
+		    }
+
+		}
 	        case 't':
 		{
 		    cp = tstr + 1;	/* make sure it is set */
@@ -2295,34 +2582,47 @@ Var_Parse(const char *str, GNode *ctxt, 
 
 			    if (tstr[2] != endc &&
 				(tstr[3] == endc || tstr[3] == ':')) {
-				varSpace = tstr[2];
+				/* ":ts<unrecognised><endc>" or
+				 * ":ts<unrecognised>:" */
+				parsestate.varSpace = tstr[2];
 				cp = tstr + 3;
 			    } else if (tstr[2] == endc || tstr[2] == ':') {
-				varSpace = 0; /* no separator */
+				/* ":ts<endc>" or ":ts:" */
+				parsestate.varSpace = 0; /* no separator */
 				cp = tstr + 2;
 			    } else if (tstr[2] == '\\') {
 				switch (tstr[3]) {
 				case 'n':
-				    varSpace = '\n';
+				    parsestate.varSpace = '\n';
 				    cp = tstr + 4;
 				    break;
 				case 't':
-				    varSpace = '\t';
+				    parsestate.varSpace = '\t';
 				    cp = tstr + 4;
 				    break;
 				default:
 				    if (isdigit(tstr[3])) {
 					char *ep;
 					
-					varSpace = strtoul(&tstr[3], &ep, 0);
+					parsestate.varSpace =
+						strtoul(&tstr[3], &ep, 0);
+					if (*ep != ':' && *ep != endc)
+					    goto bad_modifier;
 					cp = ep;
 				    } else {
+					/*
+					 * ":ts<backslash><unrecognised>".
+					 */
 					goto bad_modifier;
 				    }
 				    break;
 				}
-			    } else				
+			    } else {
+				/*
+				 * Found ":ts<unrecognised><unrecognised>".
+				 */
 				break;	/* not us */
+			    }
 
 			    termc = *cp;
 
@@ -2337,17 +2637,39 @@ Var_Parse(const char *str, GNode *ctxt, 
 			    pattern.lhs = pattern.rhs = "\032";
 			    pattern.leftLen = pattern.rightLen = 1;
 
-			    newStr = VarModify(ctxt, nstr, VarSubstitute,
+			    newStr = VarModify(ctxt, &parsestate, nstr,
+					       VarSubstitute,
 					       (ClientData)&pattern);
 			} else if (tstr[2] == endc || tstr[2] == ':') {
+			    /*
+			     * Check for two-character options:
+			     * ":tu", ":tl"
+			     */
                             if (tstr[1] == 'u' || tstr[1] == 'l') {
                                 newStr = VarChangeCase (nstr, (tstr[1] == 'u'));
                                 cp = tstr + 2;
                                 termc = *cp;
+                            } else if (tstr[1] == 'W' || tstr[1] == 'w') {
+				parsestate.oneBigWord = (tstr[1] == 'W');
+				newStr = nstr;
+				cp = tstr + 2;
+				termc = *cp;
                             } else {
+				/* Found ":t<unrecognised>:" or
+				 * ":t<unrecognised><endc>". */
 				goto bad_modifier;
 			    }
+			} else {
+			    /*
+			     * Found ":t<unrecognised><unrecognised>".
+			     * Should this be an error?
+			     */
 			}
+		    } else {
+			/*
+			 * Found ":t<endc>" or ":t:".
+			 * Should this be an error?
+			 */
 		    }
 		    break;
 		}
@@ -2413,9 +2735,10 @@ Var_Parse(const char *str, GNode *ctxt, 
 			copy = TRUE;
 		    }
 		    if (*tstr == 'M' || *tstr == 'm') {
-			newStr = VarModify(ctxt, nstr, VarMatch, (ClientData)pattern);
+			newStr = VarModify(ctxt, &parsestate, nstr, VarMatch,
+					   (ClientData)pattern);
 		    } else {
-			newStr = VarModify(ctxt, nstr, VarNoMatch,
+			newStr = VarModify(ctxt, &parsestate, nstr, VarNoMatch,
 					   (ClientData)pattern);
 		    }
 		    if (copy) {
@@ -2426,8 +2749,10 @@ Var_Parse(const char *str, GNode *ctxt, 
 		case 'S':
 		{
 		    VarPattern 	    pattern;
+		    Var_Parse_State tmpparsestate;
 
 		    pattern.flags = 0;
+		    tmpparsestate = parsestate;
 		    delim = tstr[1];
 		    tstr += 2;
 
@@ -2441,12 +2766,17 @@ Var_Parse(const char *str, GNode *ctxt, 
 		    }
 
 		    cp = tstr;
-		    if ((pattern.lhs = VarGetPattern(ctxt, err, &cp, delim,
-			&pattern.flags, &pattern.leftLen, NULL)) == NULL)
+		    if ((pattern.lhs = VarGetPattern(ctxt, &parsestate, err,
+						     &cp, delim,
+						     &pattern.flags,
+						     &pattern.leftLen,
+						     NULL)) == NULL)
 			goto cleanup;
 
-		    if ((pattern.rhs = VarGetPattern(ctxt, err, &cp, delim,
-			NULL, &pattern.rightLen, &pattern)) == NULL)
+		    if ((pattern.rhs = VarGetPattern(ctxt, &parsestate, err,
+						     &cp, delim, NULL,
+						     &pattern.rightLen,
+						     &pattern)) == NULL)
 			goto cleanup;
 
 		    /*
@@ -2462,12 +2792,16 @@ Var_Parse(const char *str, GNode *ctxt, 
 			case '1':
 			    pattern.flags |= VAR_SUB_ONE;
 			    continue;
+			case 'W':
+			    tmpparsestate.oneBigWord = TRUE;
+			    continue;
 			}
 			break;
 		    }
 
 		    termc = *cp;
-		    newStr = VarModify(ctxt, nstr, VarSubstitute,
+		    newStr = VarModify(ctxt, &tmpparsestate, nstr,
+				       VarSubstitute,
 				       (ClientData)&pattern);
 
 		    /*
@@ -2489,14 +2823,18 @@ Var_Parse(const char *str, GNode *ctxt, 
 
 		    cp = ++tstr;
 		    delim = ':';
-		    if ((pattern.lhs = VarGetPattern(ctxt, err, &cp, delim,
-			NULL, &pattern.leftLen, NULL)) == NULL)
+		    if ((pattern.lhs = VarGetPattern(ctxt, &parsestate, err,
+						     &cp, delim, NULL,
+						     &pattern.leftLen,
+						     NULL)) == NULL)
 			goto cleanup;
 
 			/* '{' */
 		    delim = '}';
-		    if ((pattern.rhs = VarGetPattern(ctxt, err, &cp, delim,
-			NULL, &pattern.rightLen, NULL)) == NULL)
+		    if ((pattern.rhs = VarGetPattern(ctxt, &parsestate, err,
+						     &cp, delim, NULL,
+						     &pattern.rightLen,
+						     NULL)) == NULL)
 			goto cleanup;
 
 		    termc = *--cp;
@@ -2523,19 +2861,22 @@ Var_Parse(const char *str, GNode *ctxt, 
 		    VarREPattern    pattern;
 		    char           *re;
 		    int             error;
+		    Var_Parse_State tmpparsestate;
 
 		    pattern.flags = 0;
+		    tmpparsestate = parsestate;
 		    delim = tstr[1];
 		    tstr += 2;
 
 		    cp = tstr;
 
-		    if ((re = VarGetPattern(ctxt, err, &cp, delim, NULL,
-			NULL, NULL)) == NULL)
+		    if ((re = VarGetPattern(ctxt, &parsestate, err, &cp, delim,
+					    NULL, NULL, NULL)) == NULL)
 			goto cleanup;
 
-		    if ((pattern.replace = VarGetPattern(ctxt, err, &cp,
-			delim, NULL, NULL, NULL)) == NULL){
+		    if ((pattern.replace = VarGetPattern(ctxt, &parsestate,
+							 err, &cp, delim, NULL,
+							 NULL, NULL)) == NULL){
 			free(re);
 			goto cleanup;
 		    }
@@ -2548,6 +2889,9 @@ Var_Parse(const char *str, GNode *ctxt, 
 			case '1':
 			    pattern.flags |= VAR_SUB_ONE;
 			    continue;
+			case 'W':
+			    tmpparsestate.oneBigWord = TRUE;
+			    continue;
 			}
 			break;
 		    }
@@ -2560,7 +2904,6 @@ Var_Parse(const char *str, GNode *ctxt, 
 			*lengthPtr = cp - start + 1;
 			VarREError(error, &pattern.re, "RE substitution error");
 			free(pattern.replace);
-			varSpace = ' ';	/* reset this */
 			return (var_Error);
 		    }
 
@@ -2571,7 +2914,8 @@ Var_Parse(const char *str, GNode *ctxt, 
 			pattern.nsub = 10;
 		    pattern.matches = emalloc(pattern.nsub *
 					      sizeof(regmatch_t));
-		    newStr = VarModify(ctxt, nstr, VarRESubstitute,
+		    newStr = VarModify(ctxt, &tmpparsestate, nstr,
+				       VarRESubstitute,
 				       (ClientData) &pattern);
 		    regfree(&pattern.re);
 		    free(pattern.replace);
@@ -2590,7 +2934,8 @@ Var_Parse(const char *str, GNode *ctxt, 
 		    /*FALLTHRU*/
 		case 'T':
 		    if (tstr[1] == endc || tstr[1] == ':') {
-			newStr = VarModify(ctxt, nstr, VarTail, (ClientData)0);
+			newStr = VarModify(ctxt, &parsestate, nstr, VarTail,
+					   (ClientData)0);
 			cp = tstr + 1;
 			termc = *cp;
 			break;
@@ -2598,7 +2943,8 @@ Var_Parse(const char *str, GNode *ctxt, 
 		    /*FALLTHRU*/
 		case 'H':
 		    if (tstr[1] == endc || tstr[1] == ':') {
-			newStr = VarModify(ctxt, nstr, VarHead, (ClientData)0);
+			newStr = VarModify(ctxt, &parsestate, nstr, VarHead,
+					   (ClientData)0);
 			cp = tstr + 1;
 			termc = *cp;
 			break;
@@ -2606,7 +2952,8 @@ Var_Parse(const char *str, GNode *ctxt, 
 		    /*FALLTHRU*/
 		case 'E':
 		    if (tstr[1] == endc || tstr[1] == ':') {
-			newStr = VarModify(ctxt, nstr, VarSuffix, (ClientData)0);
+			newStr = VarModify(ctxt, &parsestate, nstr, VarSuffix,
+					   (ClientData)0);
 			cp = tstr + 1;
 			termc = *cp;
 			break;
@@ -2614,7 +2961,8 @@ Var_Parse(const char *str, GNode *ctxt, 
 		    /*FALLTHRU*/
 		case 'R':
 		    if (tstr[1] == endc || tstr[1] == ':') {
-			newStr = VarModify(ctxt, nstr, VarRoot, (ClientData)0);
+			newStr = VarModify(ctxt, &parsestate, nstr, VarRoot,
+					   (ClientData)0);
 			cp = tstr + 1;
 			termc = *cp;
 			break;
@@ -2710,7 +3058,8 @@ Var_Parse(const char *str, GNode *ctxt, 
 			 * SYSV modifications happen through the whole
 			 * string. Note the pattern is anchored at the end.
 			 */
-			newStr = VarModify(ctxt, nstr, VarSYSVMatch,
+			newStr = VarModify(ctxt, &parsestate, nstr,
+					   VarSYSVMatch,
 					   (ClientData)&pattern);
 
 			/*
@@ -2804,15 +3153,14 @@ Var_Parse(const char *str, GNode *ctxt, 
 	free((Address)v->name);
 	free((Address)v);
     }
-    varSpace = ' ';			/* reset this */
     return (nstr);
 
  bad_modifier:
+							/* "{(" */
     Error("Bad modifier `:%.*s' for %s", (int)strcspn(tstr, ":)}"), tstr,
 	  v->name);
 
 cleanup:
-    varSpace = ' ';			/* reset this */
     *lengthPtr = cp - start + 1;
     if (*freePtr)
 	free(nstr);
Index: unit-tests/Makefile
===================================================================
RCS file: /cvsroot/src/usr.bin/make/unit-tests/Makefile,v
retrieving revision 1.7
diff -u -p -r1.7 Makefile
--- unit-tests/Makefile	2003/08/08 06:42:38	1.7
+++ unit-tests/Makefile	2003/09/23 18:54:37
@@ -16,17 +16,22 @@
 
 UNIT_TESTS:= ${.PARSEDIR}
 
-all: mod-ts varcmd
+all: mod-ts varcmd modword
 
 LIST= one two three
 LIST+= four five six
 
 FU_mod-ts = a / b / cool
 
+AAA= a a a
+B.aaa= Baaa
+
 mod-ts:
 	@echo 'LIST="${LIST}"'
 	@echo 'LIST:ts,="${LIST:ts,}"'
 	@echo 'LIST:ts/:tu="${LIST:ts/:tu}"'
+	@echo 'LIST:ts::tu="${LIST:ts::tu}"'
+	@echo 'LIST:ts:tu="${LIST:ts:tu}"'
 	@echo 'LIST:tu:ts/="${LIST:tu:ts/}"'
 	@echo 'LIST:ts:="${LIST:ts:}"'
 	@echo 'LIST:ts="${LIST:ts}"'
@@ -41,10 +46,12 @@ mod-ts:
 	@echo 'LIST:ts/x:tu="${LIST:ts\x:tu}"'
 	@echo 'FU_$@="${FU_${@:ts}:ts}"'
 	@echo 'FU_$@:ts:T="${FU_${@:ts}:ts:T}" == cool?'
+	@echo 'B.$${AAA:ts}="${B.${AAA:ts}}" == Baaa?'
 
-.PHONY: varcmd
-varcmd:
-	@${.MAKE} -f ${UNIT_TESTS}/varcmd
+# Some tests are best handled via a sub-make
+.PHONY: varcmd modword
+varcmd modword:
+	@${.MAKE} -f ${UNIT_TESTS}/$@
 
 clean:
 	rm -f *.out *.fail *.core

--- /dev/null	Tue Sep 23 11:54:13 2003
+++ unit-tests/modword	Mon Sep 22 12:18:12 2003
@@ -0,0 +1,149 @@
+# $Id: varcmd,v 1.1 2003/07/31 00:46:15 sjg Exp $
+#
+# Test behaviour of new :[] modifier
+
+all: mod-squarebrackets mod-S-W mod-C-W mod-tW-tw
+
+LIST= one two three
+LIST+= four five six
+LONGLIST= 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
+
+EMPTY= # the space should be ignored
+ESCAPEDSPACE=\ # escaped space before the '#'
+REALLYSPACE:=${EMPTY:C/^/ /W}
+HASH= \#
+AT= @
+STAR= *
+ZERO= 0
+ONE= 1
+MINUSONE= -1
+
+mod-squarebrackets: mod-squarebrackets-0-star-at \
+	mod-squarebrackets-hash \
+	mod-squarebrackets-n \
+	mod-squarebrackets-start-end \
+	mod-squarebrackets-nested
+
+mod-squarebrackets-0-star-at:
+	@echo 'LIST:[]="${LIST:[]}" is an error'
+	@echo 'LIST:[0]="${LIST:[0]}"'
+	@echo 'LIST:[0x0]="${LIST:[0x0]}"'
+	@echo 'LIST:[000]="${LIST:[000]}"'
+	@echo 'LIST:[*]="${LIST:[*]}"'
+	@echo 'LIST:[@]="${LIST:[@]}"'
+	@echo 'LIST:[0]:C/ /,/="${LIST:[0]:C/ /,/}"'
+	@echo 'LIST:[0]:C/ /,/g="${LIST:[0]:C/ /,/g}"'
+	@echo 'LIST:[0]:C/ /,/1g="${LIST:[0]:C/ /,/1g}"'
+	@echo 'LIST:[*]:C/ /,/="${LIST:[*]:C/ /,/}"'
+	@echo 'LIST:[*]:C/ /,/g="${LIST:[*]:C/ /,/g}"'
+	@echo 'LIST:[*]:C/ /,/1g="${LIST:[*]:C/ /,/1g}"'
+	@echo 'LIST:[@]:C/ /,/="${LIST:[@]:C/ /,/}"'
+	@echo 'LIST:[@]:C/ /,/g="${LIST:[@]:C/ /,/g}"'
+	@echo 'LIST:[@]:C/ /,/1g="${LIST:[@]:C/ /,/1g}"'
+	@echo 'LIST:[@]:[0]:C/ /,/="${LIST:[@]:[0]:C/ /,/}"'
+	@echo 'LIST:[0]:[@]:C/ /,/="${LIST:[0]:[@]:C/ /,/}"'
+	@echo 'LIST:[@]:[*]:C/ /,/="${LIST:[@]:[*]:C/ /,/}"'
+	@echo 'LIST:[*]:[@]:C/ /,/="${LIST:[*]:[@]:C/ /,/}"'
+
+mod-squarebrackets-hash:
+	@echo 'EMPTY="${EMPTY}"'
+	@echo 'EMPTY:[#]="${EMPTY:[#]}" == 1 ?'
+	@echo 'ESCAPEDSPACE="${ESCAPEDSPACE}"'
+	@echo 'ESCAPEDSPACE:[#]="${ESCAPEDSPACE:[#]}" == 1 ?'
+	@echo 'REALLYSPACE="${REALLYSPACE}"'
+	@echo 'REALLYSPACE:[#]="${REALLYSPACE:[#]}" == 1 ?'
+	@echo 'LIST:[#]="${LIST:[#]}"'
+	@echo 'LIST:[0]:[#]="${LIST:[0]:[#]}" == 1 ?'
+	@echo 'LIST:[*]:[#]="${LIST:[*]:[#]}" == 1 ?'
+	@echo 'LIST:[@]:[#]="${LIST:[@]:[#]}"'
+	@echo 'LIST:[1]:[#]="${LIST:[1]:[#]}"'
+	@echo 'LIST:[1..3]:[#]="${LIST:[1..3]:[#]}"'
+
+mod-squarebrackets-n:
+	@echo 'EMPTY:[1]="${EMPTY:[1]}"'
+	@echo 'ESCAPEDSPACE="${ESCAPEDSPACE}"'
+	@echo 'ESCAPEDSPACE:[1]="${ESCAPEDSPACE:[1]}"'
+	@echo 'REALLYSPACE="${REALLYSPACE}"'
+	@echo 'REALLYSPACE:[1]="${REALLYSPACE:[1]}" == "" ?'
+	@echo 'REALLYSPACE:[*]:[1]="${REALLYSPACE:[*]:[1]}" == " " ?'
+	@echo 'LIST:[1]="${LIST:[1]}"'
+	@echo 'LIST:[1.]="${LIST:[1.]}" is an error'
+	@echo 'LIST:[1].="${LIST:[1].}" is an error'
+	@echo 'LIST:[2]="${LIST:[2]}"'
+	@echo 'LIST:[6]="${LIST:[6]}"'
+	@echo 'LIST:[7]="${LIST:[7]}"'
+	@echo 'LIST:[999]="${LIST:[999]}"'
+	@echo 'LIST:[-]="${LIST:[-]}" is an error'
+	@echo 'LIST:[--]="${LIST:[--]}" is an error'
+	@echo 'LIST:[-1]="${LIST:[-1]}"'
+	@echo 'LIST:[-2]="${LIST:[-2]}"'
+	@echo 'LIST:[-6]="${LIST:[-6]}"'
+	@echo 'LIST:[-7]="${LIST:[-7]}"'
+	@echo 'LIST:[-999]="${LIST:[-999]}"'
+	@echo 'LONGLIST:[17]="${LONGLIST:[17]}"'
+	@echo 'LONGLIST:[0x11]="${LONGLIST:[0x11]}"'
+	@echo 'LONGLIST:[021]="${LONGLIST:[021]}"'
+	@echo 'LIST:[0]:[1]="${LIST:[0]:[1]}"'
+	@echo 'LIST:[*]:[1]="${LIST:[*]:[1]}"'
+	@echo 'LIST:[@]:[1]="${LIST:[@]:[1]}"'
+	@echo 'LIST:[0]:[2]="${LIST:[0]:[2]}"'
+	@echo 'LIST:[*]:[2]="${LIST:[*]:[2]}"'
+	@echo 'LIST:[@]:[2]="${LIST:[@]:[2]}"'
+	@echo 'LIST:[*]:C/ /,/:[2]="${LIST:[*]:C/ /,/:[2]}"'
+	@echo 'LIST:[*]:C/ /,/:[*]:[2]="${LIST:[*]:C/ /,/:[*]:[2]}"'
+	@echo 'LIST:[*]:C/ /,/:[@]:[2]="${LIST:[*]:C/ /,/:[@]:[2]}"'
+
+mod-squarebrackets-start-end:
+	@echo 'LIST:[1.]="${LIST:[1.]}" is an error'
+	@echo 'LIST:[1..]="${LIST:[1..]}" is an error'
+	@echo 'LIST:[1..1]="${LIST:[1..1]}"'
+	@echo 'LIST:[1..1.]="${LIST:[1..1.]}" is an error'
+	@echo 'LIST:[1..2]="${LIST:[1..2]}"'
+	@echo 'LIST:[2..1]="${LIST:[2..1]}"'
+	@echo 'LIST:[3..-2]="${LIST:[3..-2]}"'
+	@echo 'LIST:[-2..3]="${LIST:[-2..3]}"'
+	@echo 'LIST:[0..1]="${LIST:[0..1]}" is an error'
+	@echo 'LIST:[-1..0]="${LIST:[-1..0]}" is an error'
+	@echo 'LIST:[0..0]="${LIST:[0..0]}"'
+	@echo 'LIST:[3..99]="${LIST:[3..99]}"'
+	@echo 'LIST:[-3..-99]="${LIST:[-3..-99]}"'
+
+mod-squarebrackets-nested:
+	@echo 'HASH="${HASH}" == "#" ?'
+	@echo 'LIST:[$${HASH}]="${LIST:[${HASH}]}"'
+	@echo 'LIST:[$${ZERO}]="${LIST:[${ZERO}]}"'
+	@echo 'LIST:[$${ZERO}x$${ONE}]="${LIST:[${ZERO}x${ONE}]}"'
+	@echo 'LIST:[$${ONE}]="${LIST:[${ONE}]}"'
+	@echo 'LIST:[$${MINUSONE}]="${LIST:[${MINUSONE}]}"'
+	@echo 'LIST:[$${STAR}]="${LIST:[${STAR}]}"'
+	@echo 'LIST:[$${AT}]="${LIST:[${AT}]}"'
+	@echo 'LIST:[$${EMPTY}]="${LIST:[${EMPTY}]}" is an error'
+	@echo 'LIST:[$${LONGLIST:[21]:S/2//}]="${LIST:[${LONGLIST:[21]:S/2//}]}"'
+	@echo 'LIST:[$${LIST:[#]}]="${LIST:[${LIST:[#]}]}"'
+	@echo 'LIST:[$${LIST:[$${HASH}]}]="${LIST:[${LIST:[${HASH}]}]}"'
+
+mod-C-W:
+	@echo 'LIST:C/ /,/="${LIST:C/ /,/}"'
+	@echo 'LIST:C/ /,/W="${LIST:C/ /,/W}"'
+	@echo 'LIST:C/ /,/gW="${LIST:C/ /,/gW}"'
+	@echo 'EMPTY:C/^/,/="${EMPTY:C/^/,/}"'
+	@echo 'EMPTY:C/^/,/W="${EMPTY:C/^/,/W}"'
+
+mod-S-W:
+	@echo 'LIST:S/ /,/="${LIST:S/ /,/}"'
+	@echo 'LIST:S/ /,/W="${LIST:S/ /,/W}"'
+	@echo 'LIST:S/ /,/gW="${LIST:S/ /,/gW}"'
+	@echo 'EMPTY:S/^/,/="${EMPTY:S/^/,/}"'
+	@echo 'EMPTY:S/^/,/W="${EMPTY:S/^/,/W}"'
+
+mod-tW-tw:
+	@echo 'LIST:tW="${LIST:tW}"'
+	@echo 'LIST:tw="${LIST:tw}"'
+	@echo 'LIST:tW:C/ /,/="${LIST:tW:C/ /,/}"'
+	@echo 'LIST:tW:C/ /,/g="${LIST:tW:C/ /,/g}"'
+	@echo 'LIST:tW:C/ /,/1g="${LIST:tW:C/ /,/1g}"'
+	@echo 'LIST:tw:C/ /,/="${LIST:tw:C/ /,/}"'
+	@echo 'LIST:tw:C/ /,/g="${LIST:tw:C/ /,/g}"'
+	@echo 'LIST:tw:C/ /,/1g="${LIST:tw:C/ /,/1g}"'
+	@echo 'LIST:tw:tW:C/ /,/="${LIST:tw:tW:C/ /,/}"'
+	@echo 'LIST:tW:tw:C/ /,/="${LIST:tW:tw:C/ /,/}"'