zsh-workers
 help / color / mirror / code / Atom feed
From: Oliver Kiddle <opk@zsh.org>
To: Leah Neukirchen <leah@vuxu.org>
Cc: zsh-workers@zsh.org
Subject: Re: [BUG] Trailing backslash in history file confuses entries
Date: Thu, 25 Nov 2021 00:04:54 +0100	[thread overview]
Message-ID: <49858-1637795094.530308@-8CI.F0za.0QQo> (raw)
In-Reply-To: <87v911ymjw.fsf@vuxu.org>

On 9 Nov, Leah Neukirchen wrote:
> rhea% less foo\<RET><C-c>   # this saves a line with final \ to .zsh_history

Thanks for narrowing this down with a clear example.

You can also break this with print -s but this effect of pressing Ctrl-C
on a PS2 prompt is interesting in that I had never really considered it
before.

The history file use of a trailing backslash to indicate continuation
lines has caused problems before, e.g. in 32882 a space was added where
lines end in an even number of backslashes.

The patch below includes a test case that puts problematic strings in
history, writes them and reads them back in. I'd be grateful if anyone
else can think of further cases and history file contents that might
cause issues.

What this test case doesn't encapsulate is requirements in terms of the
on-disk history file format. Are there any requirements here that I
might have missed? It's clear that some care has gone into using
something that it is close to being valid shell input but it isn't for
multi-line commands.

(ksh93 uses doubled null characters which you can't reinput and
Bash reformats multi-line commands to be single-line commands.)

Currently an extra space is added where the full line ends in an even
number of backslashes. The approach taken in this patch is to add a
single space to any line that ends with any number of backslashes
followed by zero or more spaces. It then strips one space when reading
history if the line ends with backslash followed by one or more spaces.
A backslash followed directly by a newline now always indicates a line
break within a command-line. I hope this preserves original lines
without introducing new issues. And compatibility with old history files
is only broken for fairly contrived cases or those that were already
broken (such as that demonstrated by Leah). It has no problems
with my history file.

Oliver

diff --git a/Src/hist.c b/Src/hist.c
index 6ac581fda..af62cf931 100644
--- a/Src/hist.c
+++ b/Src/hist.c
@@ -2615,12 +2615,19 @@ readhistline(int start, char **bufp, int *bufsiz, FILE *in, int *readbytes)
 	    }
 	}
 	else {
+	    int spc;
 	    buf[len - 1] = '\0';
 	    if (len > 1 && buf[len - 2] == '\\') {
 		buf[--len - 1] = '\n';
 		if (!feof(in))
 		    return readhistline(len, bufp, bufsiz, in, readbytes);
 	    }
+
+	    spc = len - 2;
+	    while (spc >= 0 && buf[spc] == ' ')
+		spc--;
+	    if (spc != len - 2 && buf[spc] == '\\')
+		buf[--len - 1] = '\0';
 	}
 	return len;
     }
@@ -2984,7 +2991,7 @@ savehistfile(char *fn, int err, int writeflags)
 
 	ret = 0;
 	for (; he && he->histnum <= xcurhist; he = down_histent(he)) {
-	    int count_backslashes = 0;
+	    int end_backslashes = 0;
 
 	    if ((writeflags & HFILE_SKIPDUPS && he->node.flags & HIST_DUP)
 	     || (writeflags & HFILE_SKIPFOREIGN && he->node.flags & HIST_FOREIGN)
@@ -3017,18 +3024,14 @@ savehistfile(char *fn, int err, int writeflags)
 		if (*t == '\n')
 		    if ((ret = fputc('\\', out)) < 0)
 			break;
-		if (*t == '\\')
-		    count_backslashes++;
-		else
-		    count_backslashes = 0;
+		end_backslashes = (*t == '\\' || (end_backslashes && *t == ' '));
 		if ((ret = fputc(*t, out)) < 0)
 		    break;
 	    }
 	    if (ret < 0)
 	    	break;
-	    if (count_backslashes && (count_backslashes % 2 == 0))
-		if ((ret = fputc(' ', out)) < 0)
-		    break;
+	    if (end_backslashes)
+		ret = fputc(' ', out);
 	    if (ret < 0 || (ret = fputc('\n', out)) < 0)
 		break;
 	}
diff --git a/Test/W01history.ztst b/Test/W01history.ztst
index 0b2f60d1e..1d3f3cf6f 100644
--- a/Test/W01history.ztst
+++ b/Test/W01history.ztst
@@ -88,3 +88,25 @@ F:Check that a history bug introduced by workers/34160 is working again.
 0:Modifier :P
 >/my/path/for/testing
 >/my/path/for/testing
+
+ $ZTST_testdir/../Src/zsh -fgis <<<'
+ SAVEHIST=7
+ print -rs "one\\"
+ print -rs "two\\\\"
+ print -rs "three\\\\\\"
+ print -rs "four\\\\\\\\"
+ print -rs "five\\\\\\\\\\"
+ print -s  "while false\ndo\ntrue\\\\\n && break\ndone"
+ print -s  "echo one\\\\\ntwo"
+ fc -W hist
+ fc -p -R hist
+ fc -l
+ rm hist' 2>/dev/null
+0:Lines ending in backslash saved and restored to history
+>    1  one\
+>    2  two\\
+>    3  three\\\
+>    4  four\\\\
+>    5  five\\\\\
+>    6  while false\ndo\ntrue\\n && break\ndone
+>    7  echo one\\ntwo


      reply	other threads:[~2021-11-24 23:05 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-11-09 22:46 Leah Neukirchen
2021-11-24 23:04 ` Oliver Kiddle [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=49858-1637795094.530308@-8CI.F0za.0QQo \
    --to=opk@zsh.org \
    --cc=leah@vuxu.org \
    --cc=zsh-workers@zsh.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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).