144 lines · python
1import os2import re3import sys4import subprocess5import tempfile6 7import lldb8 9 10@lldb.command()11def 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 return17 history = _load_history(debugger, 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 return33 34 # Capture the current pasteboard contents to restore after overwriting.35 paste_snapshot = subprocess.run("pbpaste", text=True, capture_output=True).stdout36 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 return49 50 # Get the user's selected history entry.51 selected_command = subprocess.run("pbpaste", text=True, capture_output=True).stdout52 if selected_command == paste_snapshot:53 # Nothing was selected, no cleanup needed.54 return55 56 _handle_command(debugger, selected_command)57 58 # Restore the pasteboard's contents.59 subprocess.run("pbcopy", input=paste_snapshot, text=True)60 61 62def _handle_command(debugger, command):63 """Try pasting the command, and failing that, run it directly."""64 if not command:65 return66 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 > Accessibility78 # If not enabled, fallback to running the command.79 debugger.HandleCommand(command)80 81 82# `session history` example formatting:83# 1: first command84# 2: penultimate command85# 3: latest command86_HISTORY_PREFIX = re.compile(r"^\s+\d+:\s+")87 88 89def _load_session_history(debugger):90 """Load and parse lldb session history."""91 result = lldb.SBCommandReturnObject()92 interp = debugger.GetCommandInterpreter()93 interp.HandleCommand("session history", result)94 history = result.GetOutput()95 commands = []96 for line in history.splitlines():97 # Strip the prefix.98 command = _HISTORY_PREFIX.sub("", line)99 commands.append(command)100 return commands101 102 103def _load_persisted_history(history_file):104 """Load and decode lldb persisted history."""105 with open(history_file) as f:106 history_contents = f.read()107 108 # Some characters (ex spaces and newlines) are encoded as octal values, but109 # as _characters_ (not bytes). Space is the string r"\\040".110 history_decoded = re.sub(r"\\0([0-7][0-7])", _decode_char, history_contents)111 history_lines = history_decoded.splitlines()112 113 # Skip the header line (_HiStOrY_V2_)114 del history_lines[0]115 return history_lines116 117 118def _load_history(debugger, history_file):119 """Load, decode, parse, and prepare lldb history for fzf."""120 # Persisted history is older (earlier).121 history_lines = _load_persisted_history(history_file)122 # Session history is newer (later).123 history_lines.extend(_load_session_history(debugger))124 125 # Reverse to show latest first.126 history_lines.reverse()127 128 history_commands = []129 history_seen = set()130 for line in history_lines:131 line = line.strip()132 # Skip empty lines, single character commands, and duplicates.133 if line and len(line) > 1 and line not in history_seen:134 history_commands.append(line)135 history_seen.add(line)136 137 return "\n".join(history_commands)138 139 140def _decode_char(match):141 """Decode octal strings ('\0NN') into a single character string."""142 code = int(match.group(1), base=8)143 return chr(code)144