find2perlで楽々ファイル管理


この記事は、<a href="https://qiita.com/advent-calendar/2019/perl">Perl Advent Calendar 2019</a>の17日目の記事です。16日目は[papix](https://qiita.com/papix)さんの「チームでPerlを書く時に考えていること」でした。

# 概要

**いろんな深さのサブディレクトリ中に散らばった多数のテキストファイルに対して一括的に何らかの処理をしたい。そんなときに超絶便利なツールfind2perlを紹介します。**

* find2perlは、findコマンドと同様に、指定したディレクトリ以下を探索しながら見つかったファイルに対して何かを行なうためのツールです。
* find2perlは、findと共通性の高いコマンドラインパラメータを受け付けます。ただし、処理を直接実行するのではなく、そのためのperlスクリプトを生成します。
* find2perlが生成したperlスクリプトをさらに改変することで、perlの豊富な機能を活かした複雑な処理が簡単に実現できます。

**perlに馴染みのない方や何となく敬遠している方にも納得できるよう、わかりやすい解説を試みます。**

# findコマンドはもちろんご存知ですよね？

複雑なディレクトリの上やら下やらに散らばっているファイルを探し回って、一律に何かしたいことがあります。

- .tmpなファイルを全部消したい。
- ウェブサイトを構成する全てのhtmlファイルに「シュミレーション」なんて恥ずかしい言葉を使いまくっていたのですべて「シミュレーション」に修正したい。

こういうとき、unix-like OSユーザの多くはfindというコマンドを思い出すはずです。

findはとても強力なツールです。たとえば、カレントディレクトリ以下のすべてのサブディレクトリから`*.tmp`なファイルを消去したいなら以下のようになるでしょう。

```
find . -name '*.tmp' -delete
```

ファイルの改変日時基準で操作対象のファイルを選択することもできます。たとえば、~/a.txtよりも新しいファイルのみを消去対象とするという条件を上記のファイル操作に加えるとします。コマンドラインの内容は以下のようになるでしょう。

```
find . -name '*.tmp' -newer '~/a.txt' -delete
```

しかしながら、もう少し複雑なファイルの改変が必要となると、findひとつでは対応が困難。

そういうときは他の様々なツールを組み合わせてパイプラインを組むのが一つの定石と言えるでしょう。よく使われることになるのはxargsやgrep, sed, paste, sort, uniq, awkなど。ワンライナーでしんどいレベルなら、シェルスクリプトファイルを構築しようとする人も出てくるでしょう。

# ではperlはご存知ですか？

find+shスクリプトに対して、もう一つたいへん便利なアプローチがあります。テンプレートを利用して即席perlスクリプトを作ってしまうのです。findベースのシェルスクリプトより小回りがきき、デバッグや機能拡張も自由自在。

この「テンプレート」を作るのに、find2perlを使います。

find2perlは、perl 5.20まではperl本体に同梱されているのでいつでも使えます。5.22以降であれば App::find2perlをインストールすることで利用することができます。

## findする代わりに「findするperlスクリプト」を作ってくれるfind2perl

findを使ったことのある人なら、find2perlを使い始めることに何の困難もありません。その上で、入門レベルのperlの知識を持っていれば充分に無双できます。

find2perlに関してまず理解しておくべきなのは次の２点です。

* 構文はfindと（基本的には）同じ
* 既存ファイルに対して何かが為される代わりにperlスクリプトが誕生する

## 簡単な例

まずは、カレントディレクトリ以下にある`*.txt`なファイルを列挙してみます。findコマンドだと次のようにすれば目標達成です。

```
find . -name '*.txt'
```

find2perlも同じように書きます。ただし、出力されるのはあくまでもperlスクリプトで、これをperlインタープリタに渡してやる必要があります。

```
find2perl . -name '*.txt' | perl
```

>「よくわからんが、find2perlはfindと同じようなノリで使えるのだな」

こう思っていただければ結構です。

上の例は本当に使い捨てのワンライナーとなりますが、もちろん次のようにもできます。

```
find2perl . -name '*.txt' > test.pl
```

findするperlスクリプトがこれで一つ出来ました。

## 少し複雑なことを考え始めたときperlの特性が活かせる

こう言いたくなる人がいるかもしれません。

> 「しかし、それなら普通にfind使うよワシは。シェルスクリプトならマスターしとるからな」

まあちょっとお待ちください。

perlは、次のような有用な機能をサポートしています。

* すごく高性能な正規表現
* bashやawkより多様なループ制御構文
* 「リファレンス」と組み合わせて事実上どんなデータでも格納できる連想配列（ハッシュ）

前節で作ったtest.plを元にして、こうした機能を存分に生かした複雑な一括処理システムを作り上げることができるのです。

## 関数wanted()を読んでみる

find2perlで作り出すスクリプトには、必ず`wanted`という名前の関数が定義されています。この関数のカスタマイズこそがfind2perlを実務で活かすためのキモになります。

スクリプトが実行されると、指定したディレクトリ以下が検索され、ファイル・ディレクトリが見つかる都度wanted関数が呼び出されます。呼び出されたwantedの中では、あらかじめ定義済みの変数として以下が使用可能です。

* `$dir` -- 当該のファイルが存在するディレクトリの名前
* `$name` -- 当該のファイルの名前
* `$_` -- 上記を`ディレクトリ/ファイル`の形で結合したもの

これらの変数をうまく活用し、見つかったファイル/ディレクトのそれぞれを対象として施したい処理をwantedの中に書いていきます。

上で使った例をもう一度見てみましょう。`find2perl -name '*.txt'`を実行すると、次のようなwantedがみつかるはずです。

```
sub wanted {
    /^.*\.txt\z/s
    && print("$name\n");
}
```

最初の行が条件判定、次行がそのファイルに対するアクションに対応しています。

条件判定は、`$_`、すなわち対象ファイルの`ディレクトリ名/ファイル名`に対し、正規表現によるパターンマッチングを試みるという内容です。--name で指定されたワイルドカードつきのパラメータが、うまく正規表現に翻訳されているのが判ります。

２行目は、ファイル名の出力です。本家findコマンド同様、アクションを指定しなければ--printが指定されたものと見なされるのがfind2perlのお約束です。

パターンマッチ用オプションとしては、--name以外にもいろいろサポートされています。--mtime や --newer FILEなどのオプションを使えば、ファイルのタイムスタンプに基づいてフィルタリングを掛けることが可能です。他に--typeやら--sizeやらいろいろ用意されているので`man find2perl`してみましょう。

## 関数wanted()を書き換えてみる

wantedの中身を「所定の変数を参照しつつ特定のファイルに対して何かする」式に置き換えれば、様々な処理を自由にこなせることになります。

検索と処理それぞれについて、特に多用するであろう機能を幾つか以下に列挙してみます。

### 検索オプションの拡充

* パス名の検査で「拡張正規表現」が使用できます。
* bash的なファイル演算子（-f -dなど）が使用できます。
* `lstat(パス名)`でファイルの様々な情報を取得できます。
* `open(my $fh, '<', $_); my $cont=join('', <$fh>);`で、当該ファイルの中身を変数$contに丸呑みできます。そうしておいて`$cont=~/正規表現/;`などとすればファイルの中身を検索できます。

### 処理オプションの拡充

<dl>
<dt>外部ツールの利用
<dd>
<ul>
<li> system("hoge")で外部コマンドを呼び出せます。パラメータをつけたりパイプを書くことも自由で、それらはシェルにそっくり渡されて処理されます。
<li> シェルのように$a=`hoge`でコマンドhogeを実行し、その標準出力を変数に取り込むことが出来ます。
</ul>
<dt>ファイルのコピー・移動
<dd>スクリプトの始めに
<pre>
use File::Copy;
</pre>
と書き加えておくと、シェルのcp, mvに似たcopy(), move()関数が使えるようになります。
<dt>ファイルの削除
<dd>シェルのrmに相当する関数はunlink()です。どうやら本家findの-deleteオプションはfind2perlでは直接サポートされていないようです。
<dt>ファイルの中身を見て何かをする
<dd>
以下のような感じで、見つかったファイルのそれぞれについて中身を読んで処理できます。
<pre>
open(my $fh, '&lt;', $_);
while(&lt;$fh&gt;){
  ...
  何らかの処理
  ...
}
close $fh;
</pre>
</dl>

# 未来のために

未来のために、こうして作ったスクリプトは必ず保存しておけ！ と強調しておきたいです。仮に使い捨てのつもりで作ったとしても。

理由は２つ。

* そのスクリプト自体が、**何よりも雄弁に貴方がやったことを後に続く人に伝える**ログになる。「後に続く人」には、1週間後の貴方自身も含まれる。
* 高い確率で、もっと気の利いた処理を追加したい、やり直したいという欲求が出てくる。スクリプトを残しておけばそれを発展させればいいので断然楽になり、時間が節約できる。

正直コマンドラインでfind使って30文字ぐらいで片が付きそうだね、という処理を行なうことがありますが、私はそうした場合でもスクリプトファイルにします。ファイル名は、20191217a.plのような日付ベースで。

perlはいつもいざというとき頼りになるツールです。find2perlは良い例です。うまく使って業務の効率を更に高めていきましょう。

明日の<a href="https://qiita.com/advent-calendar/2019/perl">Perl Advent Calendar 2019</a>は[xtetsuji](https://qiita.com/tetsuji)さんによる「ここ数年の間で便利に使っているPerlの正規表現〜2019年版〜」の予定です。