Duplicate Detector
This page shows how to find duplicate lines in any text stream using a single Perl one-liner. It covers the core idiom, common variations, and real world log parsing.That command prints every line that shows up more than once.perl -ne 'print if $seen{$_}++'
The trick is the post-increment operator. The first time Perl sees a line, $seen{$_} evaluates to 0 (false), so the command skips the print. The second time, the value is 1 (true), so Perl prints the line. A third or fourth appearance still evaluates as true, so it prints again.
If you only want to catch the second occurrence, we will walk through that next.
Part 1: HOW IT WORKS
Break it down:print if $seen{$_}++
On the first encounter,PIECE WHAT IT DOES ------------ ------------------------------------------ $seen{$_} Hash lookup - has this line been seen? ++ Post-increment - add 1 AFTER returning value $seen{$_}++ Returns OLD value (0 first time), then increments print if ... Print if the condition is true (non-zero)
$seen{$_} holds undef, which Perl treats as 0 in numeric context. The expression returns 0 and then bumps the stored value to 1. The condition fails, so nothing prints.
On the second encounter, $seen{$_} already holds 1. The expression returns 1 and then bumps the stored value to 2. The condition passes, so Perl prints the line.
.--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/
Part 2: VARIATIONS
Print duplicates only once (not every repeat):Theperl -ne 'print if $seen{$_}++ == 1'
== 1 clause tells Perl to print only on the second occurrence.
Print unique lines only (no duplicates at all):
This flips the logic. Perl prints the first time it sees a line and skips every repeat.perl -ne 'print unless $seen{$_}++'
Print lines that appear exactly N times (requires two passes):
perl -ne '$c{$_}++; END { print for grep { $c{$_} == 3 } keys %c }'
Part 3: FIRST VS LAST OCCURRENCE
Print the first occurrence of each line:Print the last occurrence of each line:perl -ne 'print unless $seen{$_}++'
This approach overwrites the stored value each time, so only the final line survives.perl -ne '$last{$_} = $_; END { print values %last }'
That approach drops the original order. If you want the last occurrence while keeping the original sequence, use this instead:
The trick here is to store the line number (perl -ne '$last{$_} = $.; END { print sort { $last{$a} <=> $last{$b} } keys %last }'
$.) instead of the line itself, then sort by those numbers at the end.
Part 4: CASE INSENSITIVE
Ignore case when detecting duplicates:Theperl -ne 'print if $seen{lc $_}++'
lc function lowercases the hash key. "Hello" and "HELLO" now share the same bucket.
You can also normalize whitespace at the same time:
perl -ne '$k = lc; $k =~ s/\s+/ /g; print if $seen{$k}++'
Part 5: FIELD-BASED DUPLICATES
Detect duplicates on a specific column:Theperl -ane 'print if $seen{$F[0]}++'
-a flag splits each input line into the @F array. This version checks only the first field.
Find duplicate IPs in a log:
Find duplicate usernames inperl -ane 'print if $seen{$F[0]}++' access.log
/etc/passwd:
perl -F: -ane 'print if $seen{$F[0]}++' /etc/passwd
Part 6: COUNTING DUPLICATES
Count how many times each line appears:The output looks like this:perl -ne '$c{$_}++; END { print "$c{$_}: $_" for keys %c }'
Sort the results by frequency:3: this line appeared three times 1: this line appeared once 5: this line appeared five times
The most frequent lines now appear first.perl -ne '$c{$_}++; END { print "$c{$_}: $_" for sort { $c{$b} <=> $c{$a} } keys %c }'
Part 7: ADJACENT DUPLICATES
Theuniq command only removes adjacent duplicates:
The current line must match the previous line to be skipped.perl -ne 'print unless $_ eq $last; $last = $_'
This approach runs faster because it avoids a hash, but it only catches consecutive repeats:
aaa aaa <- removed bbb aaa <- NOT removed, not adjacent
Part 8: REAL WORLD EXAMPLES
Find duplicate lines in a config file:Theperl -ne 'print "$.: $_" if $seen{$_}++' config.ini
$. variable adds line numbers so you can locate them quickly.
Find duplicate hostnames in /etc/hosts:
This checks the hostname field, which sits in the second column.perl -ane 'print if $seen{$F[1]}++' /etc/hosts
Find duplicate SSH public keys:
The key string sits in the second field, so this catches repeated entries.perl -ne 'print if $seen{(split)[1]}++' ~/.ssh/authorized_keys
Find duplicate cron jobs:
crontab -l | perl -ne 'print if $seen{$_}++'
Part 9: MEMORY CONSIDERATIONS
The hash stores every unique line in memory. For huge files with many unique lines, this approach eats RAM quickly.For massive files, consider this pipeline instead:
An external sort handles files larger than available memory, though it drops the original order.sort file.txt | uniq -d
You can also process only the tail end if you only care about recent duplicates:
tail -10000 huge.log | perl -ne 'print if $seen{$_}++'
Part 10: THE FAMILY
These patterns share the same core idiom:The post-increment idiom powers every one of them.perl -ne 'print if $seen{$_}++' # All duplicates perl -ne 'print if $seen{$_}++ == 1' # Each duplicate once perl -ne 'print unless $seen{$_}++' # Unique lines only perl -ne '$c{$_}++; END{print for grep{$c{$_}>1}keys%c}' # Dupes, one each
Part 11: WHY POST-INCREMENT
Why use$seen{$_}++ instead of ++$seen{$_}?
Pre-increment returns the new value:
Post-increment returns the old value:++$seen{$_} # Returns 1 on first encounter (true!)
With pre-increment, every line prints because the bump happens before the comparison. Post-increment is what makes the logic work.$seen{$_}++ # Returns 0 on first encounter (false!)
Part 12: COMBINING WITH OTHER PATTERNS
Find duplicates that also match a pattern:This catches only repeated error lines.perl -ne 'print if /error/i && $seen{$_}++'
Find duplicates across multiple files:
Theperl -ne 'print "$ARGV: $_" if $seen{$_}++' *.log
$ARGV variable shows which file contained the duplicate.
Find duplicates within a time window (log files):
This resets the seen hash whenever the timestamp column changes.perl -ane ' $t = $F[0]; %seen = () if $t ne $last_t; print if $seen{$_}++; $last_t = $t ' timestamped.log
perl.gg$seen{$_}++ | +----+----+ | | first again | | skip print The post-increment trick