]> git.ipfire.org Git - thirdparty/binutils-gdb.git/commitdiff
gdb/python: introduce gdb.Symtab.source_lines method
authorAndrew Burgess <aburgess@redhat.com>
Sun, 22 Feb 2026 17:37:34 +0000 (17:37 +0000)
committerAndrew Burgess <aburgess@redhat.com>
Thu, 5 Mar 2026 10:17:08 +0000 (10:17 +0000)
This commit adds a new method gdb.Symtab.source_lines.  This method
can be used to read the lines from a symtab's source file.  This is
similar to GDB's internal source_cache::get_source_lines function.

Currently using the Python API, if a user wants to display source
lines then they need to use Symtab.fullname() to get the source file
name, then open this file and parse out the lines themselves.

This isn't too much effort, but the problem is that these lines will
not be styled.  The user could style the source content themselves,
but will this be styled exactly as GDB would style it?

The new Symtab.source_lines() method returns source lines with styling
included (as ANSI terminal escape sequences), assuming of course, that
styling is currently enabled.

Of course, in some cases, a user of the Python API might want source
code without styling.  That's supported too, the new method has an
'unstyled' argument.  If this is True then the output is forced to be
unstyled.  The argument is named 'unstyled' rather than 'styled'
because the API call cannot force styling on.  If 'set style enabled
off' is in effect then making the API call will never return styled
source lines.

The new API call allows for a range of lines to be requested if
desired.

As part of this commit I've updated the host_string_to_python_string
utility function to take a std::string_view.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
Approved-By: Tom Tromey <tom@tromey.com>
gdb/NEWS
gdb/doc/python.texi
gdb/python/py-symtab.c
gdb/python/py-utils.c
gdb/python/python-internal.h
gdb/testsuite/gdb.python/py-symtab-source-lines.c [new file with mode: 0644]
gdb/testsuite/gdb.python/py-symtab-source-lines.exp [new file with mode: 0644]
gdb/testsuite/gdb.python/py-symtab-source-lines.py [new file with mode: 0644]

index 72471dbc751590202a1681b1cd7fa2340c957eb0..6d71ee5a23d7e0d50c458ac2f43af9ee9331f657 100644 (file)
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -207,6 +207,16 @@ qExecAndArgs
      'thread' and 'frame' attributes containing gdb.Inferior,
      gdb.InferiorThread, and gdb.Frame objects respectively.
 
+  ** New method gdb.Symtab.source_lines(start, end, unstyled).  This
+     returns a tuple of strings, each string represents a single
+     source line for the file associated with the symtab.  The 'start'
+     and 'end' are line numbers, and allow for only some lines to be
+     returned, these default to the first and last lines in the file
+     respectively.  If 'unstyled' is true then the returned lines will
+     have no styling applied, otherwise, styling will be applied if
+     the appropriate user setting is enabled, and GDB knows how to
+     style this source file.
+
 * Guile API
 
   ** Procedures 'memory-port-read-buffer-size',
index 6f1f35101f92fe4163ebec89e224568b53ddf342..009ac7a3f9fd52fa0228ad5c43ec6b3b22e7767b 100644 (file)
@@ -6817,6 +6817,32 @@ Return the line table associated with the symbol table.
 @xref{Line Tables In Python}.
 @end defun
 
+@defun Symtab.source_lines (first = @code{1}, last = @code{0}, unstyled = @code{False})
+Read source lines from the file associated with this Symtab, this will
+be the filename pointed to by @code{Symtab.fullname}.  The source
+lines are returned as a tuple of strings, each string represents a
+single source line.
+
+The @var{first} and @var{last} arguments can be used to only return
+some lines from the file, @var{first} will be the first line returned,
+and @var{last} will be the last line returned, as such @var{first}
+must be less than, or equal to, @var{last}.
+
+The default for @var{first} is @code{1}, which is the first line of
+the file.  The default for @var{last} is @code{0}, which is a special
+value meaning the last line of the file.
+
+When @var{unstyled} is @code{True} then the strings returned will have
+no style information included.  When @var{unstyled} is @code{False},
+the default, then the returned strings will include style information
+if styling is enabled (@pxref{Output Styling}), and @value{GDBN} knows
+how to style the source file.  Styling information will take the form of
+ANSI terminal escape sequences embedded within the strings.
+
+If the source file cannot be read, for example, if the source file is
+missing, then this method returns @code{None}.
+@end defun
+
 @node Line Tables In Python
 @subsubsection Manipulating line tables using Python
 
index 27d6c4beba9c5e34c9e453b52600875400d9ff88..c5e18543740fe326b78ea94a18e9f7704f0704fc 100644 (file)
@@ -23,6 +23,8 @@
 #include "python-internal.h"
 #include "objfiles.h"
 #include "block.h"
+#include "source-cache.h"
+#include "cli/cli-style.h"
 
 struct symtab_object : public PyObject
 {
@@ -223,6 +225,113 @@ stpy_get_linetable (PyObject *self, PyObject *args)
   return symtab_to_linetable_object (self).release ();
 }
 
+/* Implement gdb.Symtab.source_lines().  Return a tuple of strings, each
+   string representing a source line.  Return None if the source could not
+   be read (e.g. if the source file is missing).  */
+
+static PyObject *
+stpy_source_lines (PyObject *self, PyObject *args, PyObject *kw)
+{
+  struct symtab *symtab = nullptr;
+
+  STPY_REQUIRE_VALID (self, symtab);
+
+  static const char *keywords[] = { "first", "last", "unstyled", nullptr };
+  int first = 1, last = 0, unstyled_p = 0;
+
+  if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "|iip", keywords,
+                                       &first, &last, &unstyled_p))
+    return nullptr;
+
+  gdb_assert (unstyled_p == 0 || unstyled_p == 1);
+
+  std::optional<int> last_lineno = last_symtab_line (symtab);
+  if (!last_lineno.has_value ())
+    Py_RETURN_NONE;
+
+
+  if (first < 1)
+    {
+      PyErr_Format (PyExc_ValueError,
+                   _("Invalid value %d for first line number, "
+                     "minimum value is 1."),
+                   first);
+      return nullptr;
+    }
+  else if (first > last_lineno.value ())
+    {
+      PyErr_Format (PyExc_ValueError,
+                   _("Line number %d out of range, file has %d lines."),
+                   first, last_lineno.value ());
+      return nullptr;
+    }
+
+  if (last < 0)
+    {
+      PyErr_Format (PyExc_ValueError,
+                   _("Invalid value %d for last line number."),
+                   last);
+      return nullptr;
+    }
+  else if (last == 0 || last > last_lineno.value ())
+    last = last_lineno.value ();
+
+  if (first > last)
+    {
+      PyErr_Format (PyExc_ValueError,
+                   _("First line %d is after the last line %d."),
+                   first, last);
+      return nullptr;
+    }
+
+  try
+    {
+      std::string lines;
+
+      {
+       bool required_styling = (unstyled_p ? false : source_styling);
+       scoped_restore restore_styling
+         = make_scoped_restore (&source_styling, required_styling);
+
+       if (!g_source_cache.get_source_lines (symtab, first, last, &lines))
+         Py_RETURN_NONE;
+      }
+
+      gdbpy_ref<> list (PyList_New (0));
+      if (list == nullptr)
+       return nullptr;
+
+      for (std::string::size_type pos = 0; pos != lines.size (); )
+       {
+         std::string::size_type len;
+         std::string::size_type new_pos = lines.find ('\n', pos);
+         if (new_pos == std::string::npos)
+           len = lines.size () - pos;
+         else
+           {
+             new_pos++;
+             len = new_pos - pos;
+           }
+
+         std::string_view view (lines.c_str () + pos, len);
+         gdbpy_ref<> str = host_string_to_python_string (view);
+         if (str == nullptr)
+           return nullptr;
+
+         if (PyList_Append (list.get (), str.get ()) == -1)
+           return nullptr;
+
+         pos = new_pos;
+       }
+
+      return PyList_AsTuple (list.get ());
+    }
+  catch (const gdb_exception &except)
+    {
+      return gdbpy_handle_gdb_exception (nullptr, except);
+    }
+}
+
 static PyObject *
 salpy_str (PyObject *self)
 {
@@ -467,6 +576,11 @@ Return the static block of the symbol table." },
     { "linetable", stpy_get_linetable, METH_NOARGS,
     "linetable () -> gdb.LineTable.\n\
 Return the LineTable associated with this symbol table" },
+    { "source_lines", (PyCFunction) stpy_source_lines,
+      METH_VARARGS | METH_KEYWORDS,
+      "source_lines(Int,Int,Bool)->None|[String].\n\
+Return a list of source lines, or None if source could not\n\
+be read." },
   {NULL}  /* Sentinel */
 };
 
index 8283b30db04843f497565839e6015c33ea3f5264..484fc4611b719365e23fd9eea7bfb63e1104afcd 100644 (file)
@@ -147,13 +147,13 @@ python_string_to_host_string (PyObject *obj)
   return unicode_to_encoded_string (str.get (), host_charset ());
 }
 
-/* Convert a host string to a python string.  */
+/* See python/python-internal.h.  */
 
 gdbpy_ref<>
-host_string_to_python_string (const char *str)
+host_string_to_python_string (std::string_view str)
 {
-  return gdbpy_ref<> (PyUnicode_Decode (str, strlen (str), host_charset (),
-                                       NULL));
+  return gdbpy_ref<> (PyUnicode_Decode (str.data (), str.size (),
+                                       host_charset (), nullptr));
 }
 
 /* Return true if OBJ is a Python string or unicode object, false
index fdd353ffbeb86f15c8035ccb904f3e1c7ed906d8..bdc960ed20864545d8e9990245766473a08fa74f 100644 (file)
@@ -939,7 +939,11 @@ gdb::unique_xmalloc_ptr<char> unicode_to_target_string (PyObject *unicode_str);
 gdb::unique_xmalloc_ptr<char> python_string_to_target_string (PyObject *obj);
 gdbpy_ref<> python_string_to_target_python_string (PyObject *obj);
 gdb::unique_xmalloc_ptr<char> python_string_to_host_string (PyObject *obj);
-gdbpy_ref<> host_string_to_python_string (const char *str);
+
+/* Convert a host string STR to a python string.  */
+
+extern gdbpy_ref<> host_string_to_python_string (std::string_view str);
+
 int gdbpy_is_string (PyObject *obj);
 gdb::unique_xmalloc_ptr<char> gdbpy_obj_to_string (PyObject *obj);
 
diff --git a/gdb/testsuite/gdb.python/py-symtab-source-lines.c b/gdb/testsuite/gdb.python/py-symtab-source-lines.c
new file mode 100644 (file)
index 0000000..8c6ebb1
--- /dev/null
@@ -0,0 +1,42 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2026 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see  <http://www.gnu.org/licenses/>.  */
+
+int                    /* Location A.  */
+func (int arg)
+{
+  int i = 2;
+  i = i * arg;
+  return arg;
+}
+
+struct simple_struct
+{
+  int a;
+  int b;
+};                     /* Location B.  */
+
+int
+main (void)
+{
+  int result;
+  struct simple_struct ss = { 10, 11 };
+
+  result = func (ss.a);
+  result -= ss.b;
+
+  return result;
+}                      /* Location C.  */
diff --git a/gdb/testsuite/gdb.python/py-symtab-source-lines.exp b/gdb/testsuite/gdb.python/py-symtab-source-lines.exp
new file mode 100644 (file)
index 0000000..771505a
--- /dev/null
@@ -0,0 +1,176 @@
+# Copyright (C) 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test the gdb.Symtab.source_lines method.
+
+load_lib gdb-python.exp
+
+require allow_python_tests
+
+standard_testfile
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+if {[build_executable "failed to build" $testfile $srcfile]} {
+    return
+}
+
+# Start with a fresh GDB, but enable color support.
+with_ansi_styling_terminal {
+    clean_restart $testfile
+}
+
+if {![runto_main]} {
+    return
+}
+
+set lineno_a [gdb_get_line_number "Location A"]
+set lineno_b [gdb_get_line_number "Location B"]
+set lineno_c [gdb_get_line_number "Location C"]
+
+# Run CMD, either 'list' or a Python replacement for that command,
+# passing in ARGS.  Collect all of the command output and return it.
+# Requires that the command output not be empty (otherwise this will
+# emit a FAIL).
+proc list_lines { cmd args testname } {
+    if { $args ne "" } {
+       set args " $args"
+    }
+
+    set content ""
+    gdb_test_multiple "$cmd$args" $testname {
+       -re "$cmd$args\r\n" {
+           exp_continue
+       }
+
+       -re "^$::gdb_prompt $" {
+           gdb_assert { $content ne "" } $gdb_test_name
+       }
+
+       -re "(^\[^\r\n\]*\r\n)" {
+           set content $content$expect_out(1,string)
+           exp_continue
+       }
+    }
+
+    return $content
+}
+
+# Run 'list' and compare the output to the output of 'py-list' or
+# 'py-list-unstyled', which are commands implemented in this tests
+# Python script file.
+#
+# CLI_ARGS is a string passed to 'list', and PY_ARGS is a string
+# passed to the two 'py-list*' commands.  Passing the empty string to
+# the py-list* commands is a valid possibility, which is why the
+# default here is "NONE".  If PY_ARGS is the default value then
+# CLI_ARGS is used for both.
+proc compare_list_cmds { cli_args { py_args "NONE" } } {
+    if { $py_args eq "NONE" } {
+       set py_args $cli_args
+    }
+
+    with_test_prefix "styling on" {
+       set content_cli_styled [list_lines list $cli_args \
+                                   "get lines from builtin list command"]
+
+       set content_py_styled [list_lines py-list $py_args \
+                                  "get lines from python list command"]
+
+       gdb_assert { $content_cli_styled == $content_py_styled } \
+           "cli and py commands gave same output"
+    }
+
+    gdb_test_no_output -nopass "set style enabled off"
+
+    with_test_prefix "styling off" {
+       set content_cli_unstyled [list_lines list $cli_args \
+                                     "get lines from builtin list command"]
+
+       set content_py_unstyled [list_lines py-list $py_args \
+                                    "get lines from python list command"]
+
+       gdb_assert { $content_cli_unstyled == $content_py_unstyled } \
+           "cli and py commands gave same output"
+    }
+
+    gdb_test_no_output -nopass "set style enabled on"
+
+    with_test_prefix "force unstyled" {
+       set content_py_unstyled [list_lines py-list-unstyled $py_args \
+                                    "get lines from python list command"]
+
+       gdb_assert { $content_cli_unstyled == $content_py_unstyled } \
+           "cli and py commands gave same output"
+    }
+}
+
+gdb_test_no_output -nopass "source $pyfile"
+
+foreach {first last} [list $lineno_a $lineno_b \
+                          $lineno_a $lineno_c \
+                          1         $lineno_b \
+                          1         999] {
+
+    with_test_prefix "$first..$last" {
+       compare_list_cmds $first,$last
+    }
+}
+
+# This invokes the Python py-list command with no arguments.  This
+# then exercises the Symtab.source_lines method with no arguments
+# being passed.
+compare_list_cmds 1,999 ""
+
+# Now some error checking.
+foreach_with_prefix cmd { py-list py-list-unstyled } {
+    gdb_test "$cmd -1,5" \
+       "Error occurred in Python: Invalid value -1 for first line number, minimum value is 1\\."
+    gdb_test "$cmd 10,5" \
+       "Error occurred in Python: First line 10 is after the last line 5\\."
+    gdb_test "$cmd 999,10" \
+       "Error occurred in Python: Line number 999 out of range, file has $lineno_c lines\\."
+    gdb_test "$cmd 10,-10" \
+       "Error occurred in Python: Invalid value -10 for last line number\\."
+}
+
+# Now test the case where the source file is missing.  To achieve this
+# we first need to compile a new executable using a copy of the source
+# file.
+set srcfile2 [standard_output_file $testfile-copy.c]
+file copy ${srcdir}/${subdir}/${srcfile} $srcfile2
+
+set testfile2 ${testfile}-missing-src
+if {[build_executable "failed to build" $testfile2 $srcfile2]} {
+    return
+}
+
+file delete $srcfile2
+
+# Start with a fresh GDB, but enable color support.
+with_ansi_styling_terminal {
+    clean_restart $testfile2
+}
+
+if {![runto_main]} {
+    return
+}
+
+gdb_test_no_output -nopass "source $pyfile"
+
+foreach_with_prefix cmd { py-list py-list-unstyled } {
+    gdb_test "$cmd 5,10" \
+       "^Source file [string_to_regexp $srcfile2] not found\\."
+}
diff --git a/gdb/testsuite/gdb.python/py-symtab-source-lines.py b/gdb/testsuite/gdb.python/py-symtab-source-lines.py
new file mode 100644 (file)
index 0000000..a44da26
--- /dev/null
@@ -0,0 +1,77 @@
+# Copyright (C) 2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+
+class PythonListCommand(gdb.Command):
+    def __init__(self):
+        gdb.Command.__init__(self, "py-list", gdb.COMMAND_USER)
+
+    def invoke(self, args, from_tty):
+        argv = gdb.string_to_argv(args)
+        assert len(argv) == 0 or len(argv) == 1
+
+        pc = gdb.parse_and_eval("$pc")
+        sal = gdb.current_progspace().find_pc_line(pc)
+        assert sal.symtab is not None
+
+        if len(argv) == 0:
+            source_lines = sal.symtab.source_lines()
+            start = 1
+        else:
+            parts = argv[0].split(",")
+            source_lines = sal.symtab.source_lines(
+                first=int(parts[0]), last=int(parts[1])
+            )
+            start = int(parts[0])
+        if source_lines is None:
+            print("Source file {} not found.".format(sal.symtab.fullname()))
+        else:
+            lns = gdb.Style("line-number")
+            for i, l in enumerate(source_lines, start=start):
+                print("%s\t%s" % (lns.apply(str(i)), l), end="")
+
+
+class PythonListUnstyledCommand(gdb.Command):
+    def __init__(self):
+        gdb.Command.__init__(self, "py-list-unstyled", gdb.COMMAND_USER)
+
+    def invoke(self, args, from_tty):
+        argv = gdb.string_to_argv(args)
+        assert len(argv) == 0 or len(argv) == 1
+
+        pc = gdb.parse_and_eval("$pc")
+        sal = gdb.current_progspace().find_pc_line(pc)
+        assert sal.symtab is not None
+
+        if len(argv) == 0:
+            source_lines = sal.symtab.source_lines(unstyled=True)
+            start = 1
+        else:
+            parts = argv[0].split(",")
+            source_lines = sal.symtab.source_lines(
+                first=int(parts[0]), last=int(parts[1]), unstyled=True
+            )
+            start = int(parts[0])
+        if source_lines is None:
+            print("Source file {} not found.".format(sal.symtab.fullname()))
+        else:
+            for i, l in enumerate(source_lines, start=start):
+                print("%d\t%s" % (i, l), end="")
+
+
+PythonListCommand()
+PythonListUnstyledCommand()