gdb/python: introduce gdb.Symtab.source_lines method

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>
This commit is contained in:
Andrew Burgess 2026-02-22 17:37:34 +00:00
parent 8012cfca41
commit 030f56e0e8
8 changed files with 454 additions and 5 deletions

View file

@ -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',

View 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

View 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 */
};

View 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

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

View file

@ -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. */

View file

@ -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\\."
}

View file

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