Skip to content

fix: tac does not invent a newline on an unterminated last record - #92

Merged
davydog187 merged 1 commit into
mainfrom
cursor/tac-unterminated-newline-56dc
Aug 21, 2026
Merged

fix: tac does not invent a newline on an unterminated last record#92
davydog187 merged 1 commit into
mainfrom
cursor/tac-unterminated-newline-56dc

Conversation

@davydog187

@davydog187 davydog187 commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes #82

tac reversed by splitting on newlines and rejoining, which manufactured a separator the input never had.

# /n = "1\n2"   (no trailing newline)
# before
$ tac /n          →  "2\n1\n"
$ tac /n | tac    →  "1\n2\n"

# after (matches GNU /usr/bin/tac on this host)
$ tac /n          →  "21\n"
$ tac /n | tac    →  "21\n"
$ tac /a          →  "2\n1\n"     (/a = "1\n2\n")
$ tac /a | tac    →  "1\n2\n"

tac reverses records. A record is text up to and including its separator. The unterminated last record of "1\n2" is "2" — no separator — so reverse is "2" then "1\n", i.e. "21\n".

Two-file case

Pinned against /usr/bin/tac on this host (not gtac; they are the same GNU binary here). Matches the string in #82:

# /n = "1\n2",  /a = "1\n2\n"
$ tac /n /a   →  "21\n2\n1\n"
$ tac /a /n   →  "2\n1\n21\n"

Ordering was already tac a; tac b after #74. The remaining hole was the same record split on each operand.

tac | tac invertibility

Terminated input is invertible: "1\n2\n""2\n1\n""1\n2\n".

Unterminated input is not invertible on GNU either. After the first reverse the missing separator is gone, so the second tac sees the single record "21\n" and prints it again. Tests byte-compare that GNU result rather than claiming a round-trip the oracle does not do.

rev

Single-file rev already kept an unterminated last line ("ab\ncd""ba\ndc"). The analogous hole was two-file: concatenating first glued the last line of n onto the first line of a and reversed them as one record.

# /n = "ab\ncd",  /a = "ef\ngh\n"
# before (cat then rev):  "ba\nfedc\nhg\n"
# after  (rev n; rev a), pinned against /usr/bin/rev:  "ba\ndcfe\nhg\n"

tail -r

GNU tail has no -r. JustBash tail does not implement it. No reverse-record path there.

Changes

  • tac splits into records that carry their own separator and reverses those.
  • rev uses the same record split, and reverses each operand on its own (rev a; rev b).
  • Byte-compare tests for unterminated single-file, stdin, two-file, terminated identity, and tac | tac.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Testing

  • Added new tests
  • All existing tests pass

Local gates on this revision:

$ mix compile --warnings-as-errors
Generated just_bash app

$ mix format --check-formatted

$ mix credo
Analysis took 2.1 seconds
5443 mods/funs, found no issues.

$ mix credo --strict
[F] Avoid apply/2 and apply/3 when the number of arguments is known.
    test/support/banned_fixture_apply.ex:4:16
# pre-existing intentional test fixture

$ mix test
Finished in 36.2 seconds (35.7s async, 0.4s sync)
2 doctests, 62 properties, 5439 tests, 0 failures (5 excluded)

$ mix docs --warnings-as-errors

$ mix dialyzer
Total errors: 13, Skipped: 13, Unnecessary Skips: 0
done (passed successfully)

Checklist

  • My code follows the style guidelines of this project
  • I have run mix format
  • I have run mix credo and addressed any issues
  • I have added tests that prove my fix is effective
  • New and existing tests pass locally with my changes
  • I have updated the CHANGELOG.md
Open in Web Open in Cursor 

Co-authored-by: Dave Lucia <davelucianyc@gmail.com>
@davydog187
davydog187 marked this pull request as ready for review August 21, 2026 18:22
@davydog187
davydog187 merged commit 6c92a44 into main Aug 21, 2026
4 checks passed
@davydog187
davydog187 deleted the cursor/tac-unterminated-newline-56dc branch August 21, 2026 18:24
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.

tac invents a trailing newline when the input's final line is unterminated

2 participants