macOS `head: illegal line count -- -1`: dropping the last lines with `sed '$d'` or awk, and why `tail -r` glues a line without a trailing newline
A script that drops the last line of a file stops on a Mac:
$ mkdir demo && cd demo
$ printf '1\n2\n3\n4\n5\n' > n.txt
$ head -n -1 n.txt; echo "exit $?"
head: illegal line count -- -1
exit 1
A negative count (“everything except the last N”) is a GNU head feature. macOS ships BSD head, which only takes a positive count. The byte form fails the same way:
$ head -c -1 n.txt; echo "exit $?"
head: illegal byte count -- -1
exit 1
There’s no --version; what shows the build:
$ head --version; echo "exit $?"
head: unrecognized option `--version'
usage: head [-n lines | -c bytes] [file ...]
exit 1
$ what /usr/bin/head /usr/bin/tail /usr/bin/sed /usr/bin/awk
/usr/bin/head:
PROGRAM:head PROJECT:text_cmds-199
PROGRAM:head PROJECT:text_cmds-199
/usr/bin/tail:
PROGRAM:tail PROJECT:text_cmds-199
PROGRAM:tail PROJECT:text_cmds-199
/usr/bin/sed:
PROGRAM:sed PROJECT:text_cmds-199
PROGRAM:sed PROJECT:text_cmds-199
/usr/bin/awk:
PROGRAM:awk PROJECT:awk-40
PROGRAM:awk PROJECT:awk-40
In a pipeline it looks like empty input
When head -n -1 sits in the middle of a pipeline, the next command gets
nothing and carries on. Without pipefail, the pipeline’s exit status is the
last command’s, so a script sees success:
$ seq 5 | head -n -1 | wc -l; echo "exit $?"
head: illegal line count -- -1
0
exit 0
If the stderr line scrolls past in a log, what’s left is a count of 0 and exit 0.
Drop the last line: sed '$d'
$ is the last line and d deletes it:
$ sed '$d' n.txt
1
2
3
4
Drop the last N lines: awk ring buffer
Hold the last n lines and print a line only once n newer ones have
arrived:
$ awk -v n=2 'NR>n{print buf[NR%n]} {buf[NR%n]=$0}' n.txt
1
2
3
n has to be 1 or more. With n=0 the % divides by zero:
$ awk -v n=0 'NR>n{print buf[NR%n]} {buf[NR%n]=$0}' n.txt; echo "exit $?"
awk: division by zero in mod
input record number 1, file n.txt
source line number 1
exit 2
If n is larger than the file, nothing is printed:
$ awk -v n=10 'NR>n{print buf[NR%n]} {buf[NR%n]=$0}' n.txt | wc -l
0
The tail -r trick and a file without a final newline
macOS has no tac, but tail -r prints lines in reverse, so another common
answer is “reverse, skip the first N, reverse back”. On a normal file it
works:
$ tail -r n.txt | tail -n +3 | tail -r
1
2
3
Now take a file whose last line has no newline, for example one
written by printf without a final \n:
$ printf '1\n2\n3' > nonl.txt
$ tail -r nonl.txt | od -c
0000000 3 2 \n 1 \n
0000005
tail -r put the unterminated 3 first, but didn’t add a newline after it,
so it ran straight into 2. Dropping “one line” from that drops two:
$ tail -r nonl.txt | tail -n +2 | tail -r
1
$ sed '$d' nonl.txt
1
2
sed '$d' and the awk version both treat the unterminated 3 as the last
line and drop only that:
$ awk -v n=1 'NR>n{print buf[NR%n]} {buf[NR%n]=$0}' nonl.txt
1
2
Instead of head -c -1
If what you wanted from head -c -1 was to strip a trailing newline, Perl (in
/usr/bin on this Mac) can remove the newline at the end of the input only:
$ printf 'abc\n' | perl -pe 'chomp if eof' | od -c
0000000 a b c
0000003
$ printf 'abc\n\n' | perl -pe 'chomp if eof' | od -c
0000000 a b c \n
0000004
It removes exactly one final newline. I only checked it for that, not as a general “drop the last N bytes”.
Checklist
head: illegal line count -- -1/illegal byte count -- -1: BSD head on macOS takes no negative counts.- In a pipeline the next command just gets no input; without
pipefailthe pipeline’s exit status is the last command’s (0 here). - Last line:
sed '$d'. - Last N lines (N ≥ 1):
awk -v n=N 'NR>n{print buf[NR%n]} {buf[NR%n]=$0}'. tail -r | tail -n +2 | tail -rlost two lines on a file without a final newline in my tests;sed '$d'and the awk version didn’t.- Strip one final newline:
perl -pe 'chomp if eof'.
Tested on macOS 26.5.2 (Apple silicon) with /usr/bin/head, /usr/bin/tail,
/usr/bin/sed (text_cmds-199), /usr/bin/awk (awk-40, “awk version
20200816”), perl 5.34.1, in zsh 5.9 (arm64-apple-darwin25.0). GNU coreutils
and Linux were not tested.