2008年02月24日 22:45 [Edit]

perl - the best friend of find(1)

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


この記事へのトラックバックURL

この記事へのトラックバック
perl - the best friend of find(1) 404 Blog Not Foundを見てて、シェルについて書いてみたくなりました。 丁度、Linuxを学ぶための10個の効果的な方法で言及したシェルを覚えた経緯を詳しく書いてみたかったのでだらだらと書いてみる。 前回書いた内容がこちら。 順を追っ
[Linux]シェルを覚えた経緯を詳し目に書いてみる【zenpouの日記】at 2008年02月25日 01:17
まあ、ruby のコマンドラインオプションって、Perl由来ですから。 Rubyでワンライナーを書く方法のまとめ まぁ、Perlもあるしあんまり需要が無いのかも知れませんが。 というわけで、Rubyistにも役立つPerlのワンライナー入門です。
perl - ワンライナーの書き方入門【404 Blog Not Found】at 2008年03月30日 19:52
find(1)ねたというのは、定期的にblogosphereを賑わせるものだし、それはそれでよいし、私自身いくつか書いているのだけど.... findを極める! - IDEA*IDEA 〜 百式管理人のライフハックブログ 〜 タグ「find」を含む新着エントリー - はてなブックマーク 404 Blog Not ...
findを極めたかったら、statを抑えよ【404 Blog Not Found】at 2009年05月13日 13:35
この記事へのコメント
macportからいれたGNU utilsとかPerlのFile::*って、ACL拡張属性とかの扱いとか、リソースフォークの._filenameって大丈夫だっけ?
Posted by otsune at 2008年02月24日 23:10
どっちも対応していないですよ。標準のcp,mvはresource forkに対応していて、rsyncは-Eを付ければOK。

なお、xargs(1)はbinutilsではなくてfindutilsの一員だと思う。
Posted by musha at 2008年02月25日 21:14