zsh-workers
 help / color / mirror / code / Atom feed
* [PATCH] Per-directory history implementation
@ 2020-01-17 16:46 Dan Aloni
  2020-01-28  8:30 ` Vincent Lefevre
  0 siblings, 1 reply; 2+ messages in thread
From: Dan Aloni @ 2020-01-17 16:46 UTC (permalink / raw)
  To: zsh-workers

This change is an attempt to implement this feature straight into zsh,
so that its entry pruning and history file locking logic cover it as
well.

NOTE: There is external plugin that tries to implement this in the
following manner: 1) switching the history file with current directory
change to maintain a separate one, and reloading the zsh history each
time it happens 2) maintaining the global history file in addition.
While I wanted that plugin to work for me, it felt far from perfect and
its downsides were too great for me.

This is a first version, so I'm guessing there's much to iron out.

--

Conditional on the activation of the `extended_history` feature, these
changes implement a `histsavecwd` feature. The current directory of the
command is added to each command in the history file, in this manner:

    : 1579121354 /home/user:0;ls -l

Instead of the old format:

    : 1579121354:0;ls -l

In addition, the 'fc' commands get a '-c' option that filters out
commands not pertaining to the current directory. This can serve to
implement a command similar to `Ctrl-R` showing only the per-directory
history.

Note that that pathnames that contain the ':' character are not
supported for now - these are expected to mess up history parsing. We
should escape the `:` character if we want to support them, or change
the format altogether.

Older zsh versions should ignore the added information.
---
 Completion/Zsh/Command/_set |  2 +-
 Src/builtin.c               | 26 ++++++++++++++++++++++++--
 Src/hashtable.c             |  3 +++
 Src/hist.c                  | 26 +++++++++++++++++++++++---
 Src/options.c               |  3 ++-
 Src/zsh.h                   |  2 ++
 6 files changed, 55 insertions(+), 7 deletions(-)

diff --git a/Completion/Zsh/Command/_set b/Completion/Zsh/Command/_set
index 27c7f3c7d82a..3b8f4ff87a81 100644
--- a/Completion/Zsh/Command/_set
+++ b/Completion/Zsh/Command/_set
@@ -22,4 +22,4 @@ noglob _arguments -s -S \
   {-,+}h[histignoredups] {-,+}i[interactive] {-,+}k[interactivecomments] \
   {-,+}l[login] {-,+}m[monitor] {-,+}n[no-exec] {-,+}p[privileged] \
   {-,+}r[restricted] {-,+}t[singlecommand] {-,+}u[no-unset] {-,+}v[verbose] \
-  {-,+}w[chaselinks] {-,+}x[xtrace] {-,+}y[shwordsplit]
+  {-,+}w[chaselinks] {-,+}x[xtrace] {-,+}y[shwordsplit] {-,+}j[histsavecwd]
diff --git a/Src/builtin.c b/Src/builtin.c
index aa5767cf135f..d48d99b4bc73 100644
--- a/Src/builtin.c
+++ b/Src/builtin.c
@@ -71,7 +71,7 @@ static struct builtin builtins[] =
      * But that's actually not useful, so it's more consistent to
      * cause an error.
      */
-    BUILTIN("fc", 0, bin_fc, 0, -1, BIN_FC, "aAdDe:EfiIlLmnpPrRt:W", NULL),
+    BUILTIN("fc", 0, bin_fc, 0, -1, BIN_FC, "acAdDe:EfiIlLmnpPrRt:W", NULL),
     BUILTIN("fg", 0, bin_fg, 0, -1, BIN_FG, NULL, NULL),
     BUILTIN("float", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, (HandlerFunc)bin_typeset, 0, -1, 0, "E:%F:%HL:%R:%Z:%ghlp:%rtux", "E"),
     BUILTIN("functions", BINF_PLUSOPTS, bin_functions, 0, -1, 0, "ckmMstTuUWx:z", NULL),
@@ -83,7 +83,7 @@ static struct builtin builtins[] =
     BUILTIN("hashinfo", 0, bin_hashinfo, 0, 0, 0, NULL, NULL),
 #endif
 
-    BUILTIN("history", 0, bin_fc, 0, -1, BIN_FC, "adDEfiLmnpPrt:", "l"),
+    BUILTIN("history", 0, bin_fc, 0, -1, BIN_FC, "acdDEfiLmnpPrt:", "l"),
     BUILTIN("integer", BINF_PLUSOPTS | BINF_MAGICEQUALS | BINF_PSPECIAL | BINF_ASSIGN, (HandlerFunc)bin_typeset, 0, -1, 0, "HL:%R:%Z:%ghi:%lp:%rtux", "i"),
     BUILTIN("jobs", 0, bin_fg, 0, -1, BIN_JOBS, "dlpZrs", NULL),
     BUILTIN("kill", BINF_HANDLES_OPTS, bin_kill, 0, -1, 0, NULL, NULL),
@@ -1755,6 +1755,7 @@ fclist(FILE *f, Options ops, zlong first, zlong last,
     int fclistdone = 0, xflags = 0;
     zlong tmp;
     char *s, *tdfmt, *timebuf;
+    const char *cwd = NULL;
     Histent ent;
 
     /* reverse range if required */
@@ -1810,7 +1811,26 @@ fclist(FILE *f, Options ops, zlong first, zlong last,
 	xflags |= HIST_READ;
     }
 
+    if (OPT_ISSET(ops,'c')) {
+	cwd = zgetcwd();
+    }
     for (;;) {
+	if (OPT_ISSET(ops,'c') && cwd) {
+	    if (!ent->cwd) {
+		/* the entry does not a have a current directory,
+		 * these are entries before the user has
+		 * activated the histsavecwd feature.
+		 */
+		goto next;
+	    }
+	    if (strcmp(ent->cwd, cwd)) {
+		/* filter out commands not belonging to the
+		 * current directory.
+		 */
+		goto next;
+	    }
+	}
+
 	if (ent->node.flags & xflags)
 	    s = NULL;
 	else
@@ -1856,6 +1876,8 @@ fclist(FILE *f, Options ops, zlong first, zlong last,
 		putc('\n', f);
 	    }
 	}
+
+next:
 	/* move on to the next history line, or quit the loop */
 	if (first < last) {
 	    if (!(ent = down_histent(ent)) || ent->histnum > last)
diff --git a/Src/hashtable.c b/Src/hashtable.c
index e210ddecac1b..d641019cba8e 100644
--- a/Src/hashtable.c
+++ b/Src/hashtable.c
@@ -1453,6 +1453,9 @@ freehistdata(Histent he, int unlink)
     if (!(he->node.flags & (HIST_DUP | HIST_TMPSTORE)))
 	removehashnode(histtab, he->node.nam);
 
+    if (he->cwd)
+	zsfree(he->cwd);
+
     zsfree(he->node.nam);
     if (he->nwords)
 	zfree(he->words, he->nwords*2*sizeof(short));
diff --git a/Src/hist.c b/Src/hist.c
index 5281e87181ac..c3a5372598b3 100644
--- a/Src/hist.c
+++ b/Src/hist.c
@@ -1595,6 +1595,9 @@ hend(Eprog prog)
 	    he = prepnexthistent();
 
 	he->node.nam = ztrdup(chline);
+	if (isset(HISTSAVECWD)) {
+	    he->cwd = ztrdup(zgetcwd());
+	}
 	he->stim = time(NULL);
 	he->ftim = 0L;
 	he->node.flags = newflags;
@@ -2666,6 +2669,8 @@ readhistfile(char *fn, int err, int readflags)
 	    newflags |= HIST_MAKEUNIQUE;
 	while (fpos = ftell(in), (l = readhistline(0, &buf, &bufsiz, in))) {
 	    char *pt;
+	    char *directory = NULL;
+	    char *directory_end = NULL;
 	    int remeta = 0;
 
 	    if (l < 0) {
@@ -2701,8 +2706,12 @@ readhistfile(char *fn, int err, int readflags)
 
 	    if (*pt == ':') {
 		pt++;
-		stim = zstrtol(pt, NULL, 0);
+		stim = zstrtol(pt, &pt, 0);
+		if (*pt == ' ' && pt[1] == '/') {
+		    directory = &pt[1];
+		}
 		for (; *pt != ':' && *pt; pt++);
+		directory_end = pt;
 		if (*pt) {
 		    pt++;
 		    ftim = zstrtol(pt, NULL, 0);
@@ -2745,6 +2754,11 @@ readhistfile(char *fn, int err, int readflags)
 	    he = prepnexthistent();
 	    he->node.nam = ztrdup(pt);
 	    he->node.flags = newflags;
+	    if (directory && directory_end) {
+		*directory_end = '\0';
+		he->cwd = ztrdup(directory);
+		*directory_end = ':';
+	    }
 	    if ((he->stim = stim) == 0)
 		he->stim = he->ftim = tim;
 	    else if (ftim < stim)
@@ -2983,8 +2997,14 @@ savehistfile(char *fn, int err, int writeflags)
 	    }
 	    t = start = he->node.nam;
 	    if (extended_history) {
-		ret = fprintf(out, ": %ld:%ld;", (long)he->stim,
-			      he->ftim? (long)(he->ftim - he->stim) : 0L);
+		if (isset(HISTSAVECWD)) {
+		    ret = fprintf(out, ": %ld %s:%ld;", (long)he->stim,
+				  he->cwd ? he->cwd : zgetcwd(),
+				  he->ftim? (long)(he->ftim - he->stim) : 0L);
+		} else {
+		    ret = fprintf(out, ": %ld:%ld;", (long)he->stim,
+				  he->ftim? (long)(he->ftim - he->stim) : 0L);
+		}
 	    } else if (*t == ':')
 		ret = fputc('\\', out);
 
diff --git a/Src/options.c b/Src/options.c
index 48c14c1795f6..7579f2000e0f 100644
--- a/Src/options.c
+++ b/Src/options.c
@@ -157,6 +157,7 @@ static struct optname optns[] = {
 {{NULL, "histignorealldups",  0},			 HISTIGNOREALLDUPS},
 {{NULL, "histignoredups",     0},			 HISTIGNOREDUPS},
 {{NULL, "histignorespace",    0},			 HISTIGNORESPACE},
+{{NULL, "histsavecwd",        0},			 HISTSAVECWD},
 {{NULL, "histlexwords",	      0},			 HISTLEXWORDS},
 {{NULL, "histnofunctions",    0},			 HISTNOFUNCTIONS},
 {{NULL, "histnostore",	      0},			 HISTNOSTORE},
@@ -345,7 +346,7 @@ static short zshletters[LAST_OPT - FIRST_OPT + 1] = {
     /* g */  HISTIGNORESPACE,
     /* h */  HISTIGNOREDUPS,
     /* i */  INTERACTIVE,
-    /* j */  0,
+    /* j */  HISTSAVECWD,
     /* k */  INTERACTIVECOMMENTS,
     /* l */  LOGINSHELL,
     /* m */  MONITOR,
diff --git a/Src/zsh.h b/Src/zsh.h
index 8341428954c0..2f8b2f088d60 100644
--- a/Src/zsh.h
+++ b/Src/zsh.h
@@ -2199,6 +2199,7 @@ struct histent {
 				 * i.e the same format as the original
 				 * entry
 				 */
+    char *cwd;			/* direcory where the command ran   */
     time_t stim;		/* command started time (datestamp) */
     time_t ftim;		/* command finished time            */
     short *words;		/* Position of words in history     */
@@ -2399,6 +2400,7 @@ enum {
     HISTIGNOREALLDUPS,
     HISTIGNOREDUPS,
     HISTIGNORESPACE,
+    HISTSAVECWD,
     HISTLEXWORDS,
     HISTNOFUNCTIONS,
     HISTNOSTORE,
-- 
2.21.0


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

* Re: [PATCH] Per-directory history implementation
  2020-01-17 16:46 [PATCH] Per-directory history implementation Dan Aloni
@ 2020-01-28  8:30 ` Vincent Lefevre
  0 siblings, 0 replies; 2+ messages in thread
From: Vincent Lefevre @ 2020-01-28  8:30 UTC (permalink / raw)
  To: zsh-workers

On 2020-01-17 18:46:21 +0200, Dan Aloni wrote:
> This change is an attempt to implement this feature straight into zsh,
> so that its entry pruning and history file locking logic cover it as
> well.
> 
> NOTE: There is external plugin that tries to implement this in the
> following manner: 1) switching the history file with current directory
> change to maintain a separate one, and reloading the zsh history each
> time it happens 2) maintaining the global history file in addition.
> While I wanted that plugin to work for me, it felt far from perfect and
> its downsides were too great for me.

What are these downsides?

I think that a solution written purely in zsh would be cleaner and
more flexible:
  * The user may want to change other things when switching
    directories.
  * The user may want to change the history on other occasions.

Hook functions are nice for this kind of things.

> This is a first version, so I'm guessing there's much to iron out.
> 
> --
> 
> Conditional on the activation of the `extended_history` feature, these
> changes implement a `histsavecwd` feature. The current directory of the
> command is added to each command in the history file, in this manner:
> 
>     : 1579121354 /home/user:0;ls -l
> 
> Instead of the old format:
> 
>     : 1579121354:0;ls -l
> 
> In addition, the 'fc' commands get a '-c' option that filters out
> commands not pertaining to the current directory. This can serve to
> implement a command similar to `Ctrl-R` showing only the per-directory
> history.
> 
> Note that that pathnames that contain the ':' character are not
> supported for now - these are expected to mess up history parsing. We
> should escape the `:` character if we want to support them, or change
> the format altogether.

That's a security issue, as the user may want/need to switch to any
directory, possibly belonging to other users. The format should be
able to handle any possible pathname (including with \n characters).

-- 
Vincent Lefèvre <vincent@vinc17.net> - Web: <https://www.vinc17.net/>
100% accessible validated (X)HTML - Blog: <https://www.vinc17.net/blog/>
Work: CR INRIA - computer arithmetic / AriC project (LIP, ENS-Lyon)

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

end of thread, other threads:[~2020-01-28  8:31 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2020-01-17 16:46 [PATCH] Per-directory history implementation Dan Aloni
2020-01-28  8:30 ` Vincent Lefevre

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).