Sorry for the noise. I forgot the cover note with v2.

Thanks for the rev, you're right.

v1 called memchr2() over the whole remaining line on every field
lookup.  With only multi-byte separators (no SP/TAB) that rescans the
suffix each time and is quadratic in the field count.  That matches
your -f999999 case; -f1 hid it.

I also tried bounding memchr2() to each ASCII run (up to the next high
bit), and caching the last multi-byte blank sequence as you suggested.
Both still leave a quadratic hole on long pure-ASCII lines: computing
the ASCII run end walks to EOL on every field.  Caching what counts as
a separator cheapens the local classification, but does not remove that
rescan.

So v2 uses a single forward pass for UTF-8 -w: check SP/TAB byte-wise,
and mcel_scan/c32issep for high bytes.  Each field lookup only examines
bytes up to the next delimiter, so the whole line stays O(n).  Unibyte
locales still use memchr2.

Best-of-3 wall times on this host (LC_ALL=C.UTF-8):

  # your recipe: (a+U+2003)*40000 + b, cut -w -f999999
  v1 (suffix memchr2)     0.66s
  bounded ASCII-run       0.00s
  v2 (linear)             0.00s
  master (mb_any)         0.00s

  # pure ASCII many fields: (a+SP)*500000 + z, cut -w -f250000
  v1                      0.00s
  bounded ASCII-run       8.47s
  v2 (linear)             0.00s
  master                  0.00s

  # mostly-ASCII ~21MiB, cut -w -f1
  master                  0.06s
  v2                      0.02s

tests/cut/w-utf8-pathological.sh covers MB-only (U+2003 and U+3000),
dense ASCII blanks with a late -f (timeout 2), mixed blank runs,
leading MB blanks, and SP after a non-blank glyph; plus cut.pl cases
and w-utf8-responsive.sh for incomplete-UTF-8 hold behavior.

The [PATCH v2] is already on the list.

Best.
Iván

El vie, 14 ago 2026 a la(s) 11:04 a.m., Iván Ezequiel Rodriguez (
[email protected]) escribió:

> In UTF-8 locales, -w (without --whitespace-delimited=trimmed) used the
> slow cut_fields_mb_any path.  Route it through cut_fields_bytesearch
> with a single forward scan for ASCII SP/TAB and c32issep multi-byte
> blanks, with the same semantics as mcel_isblank.
>
> Do not memchr2 the remaining line (or pre-scan a whole ASCII run to
> its end) on each field lookup: both are quadratic in the field count
> for long lines of only multi-byte blanks or only ASCII blanks.
>
> Preserve only truly incomplete UTF-8 tails via
> mbrtoc32 == (size_t) -2, so complete characters are not held across
> refill (cf. mbbuf_fill responsiveness).  Invalid UTF-8 and
> continuation-only input remain field data and always make progress.
>
> Timings on this host (LC_ALL=C.UTF-8):
>
>   (a+U+2003)*40000 + b, cut -w -f999999:  ~0.00s (was ~0.69s with
>     suffix-wide memchr2; matches master order)
>   (a+SP)*500000 + z, cut -w -f250000:     ~0.00s (was ~10s with
>     ASCII-run pre-scan + memchr2)
>   mostly-ASCII ~21MiB, cut -w -f1:        ~0.05s (master ~0.12s)
>
> * src/cut.c (utf8_tail_hold, utf8_is_incomplete_prefix)
> (find_blank_delimiter): New.
> (find_field_terminator, cut_fields_bytesearch, cut_fields_ws): Use them.
> * tests/cut/cut.pl: Add boundary, invalid, mixed-run, glyph+SP, and
> U+3000 cases.
> * tests/cut/w-utf8-responsive.sh: Test streaming complete UTF-8
> responsiveness.
> * tests/cut/w-utf8-pathological.sh: Guard quadratic ASCII and MB blank
> scans; cover mixed runs, leading MB blanks, and non-blank MB content.
> * tests/local.mk: Reference the new tests.
> * NEWS: Mention the improvement.
>
> Signed-off-by: Iván Ezequiel Rodriguez <[email protected]>
> ---
>  NEWS                             |   6 ++
>  src/cut.c                        | 150 ++++++++++++++++++++++++++++---
>  tests/cut/cut.pl                 |  32 +++++++
>  tests/cut/w-utf8-pathological.sh | 134 +++++++++++++++++++++++++++
>  tests/cut/w-utf8-responsive.sh   |  71 +++++++++++++++
>  tests/local.mk                   |   2 +
>  6 files changed, 382 insertions(+), 13 deletions(-)
>  create mode 100755 tests/cut/w-utf8-pathological.sh
>  create mode 100755 tests/cut/w-utf8-responsive.sh
>
> diff --git a/NEWS b/NEWS
> index 17a6333d3..d68952854 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -95,6 +95,12 @@ GNU coreutils NEWS
> -*- outline -*-
>
>  ** Improvements
>
> +  'cut -w' is much faster in UTF-8 locales when not using
> +  '--whitespace-delimited=trimmed', by scanning for ASCII blanks and
> +  Unicode blank separators in a single forward pass (avoiding quadratic
> +  suffix rescans), while preserving incomplete multi-byte sequences
> +  across buffer boundaries.
> +
>    When built with the configure option '--with-wtmpdb', invocations of
>    'who /var/log/wtmp' and 'users /var/log/wtmp' use the wtmpdb database
>    instead of the file /var/log/wtmp.  This makes them Y2038-safe.
> diff --git a/src/cut.c b/src/cut.c
> index 83f24244d..e8c8b3137 100644
> --- a/src/cut.c
> +++ b/src/cut.c
> @@ -337,6 +337,8 @@ struct bytesearch_context
>    bool at_eof;
>    char *line_end;
>    bool line_end_known;
> +  /* Length of the blank delimiter found by find_blank_delimiter.  */
> +  idx_t blank_delim_len;
>  };
>
>  static inline void
> @@ -345,6 +347,94 @@ bytesearch_context_reset (struct bytesearch_context
> *ctx)
>    ctx->mode = BYTESEARCH_FIELDS;
>    ctx->line_end = NULL;
>    ctx->line_end_known = false;
> +  ctx->blank_delim_len = 1;
> +}
> +
> +/* Return how many trailing bytes of BUF[0..LEN) form an incomplete UTF-8
> +   sequence ((size_t) -2 from mbrtoc32) and must be retained for the next
> +   refill.  Complete characters and encoding errors are not held, so a
> +   writer that pauses after valid UTF-8 does not stall cut waiting for
> more
> +   input (cf. mbbuf_fill responsiveness).  At most one mbrtoc32 call per
> +   candidate start in the last MCEL_LEN_MAX bytes.  */
> +static idx_t
> +utf8_tail_hold (char const *buf, idx_t len)
> +{
> +  if (len == 0 || to_uchar (buf[len - 1]) < 0x80)
> +    return 0;
> +
> +  idx_t max_try = MIN (len, (idx_t) MCEL_LEN_MAX);
> +  for (idx_t hold = max_try; hold >= 1; hold--)
> +    {
> +      char const *p = buf + len - hold;
> +      if (to_uchar (*p) < 0x80)
> +        continue;
> +
> +      mbstate_t mbs;
> +      mbszero (&mbs);
> +      char32_t wc;
> +      size_t n = mbrtoc32 (&wc, p, hold, &mbs);
> +      if (n == (size_t) -2)
> +        return hold;
> +    }
> +
> +  return 0;
> +}
> +
> +/* True if BUF[0..LEN) begins with an incomplete UTF-8 sequence.  */
> +static bool
> +utf8_is_incomplete_prefix (char const *buf, idx_t len)
> +{
> +  if (len == 0 || to_uchar (buf[0]) < 0x80)
> +    return false;
> +
> +  mbstate_t mbs;
> +  mbszero (&mbs);
> +  char32_t wc;
> +  return mbrtoc32 (&wc, buf, len, &mbs) == (size_t) -2;
> +}
> +
> +/* Locate the next -w field delimiter in BUF of length LEN.
> +   In UTF-8 treat ASCII SP/TAB and c32issep multi-byte blanks as
> +   delimiters.  Set *DELIM_LEN to the delimiter width.  Return NULL if
> +   none is found.
> +
> +   Use a single forward pass: do not call memchr2 over the remaining
> +   line.  Pre-scanning an ASCII run to its end (or memchr2 of the full
> +   suffix) before classifying bytes is quadratic in the field count for
> +   long lines of only ASCII blanks or only multi-byte blanks.  */
> +
> +static char *
> +find_blank_delimiter (char *buf, idx_t len, idx_t *delim_len)
> +{
> +  *delim_len = 1;
> +
> +  if (! is_utf8_charset ())
> +    return memchr2 (buf, ' ', '\t', len);
> +
> +  char *q = buf;
> +  char *end = buf + len;
> +
> +  while (q < end)
> +    {
> +      unsigned char c = to_uchar (*q);
> +      if (c < 0x80)
> +        {
> +          if (c == ' ' || c == '\t')
> +            return q;
> +          q++;
> +          continue;
> +        }
> +
> +      mcel_t g = mcel_scan (q, end);
> +      if (! g.err && c32issep (g.ch))
> +        {
> +          *delim_len = g.len;
> +          return q;
> +        }
> +      q += g.err ? 1 : g.len;
> +    }
> +
> +  return NULL;
>  }
>
>  struct mbfield_parser
> @@ -674,9 +764,11 @@ find_field_terminator (char *buf, idx_t len,
>
>    idx_t field_len = ctx->line_end ? ctx->line_end - buf : len;
>
> -  char *field_end = (ctx->blank_delimited
> -                     ? memchr2 (buf, ' ', '\t', field_len)
> -                     : find_field_delim (buf, field_len));
> +  char *field_end;
> +  if (ctx->blank_delimited)
> +    field_end = find_blank_delimiter (buf, field_len,
> &ctx->blank_delim_len);
> +  else
> +    field_end = find_field_delim (buf, field_len);
>
>    if (field_end)
>      {
> @@ -1118,9 +1210,37 @@ cut_fields_bytesearch (FILE *stream)
>
>            if (skip_blank_run)
>              {
> -              while (processed < n_avail && c_isblank (chunk[processed]))
> -                processed++;
> -              if (processed == n_avail)
> +              bool held_incomplete = false;
> +              while (processed < n_avail)
> +                {
> +                  unsigned char c = to_uchar (chunk[processed]);
> +                  if (c == ' ' || c == '\t')
> +                    {
> +                      processed++;
> +                      continue;
> +                    }
> +                  if (c < 0x80 || ! is_utf8_charset ())
> +                    break;
> +
> +                  /* Hold only a truly incomplete UTF-8 prefix, not every
> +                     short high-bit tail (complete chars must not
> stall).  */
> +                  if (! search.at_eof
> +                      && utf8_is_incomplete_prefix (chunk + processed,
> +                                                    n_avail - processed))
> +                    {
> +                      held_incomplete = true;
> +                      break;
> +                    }
> +
> +                  mcel_t g = mcel_scan (chunk + processed, chunk +
> n_avail);
> +                  if (! g.err && c32issep (g.ch))
> +                    {
> +                      processed += g.len;
> +                      continue;
> +                    }
> +                  break;
> +                }
> +              if (processed == n_avail || held_incomplete)
>                  break;
>                skip_blank_run = false;
>              }
> @@ -1155,11 +1275,13 @@ cut_fields_bytesearch (FILE *stream)
>            idx_t field_len = terminator ? terminator - (chunk + processed)
>                                         : n_avail - processed;
>
> -          if (terminator_kind == FIELD_DATA
> -              && !search.at_eof
> -              && !whitespace_delimited
> -              && !field_delim_is_line_delim ())
> -            field_len -= field_delim_overlap (chunk + processed,
> field_len);
> +          if (terminator_kind == FIELD_DATA && !search.at_eof)
> +            {
> +              if (!whitespace_delimited && !field_delim_is_line_delim ())
> +                field_len -= field_delim_overlap (chunk + processed,
> field_len);
> +              else if (whitespace_delimited && is_utf8_charset ())
> +                field_len -= utf8_tail_hold (chunk + processed,
> field_len);
> +            }
>
>            if (field_len || terminator)
>              have_pending_line = true;
> @@ -1184,7 +1306,8 @@ cut_fields_bytesearch (FILE *stream)
>                    break;
>                  }
>
> -              processed += whitespace_delimited ? 1 : delim_mcel.len;
> +              processed += (whitespace_delimited
> +                            ? search.blank_delim_len : delim_mcel.len);
>                handle_field_delimiter (&field_idx, buffer_first_field,
>                                        &field_1_n_bytes,
>                                        &found_any_selected_field,
> &write_field,
> @@ -1225,7 +1348,8 @@ cut_fields_bytesearch (FILE *stream)
>  static void
>  cut_fields_ws (FILE *stream)
>  {
> -  if (MB_CUR_MAX <= 1 && !trim_outer_whitespace)
> +  if (!trim_outer_whitespace
> +      && (MB_CUR_MAX <= 1 || is_utf8_charset ()))
>      cut_fields_bytesearch (stream);
>    else
>      cut_fields_mb_any (stream, true);
> diff --git a/tests/cut/cut.pl b/tests/cut/cut.pl
> index 7e4a7ef80..b4033da3e 100755
> --- a/tests/cut/cut.pl
> +++ b/tests/cut/cut.pl
> @@ -369,6 +369,38 @@ if ($mb_locale ne 'C')
>         {ENV => "LC_ALL=$mb_locale"}],
>        ['mb-w-nodelim-1', '-w', '-f2', {IN=>"abc"}, {OUT=>"abc\n"},
>         {ENV => "LC_ALL=$mb_locale"}],
> +      # U+2003 split across IO_BUFSIZE (analogous to mb-delim-9).
> +      ['mb-w-delim-boundary', '-w', '-f2',
> +       {IN=>('a' x ($IO_BUFSIZE - 1)) . "\xe2\x80\x83b\n"}, {OUT=>"b\n"},
> +       {ENV => "LC_ALL=$mb_locale"}],
> +      # Whitespace run spanning the buffer boundary stays one delimiter.
> +      ['mb-w-run-boundary', '-w', '-f2',
> +       {IN=>('a' x ($IO_BUFSIZE - 1)) . " \xe2\x80\x83b\n"}, {OUT=>"b\n"},
> +       {ENV => "LC_ALL=$mb_locale"}],
> +      # Invalid UTF-8 is field data, not a delimiter.
> +      ['mb-w-invalid', '-w', '-f1,2',
> +       {IN=>"a\xffb\tc\n"}, {OUT=>"a\xffb\tc\n"},
> +       {ENV => "LC_ALL=$mb_locale"}],
> +      # Invalid lead then ASCII blank: blank remains a -w delimiter.
> +      ['mb-w-invalid-lead-ws', '-w', '-f2',
> +       {IN=>"a\xe2 b\n"}, {OUT=>"b\n"},
> +       {ENV => "LC_ALL=$mb_locale"}],
> +      # Continuation-only bytes are consumed as data (no hang / hold-all).
> +      ['mb-w-cont-only', '-w', '-f1',
> +       {IN=>("\x80" x 8) . "\n"}, {OUT=>("\x80" x 8) . "\n"},
> +       {ENV => "LC_ALL=$mb_locale"}],
> +      # Mixed blank run (ASCII + U+2003) is a single -w delimiter.
> +      ['mb-w-mixed-run', '-w', '-f2',
> +       {IN=>"a \t\xe2\x80\x83\t b\n"}, {OUT=>"b\n"},
> +       {ENV => "LC_ALL=$mb_locale"}],
> +      # SP after non-blank multi-byte glyph.
> +      ['mb-w-sp-after-glyph', '-w', '-f1,2',
> +       {IN=>"a\xe2\x9c\x93 b\n"}, {OUT=>"a\xe2\x9c\x93\tb\n"},
> +       {ENV => "LC_ALL=$mb_locale"}],
> +      # U+3000 ideographic space as -w delimiter.
> +      ['mb-w-ideo-space', '-w', '-f2',
> +       {IN=>"a\xe3\x80\x80b\n"}, {OUT=>"b\n"},
> +       {ENV => "LC_ALL=$mb_locale"}],
>
>        # --complement with multi-byte
>        ['mb-compl-c1', '--complement', '-c1',
> diff --git a/tests/cut/w-utf8-pathological.sh
> b/tests/cut/w-utf8-pathological.sh
> new file mode 100755
> index 000000000..50848f3ee
> --- /dev/null
> +++ b/tests/cut/w-utf8-pathological.sh
> @@ -0,0 +1,134 @@
> +#!/bin/sh
> +# Ensure cut -w stays linear on many blank delimiters (ASCII or UTF-8).
> +# Regresses suffix-wide memchr2 / ASCII-run pre-scans that are quadratic
> +# in the field count (see also the -f999999 multi-byte case on the list).
> +
> +# 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 <https://www.gnu.org/licenses/>.
> +
> +. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
> +print_ver_ cut
> +
> +# Force a UTF-8 locale for the -w fast path under test.
> +for loc in C.UTF-8 en_US.UTF-8 "$LOCALE_FR_UTF8"; do
> +  test "$loc" && test "$loc" != none || continue
> +  LC_ALL=$loc locale charmap 2>/dev/null | grep -i utf-8 >/dev/null &&
> +    { export LC_ALL=$loc; break; }
> +done
> +LC_ALL=${LC_ALL:-C} locale charmap 2>/dev/null | grep -i utf-8 >/dev/null
> ||
> +  skip_ 'UTF-8 locale required'
> +
> +em=$(printf '\xe2\x80\x83')   # U+2003 EM SPACE
> +ideo=$(printf '\xe3\x80\x80') # U+3000 IDEOGRAPHIC SPACE
> +
> +# Helper: N copies of "a"<SEP>, then "b\n".  Late -f must finish quickly.
> +make_fields() {
> +  local sep=$1 n=$2 out=$3
> +  {
> +    yes "a$sep" | head -n"$n" | tr -d '\n'
> +    printf 'b\n'
> +  } > "$out" || framework_failure_
> +}
> +
> +check_late_field() {
> +  local in=$1 n=$2
> +  local last=$((n + 1)) mid=$((n / 2))
> +
> +  echo a > exp || framework_failure_
> +  timeout 2 cut -w -f1 "$in" > out || fail=1
> +  compare exp out || fail=1
> +
> +  echo a > exp || framework_failure_
> +  timeout 2 cut -w -f"$mid" "$in" > out || fail=1
> +  compare exp out || fail=1
> +
> +  echo b > exp || framework_failure_
> +  timeout 2 cut -w -f"$last" "$in" > out || fail=1
> +  compare exp out || fail=1
> +
> +  echo > exp || framework_failure_
> +  timeout 2 cut -w -f999999 "$in" > out || fail=1
> +  compare exp out || fail=1
> +}
> +
> +# Multi-byte blanks only (maintainer recipe, smaller N for the harness).
> +n=8000
> +make_fields "$em" "$n" in-em
> +check_late_field in-em "$n"
> +
> +# Different multi-byte blank (U+3000).
> +make_fields "$ideo" "$n" in-ideo
> +check_late_field in-ideo "$n"
> +
> +# Pure ASCII blanks: pre-scanning each ASCII run to EOL is also quadratic.
> +n_ascii=50000
> +make_fields ' ' "$n_ascii" in-ascii
> +check_late_field in-ascii "$n_ascii"
> +
> +# Short alternating ASCII + EM SPACE.
> +{
> +  yes "ab$em" | head -n"$n" | tr -d '\n'
> +  printf 'z\n'
> +} > in-alt || framework_failure_
> +echo z > exp || framework_failure_
> +timeout 2 cut -w -f$((n + 1)) in-alt > out || fail=1
> +compare exp out || fail=1
> +
> +# Mixed blank run collapses to one delimiter.
> +printf 'a \t%s\t b\n' "$em" > mix || framework_failure_
> +printf 'b\n' > exp || framework_failure_
> +cut -w -f2 mix > out || fail=1
> +compare exp out || fail=1
> +
> +# SP/TAB after a non-blank multi-byte glyph.
> +printf 'a\xe2\x9c\x93 b\n' > mix2 || framework_failure_
> +printf 'a\xe2\x9c\x93\n' > exp || framework_failure_
> +cut -w -f1 mix2 > out || fail=1
> +compare exp out || fail=1
> +printf 'b\n' > exp || framework_failure_
> +cut -w -f2 mix2 > out || fail=1
> +compare exp out || fail=1
> +
> +# Non-blank MB must not split the field.
> +printf 'a\xe2\x9c\x93b c\n' > mix3 || framework_failure_
> +printf 'a\xe2\x9c\x93b\n' > exp || framework_failure_
> +cut -w -f1 mix3 > out || fail=1
> +compare exp out || fail=1
> +
> +# Leading / consecutive MB blanks are one delimiter run under -w
> +# (leading blanks yield an empty field 1, same as ASCII -w).
> +printf '%s%sx%sy\n' "$em" "$em" "$em" > mix4 || framework_failure_
> +echo > exp || framework_failure_
> +cut -w -f1 mix4 > out || fail=1
> +compare exp out || fail=1
> +printf 'x\n' > exp || framework_failure_
> +cut -w -f2 mix4 > out || fail=1
> +compare exp out || fail=1
> +printf 'y\n' > exp || framework_failure_
> +cut -w -f3 mix4 > out || fail=1
> +compare exp out || fail=1
> +
> +# Oracle: UTF-8 -w fast path must match trimmed=off mb path semantics.
> +# Compare against a reference built from the same binary with a locale
> +# that still uses UTF-8... we compare selected fields on crafted lines
> +# against expected strings already checked above; add random-ish lines
> +# vs an independent reimplementation using tr/awk only for ASCII, and
> +# vs explicit expectations for MB.
> +printf 'p%sq%sr\n' "$em" "$ideo" > oracle || framework_failure_
> +printf 'p\tq\tr\n' > exp || framework_failure_
> +cut -w -f1,2,3 oracle > out || fail=1
> +compare exp out || fail=1
> +
> +Exit $fail
> diff --git a/tests/cut/w-utf8-responsive.sh
> b/tests/cut/w-utf8-responsive.sh
> new file mode 100755
> index 000000000..cb7418bfc
> --- /dev/null
> +++ b/tests/cut/w-utf8-responsive.sh
> @@ -0,0 +1,71 @@
> +#!/bin/sh
> +# Ensure cut -w processes complete UTF-8 without waiting for more input.
> +# Regresses a hold-too-much fast path that reintroduced the latency
> +# fixed by commit 57c87043f (mbbuf_fill responsiveness).
> +
> +# 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 <https://www.gnu.org/licenses/>.
> +
> +. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
> +print_ver_ cut stdbuf
> +
> +require_built_ stdbuf
> +
> +# Force a UTF-8 locale for the -w fast path under test.
> +for loc in C.UTF-8 en_US.UTF-8 "$LOCALE_FR_UTF8"; do
> +  test "$loc" && test "$loc" != none || continue
> +  LC_ALL=$loc locale charmap 2>/dev/null | grep -i utf-8 >/dev/null &&
> +    { export LC_ALL=$loc; break; }
> +done
> +LC_ALL=${LC_ALL:-C} locale charmap 2>/dev/null | grep -i utf-8 >/dev/null
> ||
> +  skip_ 'UTF-8 locale required'
> +
> +mkfifo_or_skip_ fifo
> +
> +# Writer pauses after a complete multi-byte character.  With an over-eager
> +# hold of any high-bit tail, cut would block in read() before emitting
> "é".
> +# stdbuf -o0 makes the emission observable before the writer resumes.
> +check_responsive()
> +{
> +  local delay="$1"
> +  compare exp out >/dev/null 2>&1 ||
> +    { sleep $delay; return 1; }
> +}
> +
> +printf 'caf\xc3\xa9' > exp || framework_failure_
> +
> +stdbuf -o0 cut -w -f1 > out < fifo & pid=$!
> +
> +# Keep the fifo writer in this shell so cut does not see EOF yet.
> +exec 3>fifo
> +printf 'caf\xc3\xa9' >&3 || framework_failure_
> +
> +# Before sending newline/EOF, cut must already have written complete
> UTF-8.
> +retry_delay_ check_responsive .1 6 ||
> +  {
> +    cat out
> +    fail=1
> +  }
> +
> +printf ' x\n' >&3 || framework_failure_
> +exec 3>&-
> +
> +wait $pid || fail=1
> +
> +# Final line: field1 is "café\n" (space starts field 2; line ends).
> +printf 'caf\xc3\xa9\n' > exp || framework_failure_
> +compare exp out || fail=1
> +
> +Exit $fail
> diff --git a/tests/local.mk b/tests/local.mk
> index 33abb9d72..7496a4819 100644
> --- a/tests/local.mk
> +++ b/tests/local.mk
> @@ -305,6 +305,8 @@ all_tests =                                 \
>    tests/misc/coreutils.sh                      \
>    tests/cut/cut.pl                             \
>    tests/cut/mb-non-utf8.sh                     \
> +  tests/cut/w-utf8-responsive.sh               \
> +  tests/cut/w-utf8-pathological.sh             \
>    tests/cut/bounded-memory.sh                  \
>    tests/cut/cut-huge-range.sh                  \
>    tests/wc/wc.pl                               \
> --
> 2.43.0
>
>

Reply via email to