]> git.ipfire.org Git - thirdparty/binutils-gdb.git/commitdiff
gdb/python: extend gdb.write to support styled output
authorAndrew Burgess <aburgess@redhat.com>
Tue, 17 Jun 2025 17:09:49 +0000 (18:09 +0100)
committerAndrew Burgess <aburgess@redhat.com>
Sun, 5 Oct 2025 12:48:06 +0000 (13:48 +0100)
It is already possible to produce styled output from Python by
converting the gdb.Style to its escape code sequence, and writing that
to the output stream.

But this commit adds an alternative option to the mix by extending the
existing gdb.write() function to accept a 'style' argument.  The value
of this argument can be 'None' to indicate no style change should be
performed, this is the default, and matches the existing behaviour.

Or the new 'style' argument can be a gdb.Style object, in which case
the specified style is applied only for the string passed to
gdb.write, after which the default style is re-applied.

Using gdb.write with a style object more closely matches how GDB
handles styling internally, and has the benefit that the user doesn't
need to remember to restore the default style when they are done.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
Approved-By: Tom Tromey <tom@tromey.com>
gdb/NEWS
gdb/doc/python.texi
gdb/python/py-style.c
gdb/python/python-internal.h
gdb/python/python.c
gdb/testsuite/gdb.python/py-color-pagination.exp
gdb/testsuite/gdb.python/py-color-pagination.py
gdb/testsuite/gdb.python/py-style.exp

index b9c9afc1d77a44f37937e421b04e26d30fb69e39..2c73776944ff43ae9a3d0c35a82ab0d105bc8168 100644 (file)
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -80,6 +80,9 @@ qExecAndArgs
      Use gdb.StyleParameterSet(NAME) to create 'set style NAME ...'
      and 'show style NAME ...' parameters.
 
+  ** The gdb.write() function now takes an additional, optional,
+     'style' argument, which can be used to style the output.
+
 *** Changes in GDB 17
 
 * Debugging Linux programs that use x86-64 or x86-64 with 32-bit pointer
index e3f95ee1d24e73b5fc0e53d8f6f7c3b247096b2b..2f74a2311a67cf4a117cdd72640b84d7e6b3fafb 100644 (file)
@@ -452,7 +452,7 @@ will be @code{None} and 0 respectively.  This is identical to
 historical compatibility.
 @end defun
 
-@defun gdb.write (string @r{[}, stream@r{]})
+@defun gdb.write (string @r{[}, stream@r{]} @r{[}, style@r{]})
 Print a string to @value{GDBN}'s paginated output stream.  The
 optional @var{stream} determines the stream to print to.  The default
 stream is @value{GDBN}'s standard output stream.  Possible stream
@@ -475,6 +475,13 @@ values are:
 @value{GDBN}'s log stream (@pxref{Logging Output}).
 @end table
 
+The @var{style} should be a @code{gdb.Style} object (@pxref{Styles In
+Python}), or @code{None} (the default).  If @var{style} is @code{None}
+then the current style for @var{stream} will be applied to @var{text}.
+If @var{style} is a @code{gdb.Style} object, then this style is
+applied to @var{text}, after which the default output style is
+restored.
+
 Writing to @code{sys.stdout} or @code{sys.stderr} will automatically
 call this function and will automatically direct the output to the
 relevant stream.
index 51b35f2fa7e6967dcb6ade236afe195f2205baa8..cf65e3115b20db701f1b2f6c1407588c8f86ee09 100644 (file)
@@ -353,6 +353,15 @@ stylepy_init (PyObject *self, PyObject *args, PyObject *kwargs)
 
 \f
 
+/* See python-internal.h.   */
+
+bool
+gdbpy_is_style (PyObject *obj)
+{
+  gdb_assert (obj != nullptr);
+  return PyObject_TypeCheck (obj, &style_object_type);
+}
+
 /* Return the ui_file_style for STYLEPY.  If the style cannot be found,
    then return an empty optional, and set a Python error.  */
 
@@ -369,6 +378,18 @@ stylepy_to_style (style_object *stylepy)
   return style;
 }
 
+/* See python-internal.h.   */
+
+std::optional<ui_file_style>
+gdbpy_style_object_to_ui_file_style (PyObject *obj)
+{
+  gdb_assert (obj != nullptr);
+  gdb_assert (gdbpy_is_style (obj));
+
+  style_object *style_obj = (style_object *) obj;
+  return stylepy_to_style (style_obj);
+}
+
 /* Implementation of gdb.Style.escape_sequence().  Return the escape
    sequence to apply Style.  If styling is turned off, then this returns
    the empty string.  Can raise an exception if a named style can no longer
index f61a1753ac40988745a1530370325b9a80e27d54..dbb2d7eb7e05c9958ff54cd1f74a0e3c0ad88ba5 100644 (file)
@@ -570,6 +570,20 @@ struct symtab_and_line *sal_object_to_symtab_and_line (PyObject *obj);
 frame_info_ptr frame_object_to_frame_info (PyObject *frame_obj);
 struct gdbarch *arch_object_to_gdbarch (PyObject *obj);
 
+/* Return true if OBJ is a gdb.Style object.  OBJ must not be NULL.  */
+
+extern bool gdbpy_is_style (PyObject *obj);
+
+/* Return the ui_file_style from OBJ, a gdb.Style object.  OBJ must not be
+   NULL.
+
+   It is possible that OBJ is a gdb.Style object, but the underlying style
+   cannot be fetched for some reason.  If this happens then a Python error
+   is set and an empty optional is returned.  */
+
+extern std::optional<ui_file_style>
+  gdbpy_style_object_to_ui_file_style (PyObject *obj);
+
 extern PyObject *gdbpy_execute_mi_command (PyObject *self, PyObject *args,
                                           PyObject *kw);
 
index 740b196874286dded845788e91006a5031461314..51e7a0aa16558bda293101e5c07bd138eb6177c9 100644 (file)
@@ -1567,12 +1567,22 @@ static PyObject *
 gdbpy_write (PyObject *self, PyObject *args, PyObject *kw)
 {
   const char *arg;
-  static const char *keywords[] = { "text", "stream", NULL };
+  static const char *keywords[] = { "text", "stream", "style", nullptr };
   int stream_type = 0;
+  PyObject *style_obj = Py_None;
 
-  if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s|i", keywords, &arg,
-                                       &stream_type))
-    return NULL;
+  if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s|iO", keywords, &arg,
+                                       &stream_type, &style_obj))
+    return nullptr;
+
+  if (style_obj != Py_None && !gdbpy_is_style (style_obj))
+    {
+      PyErr_Format
+       (PyExc_TypeError,
+        _("'style' argument must be gdb.Style or None, not %s."),
+        Py_TYPE (style_obj)->tp_name);
+      return nullptr;
+    }
 
   try
     {
@@ -1590,7 +1600,17 @@ gdbpy_write (PyObject *self, PyObject *args, PyObject *kw)
          break;
        }
 
-      gdb_puts (arg, stream);
+      if (style_obj == Py_None)
+       gdb_puts (arg, stream);
+      else
+       {
+         std::optional<ui_file_style> style
+           = gdbpy_style_object_to_ui_file_style (style_obj);
+         if (!style.has_value ())
+           return nullptr;
+
+         fputs_styled (arg, style.value (), stream);
+       }
     }
   catch (const gdb_exception &except)
     {
index 1974dbb41868c1d7e4e1ba00a5f2964b21aa8075..6b10f9ca793cff633954bab147ec129b4537535f 100644 (file)
@@ -131,8 +131,64 @@ proc test_pagination { type mode } {
     }
 }
 
+# Run the command 'style-fill-v2' which fills the screen with output and
+# triggers the pagination prompt.  Check that styling is applied correctly
+# to the output.  This v2 command is exercising passing a style to
+# gdb.write() rather than passing the escape sequence for the style.
+proc test_pagination_v2 { } {
+    set saw_bad_color_handling false
+    set expected_restore_color ""
+    set last_color ""
+    gdb_test_multiple "style-fill-v2" "" {
+       -re "^style-fill-v2\r\n" {
+           exp_continue
+       }
+
+       -re "^(${::any_color}\033\\\[m)(${::any_color})$::str\033\\\[m" {
+           # After a continuation prompt GDB will restore the previous
+           # color, and then we immediately switch to a new color.
+           set restored_color $expect_out(1,string)
+           if { $restored_color ne $expected_restore_color } {
+               set saw_bad_color_handling true
+           }
+           set last_color $expect_out(2,string)
+           exp_continue
+       }
+
+       -re "^(${::any_color})$::str\033\\\[m" {
+           # This pattern matches printing STR in all cases that are not
+           # immediately after a pagination prompt.  In this case there is
+           # a single escape sequence to set the color.
+           set last_color $expect_out(1,string)
+           exp_continue
+       }
+
+       -re "^$::pagination_prompt$" {
+           # After a pagination prompt we expect GDB to restore the last
+           # color, but this will then be disabled due to a styled
+           # gdb.write emitting a return to default style escape sequence.
+           set expected_restore_color "$last_color\033\[m"
+
+           # Send '\n' to view more output.
+           send_gdb "\n"
+           exp_continue
+       }
+
+       -re "^\r\n" {
+           # The matches the newline sent to the continuation prompt.
+           exp_continue
+       }
+
+       -re "^$::gdb_prompt $" {
+           gdb_assert { !$saw_bad_color_handling } $gdb_test_name
+       }
+    }
+}
+
 foreach_with_prefix type { color style } {
     foreach_with_prefix mode { write print } {
        test_pagination $type $mode
     }
 }
+
+test_pagination_v2
index f0252e5bf865893a195f2393b2f276e516932425..9cdc76cb0ce6918c49c74132eacc03231b8e5766 100644 (file)
@@ -62,5 +62,21 @@ class StyleTester(gdb.Command):
         write(mode, "\n")
 
 
+class StyleTester2(gdb.Command):
+    def __init__(self):
+        super().__init__("style-fill-v2", gdb.COMMAND_USER)
+
+    def invoke(self, args, from_tty):
+        str = "<" + "-" * 78 + ">"
+        for i in range(0, 20):
+            for color_name in basic_colors:
+                c = gdb.Color(color_name)
+                s = gdb.Style(foreground=c)
+                gdb.write(str, style=s)
+
+        gdb.write("\n")
+
+
 ColorTester()
 StyleTester()
+StyleTester2()
index b2efe9a97e36ce79200faf49d802c66064a55cd6..491e189774ecff1aa8eec98b457b3e50190e5082 100644 (file)
@@ -340,3 +340,32 @@ gdb_test_no_output "python input_text = \"a totally different string that is als
 gdb_test "python print(output_text)" \
     "^this is a unique string that is unlikely to appear elsewhere 12345" \
     "check the output_text is still valid"
+
+# Test gdb.write passing in a style.  Define a helper function to
+# ensure all output is flushed before we return to the prompt.
+gdb_test_multiline "create function to call gdb.write then flush" \
+    "python" "" \
+    "def write_and_flush(*args, **kwargs):" "" \
+    "  gdb.write(*args, **kwargs)" "" \
+    "  gdb.write(\"\\n\")" "" \
+    "  gdb.flush(gdb.STDOUT)" "" \
+    "end" ""
+
+gdb_test "python write_and_flush(\"some text\")" \
+    "^some text" "unstyled text, no style passed"
+
+gdb_test "python write_and_flush(\"some text\", style=None)" \
+    "^some text" "unstyled text, pass style as None"
+
+gdb_test "python write_and_flush(\"some text\", style=filename_style)" \
+    "^\033\\\[34;41;2;23;24;27msome text\033\\\[m" \
+    "styled output, pass style by keyword"
+
+gdb_test "python write_and_flush(\"some text\", gdb.STDOUT, filename_style)" \
+    "^\033\\\[34;41;2;23;24;27msome text\033\\\[m" \
+    "styled output, pass style by position"
+
+gdb_test "python write_and_flush(\"some text\", style='filename')" \
+    [multi_line \
+        "Python Exception <class 'TypeError'>: 'style' argument must be gdb\\.Style or None, not str\\." \
+        "Error occurred in Python: 'style' argument must be gdb\\.Style or None, not str\\."]