1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
|
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)
|