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/ /,/}"'