* 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