--- /dev/null
+# Copyright 2025 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 that the ANSI escape sequence matcher works
+# character-by-character.
+
+load_lib gdb-python.exp
+require allow_python_tests allow_tui_tests
+
+tuiterm_env
+
+Term::clean_restart 24 80
+
+set remote_python_file [gdb_remote_download host \
+ ${srcdir}/${subdir}/esc-match.py]
+gdb_test_no_output "source ${remote_python_file}" \
+ "source esc-match.py"
+
+if {![Term::enter_tui]} {
+ unsupported "TUI not supported"
+ return
+}
+
+Term::command "python print_it()"
+
+Term::dump_screen
+
+set text [Term::get_all_lines]
+# We should not see the control sequence here.
+gdb_assert {![regexp -- "\\\[35;1mOUTPUT\\\[m" $text]} \
+ "output visible without control sequences"
+
+# Also check the styling.
+set text [Term::get_region 0 1 78 23 "\n" true]
+gdb_assert {[regexp -- "<fg:magenta>.*OUTPUT" $text]} \
+ "output is magenta"
--- /dev/null
+# Copyright (C) 2025 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 sys
+
+# Some text to print that includes styling.
+OUT = "\033[35;1mOUTPUT\033[m"
+
+
+def print_it():
+ # Print to stderr avoids any buffering, showing the bug.
+ for c in OUT:
+ print(c, end="", file=sys.stderr)
+ print(file=sys.stderr)
#include "tui/tui-command.h"
void
-tui_file::puts (const char *linebuffer)
+tui_file::do_puts (const char *linebuffer)
{
tui_puts (linebuffer);
if (!m_buffered)
}
void
-tui_file::write (const char *buf, long length_buf)
+tui_file::do_write (const char *buf, long length_buf)
{
tui_write (buf, length_buf);
if (!m_buffered)
{
if (m_buffered)
tui_cmd_win ()->refresh_window ();
- stdio_file::flush ();
+ escape_buffering_file::flush ();
}
/* A STDIO-like output stream for the TUI. */
-class tui_file : public stdio_file
+class tui_file : public escape_buffering_file
{
public:
tui_file (FILE *stream, bool buffered)
- : stdio_file (stream),
+ : escape_buffering_file (stream),
m_buffered (buffered)
{}
- void write (const char *buf, long length_buf) override;
- void puts (const char *) override;
void flush () override;
+protected:
+
+ void do_write (const char *buf, long length_buf) override;
+ void do_puts (const char *) override;
+
private:
/* True if this stream is buffered. */
/* See ui-file.h. */
void
-no_terminal_escape_file::write (const char *buf, long length_buf)
+escape_buffering_file::write (const char *buf, long length_buf)
{
std::string copy (buf, length_buf);
this->puts (copy.c_str ());
/* See ui-file.h. */
void
-no_terminal_escape_file::puts (const char *buf)
+escape_buffering_file::puts (const char *buf)
+{
+ std::string local_buffer;
+ if (!m_buffer.empty ())
+ {
+ gdb_assert (m_buffer[0] == '\033');
+ m_buffer += buf;
+ /* If we need to keep buffering, we'll handle that below. */
+ local_buffer = std::move (m_buffer);
+ buf = local_buffer.c_str ();
+ }
+
+ while (*buf != '\0')
+ {
+ const char *esc = strchr (buf, '\033');
+ if (esc == nullptr)
+ break;
+
+ /* First, write out any prefix. */
+ if (esc > buf)
+ {
+ do_write (buf, esc - buf);
+ buf = esc;
+ }
+
+ int n_read = 0;
+ ansi_escape_result seen = examine_ansi_escape (esc, &n_read);
+ if (seen == ansi_escape_result::INCOMPLETE)
+ {
+ /* Start buffering. */
+ m_buffer = buf;
+ return;
+ }
+ else if (seen == ansi_escape_result::NO_MATCH)
+ {
+ /* Just emit the ESC . */
+ n_read = 1;
+ }
+ else
+ gdb_assert (seen == ansi_escape_result::MATCHED);
+
+ do_write (esc, n_read);
+ buf += n_read;
+ }
+
+ /* If there is any data remaining in BUF, we can flush it now. */
+ if (*buf != '\0')
+ do_puts (buf);
+}
+
+/* See ui-file.h. */
+
+void
+no_terminal_escape_file::do_puts (const char *buf)
{
while (*buf != '\0')
{
this->stdio_file::write (buf, strlen (buf));
}
+void
+no_terminal_escape_file::do_write (const char *buf, long len)
+{
+ std::string copy (buf, len);
+ do_puts (copy.c_str ());
+}
+
void
timestamped_file::write (const char *buf, long len)
{
ui_file *m_two;
};
+/* A ui_file implementation that buffers terminal escape sequences.
+ Note that this does not buffer in general -- it only buffers when
+ an incomplete but potentially recognizable escape sequence is
+ started. */
+
+class escape_buffering_file : public stdio_file
+{
+public:
+ using stdio_file::stdio_file;
+
+ /* Like the stdio_file methods but these forward to do_write and
+ do_puts, respectively. */
+ void write (const char *buf, long length_buf) override final;
+ void puts (const char *linebuffer) override final;
+
+ /* This class does not override 'flush'. While it does have an
+ internal buffer, it does not really make sense to flush the
+ buffer until an escape sequence has been fully processed. */
+
+protected:
+
+ /* Called to output some text. If the text contains a recognizable
+ terminal escape sequence, then it is guaranteed to be complete.
+ "Recognizable" here means that examine_ansi_escape did not return
+ INCOMPLETE. */
+ virtual void do_puts (const char *buf) = 0;
+ virtual void do_write (const char *buf, long len) = 0;
+
+private:
+
+ /* Buffer used only for incomplete escape sequences. */
+ std::string m_buffer;
+};
+
/* A ui_file implementation that filters out terminal escape
sequences. */
-class no_terminal_escape_file : public stdio_file
+class no_terminal_escape_file : public escape_buffering_file
{
public:
no_terminal_escape_file ()
{
}
- /* Like the stdio_file methods, but these filter out terminal escape
- sequences. */
- void write (const char *buf, long length_buf) override;
- void puts (const char *linebuffer) override;
-
void emit_style_escape (const ui_file_style &style) override
{
}
+
+protected:
+
+ void do_puts (const char *linebuffer) override;
+ void do_write (const char *buf, long len) override;
};
/* A base class for ui_file types that wrap another ui_file. */
#include "gdbsupport/gdb_regex.h"
/* A regular expression that is used for matching ANSI terminal escape
- sequences. */
+ sequences. Note that this will actually match any prefix of such a
+ sequence. This property is used so that other code can buffer
+ incomplete sequences as needed. */
static const char ansi_regex_text[] =
- /* Introduction. */
- "^\033\\["
-#define DATA_SUBEXP 1
+ /* Introduction. Only the escape character is truly required. */
+ "^\033(\\["
+#define DATA_SUBEXP 2
/* Capture parameter and intermediate bytes. */
"("
/* Parameter bytes. */
/* End the first capture. */
")"
/* The final byte. */
-#define FINAL_SUBEXP 2
- "([\x40-\x7e])";
+#define FINAL_SUBEXP 3
+ "([\x40-\x7e]))?";
/* The number of subexpressions to allocate space for, including the
"0th" whole match subexpression. */
-#define NUM_SUBEXPRESSIONS 3
+#define NUM_SUBEXPRESSIONS 4
/* The compiled form of ansi_regex_text. */
*n_read = 0;
return false;
}
+
+ /* If the final subexpression did not match, then that means there
+ was an incomplete sequence. These are ignored here. */
+ if (subexps[FINAL_SUBEXP].rm_so == -1)
+ {
+ *n_read = 0;
+ return false;
+ }
+
/* Other failures mean the regexp is broken. */
gdb_assert (match == 0);
/* The regexp is anchored. */
/* See ui-style.h. */
-bool
-skip_ansi_escape (const char *buf, int *n_read)
+ansi_escape_result
+examine_ansi_escape (const char *buf, int *n_read)
{
+ gdb_assert (*buf == '\033');
+
regmatch_t subexps[NUM_SUBEXPRESSIONS];
int match = ansi_regex.exec (buf, ARRAY_SIZE (subexps), subexps, 0);
- if (match == REG_NOMATCH || buf[subexps[FINAL_SUBEXP].rm_so] != 'm')
- return false;
+ if (match == REG_NOMATCH)
+ return ansi_escape_result::NO_MATCH;
+
+ if (subexps[FINAL_SUBEXP].rm_so == -1)
+ return ansi_escape_result::INCOMPLETE;
+
+ if (buf[subexps[FINAL_SUBEXP].rm_so] != 'm')
+ return ansi_escape_result::NO_MATCH;
*n_read = subexps[FINAL_SUBEXP].rm_eo;
- return true;
+ return ansi_escape_result::MATCHED;
}
/* See ui-style.h. */
bool m_reverse = false;
};
+/* Possible results for checking an ANSI escape sequence. */
+enum class ansi_escape_result
+{
+ /* The escape sequence is definitely not recognizable. */
+ NO_MATCH,
+
+ /* The escape sequence might be recognizable with more input. */
+ INCOMPLETE,
+
+ /* The escape sequence is definitely recognizable. */
+ MATCHED,
+};
+
+/* Examine an ANSI escape sequence in BUF. BUF must begin with an ESC
+ character. Return a value indicating whether the sequence was
+ recognizable. If MATCHED is returned, then N_READ is updated to
+ reflect the number of chars read from BUF. */
+
+extern ansi_escape_result examine_ansi_escape (const char *buf, int *n_read);
+
/* Skip an ANSI escape sequence in BUF. BUF must begin with an ESC
character. Return true if an escape sequence was successfully
skipped; false otherwise. If an escape sequence was skipped,
N_READ is updated to reflect the number of chars read from BUF. */
-extern bool skip_ansi_escape (const char *buf, int *n_read);
+static inline bool
+skip_ansi_escape (const char *buf, int *n_read)
+{
+ return examine_ansi_escape (buf, n_read) == ansi_escape_result::MATCHED;
+}
#endif /* GDB_UI_STYLE_H */
pager_file::write (const char *buf, long length_buf)
{
/* We have to make a string here because the pager uses
- skip_ansi_escape, which requires NUL-termination. */
+ examine_ansi_escape, which requires NUL-termination. */
std::string str (buf, length_buf);
this->puts (str.c_str ());
}