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

Re: PATCH: trailing components



On Tue, 2019-06-18 at 19:37 +0000, Daniel Shahaf wrote:
> Peter Stephenson wrote on Tue, 18 Jun 2019 13:55 +00:00:
> > 
> > to be clear, what I was hoping for and lacking was something along the
> > lines of:
> > 
> > echo /blah/blah/blah/**/*.oogabooga(<stuff-here>)
> What about
> .
>     echo /blah/blah/blah/**/*.oogabooga(e.:t 2.)
> .
> after defining «function ":t" { … }» using one of the previous snippets?
> You could even define curried versions, «_t2() { :t 2 "$@" }», for use
> with the «(+_t2)» syntax.

Yes, that's about the best I can think of, and the :t naming convention
is a useful addition.

Still probably worth having something this basic to do with the path
built in.  Here's the change with a reasonably full set of tests.

pws

diff --git a/Doc/Zsh/expn.yo b/Doc/Zsh/expn.yo
index a212d742d..61e41b9a7 100644
--- a/Doc/Zsh/expn.yo
+++ b/Doc/Zsh/expn.yo
@@ -260,9 +260,23 @@ see the definition of the filename extension in the description of the
 tt(r) modifier below.  Note that according to that definition the result
 will be empty if the string ends with a `tt(.)'.
 )
-item(tt(h))(
-Remove a trailing pathname component, leaving the head.  This works
-like `tt(dirname)'.
+item(tt(h) [ var(digits) ])(
+Remove a trailing pathname component, shortening the path by one
+directory level: this is the `head' of the pathname.  This works like
+`tt(dirname)'.  If the tt(h) is followed immediately (with no spaces or
+other separator) by any number of decimal digits, and the value of the
+resulting number is non-zero, that number of leading components is
+preserved instead of the final component being removed.  In an
+absolute path the leading `tt(/)' is the first component, so,
+for example, if tt(var=/my/path/to/something), then tt(${var:h3})
+substitutes tt(/my/path).  Consecutive `/'s are treated the same as
+a single `/'.  In parameter substitution, digits may only be
+used if the expression is in braces, so for example the short form
+substitution tt($var:h2) is treated as tt(${var:h}2), not as
+tt(${var:h2}).  No restriction applies to the use of digits in history
+substitution or globbing qualifiers.  If more components are requested
+than are present, the entire path is substituted (so this does not
+trigger a `failed modifier' error in history expansion).
 )
 item(tt(l))(
 Convert the words to all lowercase.
@@ -316,9 +330,12 @@ immediately by a tt(g).  In parameter expansion the tt(&) must appear
 inside braces, and in filename generation it must be quoted with a
 backslash.
 )
-item(tt(t))(
-Remove all leading pathname components, leaving the tail.  This works
-like `tt(basename)'.
+item(tt(t) [ var(digits) ])(
+Remove all leading pathname components, leaving the final component (tail).
+This works like `tt(basename)'.  Any trailing slashes are first removed.
+Decimal digits are handled as described above for (h), but in this
+case that number of trailing components are preserved instead of
+the default 1; 0 is treated the same as 1.
 )
 item(tt(u))(
 Convert the words to all uppercase.
diff --git a/NEWS b/NEWS
index ec20b4982..4603c62a3 100644
--- a/NEWS
+++ b/NEWS
@@ -26,6 +26,12 @@ specify the order of completion matches. This affects the display
 of candidate matches and the order in which they are selected when
 cycling between them using menu completion.
 
+The :h and :t modifiers in parameter expansion (if braces are present),
+glob qualifiers and history expansion may take following decimal digit
+arguments in order to keep that many leading or trailing path components
+instead of the defaults of all but one (:h) and one (:t).  In an absolute
+path the leading '/' counts as one component.
+
 Changes from 5.6.2 to 5.7.1
 ---------------------------
 
diff --git a/README b/README
index 9763e7aa6..be7929164 100644
--- a/README
+++ b/README
@@ -30,9 +30,28 @@ Zsh is a shell with lots of features.  For a list of some of these, see the
 file FEATURES, and for the latest changes see NEWS.  For more
 details, see the documentation.
 
-Incompatibilities since 5.6.2
+Incompatibilities since 5.7.1
 -----------------------------
 
+The history expansion !:1:t2 used to be interpreted such that the 2
+was a separate character added after the history expansion.  Now
+it is an argument to the :t modifier.
+
+For example
+
+% echo /my/interesting/path
+% echo !:1:t2
+
+used to echo "path2", but now echoes "interesting/path".
+
+The behaviour of :h has similarly changed.
+
+The behaviour has also changed in forms such as ${foo:t2) and *(:t2),
+but in those cases the previous behaviour was not meaningful.
+
+Incompatibilities between 5.6.2 and 5.7.1
+-----------------------------------------
+
 1) vcs_info git: The gen-unapplied-string hook receives the patches in
 order (next to be applied first).  This is consistent with the hg
 backend and with one of two contradictory claims in the documentation
diff --git a/Src/Zle/compctl.c b/Src/Zle/compctl.c
index f963d5712..f242e1b28 100644
--- a/Src/Zle/compctl.c
+++ b/Src/Zle/compctl.c
@@ -2511,7 +2511,7 @@ makecomplistcmd(char *os, int incmd, int flags)
     else if (!(cmdstr &&
 	  (((ccp = (Compctlp) compctltab->getnode(compctltab, cmdstr)) &&
 	    (cc = ccp->cc)) ||
-	   ((s = dupstring(cmdstr)) && remlpaths(&s) &&
+	   ((s = dupstring(cmdstr)) && remlpaths(&s, 1) &&
 	    (ccp = (Compctlp) compctltab->getnode(compctltab, s)) &&
 	    (cc = ccp->cc))))) {
 	if (flags & CFN_DEFAULT)
diff --git a/Src/glob.c b/Src/glob.c
index ed2c90bd8..92fd64e7c 100644
--- a/Src/glob.c
+++ b/Src/glob.c
@@ -400,7 +400,7 @@ insert(char *s, int checked)
 	if (colonmod) {
 	    /* Handle the remainder of the qualifier:  e.g. (:r:s/foo/bar/). */
 	    char *mod = colonmod;
-	    modify(&news, &mod);
+	    modify(&news, &mod, 1);
 	}
 	if (!statted && (gf_sorts & GS_NORMAL)) {
 	    statfullpath(s, &buf, 1);
diff --git a/Src/hist.c b/Src/hist.c
index 901cd3b1a..fd5606dc3 100644
--- a/Src/hist.c
+++ b/Src/hist.c
@@ -555,6 +555,27 @@ substfailed(void)
     return -1;
 }
 
+/*
+ * Return a count given by decimal digits after a modifier.
+ */
+static int
+digitcount(void)
+{
+    int c = ingetc(), count;
+
+    if (idigit(c)) {
+	count = 0;
+	do {
+	    count = 10 * count + (c - '0');
+	    c = ingetc();
+	} while (idigit(c));
+    }
+    else
+	count = 0;
+    inungetc(c);
+    return count;
+}
+
 /* Perform history substitution, returning the next character afterwards. */
 
 /**/
@@ -835,7 +856,7 @@ histsubchar(int c)
 		}
 		break;
 	    case 'h':
-		if (!remtpath(&sline)) {
+		if (!remtpath(&sline, digitcount())) {
 		    herrflush();
 		    zerr("modifier failed: h");
 		    return -1;
@@ -856,7 +877,7 @@ histsubchar(int c)
 		}
 		break;
 	    case 't':
-		if (!remlpaths(&sline)) {
+		if (!remlpaths(&sline, digitcount())) {
 		    herrflush();
 		    zerr("modifier failed: t");
 		    return -1;
@@ -1974,16 +1995,18 @@ chrealpath(char **junkptr)
 
 /**/
 int
-remtpath(char **junkptr)
+remtpath(char **junkptr, int count)
 {
     char *str = strend(*junkptr);
 
     /* ignore trailing slashes */
     while (str >= *junkptr && IS_DIRSEP(*str))
 	--str;
-    /* skip filename */
-    while (str >= *junkptr && !IS_DIRSEP(*str))
-	--str;
+    if (!count) {
+	/* skip filename */
+	while (str >= *junkptr && !IS_DIRSEP(*str))
+	    --str;
+    }
     if (str < *junkptr) {
 	if (IS_DIRSEP(**junkptr))
 	    *junkptr = dupstring ("/");
@@ -1992,6 +2015,34 @@ remtpath(char **junkptr)
 
 	return 0;
     }
+
+    if (count)
+    {
+	/*
+	 * Return this many components, so start from the front.
+	 * Leading slash counts as one component, consistent with
+	 * behaviour of repeated applications of :h.
+	 */
+	char *strp = *junkptr;
+	while (strp < str) {
+	    if (IS_DIRSEP(*strp)) {
+		if (--count <= 0) {
+		    if (strp == *junkptr)
+			++strp;
+		    *strp = '\0';
+		    return 1;
+		}
+		/* Count consecutive separators as one */
+		while (IS_DIRSEP(strp[1]))
+		    ++strp;
+	    }
+	    ++strp;
+	}
+
+	/* Full string needed */
+	return 1;
+    }
+
     /* repeated slashes are considered like a single slash */
     while (str > *junkptr && IS_DIRSEP(str[-1]))
 	--str;
@@ -2040,7 +2091,7 @@ rembutext(char **junkptr)
 
 /**/
 mod_export int
-remlpaths(char **junkptr)
+remlpaths(char **junkptr, int count)
 {
     char *str = strend(*junkptr);
 
@@ -2050,12 +2101,29 @@ remlpaths(char **junkptr)
 	    --str;
 	str[1] = '\0';
     }
-    for (; str >= *junkptr; --str)
-	if (IS_DIRSEP(*str)) {
-	    *str = '\0';
-	    *junkptr = dupstring(str + 1);
-	    return 1;
+    for (;;) {
+	for (; str >= *junkptr; --str) {
+	    if (IS_DIRSEP(*str)) {
+		if (--count > 0) {
+		    if (str > *junkptr) {
+			--str;
+			break;
+		    } else {
+			/* Whole string needed */
+			return 1;
+		    }
+		}
+		*str = '\0';
+		*junkptr = dupstring(str + 1);
+		return 1;
+	    }
 	}
+	/* Count consecutive separators as 1 */
+	while (str >= *junkptr && IS_DIRSEP(*str))
+	    --str;
+	if (str <= *junkptr)
+	    break;
+    }
     return 0;
 }
 
diff --git a/Src/subst.c b/Src/subst.c
index 60eb33390..b132f251b 100644
--- a/Src/subst.c
+++ b/Src/subst.c
@@ -3438,7 +3438,7 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int pf_flags,
 	    s--;
 	    if (unset(KSHARRAYS) || inbrace) {
 		if (!isarr)
-		    modify(&val, &s);
+		    modify(&val, &s, inbrace);
 		else {
 		    char *ss;
 		    char **ap = aval;
@@ -3447,12 +3447,12 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int pf_flags,
 
 		    while ((*pp = *ap++)) {
 			ss = s;
-			modify(pp++, &ss);
+			modify(pp++, &ss, inbrace);
 		    }
 		    if (pp == aval) {
 			char *t = "";
 			ss = s;
-			modify(&t, &ss);
+			modify(&t, &ss, inbrace);
 		    }
 		    s = ss;
 		}
@@ -4182,6 +4182,12 @@ arithsubst(char *a, char **bptr, char *rest)
  * PTR is an in/out parameter.  On entry it contains the string of colon
  * modifiers.  On return it points past the last recognised modifier.
  *
+ * INBRACE is non-zero if we are in some form of a bracketed or
+ * parenthesised expression; it is zero for modifiers ocurring
+ * in an an unbracketed variable substitution.  This means that
+ * $foo:t222 is treated ias ${foo:t}222 rather than ${foo:t222}
+ * for backward compatibility.
+ *
  * Example:
  *     ENTRY:   *str is "."   *ptr is ":AN"
  *     RETURN:  *str is "/home/foobar" (equal to $PWD)   *ptr points to the "N"
@@ -4189,7 +4195,7 @@ arithsubst(char *a, char **bptr, char *rest)
 
 /**/
 void
-modify(char **str, char **ptr)
+modify(char **str, char **ptr, int inbrace)
 {
     char *ptr1, *ptr2, *ptr3, *lptr, c, *test, *sep, *t, *tt, tc, *e;
     char *copy, *all, *tmp, sav, sav1, *ptr1end;
@@ -4202,6 +4208,8 @@ modify(char **str, char **ptr)
 	*str = dupstring(*str);
 
     while (**ptr == ':') {
+	int count = 0;
+
 	lptr = *ptr;
 	(*ptr)++;
 	wall = gbal = 0;
@@ -4214,10 +4222,8 @@ modify(char **str, char **ptr)
             case 'a':
             case 'A':
 	    case 'c':
-	    case 'h':
 	    case 'r':
 	    case 'e':
-	    case 't':
 	    case 'l':
 	    case 'u':
 	    case 'q':
@@ -4226,6 +4232,17 @@ modify(char **str, char **ptr)
 		c = **ptr;
 		break;
 
+	    case 'h':
+	    case 't':
+		c = **ptr;
+		if (inbrace && idigit((*ptr)[1])) {
+		    do {
+			count = 10 * count + ((*ptr)[1] - '0');
+			++(*ptr);
+		    } while (idigit((*ptr)[1]));
+		}
+		break;
+
 	    case 's':
 		c = **ptr;
 		(*ptr)++;
@@ -4392,7 +4409,7 @@ modify(char **str, char **ptr)
 			break;
 		    }
 		    case 'h':
-			remtpath(&copy);
+			remtpath(&copy, count);
 			break;
 		    case 'r':
 			remtext(&copy);
@@ -4401,7 +4418,7 @@ modify(char **str, char **ptr)
 			rembutext(&copy);
 			break;
 		    case 't':
-			remlpaths(&copy);
+			remlpaths(&copy, count);
 			break;
 		    case 'l':
 			copy = casemodify(tt, CASMOD_LOWER);
@@ -4478,7 +4495,7 @@ modify(char **str, char **ptr)
 		    break;
 		}
 		case 'h':
-		    remtpath(str);
+		    remtpath(str, count);
 		    break;
 		case 'r':
 		    remtext(str);
@@ -4487,7 +4504,7 @@ modify(char **str, char **ptr)
 		    rembutext(str);
 		    break;
 		case 't':
-		    remlpaths(str);
+		    remlpaths(str, count);
 		    break;
 		case 'l':
 		    *str = casemodify(*str, CASMOD_LOWER);
diff --git a/Test/D02glob.ztst b/Test/D02glob.ztst
index 08b71dc8e..5638e1255 100644
--- a/Test/D02glob.ztst
+++ b/Test/D02glob.ztst
@@ -700,3 +700,31 @@
  print ${value//[${foo}b-z]/x}
 0:handling of - range in complicated pattern context
 >xx
+
+ pathtotest=glob.tmp/my/test/dir/that/does/not/exist
+ mkdir -p $pathtotest
+ print $pathtotest(:h)
+ print $pathtotest(:h0)
+ print $pathtotest(:h10)
+ print $pathtotest(:h3)
+ print $pathtotest(:h2)
+ print $pathtotest(:h1)
+ print $pathtotest(:t)
+ print $pathtotest(:t0)
+ print $pathtotest(:t10)
+ print $pathtotest(:t3)
+ print $pathtotest(:t2)
+ print $pathtotest(:t1)
+0:modifiers :h and :t with numbers (main test is in D04parameter.ztst)
+>glob.tmp/my/test/dir/that/does/not
+>glob.tmp/my/test/dir/that/does/not
+>glob.tmp/my/test/dir/that/does/not/exist
+>glob.tmp/my/test
+>glob.tmp/my
+>glob.tmp
+>exist
+>exist
+>glob.tmp/my/test/dir/that/does/not/exist
+>does/not/exist
+>not/exist
+>exist
diff --git a/Test/D04parameter.ztst b/Test/D04parameter.ztst
index 1ec650352..194c3e287 100644
--- a/Test/D04parameter.ztst
+++ b/Test/D04parameter.ztst
@@ -2445,3 +2445,80 @@ F:behavior, see http://austingroupbugs.net/view.php?id=888
     : <<< ${(F)x/y}
   }
 0:Separation / join logic regresssion test
+
+  testpath=/one/two/three/four
+  for (( i = 0; i <= 6; ++i )); do
+    eval "print \$testpath:t$i"
+    eval "print \${testpath:t$i}"
+  done
+0:t with trailing digits
+>four0
+>four
+>four1
+>four
+>four2
+>three/four
+>four3
+>two/three/four
+>four4
+>one/two/three/four
+>four5
+>/one/two/three/four
+>four6
+>/one/two/three/four
+
+  testpath=/one/two/three/four
+  for (( i = 0; i <= 6; ++i )); do
+    eval "print \$testpath:h$i"
+    eval "print \${testpath:h$i}"
+  done
+0:h with trailing digits
+>/one/two/three0
+>/one/two/three
+>/one/two/three1
+>/
+>/one/two/three2
+>/one
+>/one/two/three3
+>/one/two
+>/one/two/three4
+>/one/two/three
+>/one/two/three5
+>/one/two/three/four
+>/one/two/three6
+>/one/two/three/four
+
+  testpath=/a/quite/long/path/to/do/messy/stuff/with
+  print $testpath:h2:t3:h5:t16:h2n2
+  print ${testpath:t5:h2}
+  print ${testpath:t5:h2:t}
+  print ${testpath:h6:t4:h3:t2:h}
+  print ${testpath:h10:t10:t6:h3}
+  print ${testpath:t9:h}
+  print ${testpath:t9:h:t}
+0:Combinations of :h and :t with and without trailing digits
+>/a/quite/long/path/to/do/messy/stuff2:t3:h5:t16:h2n2
+>to/do
+>do
+>long
+>path/to/do
+>a/quite/long/path/to/do/messy/stuff
+>stuff
+
+ testpath=///this//has////lots//and////lots//of////slashes
+ print ${testpath:h3}
+ print ${testpath:t4}
+0:Multiple slashes are treated as one in :h and :t but are not removed
+>///this//has
+>and////lots//of////slashes
+
+ testpath=trailing/slashes/are/removed///
+ print ${testpath:h}
+ print ${testpath:h2}
+ print ${testpath:t}
+ print ${testpath:t2}
+0:Modifiers :h and :t remove trailing slashes before examining path
+>trailing/slashes/are
+>trailing/slashes
+>removed
+>are/removed
diff --git a/Test/W01history.ztst b/Test/W01history.ztst
index 6ef9b11cc..96d0beb61 100644
--- a/Test/W01history.ztst
+++ b/Test/W01history.ztst
@@ -58,3 +58,26 @@
 *?*
 F:Check that a history bug introduced by workers/34160 is working again.
 # Discarded line of error output consumes prompts printed by "zsh -i".
+
+ $ZTST_testdir/../Src/zsh -fis <<<'
+ echo /my/path/for/testing
+ echo !1:1:h10
+ echo !1:1:h3
+ echo !1:1:h2
+ echo !1:1:h1
+ echo !1:1:t10
+ echo !1:1:t3
+ echo !1:1:t2
+ echo !1:1:t1
+ echo !1:1:t3:h2' 2>/dev/null
+0:Modifiers :h and :t with arguments
+>/my/path/for/testing
+>/my/path/for/testing
+>/my/path
+>/my
+>/
+>/my/path/for/testing
+>path/for/testing
+>for/testing
+>testing
+>path/for



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