Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

Re: globbing in conditional expressions



On Fri, 30 May 2014 20:19:42 +0100
Peter Stephenson <p.w.stephenson@xxxxxxxxxxxx> wrote:
> On Fri, 30 May 2014 19:57:34 +0100
> Peter Stephenson <p.w.stephenson@xxxxxxxxxxxx> wrote:
> > One thing we could do is make [[ ... = ... ]] expand its right hand
> > argument the same way as normal command arguments when told to do so, so
> > there's no retokenization.
> 
> No reason not to do this for all string arguments, I suppose --- there's
> nothing here that's specific to pattern matching.

Here's a finalish patch with test and documentation, if anyone wants to
comment.  Haven't thought of a good reason not to do this, myself.

diff --git a/Doc/Zsh/cond.yo b/Doc/Zsh/cond.yo
index 26c0eaa..d04ceb2 100644
--- a/Doc/Zsh/cond.yo
+++ b/Doc/Zsh/cond.yo
@@ -196,8 +196,34 @@ where possible.
 Normal shell expansion is performed on the var(file), var(string) and
 var(pattern) arguments, but the result of each expansion is constrained to
 be a single word, similar to the effect of double quotes.
-Filename generation is not performed on any form of argument to conditions.
-However, pattern metacharacters are active for the var(pattern) arguments;
+
+Filename generation is not performed on any form of argument to
+conditions.  However, it can be forced in any case where normal shell
+expansion is valid and when the option tt(EXTENDED_GLOB) is in effect by
+using an explicit glob qualifier of the form tt(LPAR()#q+RPAR()) at the
+end of the string.  A normal glob qualifier expression may appear
+between the `tt(q)' and the closing parenthesis; if none appears the
+expression has no effect beyond causing filename generation.  The
+results of filename generation are joined together to form a single
+word, as with the results of other forms of expansion.
+
+This special use of filename generation is only available with the
+tt([[) syntax.  If the condition occurs within the tt([) or tt(test)
+builtin commands then globbing occurs instead as part of normal command
+line expansion before the condition is evaluated.  In this case it may
+generate multiple words which are likely to confuse the syntax of the
+test command.
+
+For example,
+
+tt([[ -n file*(#qN) ]])
+
+produces status zero if and only if there is at least one file in the
+current directory beginning with the string `tt(file)'.  The globbing
+qualifier tt(N) ensures that the expression is empty if there is
+no matching file.
+
+Pattern metacharacters are active for the var(pattern) arguments;
 the patterns are the same as those used for filename generation, see
 ifzman(\
 zmanref(zshexpn)\
diff --git a/Doc/Zsh/expn.yo b/Doc/Zsh/expn.yo
index de0f454..6de73ea 100644
--- a/Doc/Zsh/expn.yo
+++ b/Doc/Zsh/expn.yo
@@ -2305,7 +2305,11 @@ contained within it are balanced; appearance of `tt(|)', `tt(LPAR())' or
 recognised in this form even if a bare glob qualifier exists at the end of
 the pattern, for example `tt(*(#q*)(.))' will recognise executable regular
 files if both options are set; however, mixed syntax should probably be
-avoided for the sake of clarity.
+avoided for the sake of clarity.  Note that within conditions using the
+`tt([[)' form the presence of a parenthesised expression
+tt(LPAR()#q...+RPAR()) at the end of a string indicates that globbing
+should be performed; the expression may include glob qualifiers, but
+it is also valid if it is simply tt(LPAR()#q+RPAR()).
 
 A qualifier may be any one of the following:
 
diff --git a/NEWS b/NEWS
index e4d747e..87e67fd 100644
--- a/NEWS
+++ b/NEWS
@@ -58,6 +58,13 @@ between the right hand side of the screen (this causes problems with
 some terminals).  It is not special and is not set by default; the
 effect in that case is as if it was 1, as in previous versions.
 
+If the option EXTENDED_GLOB is in effect, it is possible to force
+globbing within conditional code using the [[ ... ]] syntax by flagging
+that a certain string is a glob using the (#q) glob qualifier syntax.
+The resulting glob is treated as a single argument.  For example,
+[[ -n *.c(#qN) ]] tests whether there are any .c files in the current
+directory.
+
 
 Changes between 4.2 and 5.0.0
 -----------------------------
diff --git a/Src/cond.c b/Src/cond.c
index c673542..6e9b558 100644
--- a/Src/cond.c
+++ b/Src/cond.c
@@ -37,6 +37,21 @@ static char *condstr[COND_MOD] = {
     "-ne", "-lt", "-gt", "-le", "-ge", "=~"
 };
 
+static void cond_subst(char **strp, int glob_ok)
+{
+    if (glob_ok &&
+	checkglobqual(*strp, strlen(*strp), 1, NULL)) {
+	LinkList args = newlinklist();
+	addlinknode(args, *strp);
+	prefork(args, 0);
+	while (!errflag && args && nonempty(args) &&
+	       has_token((char *)peekfirst(args)))
+	    zglob(args, firstnode(args), 0);
+	*strp = sepjoin(hlinklist2array(args, 0), NULL, 1);
+    } else
+	singsub(strp);
+}
+
 /*
  * Evaluate a conditional expression given the arguments.
  * If fromtest is set, the caller is the test or [ builtin;
@@ -177,13 +192,13 @@ evalcond(Estate state, char *fromtest)
     }
     left = ecgetstr(state, EC_DUPTOK, &htok);
     if (htok) {
-	singsub(&left);
+	cond_subst(&left, !fromtest);
 	untokenize(left);
     }
     if (ctype <= COND_GE && ctype != COND_STREQ && ctype != COND_STRNEQ) {
 	right = ecgetstr(state, EC_DUPTOK, &htok);
 	if (htok) {
-	    singsub(&right);
+	    cond_subst(&right, !fromtest);
 	    untokenize(right);
 	}
     }
@@ -194,7 +209,7 @@ evalcond(Estate state, char *fromtest)
 	    fprintf(xtrerr, " %s ", condstr[ctype]);
 	    if (ctype == COND_STREQ || ctype == COND_STRNEQ) {
 		char *rt = dupstring(ecrawstr(state->prog, state->pc, NULL));
-		singsub(&rt);
+		cond_subst(&rt, !fromtest);
 		quote_tokenized_output(rt, xtrerr);
 	    }
 	    else
@@ -283,7 +298,7 @@ evalcond(Estate state, char *fromtest)
 		right = dupstring(opat = ecrawstr(state->prog, state->pc,
 						  &htok));
 		if (htok)
-		    singsub(&right);
+		    cond_subst(&right, !fromtest);
 		save = (!(state->prog->flags & EF_HEAP) &&
 			!strcmp(opat, right) && pprog != dummy_patprog2);
 
@@ -517,17 +532,6 @@ cond_val(char **args, int num)
 }
 
 /**/
-mod_export int
-cond_match(char **args, int num, char *str)
-{
-    char *s = args[num];
-
-    singsub(&s);
-
-    return matchpat(str, s);
-}
-
-/**/
 static void
 tracemodcond(char *name, char **args, int inf)
 {
diff --git a/Src/glob.c b/Src/glob.c
index 07dd7c2..57d7f99 100644
--- a/Src/glob.c
+++ b/Src/glob.c
@@ -1061,6 +1061,65 @@ insert_glob_match(LinkList list, LinkNode next, char *data)
     insertlinknode(list, next, data);
 }
 
+/*
+ * Return
+ *   1 if str ends in bare glob qualifiers
+ *   2 if str ends in non-bare glob qualifiers (#q)
+ *   0 otherwise.
+ *
+ * str is the string to check.
+ * sl is it's length (to avoid recalculation).
+ * nobareglob is 1 if bare glob qualifiers are not allowed.
+ * *sp, if sp is not null, will be a pointer to the opening parenthesis.
+ */
+
+/**/
+int
+checkglobqual(char *str, int sl, int nobareglob, char **sp)
+{
+    char *s;
+    int paren, ret = 1;
+
+    if (str[sl - 1] != Outpar)
+	return 0;
+
+    /* Check these are really qualifiers, not a set of *
+     * alternatives or exclusions.  We can be more     *
+     * lenient with an explicit (#q) than with a bare  *
+     * set of qualifiers.                              */
+    paren = 0;
+    for (s = str + sl - 2; *s && (*s != Inpar || paren); s--) {
+	switch (*s) {
+	case Outpar:
+	    paren++; /*FALLTHROUGH*/
+	case Bar:
+	    if (!zpc_disables[ZPC_BAR])
+		nobareglob = 1;
+	    break;
+	case Tilde:
+	    if (isset(EXTENDEDGLOB) && !zpc_disables[ZPC_TILDE])
+		nobareglob = 1;
+	    break;
+	case Inpar:
+	    paren--;
+	    break;
+	}
+    }
+    if (*s != Inpar)
+	return 0;
+    if (isset(EXTENDEDGLOB) && !zpc_disables[ZPC_HASH] && s[1] == Pound) {
+	if (s[2] != 'q')
+	    return 0;
+	ret = 2;
+    } else if (nobareglob)
+	return 0;
+
+    if (sp)
+	*sp = s;
+
+    return ret;
+}
+
 /* Main entry point to the globbing code for filename globbing. *
  * np points to a node in the list list which will be expanded  *
  * into a series of nodes.                                      */
@@ -1118,7 +1177,7 @@ zglob(LinkList list, LinkNode np, int nountok)
 	   (isset(EXTENDEDGLOB) && !zpc_disables[ZPC_HASH])) {
 	struct qual *newquals;
 	char *s;
-	int sense, paren;
+	int sense, qualsfound;
 	off_t data;
 	char *sdata, *newcolonmod;
 	int (*func) _((char *, Statptr, off_t, char *));
@@ -1148,40 +1207,7 @@ zglob(LinkList list, LinkNode np, int nountok)
 	newquals = qo = qn = ql = NULL;
 
 	sl = strlen(str);
-	if (str[sl - 1] != Outpar)
-	    break;
-
-	/* Check these are really qualifiers, not a set of *
-	 * alternatives or exclusions.  We can be more     *
-	 * lenient with an explicit (#q) than with a bare  *
-	 * set of qualifiers.                              */
-	paren = 0;
-	for (s = str + sl - 2; *s && (*s != Inpar || paren); s--) {
-	    switch (*s) {
-	    case Outpar:
-		paren++; /*FALLTHROUGH*/
-	    case Bar:
-		if (!zpc_disables[ZPC_BAR])
-		    nobareglob = 1;
-		break;
-	    case Tilde:
-		if (isset(EXTENDEDGLOB) && !zpc_disables[ZPC_TILDE])
-		    nobareglob = 1;
-		break;
-	    case Inpar:
-		paren--;
-		break;
-	    }
-	}
-	if (*s != Inpar)
-	    break;
-	if (isset(EXTENDEDGLOB) && !zpc_disables[ZPC_HASH] && s[1] == Pound) {
-	    if (s[2] == 'q') {
-		*s = 0;
-		s += 2;
-	    } else
-		break;
-	} else if (nobareglob)
+	if (!(qualsfound = checkglobqual(str, sl, nobareglob, &s)))
 	    break;
 
 	/* Real qualifiers found. */
@@ -1194,6 +1220,8 @@ zglob(LinkList list, LinkNode np, int nountok)
 
 	str[sl-1] = 0;
 	*s++ = 0;
+	if (qualsfound == 2)
+	    s += 2;
 	while (*s && !newcolonmod) {
 	    func = (int (*) _((char *, Statptr, off_t, char *)))0;
 	    if (idigit(*s)) {
diff --git a/Test/D02glob.ztst b/Test/D02glob.ztst
index 1f8f652..3e1ea82 100644
--- a/Test/D02glob.ztst
+++ b/Test/D02glob.ztst
@@ -526,3 +526,18 @@
 >+bus+bus matches +(+bus|-car)
 >@sinhats matches @(@sinhats|wrensinfens)
 >!kerror matches !(!somethingelse)
+
+  (
+    setopt extendedglob
+    cd glob.tmp
+    [[ -n a*(#qN) ]] && print File beginning with a
+    [[ -z z*(#qN) ]] && print No file beginning with z
+    [[ "a b c" = ?(#q) ]] && print Multiple files matched
+    setopt nonomatch
+    [[ -n z*(#q) ]] && print Normal string if nullglob not set
+  )
+0:Force glob expansion in conditions using (#q)
+>File beginning with a
+>No file beginning with z
+>Multiple files matched
+>Normal string if nullglob not set


-- 
Peter Stephenson <p.w.stephenson@xxxxxxxxxxxx>
Web page now at http://homepage.ntlworld.com/p.w.stephenson/



Messages sorted by: Reverse Date, Date, Thread, Author