Add history delete #4929 (#5172)

* Add `history delete` #4929

* Add news and satisfy linter.

* Added `history delete` for sqlite backend

* Format with black.

* Use REGEXP instead of LIKE for sqlite backend.

* History delete: iterate over entries for sqlite backend.

* Use '='-operator instead of LIKE.
This commit is contained in:
HackTheOxidation 2023-08-14 19:12:26 +02:00 committed by GitHub
parent b74d33abed
commit 44b661dcc0
Failed to generate hash of commit
5 changed files with 117 additions and 1 deletions

View file

@ -0,0 +1,23 @@
**Added:**
* Added ``history delete`` command to both the JSON and SQLite history backends allowing users to delete commands from history that matches a pattern.
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -174,6 +174,22 @@ class History:
"""
pass
def delete(self, pattern):
"""Deletes the history of the current session for commands that match
a user-provided pattern.
Parameters
----------
pattern: str
The regex pattern to match commands against.
Returns
-------
int
The number of commands deleted from history.
"""
pass
@functools.cached_property
def ignore_regex(self):
compiled_regex = None

View file

@ -2,6 +2,7 @@
import collections
import collections.abc as cabc
import os
import re
import sys
import threading
import time
@ -10,9 +11,13 @@ from xonsh.built_ins import XSH
try:
import ujson as json
JSONDecodeError = json.JSONDecodeError # type: ignore
except ImportError:
import json # type: ignore
JSONDecodeError = json.decoder.JSONDecodeError # type: ignore
import xonsh.lazyjson as xlj
import xonsh.tools as xt
import xonsh.xoreutils.uptime as uptime
@ -549,7 +554,7 @@ class JsonHistory(History):
continue
try:
commands = json_file.load()["cmds"]
except (json.decoder.JSONDecodeError, ValueError):
except (JSONDecodeError, ValueError):
# file is corrupted somehow
if XSH.env.get("XONSH_DEBUG") > 0:
msg = "xonsh history file {0!r} is not valid JSON"
@ -596,3 +601,43 @@ class JsonHistory(History):
# Flush empty history object to disk, overwriting previous data.
self.flush()
def delete(self, pattern):
"""Deletes all entries in history which matches a pattern."""
pattern = re.compile(pattern)
deleted = 0
# First, delete any matching commands in the in-memory buffer.
for i, cmd in enumerate(self.buffer):
if pattern.match(cmd["inp"]):
del self.buffer[i]
deleted += 1
# Then, delete any matching commands on disk.
while self.gc and self.gc.is_alive():
time.sleep(0.011) # gc sleeps for 0.01 secs, sleep a beat longer
for f in _xhj_get_history_files():
try:
json_file = xlj.LazyJSON(f, reopen=False)
except ValueError:
# Invalid json file
continue
try:
file_content = json_file.load()
commands = file_content["cmds"]
for i, c in enumerate(commands):
if pattern.match(c["inp"]):
del commands[i]
deleted += 1
file_content["cmds"] = commands
with open(f, "w") as fp:
xlj.ljdump(file_content, fp)
except (JSONDecodeError, ValueError):
# file is corrupted somehow
if XSH.env.get("XONSH_DEBUG") > 0:
msg = "xonsh history file {0!r} is not valid JSON"
print(msg.format(f), file=sys.stderr)
continue
return deleted

View file

@ -339,6 +339,19 @@ class HistoryAlias(xcli.ArgParserAlias):
hist.clear()
print("History cleared", file=sys.stderr)
@staticmethod
def delete(pattern):
"""Delete all commands matching a pattern
Parameters
----------
pattern:
regex pattern to match against command history
"""
hist = XSH.history
deleted = hist.delete(pattern)
print(f"Deleted {deleted} entries from history")
@staticmethod
def file(_stdout):
"""Display the current history filename"""
@ -466,6 +479,7 @@ class HistoryAlias(xcli.ArgParserAlias):
parser.add_command(self.off)
parser.add_command(self.on)
parser.add_command(self.clear)
parser.add_command(self.delete)
parser.add_command(self.gc)
parser.add_command(self.transfer)

View file

@ -2,6 +2,7 @@
import collections
import json
import os
import re
import sqlite3
import sys
import threading
@ -220,6 +221,17 @@ def xh_sqlite_wipe_session(sessionid=None, filename=None):
c.execute(sql, (str(sessionid),))
def xh_sqlite_delete_input_matching(pattern, filename=None):
"""Deletes entries from the database where the input matches a pattern."""
with _xh_sqlite_get_conn(filename=filename) as conn:
c = conn.cursor()
_xh_sqlite_create_history_table(c)
for inp, *_ in _xh_sqlite_get_records(c):
if pattern.match(inp):
sql = f"DELETE FROM xonsh_history WHERE inp = '{inp}'"
c.execute(sql)
class SqliteHistoryGC(threading.Thread):
"""Shell history garbage collection."""
@ -385,3 +397,9 @@ class SqliteHistory(History):
self.cwds = []
xh_sqlite_wipe_session(sessionid=self.sessionid, filename=self.filename)
def delete(self, pattern):
"""Deletes all entries in the database where the input matches a pattern."""
xh_sqlite_delete_input_matching(
pattern=re.compile(pattern), filename=self.filename
)