| 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) |