zsh-workers
 help / color / mirror / code / Atom feed
* PATCH: trailing components
       [not found] <CGME20190618130008eucas1p176cc19c1c8b831fc30b4bf7b3294f3af@eucas1p1.samsung.com>
@ 2019-06-18 13:00 ` Peter Stephenson
  2019-06-18 13:37   ` Daniel Shahaf
  0 siblings, 1 reply; 9+ messages in thread
From: Peter Stephenson @ 2019-06-18 13:00 UTC (permalink / raw)
  To: Zsh Hackers' List

I needed to preserve a number of trailing components of a path (other
than one) and couldn't think of an easy way of doing it.  Rather than
craft obscure pattern substitutions, or use an ad-hoc expression to
strip the prefix, I came up with a patch to add an optional number after
the "t" in history style modifiers to do this.  I put it after rather
than before because history-style colon expressions are quite sensitive
to what the first character is after the colon.

Before I come up with tests and fix up the ensuing failures, you can let
me know if there's a better way of doing this to save me the trouble.

pws

diff --git a/Doc/Zsh/expn.yo b/Doc/Zsh/expn.yo
index a212d742d..d28a30767 100644
--- a/Doc/Zsh/expn.yo
+++ b/Doc/Zsh/expn.yo
@@ -316,9 +316,11 @@ 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))(
+item(tt(t) [ var(digits) ])(
 Remove all leading pathname components, leaving the tail.  This works
-like `tt(basename)'.
+like `tt(basename)'.  Any trailing slashes are first removed.
+If followed by any number of decimal digits, that number of trailing
+components is preserved.
 )
 item(tt(u))(
 Convert the words to all uppercase.
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/hist.c b/Src/hist.c
index 901cd3b1a..2117f2bd6 100644
--- a/Src/hist.c
+++ b/Src/hist.c
@@ -798,7 +798,7 @@ histsubchar(int c)
 	c = (cflag) ? ':' : ingetc();
 	cflag = 0;
 	if (c == ':') {
-	    int gbal = 0;
+	    int gbal = 0, count;
 
 	    if ((c = ingetc()) == 'g') {
 		gbal = 1;
@@ -856,7 +856,18 @@ histsubchar(int c)
 		}
 		break;
 	    case 't':
-		if (!remlpaths(&sline)) {
+		c = ingetc();
+		if (idigit(c)) {
+		    count = 0;
+		    do {
+			count = 10 * count + (c - '0');
+			c = ingetc();
+		    } while (idigit(c));
+		}
+		else
+		    count = 1;
+		inungetc(c);
+		if (!remlpaths(&sline, count)) {
 		    herrflush();
 		    zerr("modifier failed: t");
 		    return -1;
@@ -2040,7 +2051,7 @@ rembutext(char **junkptr)
 
 /**/
 mod_export int
-remlpaths(char **junkptr)
+remlpaths(char **junkptr, int count)
 {
     char *str = strend(*junkptr);
 
@@ -2050,12 +2061,21 @@ 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 && str > *junkptr) {
+		    --str;
+		    break;
+		}
+		*str = '\0';
+		*junkptr = dupstring(str + 1);
+		return 1;
+	    }
 	}
+	if (str <= *junkptr)
+	    break;
+    }
     return 0;
 }
 
diff --git a/Src/subst.c b/Src/subst.c
index 60eb33390..d1fff2e67 100644
--- a/Src/subst.c
+++ b/Src/subst.c
@@ -4193,7 +4193,7 @@ modify(char **str, char **ptr)
 {
     char *ptr1, *ptr2, *ptr3, *lptr, c, *test, *sep, *t, *tt, tc, *e;
     char *copy, *all, *tmp, sav, sav1, *ptr1end;
-    int gbal, wall, rec, al, nl, charlen, dellen;
+    int gbal, wall, rec, al, nl, charlen, dellen, count = 1;
     convchar_t del;
 
     test = NULL;
@@ -4217,7 +4217,6 @@ modify(char **str, char **ptr)
 	    case 'h':
 	    case 'r':
 	    case 'e':
-	    case 't':
 	    case 'l':
 	    case 'u':
 	    case 'q':
@@ -4226,6 +4225,17 @@ modify(char **str, char **ptr)
 		c = **ptr;
 		break;
 
+	    case 't':
+		c = **ptr;
+		if (idigit((*ptr)[1])) {
+		    count = 0;
+		    do {
+			count = 10 * count + ((*ptr)[1] - '0');
+			++(*ptr);
+		    } while (idigit((*ptr)[1]));
+		}
+		break;
+
 	    case 's':
 		c = **ptr;
 		(*ptr)++;
@@ -4401,7 +4411,7 @@ modify(char **str, char **ptr)
 			rembutext(&copy);
 			break;
 		    case 't':
-			remlpaths(&copy);
+			remlpaths(&copy, count);
 			break;
 		    case 'l':
 			copy = casemodify(tt, CASMOD_LOWER);
@@ -4487,7 +4497,7 @@ modify(char **str, char **ptr)
 		    rembutext(str);
 		    break;
 		case 't':
-		    remlpaths(str);
+		    remlpaths(str, count);
 		    break;
 		case 'l':
 		    *str = casemodify(*str, CASMOD_LOWER);


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: PATCH: trailing components
  2019-06-18 13:00 ` PATCH: trailing components Peter Stephenson
@ 2019-06-18 13:37   ` Daniel Shahaf
  2019-06-18 13:54     ` Peter Stephenson
  2019-06-18 14:42     ` Mikael Magnusson
  0 siblings, 2 replies; 9+ messages in thread
From: Daniel Shahaf @ 2019-06-18 13:37 UTC (permalink / raw)
  To: zsh-workers

Peter Stephenson wrote on Tue, 18 Jun 2019 13:01 +00:00:
> I needed to preserve a number of trailing components of a path (other
> than one) and couldn't think of an easy way of doing it.  Rather than
> craft obscure pattern substitutions, or use an ad-hoc expression to
> strip the prefix, I came up with a patch to add an optional number after
> the "t" in history style modifiers to do this.  I put it after rather
> than before because history-style colon expressions are quite sensitive
> to what the first character is after the colon.
> 
> Before I come up with tests and fix up the ensuing failures, you can let
> me know if there's a better way of doing this to save me the trouble.

Here are some alternatives, but I'm not claiming any of them is better than «${foo:t2}»:

f() {
  REPLY=
  for 1 in {1..$1} ; do REPLY=$2:t/$REPLY; 2=$2:h ; done
}
f $N "$foo"; foo=$REPLY

or

foo=${(j./.)${${(s./.)foo}[-$N,-1]}}
(plus or minus handling of runs of several consecutive slashes)

or

tmp=$foo
for i in {1..$N} ; do tmp=$tmp:h ; done
foo=${foo#$tmp/*}

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: PATCH: trailing components
  2019-06-18 13:37   ` Daniel Shahaf
@ 2019-06-18 13:54     ` Peter Stephenson
  2019-06-18 14:45       ` Mikael Magnusson
  2019-06-18 19:37       ` Daniel Shahaf
  2019-06-18 14:42     ` Mikael Magnusson
  1 sibling, 2 replies; 9+ messages in thread
From: Peter Stephenson @ 2019-06-18 13:54 UTC (permalink / raw)
  To: zsh-workers

On Tue, 2019-06-18 at 13:37 +0000, Daniel Shahaf wrote:
> Peter Stephenson wrote on Tue, 18 Jun 2019 13:01 +00:00:
> > 
> > I needed to preserve a number of trailing components of a path (other
> > than one) and couldn't think of an easy way of doing it.  Rather than
> > craft obscure pattern substitutions, or use an ad-hoc expression to
> > strip the prefix, I came up with a patch to add an optional number after
> > the "t" in history style modifiers to do this.  I put it after rather
> > than before because history-style colon expressions are quite sensitive
> > to what the first character is after the colon.
> > 
> > Before I come up with tests and fix up the ensuing failures, you can let
> > me know if there's a better way of doing this to save me the trouble.
> Here are some alternatives, but I'm not claiming any of them is better than «${foo:t2}»:

Thanks, it's useful to know other people's thinking for comparison ---
to be clear, what I was hoping for and lacking was something along the
lines of:

echo /blah/blah/blah/**/*.oogabooga(<stuff-here>)

even if <stuff-here> wasn't quite as short as :t2.  I could live with a
few more characters but a loop or complicated substitution would lead me
to propose adding the new code instead.

pws



^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: PATCH: trailing components
  2019-06-18 13:37   ` Daniel Shahaf
  2019-06-18 13:54     ` Peter Stephenson
@ 2019-06-18 14:42     ` Mikael Magnusson
  1 sibling, 0 replies; 9+ messages in thread
From: Mikael Magnusson @ 2019-06-18 14:42 UTC (permalink / raw)
  To: Daniel Shahaf; +Cc: zsh-workers

On 6/18/19, Daniel Shahaf <d.s@daniel.shahaf.name> wrote:
> Peter Stephenson wrote on Tue, 18 Jun 2019 13:01 +00:00:
>> I needed to preserve a number of trailing components of a path (other
>> than one) and couldn't think of an easy way of doing it.  Rather than
>> craft obscure pattern substitutions, or use an ad-hoc expression to
>> strip the prefix, I came up with a patch to add an optional number after
>> the "t" in history style modifiers to do this.  I put it after rather
>> than before because history-style colon expressions are quite sensitive
>> to what the first character is after the colon.
>>
>> Before I come up with tests and fix up the ensuing failures, you can let
>> me know if there's a better way of doing this to save me the trouble.
>
> Here are some alternatives, but I'm not claiming any of them is better than
> «${foo:t2}»:
>
> f() {
>   REPLY=
>   for 1 in {1..$1} ; do REPLY=$2:t/$REPLY; 2=$2:h ; done
> }
> f $N "$foo"; foo=$REPLY
>
> or
>
> foo=${(j./.)${${(s./.)foo}[-$N,-1]}}
> (plus or minus handling of runs of several consecutive slashes)
>
> or
>
> tmp=$foo
> for i in {1..$N} ; do tmp=$tmp:h ; done
> foo=${foo#$tmp/*}

Here's one (two) more, excluding (including) empty segments in the count,
% a=a/b/c/d///e/g
% echo ${(M)a%%([^/]#(/|)##)(#c5)}
b/c/d///e/g
% echo ${(M)a%%([^/]#(/|))(#c5)}
d///e/g


-- 
Mikael Magnusson

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: PATCH: trailing components
  2019-06-18 13:54     ` Peter Stephenson
@ 2019-06-18 14:45       ` Mikael Magnusson
  2019-06-18 15:39         ` Peter Stephenson
  2019-06-18 19:37       ` Daniel Shahaf
  1 sibling, 1 reply; 9+ messages in thread
From: Mikael Magnusson @ 2019-06-18 14:45 UTC (permalink / raw)
  To: Peter Stephenson; +Cc: zsh-workers

On 6/18/19, Peter Stephenson <p.stephenson@samsung.com> wrote:
> On Tue, 2019-06-18 at 13:37 +0000, Daniel Shahaf wrote:
>> Peter Stephenson wrote on Tue, 18 Jun 2019 13:01 +00:00:
>> >
>> > I needed to preserve a number of trailing components of a path (other
>> > than one) and couldn't think of an easy way of doing it.  Rather than
>> > craft obscure pattern substitutions, or use an ad-hoc expression to
>> > strip the prefix, I came up with a patch to add an optional number
>> > after
>> > the "t" in history style modifiers to do this.  I put it after rather
>> > than before because history-style colon expressions are quite sensitive
>> > to what the first character is after the colon.
>> >
>> > Before I come up with tests and fix up the ensuing failures, you can
>> > let
>> > me know if there's a better way of doing this to save me the trouble.
>> Here are some alternatives, but I'm not claiming any of them is better
>> than «${foo:t2}»:
>
> Thanks, it's useful to know other people's thinking for comparison ---
> to be clear, what I was hoping for and lacking was something along the
> lines of:
>
> echo /blah/blah/blah/**/*.oogabooga(<stuff-here>)
>
> even if <stuff-here> wasn't quite as short as :t2.  I could live with a
> few more characters but a loop or complicated substitution would lead me
> to propose adding the new code instead.

The only complaint I could imagine here (but I'm not making it) is
that this breaks
echo $foo:t42
to echo the last path segment followed by the string 42
Would it be possible to limit the new syntax to *(:t42) and ${foo:t42}
where previously the 34 is invalid (or at least silently ignored)? If
it is I think it would be nice to do so, but I'm not going to object
if it goes in as is. It's certainly nicer than the expressions
suggested earlier in the thread.

-- 
Mikael Magnusson

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: PATCH: trailing components
  2019-06-18 14:45       ` Mikael Magnusson
@ 2019-06-18 15:39         ` Peter Stephenson
  0 siblings, 0 replies; 9+ messages in thread
From: Peter Stephenson @ 2019-06-18 15:39 UTC (permalink / raw)
  To: zsh-workers

On Tue, 2019-06-18 at 16:45 +0200, Mikael Magnusson wrote:
> The only complaint I could imagine here (but I'm not making it) is
> that this breaks
> echo $foo:t42
> to echo the last path segment followed by the string 42
> Would it be possible to limit the new syntax to *(:t42) and ${foo:t42}
> where previously the 34 is invalid (or at least silently ignored)? If
> it is I think it would be nice to do so, but I'm not going to object
> if it goes in as is. It's certainly nicer than the expressions
> suggested earlier in the thread.

That's straightforward.  The same problem applies to !-history, but (i)
what you see is what you get in that case (ii) that's exactly the place
where brevity is particularly valuable, so maybe I can just document
that as a small incompatibility.

Looks like it's worth going ahead.  I would think doing the same for :h
makes sense, if only for consistency:  it would be a bit odd to have one
and not the other.  I'll make them both mean the number of components to
leave, even though the default behaviour is different, with head taking
off one path and tail leaving one path.  I think the way repeated head
works means that / is one and /foo is two components.

pws



^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: PATCH: trailing components
  2019-06-18 13:54     ` Peter Stephenson
  2019-06-18 14:45       ` Mikael Magnusson
@ 2019-06-18 19:37       ` Daniel Shahaf
  2019-06-19 15:02         ` Peter Stephenson
  1 sibling, 1 reply; 9+ messages in thread
From: Daniel Shahaf @ 2019-06-18 19:37 UTC (permalink / raw)
  To: zsh-workers

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.

> even if <stuff-here> wasn't quite as short as :t2.  I could live with a
> few more characters but a loop or complicated substitution would lead me
> to propose adding the new code instead.

I'm not opposed to adding the new code; just brainstorming on alternatives.

Cheers,

Daniel

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: PATCH: trailing components
  2019-06-18 19:37       ` Daniel Shahaf
@ 2019-06-19 15:02         ` Peter Stephenson
  2019-06-20 10:14           ` Peter Stephenson
  0 siblings, 1 reply; 9+ messages in thread
From: Peter Stephenson @ 2019-06-19 15:02 UTC (permalink / raw)
  To: zsh-workers

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


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: PATCH: trailing components
  2019-06-19 15:02         ` Peter Stephenson
@ 2019-06-20 10:14           ` Peter Stephenson
  0 siblings, 0 replies; 9+ messages in thread
From: Peter Stephenson @ 2019-06-20 10:14 UTC (permalink / raw)
  To: zsh-workers

On Wed, 2019-06-19 at 16:02 +0100, Peter Stephenson wrote:
> 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.

Not hearing any controversy surrounding this, so I've committed the
initial implementation.

pws


^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2019-06-20 10:14 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
     [not found] <CGME20190618130008eucas1p176cc19c1c8b831fc30b4bf7b3294f3af@eucas1p1.samsung.com>
2019-06-18 13:00 ` PATCH: trailing components Peter Stephenson
2019-06-18 13:37   ` Daniel Shahaf
2019-06-18 13:54     ` Peter Stephenson
2019-06-18 14:45       ` Mikael Magnusson
2019-06-18 15:39         ` Peter Stephenson
2019-06-18 19:37       ` Daniel Shahaf
2019-06-19 15:02         ` Peter Stephenson
2019-06-20 10:14           ` Peter Stephenson
2019-06-18 14:42     ` Mikael Magnusson

Code repositories for project(s) associated with this public inbox

	https://git.vuxu.org/mirror/zsh/

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).