camel

find(1)ネタというのも、定期的にネットを賑やかにする時候ネタになりつつあるようです。

というわけで、いまさらfindとperlの相性のよさを再確認してみることにしてみます。

基本形

まずは、これを覚えておきましょう。

find [options and args of find] -print0 | perl -l0ne 'perl program'

findが見つけたファイルを一つ見つけると、perlの$_にそのファイル名が入るのでそれに対して何らかの処理を行います。

perl側の-l0neというのは、以下と同等になります。

BEGIN { $/ = "\n"; $\ = "\000"; }
LINE: while (defined($_ = <ARGV>)) {
    chomp $_;
    do something with $_;
}

-print0で締めているのは、ファイル名が変な場合でも適切に処理するため。詳しくは

を参照のこと。ただしxargs(1)とは異なり、perlの場合問題となるのはファイル名に"\n"が入っている場合のみなので、通常は

find [options and args of find] | perl -lne 'perl program'

でほとんど問題ありません。以下、実例。

% find ~/Music/iTunes/iTunes\ Music/A* -type f | xargs echo
xargs: unterminated quote
% find ~/Music/iTunes/iTunes\ Music/A* -type f | perl -lne 'print "($_)"'
(/Users/dankogai/Music/iTunes/iTunes Music/ABBA/Gold/01 Dancing Queen.m4a)
(/Users/dankogai/Music/iTunes/iTunes Music/ABBA/Gold/02 Knowing Me, Knowing You.m4a)
...

xargsがspace(\x20)を扱いそこねているのに対し、perlではspaceを含んでいてもきちんと処理されていることが確認できます。これも私がxargsをほとんど使わない理由の一つです。

ちなみに-lneの代わりに-lpeとすると、コードの実行後にprint $_してくれるので、"verbose mode"として使うことが出来ます。

perlをxargsの代わりに使うことの利点

spaceなどを含んだファイル名をきちんと扱えるというのは上で述べた通りですが、他にもあります。

いまさらxargsの便利さを主張してみる
xargsを使った場合に上の2つの例とどう違うかというと、コマンド実行時の引数の制限を気にしつつ、rmにギリギリの個数のファイル名を渡して、必要最低限の回数だけ実行してくれます。

しかし、find | perl -lne unlinkなら、perlの実行回数はファイルが何万あろうが1回です。そのおかげで、perl自体はxargsより大きなプログラムであるにも関わらず、全体としてxargsよりも効率がいいことが多いのです。

もあわせてご覧下さい。

さらに、xargsは他のbinutilsと同じく、環境によって使えるオプションが異なるなどの問題があるのに対し、perlの場合環境差がずっと少ないというのも魅力です。linuxでもBSDでも、生のOS XでもGNU (bin|core)utilsをfinkやdarwin portsなどで入れた場合でも、全く同じコマンドで行けるというのは魅力です。

実例集

  • find したファイルを削除
    % find dir -type f -print0 | perl -l0ne unlink
    % find dir -type f -delete # BSD系のみ
    
  • find したものをディレクトリも含め削除
    find dir -depth | perl -nle 'unlink or rmdir'

    find側に-depthを付けているのがポイント。この場合、ディレクトリは最後に表示されるので、ファイルを削除した後できれいにrmdirできるわけです。

  • ファイルを移動
    % find dir ... -print0 | perl -MFile::Basename -l0ne 'rename $_, "/path/to/" . basename($_)'
    % find dir ... -print0 | perl -MFile::Copy -MFile::Basename -l0ne 'move($_, "/path/to/" . basename($_)'
    

    後者はファイルシステムをまたがっている場合にも使えます。

  • 拡張子を変更
    find dir -type f -name \*.jpeg | perl -nle '$o=$_;s/\.jpe?g/.jpg/; rename $o,$_'
  • perl側で条件をさらに絞る
  • find dir -type f | perl -nle '/\jpe?g$/ or next; $o=$_;s/\.jpe?g/.jpg/; rename $o,$_'

    上とほぼ同じことをしますが、条件をさらに細かく絞るのにnextを利用できます。

find2perl - いっそfindコマンドまでperlで

さらに複雑なことをやりたい場合には、find | perlではなく、find部分もperlにやらせてしまうという手が使えます。この時に便利なのが、find2perlコマンド。find部分を書いて、findfind2perlにかえると、find相当のことをやるperlスクリプトが生成されます。

% find2perl . -type f -name \*.pl -o -name \*.pm
#! /usr/local/bin/perl -w
    eval 'exec /usr/local/bin/perl -S $0 ${1+"$@"}'
        if 0; #$running_under_some_shell

use strict;
use File::Find ();

# Set the variable $File::Find::dont_use_nlink if you're using AFS,
# since AFS cheats.

# for the convenience of &wanted calls, including -eval statements:
use vars qw/*name *dir *prune/;
*name   = *File::Find::name;
*dir    = *File::Find::dir;
*prune  = *File::Find::prune;

sub wanted;



# Traverse desired filesystems
File::Find::find({wanted => \&wanted}, '.');
exit;


sub wanted {
    my ($dev,$ino,$mode,$nlink,$uid,$gid);

    (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) &&
    -f _ &&
    /^.*\.pl\z/s
    ||
    /^.*\.pm\z/s
    && print("$name\n");
}

あとはこれを適当に編集して使えばOK。具体的に編集が必要なのは、File::Find::find({wanted => \&wanted}, '.');のディレクトリ指定とwanted()の中身のみです。

さらにいろいろ試したい方は

たとえば、perlでxargsを再実装するのもそれほど難しいことではありません。例えば

などをご覧下さい。

Enjoy!

Dan the Perl Monger