Skip to content

Commit 83c6b1a

Browse files
authored
[lldb] Add fzf_history command to examples (#128571)
Adds a `fzf_history` to the examples directory. This python command invokes [fzf](https://github.com/junegunn/fzf) to select from lldb's command history. Tighter integration is available on macOS, via commands for copy and paste. The user's chosen history entry back is pasted into the lldb console (via AppleScript). By pasting it, users have the opportunity to edit it before running it. This matches how fzf's history search works. Without copy and paste, the user's chosen history entry is printed to screen and then run automatically.
1 parent f10e0f7 commit 83c6b1a

File tree

1 file changed

+110
-0
lines changed

1 file changed

+110
-0
lines changed

lldb/examples/python/fzf_history.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import os
2+
import re
3+
import sys
4+
import subprocess
5+
import tempfile
6+
7+
import lldb
8+
9+
10+
@lldb.command()
11+
def fzf_history(debugger, cmdstr, ctx, result, _):
12+
"""Use fzf to search and select from lldb command history."""
13+
history_file = os.path.expanduser("~/.lldb/lldb-widehistory")
14+
if not os.path.exists(history_file):
15+
result.SetError("history file does not exist")
16+
return
17+
history = _load_history(history_file)
18+
19+
if sys.platform != "darwin":
20+
# The ability to integrate fzf's result into lldb uses copy and paste.
21+
# In absense of copy and paste, run the selected command directly.
22+
temp_file = tempfile.NamedTemporaryFile("r")
23+
fzf_command = (
24+
"fzf",
25+
"--no-sort",
26+
f"--query={cmdstr}",
27+
f"--bind=enter:execute-silent(echo -n {{}} > {temp_file.name})+accept",
28+
)
29+
subprocess.run(fzf_command, input=history, text=True)
30+
command = temp_file.read()
31+
debugger.HandleCommand(command)
32+
return
33+
34+
# Capture the current pasteboard contents to restore after overwriting.
35+
paste_snapshot = subprocess.run("pbpaste", text=True, capture_output=True).stdout
36+
37+
# On enter, copy the selected history entry into the pasteboard.
38+
fzf_command = (
39+
"fzf",
40+
"--no-sort",
41+
f"--query={cmdstr}",
42+
"--bind=enter:execute-silent(echo -n {} | pbcopy)+close",
43+
)
44+
completed = subprocess.run(fzf_command, input=history, text=True)
45+
# 130 is used for CTRL-C or ESC.
46+
if completed.returncode not in (0, 130):
47+
result.SetError("fzf failed")
48+
return
49+
50+
# Get the user's selected history entry.
51+
selected_command = subprocess.run("pbpaste", text=True, capture_output=True).stdout
52+
if selected_command == paste_snapshot:
53+
# Nothing was selected, no cleanup needed.
54+
return
55+
56+
_handle_command(debugger, selected_command)
57+
58+
# Restore the pasteboard's contents.
59+
subprocess.run("pbcopy", input=paste_snapshot, text=True)
60+
61+
62+
def _handle_command(debugger, command):
63+
"""Try pasting the command, and failing that, run it directly."""
64+
if not command:
65+
return
66+
67+
# Use applescript to paste the selected result into lldb's console.
68+
paste_command = (
69+
"osascript",
70+
"-e",
71+
'tell application "System Events" to keystroke "v" using command down',
72+
)
73+
completed = subprocess.run(paste_command, capture_output=True)
74+
75+
if completed.returncode != 0:
76+
# The above applescript requires the "control your computer" permission.
77+
# Settings > Private & Security > Accessibility
78+
# If not enabled, fallback to running the command.
79+
debugger.HandleCommand(command)
80+
81+
82+
def _load_history(history_file):
83+
"""Load, decode, parse, and prepare an lldb history file for fzf."""
84+
with open(history_file) as f:
85+
history_contents = f.read()
86+
87+
history_decoded = re.sub(r"\\0([0-7][0-7])", _decode_char, history_contents)
88+
history_lines = history_decoded.splitlines()
89+
90+
# Skip the header line (_HiStOrY_V2_)
91+
del history_lines[0]
92+
# Reverse to show latest first.
93+
history_lines.reverse()
94+
95+
history_commands = []
96+
history_seen = set()
97+
for line in history_lines:
98+
line = line.strip()
99+
# Skip empty lines, single character commands, and duplicates.
100+
if line and len(line) > 1 and line not in history_seen:
101+
history_commands.append(line)
102+
history_seen.add(line)
103+
104+
return "\n".join(history_commands)
105+
106+
107+
def _decode_char(match):
108+
"""Decode octal strings ('\0NN') into a single character string."""
109+
code = int(match.group(1), base=8)
110+
return chr(code)

0 commit comments

Comments
 (0)