Perl Hackers Hub

第57回自作ツールによる日常業務効率化―初歩的なコードだけで身近な問題を解決!(2)

前回の(1)こちらから。

シェルコマンドを組み合わせる

(1)では、Perlの初歩的な構文を使って作成したツールを紹介しました。⁠2)では、そこにシェルコマンドを組み合わせた例を紹介します。

openコマンドを組み合わせる

筆者は普段Macを使用していますが、Macのシェルにはopenというコマンドがあり、ターミナル操作ではこれをよく使います。

openコマンドは、引数にファイル名やURLを入れるとそれらを開いてくれます。

fooディレクトリのbar.txtをデフォルトのアプリケーションで開く
$ open foo/bar.txt
技術評論社のWebサイトをブラウザで開く
$ open https://gihyo.jp/

system関数を使う

では、このopenコマンドをPerlのコードから使うにはどうすればよいでしょうか。

Perlでシェルのコマンドを呼び出す方法はいくつかありますが、筆者は戻り値を使う場合はバッククオートを、戻り値を使わない場合はsystem関数を使っています。

今回のopenコマンドでは戻り値を使わないので、system関数で次のように書きます。

system 'open', 'https://gihyo.jp/';

複数のWebサイトを一斉に開く

では次に、これを前節で紹介したfor文、if文、__DATA__のセットと組み合わせてみましょう。

link-opener.pl
my @links = <DATA>;

for (@links) {
    if ($_ =~ /(https?:\S+)/) {
    system 'open', $1;
    }
}

__DATA__
https://gihyo.jp/
https://twitter.com/
https://www.google.co.jp/

このコードを実行すると、__DATA__の下に入れたURLのWebページが次々とブラウザで開かれます。

筆者は編集や執筆の仕事をする際、その資料として多くのURLを手もとにストックしていますが、これを一度に全部確認したいとき、ブラウザにURLを貼り付けたり、どこかにリンク集を作成して1つずつクリックしたりするのではなく、一度実行するだけですべてのURLを開けるこのコードを使います。

複数ファイルの行数や文字数をカウントする

件の音楽全集では、メインコンテンツとして毎回3万字弱の座談会記事を作成していました。しかしその素材となる文字起こしは8~10万字という膨大なものだったので、これを1本のファイル上で編集していくのではなく、まず大まかなトピックごとにファイルを分割し、それぞれのファイルで編集を行ってから1本にマージして仕上げていました。

この際、各ファイルの進捗を把握するために、それらの行数や文字数をカウントしたいと考えて作ったのが次のツールです。

line-word-count.pl
use feature 'say';

my @iter = glob "*";
my $line_sum = 0;
my $word_sum = 0;

for (@iter) {
    next if (-d "$_");
    next unless ($_ =~ /(txt|md)\z/);

    my $name = `wc -lm "$_" | awk '{ print \$NF}'`  
    my $line = `wc -lm "$_" | awk '{ print \$1}'`;  |(1)
    my $word = `wc -lm "$_" | awk '{ print \$2}'`;  

    chomp $name;
    chomp $line;
    chomp $word;

    $name =~ s/.*?\/?([^\/]+)\z/$1/;
    $word = $word - $line if $line > 1;
    $line++;
    print "$name =>\tlines: $line\twords: $word";

    $line_sum += $line;
    $word_sum += $word;
    say "";
}
say "";
say "Lines total:\t$line_sum";
say "Words total:\t$word_sum";

コードには、シェルコマンドのwcawkが含まれています(1)⁠。当初はその部分を含めて.bashrcの関数[1]として書いていたのですが、その他の繰り返し処理や正規表現なども含めてすべてシェルスクリプトで書くのは自分には難しいと判断し、それら周辺的な部分はPerlで書くことにしました。

使い方

では、使ってみましょう。なるべく手軽に使えるように、ここではlwcというエイリアスを設定しておきます。

.bashrc
alias lwc='perl path/to/line-word-count.pl'

前述の分割したファイルを収めたディレクトリでツールを実行すると、各テキストファイルの行数、文字数、それらの合計が出力されます[2]⁠。

$ lwc
01_beginning.txt => lines: 74 words: 6943
02_Franz-Schubert.txt =>    lines: 130 words: 3989
03_Robert-Schumann.txt =>   lines: 124 words: 5231
04_Frederic-Chopin.txt =>   lines: 110 words: 4741
05_Franz-Liszt.txt =>   lines: 59 words: 1774
06_Richard-Wagner.txt =>    lines: 186 words: 8028
07_Johannes-Brahms.txt =>   lines: 79 words: 1922
08_Gabriel-Faure.txt => lines: 111 words: 3382
09_Gustav-Mahler.txt => lines: 54 words: 1976
10_Richard-Strauss.txt =>   lines: 107 words: 2689
11_Pyotr-Tchaikovsky.txt => lines: 77 words: 3509
12_Jean-Sibelius.txt => lines: 61 words: 2011
13_conclusion.txt =>    lines: 228 words: 9214

Lines total: 1400
Words total: 55409

こうした長丁場の仕事では、進捗の把握がとても重要です。1つのコマンドを実行するだけで十数本の原稿の状況を一望できるこのツールは、作業の大きな助けになりました。

ほかのツールを組み合わせる

ここまでに、Perlの初歩的な構文で作ったツール、シェルコマンドを組み合わせたツールを紹介しました。次に紹介するのは、ほかのプログラマーが開発、公開したツールを組み合わせた例です。

ターミナル操作を楽にする

俗に「黒い画面」と呼ばれるターミナルは、非エンジニアにとって怖い対象とされがちですが、筆者はファイルの検索や操作を行う際もキーボードから手を離さずに済むという理由で、MacではFinderよりもターミナルをよく使います。

しかし、ターミナル操作でも不便な場面は少なくありません。たとえば、ディレクトリを移動するたびに毎回cdコマンドを打つのは面倒ですし、ディレクトリ内に似た名前のファイルが複数ある場合には[Tab]キーでの補完が効きづらく、次のようなファイル群から目的のファイルを選択する際には、Finderを使ったほうが手早く開けます。

$ ls -1
2018-12-31-10-25-14.md
2019-01-02-00-33-37.md
2019-01-02-01-12-25.md
2019-01-03-03-48-32.md
2019-02-15-11-21-47.md

このような問題に悩んでいるときに、牧大輔(lestrrat)さんによるpecoや、mattnさんによるchoといったコマンドラインツールの存在を知りました。pecoは標準入力から渡された行をインクリメンタルに絞り込み、選択した結果を標準出力に返すシンプルなツールです。また、choはそのシンプルさをさらに追求し、絞り込み機能を排して[J]キーまたは[K]キーだけで直感的に行を選択できるツールです。これらを自分のプログラムに組み合わせることで、上述の悩みを解消できるのではないかと考えて作ったのがchocoです。

.bashrcの設定

chocoを使うためには、事前にいくつか.bashrcの設定をしておく必要があります。

.bashrc
function choco {                       
    local path=$(perl "$@")            
    local basename=${path##*/}         
    local dirname                      
                                       
    if [ -f "$path" ]; then            |(1)
        dirname="${path%%$basename}"   
    elif [ -d "$path" ]; then          
        dirname="$path"                
    fi                                 
    cd "$dirname"                      
}                                      
alias j='choco path/to/choco.pl -c open'    ――(2)
alias ja='choco path/to/choco.pl -c open -a'  ――(3)

まず、Perlのコードからカレントディレクトリの位置を操作しても実際のシェルは影響を受けないので、ディレクトリ間の移動を行うための関数を用意します(1)⁠。また、筆者はこのツールを1日に何度も使用するので、最も打ちやすい[J]キーにエイリアスを割り当てています(2)⁠。この際、不可視ファイルを表示するオプションを付けたエイリアスも設定し(3)⁠、用途によって使い分けています。

使い方

では、実際に使ってみましょう。今回は後述の英語学習ゲームcarvoのリポジトリを例に用います。ルートディレクトリでコードを実行すると、次のようにpecoの選択画面に切り替わり、ディレクトリ内のファイルとサブディレクトリが一覧表示されます。

chocoの実行画面
QUERY>  ――(1)
exit  ――(2)
../  ――(3)
docs/
lib/
local/
src/
t/
Build.PL
ChangeLog.md
(省略)

QUERY(1)で対象を絞り込んだり、選択行を上下に移動したりしながらファイルを選択すると、そのファイルパスが返ります。このときにオプションでopenコマンドを指定しておくと、選択されたファイルが開きます。ディレクトリを選択した場合は、その中に入って同じことを繰り返します。上の階層へ移動したいときは../(3)を選択し、終了する場合にはexit(2)を選択します。

前述のとおり、選択画面にはデフォルトでpecoが使われていますが、オプションでchoを指定することもできます。エイリアスで設定する場合は、次のようにします。

.bashrc
alias jc='choco path/to/choco.pl -c open -s cho'

chocoは今や筆者にとってなくてはならない存在ですが、その中身はwhile文やif文など、初歩的な構文の組み合わせによる100行にも満たないプログラムです。主要な機能を外部ツールに任せたことにより、初めて実現できたツールだと言えます。

特定の文字列を含むファイルを検索する

chocoの応用として、find-word.plというツールも作りました。

これはカレントディレクトリ配下を対象に、特定の文字列を含むファイルを再帰的に検索するツールです。

編集の仕事をしていると、⁠あの語句はどのファイルに記述したっけ……」と探したくなることがたびたびあります。そのようなときに、MacのFinderで検索するのは効率が悪く、かといって心当たりがあるファイルを一つ一つ開いていくのも現実的ではありません。

しかしこのツールを使えば、下階層のすべてのファイルを対象に、短時間で目当てのファイルを探していくことができます。

使い方

対象となるプロジェクトのルートディレクトリでコードを実行します。この際、デフォルトでは選択したファイルのパスを返すだけなので、筆者はopenコマンドでファイルを開くようにオプションを指定し、これをfwというエイリアスに設定して呼び出しています。

.bashrc
alias fw='perl path/to/find-word.pl -c open'

たとえば、先ほどのリポジトリ内でYAMLYAMLAin't Markup Languageを扱っているファイルを探したい場合、ターミナルにfw yamlと打ち込めば、yamlという文字列を含むファイル名とその行の情報が一覧表示されます。

$ fw yaml
QUERY>
cpanfile:1:requires 'YAML::Tiny';
lib/Carvo.pm:15:use YAML::Tiny;
lib/Carvo.pm:18:    my $yaml = YAML::Tiny->read('config.ya
ml');
lib/Carvo.pm:19:    my $attr = $yaml->[0];
lib/Set/Generator.pm:8:use YAML::Tiny;
(省略)

この一覧画面はpecoで表示しているので、ここからさらに絞り込みながら対象を選択できます。選択されたファイルは、前述のopenコマンドによって開かれます。

このツールでは日本語の検索もできるので、筆者は表記揺れの検出器としてもよく使っています。

ゲーム感覚で英単語を学習する

これまでに紹介したツールは、おもに業務の効率化や負担軽減を目的としていましたが、ここで少し趣が異なるプログラムを紹介します。

Perlの入門初期にwhile文や標準入出力の方法を教わると、必ずと言ってよいほど練習問題として出題されるのがじゃんけんゲームです。

janken.pl
say '数字を入力してください。
1: グー, 2: チョキ, 3: パー';

my %janken = (
    1 => 'グー', 2 => 'チョキ', 3 => 'パー',
);

while (1) {
    print '> ';
    my $input = <STDIN>;
    chomp $input;
    next if $input !~ /[123]/;

    my $output = int(rand(3));
    $output++;

    say "あなた: $janken{$input}";
    say "わたし: $janken{$output}";

    my $result = $input - $output;

    if ($input == $output) {
        say "あいこ";
    }
    elsif ($result == 2 || $result == -1) {
        say "あなたの勝ち";
    }
    elsif ($result > 0 || $result == -2) {
        say "あなたの負け";
    }
}
実行例
$ perl janken.pl
数字を入力してください。
1: グー, 2: チョキ, 3: パー,
> 2
あなた: チョキ
わたし: パー
あなたの勝ち
> 1
あなた: グー
わたし: グー
あいこ
> (省略)

シンプルなコードですが、自分が入力を行うまで相手が何を出してくるかわからないこのしくみは、どこか人間の好奇心を根本から刺激するようなおもしろみを感じさせます。あるとき、これを筆者が以前から取り組んでいた英単語の暗記学習に活かせないかと考えて作ったのが、英単語学習ゲームcarvoです。

使い方

ターミナルでコードを実行すると、pecoのUIUserInterfaceで学習コースを選択する画面が現れます。

学習コースの選択画面
QUERY>
sample1
sample2
sample3
exit

ここで好きなコースを選択すると、コマンド選択画面に移動します。

sample1を選択した場合
Welcome to the "sample1"
You can choose a number from 1-31

play   ――(1)
again
card
exit   ――(2)
list
fail
voice
help

この画面から、選択ツールとしてchoが使われています。⁠J]キーまたは[K]キーで上下に移動してコマンドを選択します。play(1)を選択するとゲームが始まり、次のように英単語と5つの和訳候補が現れるので、英単語に該当すると思う選択肢を選びます。

expect
- ~を推測する
- ~を提案する
- (~に)頼る
- ~の特徴を述べる
- ~を予期する
- [Give up!]

選択が正解ならポイントが加算され、不正解なら間違えた単語が誤答リストに加わります。終了するには、コマンド選択画面でexit(2)を選択します。

小さなコードの積み上げ方

carvoはじゃんけんゲームと同様、while文を中心とする単純な構造のプログラムなので、初めは1本の小さなファイルに収まっていました。しかしそのうち、単語リストをYAMLファイルで管理したくなったり、機能ごとにファイルを分割したくなったりと、手を入れるたびに新しい書き方を試したくなり、結果的に今回紹介した中で最も行数が多いプログラムになりました。

コードの量が多くなれば、たとえ小さな変更でも想定外の影響が生じる可能性が高くなり、手を入れることが難しくなっていきます。このような状況に対して、筆者は普段からコード片を検証するためのファイルを1本用意しておき、そこで事前に動作を確認するようにしています。

sandbox.pl
my @arr = qw(apple orange lemon);        
say scalar @arr;                         |(1)
say $#arr;                               
__END__   ――(2)
my $foo = "abc";            
$foo =~ s/a(?=b)/x/;        
say $foo;                   |(3)
$foo =~ s/(?<=b)c/x/;       
say $foo;                   
__END__
my @foo = qw(apple lemon);   
my @bar = (@foo, @foo);      |(4)
say for @bar;                
__END__
(省略)

このファイルで扱う内容はさまざまです。配列の要素数を知るにはどうすればよいのか(1)⁠、正規表現の先読み/後読みはどう書くのか(3)⁠、配列の要素として配列を使うことはできるのか(4)など、些細ながら認識が曖昧だった部分を確認するために、以前に書いたコードの上に__END__(2)を挿入して、その上に最小のコードを書いて実行します。

__END__はその下にあるコードを一括でコメントアウトしてくれるトークンです[3]⁠。この1行を入れるだけで、その下に書かれた以前のコードを消すことなく無効化してくれるので、筆者はこれを重宝しています。

こうして事前に挙動を確認してから本番のコードに手を入れるようにすると、想定外のエラーを減らすことができます。筆者はターミナルでpと打てばこのファイルが立ち上がるようにエイリアスを設定しています。

.bashrc
alias p='open path/to/sandbox.pl'

<続きの(3)こちら。>

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧