Skip to content

head, tail: keep a + in an invalid count's error, as GNU does - #14300

Closed
arbelonson-source wants to merge 1 commit into
uutils:mainfrom
arbelonson-source:fix/head-tail-plus-sign-in-error
Closed

head, tail: keep a + in an invalid count's error, as GNU does#14300
arbelonson-source wants to merge 1 commit into
uutils:mainfrom
arbelonson-source:fix/head-tail-plus-sign-in-error

Conversation

@arbelonson-source

Copy link
Copy Markdown
Contributor

head/tail's -c/-n share a parser (parse_signed_num_max) that always dropped the sign from the value an error names — matching GNU only for -, not for +:

$ tail -c -5Ki f      # GNU and ours agree
tail: invalid number of bytes: '5Ki'

$ tail -c +5Ki f      # ours, before
tail: invalid number of bytes: '5Ki'
$ tail -c +5Ki f      # GNU 9.11
tail: invalid number of bytes: '+5Ki'

GNU's own rule, worked out by probing every sign/no-sign combination: a - is dropped from the reported value, a + is kept. head shows this identically for both -c and -n.

What changed

  • parse_signed_num_max (and its unused-but-public sibling parse_signed_num, which has the identical bug and the same doc comment already promising "the raw string") now report the original string when the sign is +, and the sign-stripped one otherwise.
  • tail's own -n branch in args.rs had a second, independent bug: it built its message from arg.quote() directly rather than from the error the parser already constructed, so it never went through the sign-handling above at all — unlike its -c sibling two lines above it, which already used the error. Both now use the error, matching head, which was already consistent between its two branches.

How I found it

A differential sweep across head/tail size suffixes (K/M/Ki/B/etc., +/-/unsigned, -c/-n) run against the multiplier-suffix behavior from the most recently merged commit — this bug predates that commit and isn't related to it, the sweep just happened to be checking that area.

Getting this right took two passes

My first attempt at a fix used the original string unconditionally, which passed the case above but failed three existing tests (test_head_invalid_num, test_invalid_count_keeps_its_leading_zeros, test_tail_obsolete_error_cases) — because GNU really does drop the sign for -, so those tests were already correct. Comparing GNU's exact output across -abc/+abc/-5xyz/+5xyz/etc. showed the sign-dependent split described above, which is what's implemented and what the new tests check for both signs together, so a future change can't silently swap back to unconditional.

Not covered

Reproducing this exposed a separate, unrelated, pre-existing gap: head -c "-³" reports '³' where GNU reports the octal-escaped bytes '\302\263' — the same non-ASCII quoting difference already known from other utilities. Unchanged by this PR either way.

Testing

  • 351-case differential sweep against GNU 9.11 across head/tail × -c/-n × unsigned/+/- × 23 suffixes (valid and invalid) plus a handful of the exact edge cases from existing tests: 350 match, the 1 remaining mismatch is the pre-existing octal-escaping gap above (confirmed identical on main).
  • New tests in both test_head.rs and test_tail.rs covering +/- on both -c and -n; the test_tail.rs one verified to fail without the args.rs fix.
  • New unit test in parse_signed_num.rs for the sign-dependent behavior directly.
  • cargo test --features head,tail --test tests -- test_head test_tail: 247 passed, 0 failed (245 pre-existing, 2 new); cargo test -p uucore --features parser-size: 32 passed, 0 failed (1 new).
  • cargo fmt --check and cargo clippy -p uu_head -p uu_tail -p uucore --all-targets -- -D warnings: clean.

Disclosure

Prepared with AI assistance (Claude Opus 5, via Claude Code), per the AI policy in CONTRIBUTING.md. GNU's behaviour was established by running the installed GNU binary as a black box; I did not read GNU coreutils source. All testing was run locally.

`head`/`tail -c`/`-n` share a parser that always dropped the sign from
the value an error names, matching GNU only for `-`:

    $ tail -c -5Ki f      # GNU and ours agree
    tail: invalid number of bytes: '5Ki'

    $ tail -c +5Ki f      # ours, before
    tail: invalid number of bytes: '5Ki'
    $ tail -c +5Ki f      # GNU
    tail: invalid number of bytes: '+5Ki'

GNU keeps a `+` but drops a `-`. The two sign parsers (`parse_signed_num_max`,
and its unused-but-public sibling `parse_signed_num`) now report the
original string when the sign was `+`, and the sign-stripped one otherwise,
matching what was already documented but not implemented for `parse_signed_num`
("returns an error with the raw string").

`tail`'s own `-n` branch had a second, independent bug: it named the
argument straight from `arg.quote()` rather than from the error the parser
already built, so it never picked up the sign handling above at all,
unlike its `-c` sibling next to it, which already went through the error.
Both now go through the error.

Found by a differential sweep across head/tail size suffixes on the most
recently merged multiplier-suffix commit -- this bug was pre-existing,
not introduced by it.
@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

Skip an intermittent issue tests/rm/isatty (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/cut/bounded-memory (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/date/date-locale-hour (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/tail/follow-name (passes in this run but fails in the 'main' branch)
Note: The gnu test tests/tail/tail-n0f is now being skipped but was previously passing.
Congrats! The gnu test tests/pr/bounded-memory is now passing!

@sylvestre

Copy link
Copy Markdown
Contributor

not sure it matters for the added complexity, closing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants