[Lldb-commits] [lldb] [lldb] Add `pipe` command for piping through shell commands (PR #201751)
via lldb-commits
lldb-commits at lists.llvm.org
Thu Jun 4 22:37:31 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-lldb
Author: Dave Lee (kastiglione)
<details>
<summary>Changes</summary>
Add a new Python utility (`lldb.utils.pipe`) that lets users pipe the output of lldb commands through shell pipelines or redirect to files.
The command distinguishs lldb commands from shell commands, both work transparently:
```
(lldb) pipe bt all | grep main
(lldb) pipe settings list > /tmp/settings.txt
(lldb) pipe /bin/ls | head -5
```
Pager detection (`less`, `more`, `bat`, `$PAGER`) is used to bypass output capture, ensuring that interactive pagers can access the tty.
---
Full diff: https://github.com/llvm/llvm-project/pull/201751.diff
3 Files Affected:
- (modified) lldb/bindings/python/CMakeLists.txt (+1)
- (added) lldb/examples/python/pipe.py (+144)
- (added) lldb/test/API/commands/command/pipe/TestPipeCommand.py (+65)
``````````diff
diff --git a/lldb/bindings/python/CMakeLists.txt b/lldb/bindings/python/CMakeLists.txt
index e4f978ed80bc3..dacfd49054ada 100644
--- a/lldb/bindings/python/CMakeLists.txt
+++ b/lldb/bindings/python/CMakeLists.txt
@@ -102,6 +102,7 @@ function(finish_swig_python swig_target lldb_python_bindings_dir lldb_python_tar
${lldb_python_target_dir}
"utils"
FILES "${LLDB_SOURCE_DIR}/examples/python/in_call_stack.py"
+ "${LLDB_SOURCE_DIR}/examples/python/pipe.py"
"${LLDB_SOURCE_DIR}/examples/python/symbolication.py"
)
diff --git a/lldb/examples/python/pipe.py b/lldb/examples/python/pipe.py
new file mode 100644
index 0000000000000..183df8d11ff5a
--- /dev/null
+++ b/lldb/examples/python/pipe.py
@@ -0,0 +1,144 @@
+r"""
+Pipe or redirect LLDB command output through shell commands.
+
+Usage:
+ (lldb) pipe <lldb-command> | <shell-pipeline>
+ (lldb) pipe <lldb-command> > <file>
+Examples:
+ (lldb) pipe image list | wc -l
+ (lldb) pipe settings list | grep color
+ (lldb) pipe bt all | grep Foundation
+ (lldb) pipe bt all > /tmp/stack.txt
+ (lldb) pipe bt all | tee /tmp/stack.txt
+ (lldb) pipe frame variable *this | pbcopy
+ (lldb) pipe breakpoint list | grep 'where = .*resolved'
+ (lldb) pipe register read | sort
+
+Import:
+ (lldb) command script import lldb.utils.pipe
+"""
+
+import os
+import shlex
+import subprocess
+from typing import Iterable, Iterator, Optional, Tuple
+
+import lldb
+
+
+# Equivalent to itertools.pairwise, available in Python 3.10+.
+def _pairwise(iterable: Iterable) -> Iterator[Tuple]:
+ """Yield consecutive overlapping pairs: _pairwise(ABCD) -> AB BC CD."""
+ iterator = iter(iterable)
+ a = next(iterator, None)
+ for b in iterator:
+ yield a, b
+ a = b
+
+
+def _tokenize(cmdstr: str) -> shlex.shlex:
+ """Splits on whitespace and treats | and > as standalone punctuation, even
+ without surrounding spaces."""
+
+ lex = shlex.shlex(cmdstr, posix=True, punctuation_chars="|>")
+ lex.whitespace_split = True
+ return lex
+
+
+def _split_cmd(cmdstr: str) -> Tuple[str, Optional[str]]:
+ """Split cmdstr at the first unquoted shell operator (| or >)."""
+ lex = _tokenize(cmdstr)
+ for token in lex:
+ if token in ("|", ">"):
+ # Search up to where shlex has consumed.
+ split = cmdstr.rindex(token, 0, lex.instream.tell())
+ first_cmd = cmdstr[:split].rstrip()
+ shell_suffix = cmdstr[split:]
+ return first_cmd, shell_suffix
+ return cmdstr, None
+
+
+def _pager_names() -> set[str]:
+ """Return known pager command names, including $PAGER if set."""
+ pagers = {"less", "more", "most", "bat"}
+ if pager := os.environ.get("PAGER"):
+ # shlex.split handles flags, e.g. "less -R" -> "less".
+ pager = shlex.split(pager)[0]
+ pagers.add(os.path.basename(pager))
+ return pagers
+
+
+_PAGERS = _pager_names()
+
+
+def _ends_with_pager(shell_cmd: str) -> bool:
+ """Check if the last command in a shell pipeline is a pager."""
+ lex = _tokenize(shell_cmd)
+ for tok1, tok2 in reversed(list(_pairwise(lex))):
+ if tok1 == "|":
+ return os.path.basename(tok2) in _PAGERS
+ return False
+
+
+def _is_lldb_command(interp: lldb.SBCommandInterpreter, cmdstr: str) -> bool:
+ result = lldb.SBCommandReturnObject()
+ interp.ResolveCommand(cmdstr, result)
+ return result.Succeeded()
+
+
+ at lldb.command()
+def pipe(
+ debugger: lldb.SBDebugger,
+ cmdstr: str,
+ exe_ctx: lldb.SBExecutionContext,
+ result: lldb.SBCommandReturnObject,
+ _,
+):
+ """Pipe or redirect LLDB command output through shell commands."""
+ interp = debugger.GetCommandInterpreter()
+
+ first_cmd, shell_suffix = _split_cmd(cmdstr)
+
+ if not first_cmd and shell_suffix:
+ result.SetError("no command before shell operator")
+ return
+
+ if _is_lldb_command(interp, first_cmd):
+ if shell_suffix is None:
+ # Run a single lldb command, shell command to pipe to.
+ interp.HandleCommand(first_cmd, exe_ctx, result)
+ return
+
+ cmd_result = lldb.SBCommandReturnObject()
+ interp.HandleCommand(first_cmd, exe_ctx, cmd_result)
+
+ if cmd_result.GetError():
+ result.SetError(cmd_result.GetError())
+ if not cmd_result.Succeeded():
+ if not cmd_result.GetError():
+ result.SetError(f"command failed: {first_cmd}")
+ return
+
+ output = cmd_result.GetOutput()
+ # `cat` works with both | and > operators.
+ shell_cmd = f"cat {shell_suffix}"
+ else:
+ # No lldb command to run, only shell.
+ output = None
+ shell_cmd = cmdstr
+
+ try:
+ if _ends_with_pager(shell_cmd):
+ proc = subprocess.Popen(shell_cmd, shell=True, stdin=subprocess.PIPE)
+ proc.communicate(output.encode() if output else None)
+ else:
+ proc = subprocess.run(
+ shell_cmd, shell=True, input=output, capture_output=True, text=True
+ )
+ if proc.stdout:
+ result.PutCString(proc.stdout)
+ if proc.stderr:
+ result.SetError(proc.stderr)
+ except OSError as e:
+ result.SetError(f"failed to run shell command: {e}")
+ return
diff --git a/lldb/test/API/commands/command/pipe/TestPipeCommand.py b/lldb/test/API/commands/command/pipe/TestPipeCommand.py
new file mode 100644
index 0000000000000..dffdee4414700
--- /dev/null
+++ b/lldb/test/API/commands/command/pipe/TestPipeCommand.py
@@ -0,0 +1,65 @@
+"""
+Test the pipe command (lldb.utils.pipe).
+"""
+
+import os
+import tempfile
+
+import lldb
+from lldbsuite.test.lldbtest import *
+
+
+class TestCase(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ def setUp(self):
+ TestBase.setUp(self)
+ self.runCmd("command script import lldb.utils.pipe")
+
+ def test_pipe_to_grep(self):
+ """Test piping an LLDB command through grep."""
+ self.expect("pipe help help | grep Syntax", substrs=["Syntax"])
+
+ def test_lldb_command_no_pipe(self):
+ """Test running a plain LLDB command (passthrough)."""
+ self.expect("pipe help help", substrs=["Syntax"])
+
+ def test_pipe_chain(self):
+ """Test piping through multiple shell commands."""
+ self.expect("pipe help help | tr s-z S-Z | grep -i syntax", substrs=["SYnTaX"])
+
+ def test_redirect_stdout(self):
+ """Test redirecting LLDB command output to a file."""
+ with tempfile.NamedTemporaryFile(mode="r", delete=False) as f:
+ path = f.name
+ try:
+ self.runCmd(f"pipe help help > {path}")
+ with open(path) as f:
+ contents = f.read()
+ self.assertIn("Syntax", contents)
+ finally:
+ os.unlink(path)
+
+ def test_shell_command_no_pipe(self):
+ """Test running a plain shell command (not an LLDB command)."""
+ self.expect("pipe echo hello", substrs=["hello"])
+
+ def test_shell_command_with_pipe(self):
+ """Test running a shell command piped to another shell command."""
+ self.expect("pipe echo hello world | tr a-z A-Z", substrs=["HELLO WORLD"])
+
+ def test_quoted_pipe_not_split(self):
+ """Test that a pipe character inside quotes is not treated as a split."""
+ self.expect("pipe echo 'abc|def' | tr a-c A-C", substrs=["ABC|def"])
+
+ def test_pipe_without_spaces(self):
+ """Test that pipe works without spaces around the | operator."""
+ self.expect("pipe help help|grep Syntax", substrs=["Syntax"])
+
+ def test_error_no_command_before_pipe(self):
+ """Test error when nothing precedes the pipe operator."""
+ self.expect("pipe | grep foo", error=True)
+
+ def test_failed_lldb_command(self):
+ """Test that a non-lldb non-shell command reports an error."""
+ self.expect("pipe not_a_real_lldb_command_xyz | wc -l", error=True)
``````````
</details>
https://github.com/llvm/llvm-project/pull/201751
More information about the lldb-commits
mailing list