
lldb's history log file is written to at the end of a debugging session. As a result, the log does not contain commands run during the current session. This extends the `fzf_history` to include the output of `session history`.
144 lines
4.6 KiB
Python
144 lines
4.6 KiB
Python
import os
|
|
import re
|
|
import sys
|
|
import subprocess
|
|
import tempfile
|
|
|
|
import lldb
|
|
|
|
|
|
@lldb.command()
|
|
def fzf_history(debugger, cmdstr, ctx, result, _):
|
|
"""Use fzf to search and select from lldb command history."""
|
|
history_file = os.path.expanduser("~/.lldb/lldb-widehistory")
|
|
if not os.path.exists(history_file):
|
|
result.SetError("history file does not exist")
|
|
return
|
|
history = _load_history(debugger, history_file)
|
|
|
|
if sys.platform != "darwin":
|
|
# The ability to integrate fzf's result into lldb uses copy and paste.
|
|
# In absense of copy and paste, run the selected command directly.
|
|
temp_file = tempfile.NamedTemporaryFile("r")
|
|
fzf_command = (
|
|
"fzf",
|
|
"--no-sort",
|
|
f"--query={cmdstr}",
|
|
f"--bind=enter:execute-silent(echo -n {{}} > {temp_file.name})+accept",
|
|
)
|
|
subprocess.run(fzf_command, input=history, text=True)
|
|
command = temp_file.read()
|
|
debugger.HandleCommand(command)
|
|
return
|
|
|
|
# Capture the current pasteboard contents to restore after overwriting.
|
|
paste_snapshot = subprocess.run("pbpaste", text=True, capture_output=True).stdout
|
|
|
|
# On enter, copy the selected history entry into the pasteboard.
|
|
fzf_command = (
|
|
"fzf",
|
|
"--no-sort",
|
|
f"--query={cmdstr}",
|
|
"--bind=enter:execute-silent(echo -n {} | pbcopy)+close",
|
|
)
|
|
completed = subprocess.run(fzf_command, input=history, text=True)
|
|
# 130 is used for CTRL-C or ESC.
|
|
if completed.returncode not in (0, 130):
|
|
result.SetError("fzf failed")
|
|
return
|
|
|
|
# Get the user's selected history entry.
|
|
selected_command = subprocess.run("pbpaste", text=True, capture_output=True).stdout
|
|
if selected_command == paste_snapshot:
|
|
# Nothing was selected, no cleanup needed.
|
|
return
|
|
|
|
_handle_command(debugger, selected_command)
|
|
|
|
# Restore the pasteboard's contents.
|
|
subprocess.run("pbcopy", input=paste_snapshot, text=True)
|
|
|
|
|
|
def _handle_command(debugger, command):
|
|
"""Try pasting the command, and failing that, run it directly."""
|
|
if not command:
|
|
return
|
|
|
|
# Use applescript to paste the selected result into lldb's console.
|
|
paste_command = (
|
|
"osascript",
|
|
"-e",
|
|
'tell application "System Events" to keystroke "v" using command down',
|
|
)
|
|
completed = subprocess.run(paste_command, capture_output=True)
|
|
|
|
if completed.returncode != 0:
|
|
# The above applescript requires the "control your computer" permission.
|
|
# Settings > Private & Security > Accessibility
|
|
# If not enabled, fallback to running the command.
|
|
debugger.HandleCommand(command)
|
|
|
|
|
|
# `session history` example formatting:
|
|
# 1: first command
|
|
# 2: penultimate command
|
|
# 3: latest command
|
|
_HISTORY_PREFIX = re.compile(r"^\s+\d+:\s+")
|
|
|
|
|
|
def _load_session_history(debugger):
|
|
"""Load and parse lldb session history."""
|
|
result = lldb.SBCommandReturnObject()
|
|
interp = debugger.GetCommandInterpreter()
|
|
interp.HandleCommand("session history", result)
|
|
history = result.GetOutput()
|
|
commands = []
|
|
for line in history.splitlines():
|
|
# Strip the prefix.
|
|
command = _HISTORY_PREFIX.sub("", line)
|
|
commands.append(command)
|
|
return commands
|
|
|
|
|
|
def _load_persisted_history(history_file):
|
|
"""Load and decode lldb persisted history."""
|
|
with open(history_file) as f:
|
|
history_contents = f.read()
|
|
|
|
# Some characters (ex spaces and newlines) are encoded as octal values, but
|
|
# as _characters_ (not bytes). Space is the string r"\\040".
|
|
history_decoded = re.sub(r"\\0([0-7][0-7])", _decode_char, history_contents)
|
|
history_lines = history_decoded.splitlines()
|
|
|
|
# Skip the header line (_HiStOrY_V2_)
|
|
del history_lines[0]
|
|
return history_lines
|
|
|
|
|
|
def _load_history(debugger, history_file):
|
|
"""Load, decode, parse, and prepare lldb history for fzf."""
|
|
# Persisted history is older (earlier).
|
|
history_lines = _load_persisted_history(history_file)
|
|
# Session history is newer (later).
|
|
history_lines.extend(_load_session_history(debugger))
|
|
|
|
# Reverse to show latest first.
|
|
history_lines.reverse()
|
|
|
|
history_commands = []
|
|
history_seen = set()
|
|
for line in history_lines:
|
|
line = line.strip()
|
|
# Skip empty lines, single character commands, and duplicates.
|
|
if line and len(line) > 1 and line not in history_seen:
|
|
history_commands.append(line)
|
|
history_seen.add(line)
|
|
|
|
return "\n".join(history_commands)
|
|
|
|
|
|
def _decode_char(match):
|
|
"""Decode octal strings ('\0NN') into a single character string."""
|
|
code = int(match.group(1), base=8)
|
|
return chr(code)
|