* src/seq.c (scan_arg): Search for the locale decimal point.
Also handle uppercase 'P' in hex format floats.
(main): Recognize locale decimal point in apparent options.
* tests/seq/seq-locale.sh: Add test cases.
* NEWS: Mention the bug fix.

Discovered by Anthropic's OSS Scanner, as bug ANT-2026-ERV7HOGN.
---
 NEWS                    |  7 +++++++
 src/seq.c               | 27 ++++++++++++++++++++++-----
 tests/seq/seq-locale.sh | 25 ++++++++++++++++++++++++-
 3 files changed, 53 insertions(+), 6 deletions(-)

diff --git a/NEWS b/NEWS
index ff16b29a6..3f672b49b 100644
--- a/NEWS
+++ b/NEWS
@@ -134,6 +134,13 @@ GNU coreutils NEWS                                    -*- 
outline -*-
   For example with 'seq -w 1e40 1e40' or 'seq 1 1e-40 1'.
   [This bug was present in "the beginning".]
 
+  'seq' no longer treats operands using the decimal point of the current locale
+  like "0,5", or hexadecimal operands with an upper case exponent like 
"0x1P-1",
+  as integers.  Previously 'seq 1,5 3' would output 2 and 3, and with
+  coreutils-8.32 or later 'seq 0 0,5 2' would output 0 indefinitely.
+  Also negative operands like "-,5" are no longer treated as options.
+  [bugs introduced in coreutils-8.24 and coreutils-8.31]
+
   'stty' again rejects empty speed arguments.
   Previously such arguments were accepted as value 0.
   [bug introduced in coreutils-9.8]
diff --git a/src/seq.c b/src/seq.c
index acae21fd0..961e057eb 100644
--- a/src/seq.c
+++ b/src/seq.c
@@ -51,6 +51,9 @@
 /* True if the locale settings were honored.  */
 static bool locale_ok;
 
+/* The locale's decimal point, or "." if it is empty.  */
+static char const *locale_point;
+
 /* If true print all number with equal width.  */
 static bool equal_width;
 
@@ -179,9 +182,17 @@ scan_arg (char const *arg)
   ret.width = 0;
   ret.precision = INT_MAX;
 
-  /* Use no precision (and possibly fast generation) for integers.  */
+  /* Like cl_strtold, accept '.' or the current locale's decimal point.  */
   char const *decimal_point = strchr (arg, '.');
-  if (! decimal_point && ! strchr (arg, 'p') /* not a hex float */)
+  idx_t decimal_point_len = 1;
+  if (! decimal_point)
+    {
+      decimal_point = strstr (arg, locale_point);
+      decimal_point_len = strlen (locale_point);
+    }
+
+  /* Use no precision (and possibly fast generation) for integers.  */
+  if (! decimal_point && ! strpbrk (arg, "pP") /* not a hex float */)
     ret.precision = 0;
 
   /* auto set width and precision for decimal inputs.  */
@@ -192,9 +203,11 @@ scan_arg (char const *arg)
 
       if (decimal_point)
         {
-          fraction_len = strcspn (decimal_point + 1, "eE");
+          fraction_len = strcspn (decimal_point + decimal_point_len, "eE");
           if (fraction_len <= INT_MAX)
             ret.precision = fraction_len;
+          /* printf counts a multi-byte decimal point as a single column.  */
+          ret.width -= decimal_point_len - 1;
           ret.width += (fraction_len == 0                       /* #.  -> #   
*/
                         ? -1
                         : (decimal_point == arg                 /* .#  -> 0.# 
*/
@@ -215,7 +228,7 @@ scan_arg (char const *arg)
             {
               if (decimal_point)
                 {
-                  if (e == decimal_point + 1) /* undo #. -> # above  */
+                  if (fraction_len == 0) /* undo #. -> # above  */
                     ret.width++;
                 }
               else
@@ -565,6 +578,9 @@ main (int argc, char **argv)
   initialize_main (&argc, &argv);
   set_program_name (argv[0]);
   locale_ok = !!setlocale (LC_ALL, "");
+  locale_point = localeconv ()->decimal_point;
+  if (! *locale_point)
+    locale_point = ".";
   bindtextdomain (PACKAGE, LOCALEDIR);
   textdomain (PACKAGE);
 
@@ -577,7 +593,8 @@ main (int argc, char **argv)
     {
       int optc;
       if (argv[optind][0] == '-'
-          && ((optc = argv[optind][1]) == '.' || c_isdigit (optc)))
+          && ((optc = argv[optind][1]) == '.' || c_isdigit (optc)
+              || STRPREFIX (&argv[optind][1], locale_point)))
         {
           /* means negative number */
           break;
diff --git a/tests/seq/seq-locale.sh b/tests/seq/seq-locale.sh
index 76cee7882..8a4beecb5 100755
--- a/tests/seq/seq-locale.sh
+++ b/tests/seq/seq-locale.sh
@@ -17,7 +17,7 @@
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 . "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
-print_ver_ seq
+print_ver_ seq printf
 
 # With coreutils-8.30 and earlier, the last decimal point would be ','
 # when setlocale(LC_ALL, "") failed, but setlocale(LC_NUMERIC, "") succeeded.
@@ -25,4 +25,27 @@ LC_ALL= LANG=invalid LC_NUMERIC=$LOCALE_FR_UTF8 seq 0.1 0.2 
0.7 > out || fail=1
 uniq -w2 out > out-merge || framework_failure_
 test "$(wc -l < out-merge)" = 1 || { fail=1; cat out; }
 
+# With coreutils-9.12 and earlier, operands using the locale's decimal point
+# were treated as integers, and so a step less than 1 output FIRST 
indefinitely.
+if test "$LOCALE_FR_UTF8" != none &&
+  test "$(LC_ALL=$LOCALE_FR_UTF8 env printf '%.1f' 0,5)" = '0,5'; then
+
+  printf '%s\n' 0,0 0,5 1,0 > exp || framework_failure_
+  LC_ALL=$LOCALE_FR_UTF8 seq 0 0,5 1 | head -n4 > out || fail=1
+  compare exp out || fail=1
+
+  printf '%s\n' 1,5 2,5 > exp || framework_failure_
+  LC_ALL=$LOCALE_FR_UTF8 seq 1,5 3 > out || fail=1
+  compare exp out || fail=1
+
+  printf '%s\n' -0,5 00,5 > exp || framework_failure_
+  LC_ALL=$LOCALE_FR_UTF8 seq -w -,5 ,5 > out || fail=1
+  compare exp out || fail=1
+fi
+
+# Likewise for hex floats with an upper case exponent character.
+printf '%s\n' 0 0.5 1 > exp || framework_failure_
+seq 0 0x1P-1 1 | head -n4 > out || fail=1
+compare exp out || fail=1
+
 Exit $fail
-- 
2.56.0


Reply via email to