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

Re: Inconsistent history expansion of characters adjacent to histchar

On Mon, Oct 7, 2013 at 8:55 AM, Hauke Petersen <hkptrsn@xxxxxxxxx> wrote:

> It does not immediately make sense to me why a single `!' would be
> interpreted as a double `!!' before a `;' and tried, unsuccessfully,
> to find an explanation.

Bluntly, it's because that history documentation is lying to you. The
correct bit is the first sentence here:

>     Event Designators
>     !   Start a history expansion, except when followed by a blank, new-line,
>     `=' or `('.  If followed immediately by a word designator, this forms a
>     history reference with no event designator.

Semicolon is not one of the things that prevents a "!" from starting
a history expansion, so everything from there forward behaves like a
history reference with no event.

This bug appears to have been around for longer than we've been keeping
source control copies of the shell. For some reason ';' is called out
in histsubchar() along with things that begin (or end) event or word
designators. The same is true of any kind of quotes, e.g.:

    print !`echo foo`
    print !!`echo foo`

are equivalent; "!}" also has the same problem though it usually causes
a parse error.

I think the intent is that those characters only end a history reference
after at least one other character [not in that set] has been read, but
that's not the way the loop is written, and it doesn't account for other
characters that can't be word designators, as seen here:

> there is no previous command starting with ';'.  Like otherwise
> similar `&':
>     % print !& print foo
>     zsh: event not found: &

Same happens with "!|" and probably a few other obscure variations.

In short, the history parser has always been very ad-hoc and deviates
from its own spec in several ways.

Here's a patch that fixes the unintended expansion, but it doesn't fix
the inconsistent behavior with !& !| et al.  There should probably be
more method to this madness instead of multiple series of individual
character comparisons.

The (c == '"') condition is probably unnecessary because '!"' has been
handled much earlier, but the loop above this also tests all three
quotes, so ...  

diff --git a/Src/hist.c b/Src/hist.c
index d1af30a..bd650e8 100644
--- a/Src/hist.c
+++ b/Src/hist.c
@@ -521,6 +521,12 @@ histsubchar(int c)
 		c = ingetc();
+	    if (ptr == buf &&
+		(c == '}' ||  c == ';' || c == '\'' || c == '"' || c == '`')) {
+	      /* Neither event nor word designator, no expansion */
+	      safeinungetc(c);
+	      return bangchar;
+	    }
 	    *ptr = 0;
 	    if (!*buf) {
 		if (c != '%') {

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