複数ファイルの共通行を抜き出す(または共通でない行を抜き出す)

(同じファイル内には同一行が無いのが前提)

共通行を抜き出すだけだったら、ワンライナーでさくっと書ける。

$ perl -nl -e 'BEGIN{$c=scalar @ARGV}; $h{$_}++; END{ for (keys %h){print if $h{$_} == $c}; }' file1 file2 file3 ...

BEGINブロック内でARGVの内容を保存してるのは、ENDブロック内では既に空だったから。
何故ENDの時点で@ARGVが空になってるかは分からない…

なぜかENDの時点では@ARGVが空になっている…
$ perl  -nl -e 'BEGIN {print scalar @ARGV}; END{print scalar @ARGV}' common_line.pl runtests.pl  file1 file2
2
0

ワンライナーがどう展開されるのかを見ても分からん…
$ perl -MO=Deparse,-p -nl -e 'BEGIN {print scalar @ARGV}; END{print scalar @ARGV}' file1 file2
2
BEGIN { $/ = "\n"; $\ = "\n"; }
LINE: while (defined(($_ = <ARGV>))) {
    chomp($_);
    sub BEGIN {
        print(scalar(@ARGV));
    }
    
    sub END {
        print(scalar(@ARGV));
    }
    ;
}
-e syntax OK

…ARGVの話は置いておいて、今回もう一つやりたかったことは、指定されたファイル中のある行が他のファイルどれか一つにでも欠けていたら、「共通でない行」として標準出力に出力するということ(言葉にすると訳分からないな…)。これはワンライナーでは難しいと思ったので、以下のコードを書いた。(汚ない…)

common_line.pl

#! /opt/local/bin/perl -w
use strict;
use Getopt::Long;

my %option = (
    reverse     => undef,
    tofile      => undef,
);

GetOptions(
    'reverse!'      => \$option{reverse},
    'tofile!'       => \$option{tofile},
) or usage();


# 1. count common lines.
my %hash;
for my $file (@ARGV) {
    open my $fh, '<', $file or die "cannot open file [$file]: $!";
    while ( defined(my $line = <$fh>) ) {
        chomp $line;
        $hash{$line}{COUNT}++;
        $hash{$line}{$file}++;
    }
}

# 2. print common line(s) or save "not" common lines to %f_hash.
my %f_hash;
for my $line (keys %hash) {
    if ($hash{$line}{COUNT} == scalar(@ARGV)) { # line that exists in all files
        next if $option{reverse};
        # print out common line(s)
        print $line, "\n";
        next;
    }
    else { # line that not exists in all files
        next if !$option{reverse};
        my @file = grep !/COUNT/, keys %{ $hash{$line} };
        for my $f (@file) {
            $f_hash{$f} .= "$line\n";
        }
    }
}

# 3. print out not common line(s)
for my $f (keys %f_hash) {
    if ($option{tofile}) { # print to FILES
        my $f_name = $f . '.not.common';
        open my $fh, '>', $f_name or die "cannot open $f_name: $!";
        print $fh $f_hash{$f};
    }
    else { # print to STDOUT
        print "[Lines that not exists in all specified files: $f]\n";
        print $f_hash{$f};
    }
}

sub usage
{
    print <<"EOF";
DESCRIPTION:
    Print lines that are common in specified files to STDOUT.
    Or do the opposite (specify --reverse option).
USAGE:
    $0 file1 file2 ...
OPTION:
    --reverse
        Print lines that are "not" common.
    --tofile (in effect only when used with --reverse option)
        Print lines to files.
EOF
    exit 1;
}

使い方

以下の3つのファイルを入力ファイルとする
$ cat a.txt 
a
b
c
$ cat b.txt
b
d
$ cat c.txt
a
b

共通行を抜き出す
$ common_line.pl a.txt b.txt c.txt
b

各ファイル毎に共通行以外を抜き出す ← これがやりたかった
$ common_line.pl a.txt b.txt c.txt --reverse
[Lines that not exists in all specified files: c.txt]
a
[Lines that not exists in all specified files: b.txt]
d
[Lines that not exists in all specified files: a.txt]
c
a

ファイルに出力
$ common_line.pl a.txt b.txt c.txt --reverse --tofile
※ 入力ファイルに「.not.common」サフィックスを付けたファイル名で出力される


もっとずっと簡単にできる悪寒がしてるけど、とりあえずこれで目的は達成できたからよしとする。