最近よく使っている自作ツール(2) delete.sh

前回に続き、掲題の件。
前回の内容は、以下。

note103.hateblo.jp

今回紹介するのは、以下。

github.com

と言っても、シェルスクリプト1本ですが。

このツールには前段があって、以前にこのブログでも紹介した「trash」というソフトウェアがあるのだけど、

note103.hateblo.jp

github.com

今回のツールはそれを自分なりにアレンジしたもの。

DEMO trash

先に、その元ネタになったtrashの方を簡単に紹介すると、使い方としてはこんな感じ。

f:id:note103:20170218145111g:plain

trashというコマンドの引数にファイル名を入れると、そのファイルを任意のフォルダへ移動してくれる。
ディレクトリを扱う場合はオプション「-r」を付ける)

その移動先が「仮のゴミ箱」みたいな役割を果たすので、いきなりrm -rfとかするより安全でいいよね、しかもコマンドラインからの簡単な操作で不要なデータを捨てられるのでいい感じだよね、みたいな話。

ただこれの場合、ちょっと面倒なのは、いちいち対象のファイル名を自分の手で打ち込まなければいけないところ。

上のデモのようなシンプルなファイル名なら指定しやすいけど、たとえばこんなファイル名がうごめくカレントディレクトリだったりすると、

2016-04-22-project1.txt
2016-05-14-project1.pdf
2016-05-24-project2.txt
2016-05-25-project3.md
2017-05-26-project4.txt
2017-06-26-project5.md

何回タブで補完しても他のファイル名と共通する文字列でいちいち詰まってしまって、目当てのファイルを指定しきるまでになかなか厳しい思いをすることになる。

そこで、こういうファイル群からプルダウン的に(ファイル名を打ち込まなくても)対象を選択できるようにしたいなあ、と思って、例のごとくpecoとchoをこれに結びつけたのが今回のスクリプト

DEMO delete.sh

f:id:note103:20170218145211g:plain

「d」を叩くとカレントディレクトリ内のファイルやディレクトリがバラララッと出てくるので、あとは画面を見ながら対象を選択。すると、それが任意のゴミ箱へ飛んでいく。

(「d.」を叩くと不可視ファイルも候補に含まれる。いずれも後述のエイリアスで設定する)

移動先のゴミ箱は初期設定だと「$HOME/.Trash」(Macのゴミ箱)になっているけど、.bash_profileからTMPTRASH変数に設定すれば自由に置き換えられる。

# ホームディレクトリ直下の tmp_trash/ をゴミ箱にする場合
export TMPTRASH=$HOME/tmp_trash

なお、前述のとおりchoとpecoを使っているので、それらも要インストール

前回紹介したchoco同様、オプションで-aを指定すると不可視ファイルも扱えるようになり、-pを指定すると選択ツールがchoからpecoになる。

よって、それらを組み合わせたエイリアスを設定するならこんな感じ。

alias d="sh /path/to/delete.sh"
alias d.="sh /path/to/delete.sh -a"
alias dj="sh /path/to/delete.sh -p"
alias dj.="sh /path/to/delete.sh -ap"

ちなみに、このエイリアス例にある「d」はdeleteのdだけど、「j」にはとくにそういう意味はない。

jは単に打ちやすいから使っているのと、僕の場合は「pecoを使う時のエイリアスはjにする」という暗黙のルールみたいのがあるのでそうしている。

なんにせよ、これらのツール&設定により、「このファイルちょっと作業の邪魔だな〜。ゴミ箱に移動しとこ」と思ったらポンと「d」を叩くだけでサクサク選択&ポイできる。

一方、このツールだと1回のアクションで捨てられるデータが1個だけなので、複数の対象を選択してポイできるようにしたいなあ、と思って作ったツールもあって、次回はそれを紹介したい。

最近よく使っている自作ツール(1) choco

プログラミングの学習にともない作った自分用のコマンドラインツールというものがあって、折りに触れこのブログでも記録&紹介しているのだけど、それでも以前に紹介した後に手元でどんどんアップデートしつつも外には出せていないものや、新たに作ったけど自分しか知らないツールというのもあって、少しずつでもそういうのをあらためて紹介していきたいと思ったのでそうする。

リストアップしてみたら結構な数になったので、何回かに分けるかもしれない。(いま書きながらどうするか考えている。書き終える頃にはどうなるか決まっているはず)

choco

まずはこれまでも何度か紹介している以下の最新版。

github.com

mattnさんによるchoと、lestrratさんによるpecoを使って、ディレクトリをスイスイ移動したり、移動した先で選択したファイルをオープンしたりする。

choで移動しているところ

f:id:note103:20170217202617g:plain

pecoで移動して最後に選択したファイルを開いているところ

f:id:note103:20170217202639g:plain

オープンするやつはmacOSのopenコマンドを使用しているので、他の環境でどうなるかはわからない。

たぶんコンピュータを開いて作業している日のうち、これを使わない日はないと思う。
我ながらすごく便利だし、俺グッジョブという感じ。

(いやもちろん、mattnさんやlestrratさんのおかげなわけですが)

名前

以前はこのツール、 "dirmove" という名前にしていたけど、それだとファイルやディレクトリ自体を移動させる意味が含まれてしまいそうなのと、移動先のファイルを開く、という目的も大きくなってきたので名前を変えたいと思い、choとpecoを使っているので合わせて "choco" にした。

これ自体は作業のメインに使うものではなく、単にちょこちょこ動きたいだけ、というグルー(のり)的な道具なので、意味を限定しすぎず、なおかつちょっと可愛い感じで良いネーミングではないかと自分では思っている。

懸案

メインのコードはPerlで書いているが、コマンドラインから居場所をどんどん移動して、なおかつ移動先に着地しなければならない都合上、bashrcにも少なからず記述が必要になる。

詳しくはREADME.mdを参照してほしいが、現時点ではこんな感じ。

# .bashrc

function dirmove {
    local result="$1"
    local basename=""
    local dirname=""

    if [ -e "$result" ]; then
        basename=${result##*/}
        if [ -f "$result" ]; then
            dirname="${result%%$basename}"
        elif [ -d "$result" ]; then
            dirname="$result"
        fi
    fi
    cd "$dirname"
}

function choco {
    local command="${@:$#}"
    local result=$(perl ~/path/to/choco/choco.pl "$@")

    if  [ $# = 0 ] ; then
        command=echo
    elif [[ "$command" =~ - ]] ; then
        if [[ "$command" =~ p ]] || [[ "$command" =~ a ]]; then
            command=echo
        fi
    fi

    if  [ -e "$result" ]; then
        dirmove "$result"
    fi
    $command $result
}

alias s=choco
alias s.="choco -a"  # 不可視ファイルを対象に含める
alias j="choco -p open"  # pecoを使う
alias j.="choco -ap open"  # pecoを使って不可視ファイルも対象に含める

zshだとどうなるか、とかはわからない。

上では関数を二つ書いていて、chocoというメインの関数の他にdirmoveという関数を分けて作っているのだけど、これは後日紹介予定の別ツールでもそのdirmove関数を使うから。
逆に言うと、chocoしか使わないならこれらを一つの関数に合わせてしまってもよい。

いずれにせよ、けっこう行数があるので、シェルスクリプトのファイルに切り出した方が良いのではないかと思って何度か試したのだけど、前述のとおりディレクトリを移動するためにはbashrcへの記述が必須のようで*1、仕方なくbashrcにいろいろ書くことになってしまっている。

最後にエイリアスをいくつか設定しているが、ようはデフォルトだとchoを使うようになっていて、オプションとして -p を指定するとpecoを使うようになり、また -a を指定すると不可視ファイルが対象に含まれ、さらに引数の最後にopenコマンドを入れると最終的に選択したファイルがオープンされる、という感じ。

次回予告

似たようなツールを少なくとももう一つ紹介するつもりだったが、結構長くなったのでここまで。

ブログ記事1本あたりの負担を軽減してコンスタントにパブリッシュできるようにしたいところだけど、どうなるか……。

*1:たとえば「start/path/to/end」というディレクトリ構造で、「startから移動してendのディレクトリに出たい」というときに、それらの関数をbashrcではなくシェルスクリプトなどに書くと、操作中はあちこちに移動できるのだけど、操作を終えるとstartに戻ってしまう。

Perlにおける日本語文字化け対策の私的まとめ

Perlで日本語のテキストを処理しているとけっこうな割合で文字化けにハマる。
近いことについては以前ここでみっちり書いたが、
note103.hateblo.jp
どうもその後、自分はbinmode関数やopen関数、およびutf8やopenプラグマについて理解が怪しいな、と思ったのでいろいろ調べつつ現時点での認識をまとめてみる。

環境づくり

まずはサンプルケース的に、文字化けしがちな状況を作る。

素材データとして、以下の内容をエンコーディングUTF-8のテキストファイルにsource.txtという名前で保存。

りんご
hello

1234
ネコ

次に、そのデータをopen関数で読み込み、split関数で切り刻んで標準出力および書き込み用ファイルresult.txtへ書き込むPerlスクリプト「openio_stdout.pl」を書く。

#!/usr/bin/env perl
#
# openio_stdout.pl

use strict;
use warnings;
use feature 'say';

use utf8;
use open IO => qw/:encoding(UTF-8)/;
binmode STDOUT, ':encoding(UTF-8)';

# 1. 素材データ読み込み
my $in = 'source.txt';
open my $fh, '<', $in or die $!;
    my @data = <$fh>;
close $fh;

# 2. いろいろ処理
my @out;
for (@data) {
    my @split = split//, $_;
    for my $s (@split) {
        push @out, "字$s";
    }
}

# 3. 処理済みデータを書き込み
my $out = 'result.txt';
open $fh, '>', $out or die $!;
    print $fh @out;
close $fh;

# 4. 標準出力に出力
print @out;

実行。

字り字ん字ご字
字h字e字l字l字o字
字犬字
字1字2字3字4字
字ネ字コ字

素材テキストの1文字ずつの間に「字」を入れてみた。
これが書き込み先のresult.txtにも、標準出力(ターミナル)にも出ている、という状況。

つまり、この時点では文字化けしていない。が、それは冒頭にある以下の宣言がちゃんと動いているからで、

use utf8;
use open IO => ':encoding(UTF-8)';
binmode STDOUT, ':encoding(UTF-8)';

これを誤るとけっこう盛大に文字化けする。
以下、それを順に見ていく。

utf8プラグマ

まず、今回の例ではソースコード内に日本語の文字(「字」)を入れているので*1、最初のutf8プラグマが必要になる。

use utf8;

もしこれをコメントアウト等で無効にすると……

f:id:note103:20160925140246p:plain

こんな感じになる。
標準出力でも、書き出しファイルでもこうなる。

utf8プラグマというのは、この例における「字」のように、ソースコード内に素材となる文字を直接書いたとき、それをPerlで処理するための「内部文字列」に変換してくれるもの。

内部文字列に変換されなかったらどうなるかと言うと、Perlがそれを「なんか文字じゃないもの」ぐらいにしか扱ってくれず、上の画像のように化けてしまう。

ちなみに、この「内部文字列に変換されなかった何か」のことは外部文字列……とは呼ばず、「バイト列」と呼ぶことが多いらしい。

で、ここではそのコード内に直接書かれた「字」をutf8プラグマの宣言によって「内部文字列」に変換している。

openプラグマ

次に、このコードではファイル入出力(source.txtを読み込んでresult.txtに書き込む)をやっているので、2番めのopenプラグマも必要になる。

これは別解として、各open関数の第2引数でこのように書いても良いようで、

open my $fh, '<:encoding(UTF-8)', $in or die $!;
(略)
open $fh, '>:encoding(UTF-8)', $out or die $!;
(略)

それを冒頭のプラグマでまとめて宣言している。

また、このプラグマ部分はこのように分けて書くこともできるが、

use open IN => ':encoding(UTF-8)';
use open OUT => ':encoding(UTF-8)';

今回はIN/OUTの両方をやってるのでIOというレイヤーでまとめている。
と同時に、IOなら省略してもOKらしい。

use open ':encoding(UTF-8)';

で、さっきのutf8プラグマは元に戻して、このopenプラグマだけ無効にするとどうなるかと言うと、こうなる。

f:id:note103:20160925140639p:plain

このうち、上側の

Wide character in print at /Users/kadomatsuhiroaki/open/openio_stdout.pl line 29.

というのは標準出力(ターミナル)に出るもので、書き出し先のファイルには出てこない。

その下の文字化け部分は、標準出力と書き出しファイルの両方に出ている。
不思議なことに、というか特徴的なこととして、一つ前の文字化けでは文字間に挟んだ「字」が化けていたのに対し、今度は「字」だけが普通に表示されている。

この段階ではutf8プラグマが生きていて、openプラグマを止めているのでそういう生死の違いが出るのだろう。

また、openプラグマの引数「:encoding(UTF-8)」は、「:utf8」と書いてもほぼ同等の働きをするようだが、前者はデータの入力時にそれが正しいutf-8かどうかをチェックするのに対し、後者はそうじゃないらしいので少なくとも入力時はあまり省略しないほうが良さそうである。

binmode関数

最後に、binmode関数。じつはこれが今まで一番わかっていなかったのだが、どうもいろいろな現象や解説書を並べて観察してみると、単に(というか)標準入出力時にその対象となるデータの文字コードを指定するもの、と考えれば良さそうである。

今回はデータをファイルから入力して、ファイル&標準出力に出しているので、STDOUT時の文字コードのみをbinmodeで指定している。

binmode STDOUT, ':encoding(UTF-8)';

例のごとく、これだけコメントアウトしてみると……。

f:id:note103:20160925140820p:plain

さっきと同じ。……かと思いきや、また微妙に違っていて、まず以下の対象行番号が29から33に変わっている。

Wide character in print at /Users/kadomatsuhiroaki/open/openio_stdout.pl line 33.

ではこの二つの行がソースコード上のどこにあたるかと言うと、以下の部分における、

# 3. 処理済みデータを書き込み
my $out = 'result.txt';
open $fh, '>', $out or die $!;
    print $fh @out;
close $fh;

# 4. 標準出力に出力
print @out;

前者のprint文が29行目で、最後のprint文が33行目。

つまり、openプラグマはopen関数内のprint文に効いていて、binmode関数の方は標準出力時に影響していることがこれらの情報から見てとれる。

また、肝心の本文のほうも、前者は文字化けしているが後者はしていない。
ただしこれは、ここで問題にしているbinmode関数の有無による違いではなく、ひとつ前の例でopenプラグマをコメントアウトした際、入力レイヤー(IN)も外してしまっているために、前者では文字化けが生じていると考えられる。

試しに、一つ前の文字化けはこのような状態で生じたが、

# use open IO => ':encoding(UTF-8)';
binmode STDOUT, ':encoding(UTF-8)';

このように、その状態からopenプラグマの入力レイヤーだけを復活させると、

use open IN => ':encoding(UTF-8)';
binmode STDOUT, ':encoding(UTF-8)';

文字化けは解消する。
(と言いつつ、たしかに文字化けは消えるが今度はbinmode関数のみを無効にした場合とは少し異なるエラーが出てくる。つまり、それらはどれも異なる状態なのだが、これ以上の追求&説明はだいぶ大変なので割愛)

標準入出力もopenプラグマにまとめる

今回の研究というか勉強に際しては、以下3冊を繰り返し見比べては突き合わせ、読み返したが、

初めてのPerl 第6版

初めてのPerl 第6版

かんたん Perl (プログラミングの教科書)

かんたん Perl (プログラミングの教科書)

Perl CPANモジュールガイド

Perl CPANモジュールガイド

そのうちのCPANモジュールガイドで紹介されていた方法。

現在はこのようにしている2行を、

use open IO => ':encoding(UTF-8)';
binmode STDOUT, ':encoding(UTF-8)';

以下のようにまとめられる。

use open IO => qw/:encoding(UTF-8) :std/;

ようはbinmode関数で指定していたことをopenプラグマの引数に「:std」とすることで代替(というか)できる。

いま自分で書いているコードにおいては、同様のことをしたいときは基本最後の形にしている。

まとめ

日本語の文字化けについては常に悩まされるところだが、原因を追っていくとそれなりに明らかになってくるのが面白い。

エラーの内容も書き換えた部分によって変わってくるし、同じエラー・メッセージでもその分量(行数というか)が異なり、それがまたマシンの気まぐれなどではなく、異なるには異なるだけの理由がある。

面倒だ、ぐぬぬ、イライラする! と思うことも少なくはないが、それでも玉ねぎの薄皮を剥がすように少しずつマシになってきていると感じる。
そのような理解を助けてくれる上述の書籍、および知見に満ちた先達やネット上の各種解説記事に感謝します。

付録

今回の出力例の画像(黄色っぽいベージュっぽいやつ)について、本来ターミナルに出るような結果なのに行番号が出ているのはなぜだろう? と思われる人もいるかもしれないが、これはコードの実行結果をVimの同一ウィンドウ&分割バッファで表示してくれるプラグインquickrun.vim」を使って表示している。
github.com

ここでやっているような利用法については以下の記事でも触れたので合わせて挙げておく。
note103.hateblo.jp

*1:サンプルの文字選択を誤ったかもしれない。まぎらわしい。

Perlのオブジェクト指向をつかんだ

経緯

2013年の春にふと「よし、プログラミングやろう」と決めてその夏にYAPC::Asiaに参加して、その直後に買ったのが以下の本。

すぐわかる オブジェクト指向 Perl

すぐわかる オブジェクト指向 Perl

たしか同YAPCDeNAの人が大規模新人研修に関する発表をしていて()、その最後のほうでお勧め入門書ベスト5みたいなやつを紹介していたのだけど、それがよくある特定の選者が一括で選ぶようなものではなく、研修を受けた側の新人さんたちに聞いたアンケートから抽出されたというリアルなもので、そこに含まれているのを見て興味を持った。

で、先日ようやくそれを読み終わって出た感想。


かるくタイポしてるが、

s/年前/年/

実感的にはほんとに「ようやくつかんだ〜」という感じだった。

オブジェクト指向というと、よく「理解は容易だが運用は難しい」という、なんだかスクラム実践入門みたいなことを言われるわけだが、ぼくはその前者の「理解」の段階だけで3年かかってしまった。

まあ、3年ずっとべったり学習し続けてその間わからなかった、ということでもないのだが(読んでない時間の方が多い)、ともあれ、たしかにわかってみると(あるいはそのような気になってみると)「理解は容易」と言われるのもわかる気はする。シンプルといえば大変シンプルなものである。

またオブジェクト指向といえば、「データと処理をセットで扱うものである」なんていうこともよく言われる。これもとくに理解してない段階で聞くと「はあ、そんなものですか」という感じだが(日本語としては理解できる、というか)、今になって思えば、その辺に関する「わかる/わからない」を分けるのは、概念を聞いて理解できるかどうかということではなく、具体的なコードをどれだけ見たか、書いたか、実行したかによって変わってくるように思える。
つまり、パッと聞いてわかるとかそういうものではなさそう。(あくまで入門者に関して言えることだが)

ひと口にオブジェクト指向と言っても、JavaRubyPerlのそれぞれでは、元になる概念に共通する面はあっても、やはりコード上ではそれなりに違いがあるだろう。
そこには、単に構文上の違いだけではなく、お作法というか慣習的な面で、「こうするのがヨイとされている」みたいな違いもあるはずで、だから結局は自分が普段よく使う言語を通してそれを学ぶのが、一番の理解の近道になるのではないかとも感じる。

冒頭の本の話に戻ると、これはけっこうなページ数と文章量があるうえ、大判のせいかやけに重たいので(質量が)、最後まで読むのはけっこう骨が折れた。
ちょっと時間ができたからといって、棚からそれを出すのも大変だし、開いて読むにもちょっとしたスペースが必要だしで、気持ちの面である程度余裕がないと「さあ、読もう」という感じになれない。

もしこれが多少なり素地のある人なら、自分のわからない/知らない部分だけを補う用途で日常的に付き合えるかもしれないが、ぼくの場合はそれこそある程度べったり読み込まないと解読できない内容だったので、その「腰を落ち着けて取り組む機会」をなかなか作れず挫折していた、というのもある。

ちなみに、同書の紹介としては小飼弾さんの以下が大変参考になって、コメント欄では著者の深澤さんもいろいろ述べておられて面白いのだけど、

404 Blog Not Found:$this->get if $you->learn(slow) - 書評 - すぐわかるオブジェクト指向Perl

そこでも言われているように、ほんとに「じっくり、ゆっくり、時間をかけて」という感じで読み継いだ。

じつは同書の前半には、非オブジェクト指向ネタというか、リファレンスに関する説明がみっちり載っていて、購入してから1〜2年ほどは、どちらかというとそのリファレンスまわりの理解を深めるために読むことが多かった。

というか、もちろん最大の目的はその時点でもオブジェクト指向だったので、そっちも読もうとはするのだけど、やはりその辺ってそこまでの前半部分を読んでいる前提で解説されているので、後半だけ読んでもまったく頭に入らず、それで前半に戻ってリファレンスのところなどを一生懸命読み返す、というのがしばらく続いた。

ちなみに、そのリファレンスまわりの解説について言えば、本書ほど丁寧にPerlのリファレンスについて教えてくれる入門書はない(おそらく今後も)だろうと思っている。
もし今つまづいている人がいたらお勧めしたい。

さて、とはいえそんな調子でのったらのったら読んでいるので、後半のオブジェクト指向にはいつまでも入れず、また普段からあまり余裕がなくて本自体にもなかなか触れられず、触れられないうちに前に読んだところも忘れて、だからようやく読み返したときにはまた最初のほうから読み直して……とかやってるうちに「もうこれ、最後まで読み切ることはないかもなあ」という諦め気分だったのだけど、なぜか今年の夏に入ったあたりで、「やっぱり……次はオブジェクト指向だな」と思った。

というのも、オブジェクト指向を多少なり理解していないことには、読めない他人のコードというのが多すぎると感じたのだ。
これは少し前にC言語の学習を始めたことや、シェルスクリプトLinuxコマンドを学びはじめたことにも近い理由であって、言い換えれば「みんなこの言葉を喋っているので自分も学ばないと彼らが何言ってるのかいつまでもわからないぞ」という焦りの気分でもある。

ということで、じゃあどこから再入門するかと思ったとき、半ば挫折しかけてはいるとはいえ、とりあえず途中までは読んでいる同書に再度取り組みはじめたら、ある程度心が決まっていたせいか最後まで理解しつつ読み終えることができたという話。

概念

ということで、以下ではここまでに理解した内容を簡単にまとめてみたい。

まず、オブジェクト指向を一言でいえば、上でもちらっと書いたように「データ(属性・プロパティ)と処理(振る舞い・メソッド)をくっつけたオブジェクトなるものを使っていろいろやる」ということになる。
この概念が、この話の一番の軸・前提になる。

また、Perlを使ったオブジェクト指向においては、おもに前者のデータをリファレンスで、後者の処理をサブルーチンで表す。
で、作ったそれらをbless関数でくっつけるとオブジェクトができる。

ここで出来たオブジェクトというのは、鋳型(いがた)のようなもので、鯛焼きやタコ焼きで使うような、型のある鉄板みたいなもの。
と同時に、プログラムの中で実際にあれこれやるのはその型じたいではなく、型をもとに作られた鯛焼きなりタコ焼きなりの1個1個。

で、前者の鋳型みたいなものをクラスと言い、その鋳型から出てきた後者の1個1個の具体的な実体をインスタンスと言う。

そしてその1個1個に対して何かを入れたり、そこから何かを引き出したり、それに何かをやらせたりしていく中でいろいろやるのがオブジェクト指向プログラミング。(雑になってきた)

実例

では概念的な話はそのぐらいにして、以下ではPerlによるそれを実例で紹介してみる。
むちゃくちゃベタだけど、動物ネタで。

こんな感じの構成を考える。

Animal
    ├─ lib
    │    ├─ Animal.pm
    │    └─ Animal
    │           ├─ Dog.pm
    │           └─ Cat.pm
    └─ main.pl

これはざっくり言うと、Animalという型が元にあって、それを継承したDog, Catという子クラスを配置したモデル。

いまあっさり「子クラス」と書いたが、そのときAnimal.pmの方は「親クラス」ということになる。

が、それは説明が簡単なのでそのように言っているものの、実際にはAnimalとDog/Catの関係は「親/子」とか「主/従」のような上下関係というより、「抽象/具体」という対照的な(お互いに呼応した)関係だと考えたほうが腑に落ちる。
これは冒頭に挙げた深澤さんの本でそのように説明されていて、なるほどと思った。

では具体的なコード。

まずAnimal.pm。

package Animal;
use strict;
use warnings;

sub new {
    my $class = shift;

    my %self = @_;
    $self{name} //= 'No name';
    $self{age} //= 0;
    $self{sound} //= '(silence)';

    bless \%self, $class;
}

sub speak {
    my $self = shift;
    return $self->{ sound };
}

1;

newメソッドの中でプロパティ(属性)を設定している。この「プロパティ」というのは他にも「アトリビュート」とか「フィールド」とも呼ばれるようだが(後者はJava用語っぽい)ここではプロパティと呼んでおく。

speakというのはサブルーチンだが、オブジェクト指向の際にはメソッドと呼ぶらしい。

上のほうで「データと処理」と表現したが、先のプロパティがデータにあたり、このメソッドが処理にあたる。

次に、Animal.pmを継承したDog.pm。

package Animal::Dog;
use strict;
use warnings;
use parent 'Animal';

sub speak {
    my $class = shift;
    return 'Bow!';
}

sub move {
    my $class = shift;
    return 'Jump!';
}

1;

継承すると、親クラスのAnimalの方で定義しているメソッド(newやspeak)を子クラスでも使えるようになる。だからここではnewメソッドを作らない。

なるほど、これなら重複する内容を書かなくて済むわけで、継承などをしない手法に比べて大きなメリットだと感じる。

その上で、ここではメソッドのオーバーロード(上書き)というやつで、speakメソッドをAnimal.pmのそれからちょっと書き換える。
Animalのspeakメソッドではプロパティ内のsound要素を呼び出していたが、Dogの方ではそうせずに独自の 'Bow!' を返すことにする。

また、Animalの方にはなかったmoveメソッドも独自に追加した。

もう一個、Animal.pmを継承したCat.pmを作ってみる。

package Animal::Cat;
use strict;
use warnings;
use parent 'Animal';

sub new {
    my $class = shift;
    my %self = @_;
    $class->SUPER::new(%self, sound => 'Nyaa..');
}

1;

こちらではnewを少し書き換える。
Dog.pmの方ではspeakメソッドを直接変更したが、ここではnewの方からsoundの値を書き換えて鳴き声を変えてみる。

この書き換えはあまりいい例じゃない気もするが(有効性を感じられないというか)、まあ、子クラスから親クラスのメソッドを呼び出す例として面白いので。

最後に、それらを実行するmain.pl。

#!/usr/bin/env perl
#
# main.pl

use strict;
use warnings;
use feature 'say';
use lib './lib';
use Animal;
use Animal::Dog;
use Animal::Cat;

my $animal = Animal->new(name => 'foo', age => 100);
say "Animal's name:\t".$animal->{ name };
say "Animal's age:\t".$animal->{ age };
say "Animal speaks:\t".$animal->speak;
say '---';
my $dog = Animal::Dog->new(name => 'Pochi', sound => 'Vow!');
say "Dog's name:\t".$dog->{ name };
say "Dog's age:\t".$dog->{ age };
say "Dog speaks:\t".$dog->speak;
say "Dog moves:\t".$dog->move;
say '---';
my $cat = Animal::Cat->new(name => 'Tama', age => 3);
say "Cat's name:\t".$cat->{ name };
say "Cat's age:\t".$cat->{ age };
say "Cat speaks:\t".$cat->speak;

結果。

Animal's name:	foo
Animal's age:	100
Animal speaks:	(silence)
---
Dog's name:	Pochi
Dog's age:	0
Dog speaks:	Bow!
Dog moves:	Jump!
---
Cat's name:	Tama
Cat's age:	3
Cat speaks:	Nyaa..

OK。

もう一個、オブジェクト指向では多重継承という手法がよく話題になるようで、ここではDogとCatの親クラスはAnimalのみだけど、複数の親からメソッドを継承したい場合にはどうするか、という話。

たとえば、「動物の中の何か」であると同時に、「その物体は何色か」という要素を規定するクラスをDogやCatで共通して流用したい場合。

ひとつの方法として、現在Animalから諸メソッドを継承しているように、そういった別の親クラス(仮にColor.pmとする)を継承しても良いが、多重継承というのはいろいろ煩雑になる面があるらしく(雑な説明だが)、それを避けたい場合は普通にuseする。

まずColor.pmを作る。

package Color;
use strict;
use warnings;

sub color {
    my $self = shift;
    my $color = shift;
    if ($color) {
        return $color;
    } else {
        return 'brown';
    }
}

1;

引数を与えたらその色、与えなければ「brown」ということにした。
(たしか「続・初めてのPerl」でこんな例があった気がしたので借りた)

ここではこのColor.pmをAnimal.pmと同階層に置いておく。
こんな感じ。

Animal
    ├─ lib
    │    ├─ Animal.pm
    │    ├─ Color.pm # <= 追加
    │    └─ Animal
    │           ├─ Dog.pm
    │           └─ Cat.pm
    └─ main.pl

で、Animal.pmを少し書き換え。

package Animal;
use strict;
use warnings;

sub new {
    my $class = shift;

    my %self = @_;
    $self{name} //= 'No name';
    $self{age} //= 0;
    $self{sound} //= '(silence)';

    bless \%self, $class;
}

sub speak {
    my $self = shift;
    return $self->{ sound };
}

# colorメソッドを追加。
sub color {
    my $self = shift;
    my $color = shift;
    return Color->color($color);
}

1;

このままDog, Catの方はとくに触らずに、main.plでColor.pmをuseして、それぞれのcolorを呼び出してみる。

#!/usr/bin/env perl
#
# main.pl

use strict;
use warnings;
use feature 'say';
use lib './lib';
use Animal;
use Animal::Dog;
use Animal::Cat;
use Color; # <= 追加

my $animal = Animal->new(name => 'foo', age => 100);
say "Animal's name:\t".$animal->{ name };
say "Animal's age:\t".$animal->{ age };
say "Animal speaks:\t".$animal->speak;
say '---';
my $dog = Animal::Dog->new(name => 'Pochi', sound => 'Vow!');
say "Dog's name:\t".$dog->{ name };
say "Dog's age:\t".$dog->{ age };
say "Dog speaks:\t".$dog->speak;
say "Dog moves:\t".$dog->move;
say "Dog's color:\t".$dog->color; # <= 追加
say '---';
my $cat = Animal::Cat->new(name => 'Tama', age => 3);
say "Cat's name:\t".$cat->{ name };
say "Cat's age:\t".$cat->{ age };
say "Cat speaks:\t".$cat->speak;
say "Cat's color:\t".$cat->color('white'); # <= 追加

結果。

Animal's name:	foo
Animal's age:	100
Animal speaks:	(silence)
---
Dog's name:	Pochi
Dog's age:	0
Dog speaks:	Bow!
Dog moves:	Jump!
Dog's color:	brown
---
Cat's name:	Tama
Cat's age:	3
Cat speaks:	Nyaa..
Cat's color:	white

OK。Dog, Catの最後にカラーが追加された。

このように、「多重継承したいけどしたくないので別の方法でいい感じにいろんなクラスを取り込みたい」みたいな時のこういう方法をMix-inというらしい。
いや、厳密に定義すれば上のような方法をそうは言わないかもしれないが(よく知らない)、まあ理念というか目的としては似たようなものかと捉え中。

参考書籍

ちなみに、そのMix-inを含めてオブジェクト指向の基礎みたいなところについては、まつもとゆきひろさんの以下の本がとても参考になった。(まだ読み切れていないが入門者向けっぽいわりにすごい濃い)

また、同書を紹介してくれたのはYAPC::AsiaやPerl入学式で時々会う人で、「あの本はオブジェクト指向の説明がすごくわかりやすかったな」と、たしか一昨年(2014年)のYAPC::Asiaで会の終了後に会場下のHUBで飲んでいるときに教えてもらったのでそのまま購入&読んだという経緯がある。
その節はありがとうございました。>その方

それから、上でもちらっと書いたとおりオブジェクト指向Perlの学習に際しては、冒頭の深澤さんの本の他、皆さんお馴染み『続・初めてのPerl』と、

続・初めてのPerl 改訂第2版

続・初めてのPerl 改訂第2版

@kaz_hiramatsuさんによる『雅なPerl入門 第3版』が参考になった。
kazhiramatsu.hatenablog.com

どちらかと言うと、『すぐわかるオブジェクト指向Perl』と『雅なPerl入門』で全貌をつかみつつ、『続・初めてのPerl』で脇を固める、みたいな読み方だったか。

まとめ・展望

ということで、本当はこのまま今度は上記のコードをMouseというモジュールを使って書き換えた版も復習がてら書いておきたかったのだけど、だいぶ長くなったのでひとまずここまで。

実際には、これらを踏まえた上で同内容をMouseで書き換えたら「なるほど、まあこの方がいろいろラクかもしれない」と思ったりしたので、そこまでを自分の理解のひとまとまり、と捉えてはいるのだけど。

あと、上では触れなかったがアクセサ(ゲッター&セッター)というやつに関する理解が薄いという自覚があり、かつたぶんカプセル化というやつを意識する上ではそれが重要だと思われるのだけど、その辺の理解度が低い段階で上のようなblessを使った方法でゴリゴリやっているといつまでもこの記事をUPできなそうなので、次にMouseの基礎的な使い方をまとめる際、あらためてその辺も勉強してみたい。

ひとまずのまとめとしては、やはりオブジェクト指向というのはどうもプログラマーの人たちにおける一種の共通言語のようになっている部分があるようで、その一部をカタコトながらも解読できつつあるのは喜ばしい。

同様に、サーバーまわりやSQL、あとちょっと外れたところで(というか)TeXなど*1、勉強したいことは山積みだが地道に触れていってみたい。

*1:自分の分野に近いので、というところもある

正規表現の最短マッチに関するVimとPerlの違い

以前にも近い話題でひとつ書いたのですが、

note103.hateblo.jp

それと重なりながらもちょっとズレるトピックでけっこうハマったので、おもに未来の自分用にまとめておきます。

例題

以下の文字列に対して、最初の apple だけマッチさせるパターンを考えてください。

appleorangeapple

という問題があったとき、「最初の apple だけ」ということは、いわゆる「最短マッチ(非欲張り型)」のパターンを考えることになりますから、Perl だとこんな感じで作れます。

Perl

my $foo = 'appleorangeapple';
say "最短マッチ: $1" if $foo =~ /^(.*?e)/;
say "最長マッチ: $1" if $foo =~ /^(.*e)/;

実行。

最短マッチ: apple
最長マッチ: appleorangeapple

パターンの部分だけを取り出すと、こんな感じになります。

/^.*?e/

Vim

次に、Vimで同じことをやりたい場合、通常のVim正規表現だと、丸括弧などのエスケープ処理がPerlのそれに比べてかなり面倒なため、ここではそうしたVim特有のクセをなるべく排除できるように \vパターンスイッチを使います。

\vパターンスイッチとは何か? ということについて、Vim教科書のマスターピースこと『実践Vim』から説明を抜き出すと、

\vパターンスイッチを使うと、すべての特殊記号に関する規則を正規化できる。(略)
\vパターンにより、Vim正規表現エンジンの振る舞いはPerlPythonRubyにより近いものに切り替わる。それでも違いはあるけれど。
(p244)

とのこと。

ぼくは少し前までは、「Vim正規表現にはいろいろクセがあるけれど、これを体得してこそ真のVimmerになれるはず……」と思って一つ一つ地道にエスケープしていましたが、最近になって「そろそろラクになってもいいのでは……」と思って\vを使うようになりました。

するとたしかに、大半のケースにおいてはそれでほぼ、Perlで使うのと同様の結果を期待できるのですが、今回とり上げる最短マッチはその例外、つまり上の説明で言うところの「それでも違いはあるけれど。」にあたるようで、Vimで\vパターンスイッチを付けながら同じことをやろうとすると、こんな感じになります。

/\v^.{-}e

先ほどのPerlの場合とパターンの中身だけ並べてみましょう。

^.*?e
^.{-}e

Perlでは「*?」となっていた部分が、Vimだと「{-}」になっています。

ちなみに、最長マッチのほうでは、PerlVimもこれで行けます。

^.*e

つまりここでの違いは、Vimにおいては「?」を最短マッチの道具として使えない、ということですね。

もう少し具体的に言うと、Vimでは単純に「?」の代わりに何かを使うとか、「?」をエスケープするとかではなく、「*?」を「{-}」で代替するという、なかなか自然には想像のつきづらい規則だったので、把握するまでにけっこうな時間を要しました。

ちなみに、Perlにおける「.+?」はVimだと「.{-1,}」になるようです。

このあたりの情報については、前回の記事でも謝辞とともに紹介しました以下のサイトのまとめが非常にわかりやすかったです。

実際の用途としては、ひとまずVimでは「.{-}」がPerlにおける「.*?」の代わりになる、とだけ覚えておけば大半の状況は乗り越えられそうな気がしますが、とはいえ1年で何回使うかわからないぐらいレアなパターンかもしれないので、忘れてしまってもすぐに思い出せるよう、ここにまとめておきました。

実践Vim 思考のスピードで編集しよう!

実践Vim 思考のスピードで編集しよう!

textlintで日本語テキストの文字校正を試してみた

はじめに

textlintについては少し前から時々名前を聞くなと思っていましたが、自分に関わりがありそうなものとして意識したきっかけは、@t_wada さんによる以下のツイートだったと思います。

これを見た時点では、まあそういうこともあるだろうな、という程度の軽い感想でしたが、その後じわじわと「気になる」感じが増してきて、ようやく週末に少し時間を取れそうだったので、とりあえず1〜2日トライしてみたのが以下の話です。

先にぼくの背景について簡単に記しておくと、ぼくは3年前のちょうど今頃からプログラミングに入門し、最初はRubyJavaScriptを触っていたのだけど、同年8月頃にたまたまPerlを教えてくれる人たちに出会って、以後このブログに書いてきたようなことをしている非エンジニアの編集者です。

※その他の履歴についてはこちらをどうぞ。 → Profile

プログラミング歴3年とは言っても、普段の仕事ではとくにプログラミングを活用したり学習したりする機会もなく、ようやくチョット使えるようになってきたのもPerlVimぐらいで、今回のテーマであるtextlintまわりで使われているJavaScriptなどの知識はほとんど無いので、いろいろ勘違いしているところもあるかと思いますが、その際にはコメント欄やTwitterにて適宜ご指摘ください。

導入

さて、上記のような興味とともに最初にチェックしたのは、textlint作者である @azu さんの以下のブログ記事でした。
efcl.info

レポジトリにも導入のチュートリアルがあるので、どちらを見ながらスタートするか少し迷いましたが、
github.com

日本語で書かれている前者の記事をぼんやり目で追いながら、順にコマンドを打ち込んでいったらいつの間にか最初の環境づくりはほぼ完了していました。

※具体的には、textlint本体とmax-ten, spellcheck-tech-word, no-mix-dearu-desumasuというルール群をインストールして、試しに動かしてみるまで。

ちなみに、とりあえずtextlintを使ってどのような校正が成されるのか体験してみたい、という目的であれば、その記事でも紹介されているように Chrome拡張を使うという選択肢もあります。

ぼくの場合は、以前にVimJSONシンタックスチェックを設定した際、少しだけnpmを触ったことがあって、

note103.hateblo.jp

かつ、このときにはけっこうハマったので、その復習もかねてコマンドラインからのインストール&実行を試すことにしました。

一瞬話がそれますが、僕がこういうことをやる一番の目的は、「文字校正さえできればいい」といった最終形を求めてのことではなく、それができるまでの過程を体験しながら、それが置かれているシステムの裏側(仕組み)を知りたいということ、またそれを身につけて自分一人でも手元でイチから再現できるようにしたい、といった点にあるようで、それがこういうトライの原動力になっているようにも思えます。

textlintrcを設置

本題に戻ると、上記の導入記事で紹介されている最初のルール群を入れた後は、そのまま案内に沿って textlintrc というvimrc的なものを作りました。

書式はJSONYAML、JSモジュール(って何?)のいずれでも良いようですが、ここは素直に @azu さんが例示していた以下の形で。

{
  "rules": {
    "max-ten": {
      "max": 3
    },
    "spellcheck-tech-word": true,
    "no-mix-dearu-desumasu": true
  }
}

これはJSON形式だと思いますが(いちいち自信がない)、JavaScriptで使うようなコメントアウトが使用可能なので後々大変助かりました。コメントアウト便利。

で、これを書いたら「.textlintrc」としてホームディレクトリに設置します。
といっても、実際にはvimrcやbashrcと同様、ファイル本体はドットファイルを集めるディレクトリに入れて、そこからシンボリックリンクでホームディレクトリへリンクしています。

$ ln -s /path/to/dotfiles/textlintrc ~/.textlintrc

このtextlintrcを設定して何が嬉しいのかというと、たとえばこれを設定せずに上記のルールを使おうとしたら、

$ textlint --rule no-mix-dearu-desumasu --rule max-ten --rule spellcheck-tech-word README.md

というふうに、けっこう壮大な引数をタイプする必要がありますが、textlintrcを設定しておけば、

$ textlint README.md

だけで済みます。(上記二つのサンプルコードは @azu さんのブログより)

これは使わない手はないですね。

感覚的には、vimrcに各種Vimプラグインやそれぞれの設定を記述していくことに近いと感じました。

最初のつまずき

環境設定はそんなところで、ここからは実際のテキストを使ってツールを動かしていきます。

サンプルテキストには、ぼくらの夏目漱石&ぼくらの青空文庫から「草枕」をお借りしました。

山路やまみちを登りながら、こう考えた。
 智ちに働けば角かどが立つ。情じょうに棹さおさせば流される。意地を通とおせば窮屈きゅうくつだ。とかくに人の世は住みにくい。
 住みにくさが高こうじると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟さとった時、詩が生れて、画えが出来る。
(略)

時間の都合上、ブラウザでそのままコピペしただけなのでルビもそのまま残っていますが、サンプルなのでご容赦ください。

全文だとさすがに長すぎるので、冒頭の一部を「kusamakura.txt」というファイルに保存して、さっそく実行。

f:id:note103:20160612202105g:plain

ん……? なんとなく良いようにも見えるけど、最後のほうがなんか変?

これってそもそもこういう表示をするツールだということなのか、それともまだ鋭意開発中のものだから環境によってはちゃんと動かない、ということなのか……? といろいろ考えてしまいました。
これが今回最初のつまずき。

これに対しては、その後「とりあえずlessで見てみるか……」と思いついたのが幸いして。

f:id:note103:20160612202146g:plain

今度は最後まで表示されました。さっきのはやはり途中で切れていたようです。

見たところ、漱石先生は一文あたりの読点がけっこう多いようですね……。

prh を使ってみる | 2度目のつまずき

基本的な動き方は確認できました。ここからは、これをどう応用できるのか、どんなバリエーションを付けていけるのか、といったことを考えていくわけですが、このツールではあまりにもいろんなことが出来そうで、また実際「こんなこともできるよ!」という説明も膨大にあるようで、もはやワクワクするのを通り越してウツになりそう、できれば最小限のことだけをとりあえずできるようになりたい……などと思いつつ、それでもとりあえずこれだけはやってみたい! と思ったのがこちら。

efcl.info

ここで紹介されている @vvakameさんは技術(書)界隈では有名な方で、ぼくも以前にRe:VIEWの開発者カンファレンスに参加した際に発表を拝見しましたが、

※参考: 書籍執筆支援システム「ReVIEW」に触ってみた話(&リンク集) - the code to rock

このtextlint-rule-prhというのは、その@vvakameさんによるproofread-helper(以下「prh」)というツールをtextlintから使うというもの。
github.com

ということで、さっそく案内にあるとおりにあれこれ設定して実行してみます。

f:id:note103:20160612035210g:plain

おぉっと、盛大なエラー……。
メッセージをコピペすると、こんな感じ。

Unhandled rejection Error: Error while loading rule 'prh': ENOENT: no such file or directory, open '/Users/kadomatsuhiroaki/sample/textlint/~/prh.yml'
    at Error (native)
    at Object.fs.openSync (fs.js:634:18)
    at Object.fs.readFileSync (fs.js:502:33)
    at Object.fromYAMLFilePath (/usr/local/lib/node_modules/textlint-rule-prh/node_modules/prh/lib/index.js:9:22)
    at createPrhEngine (/usr/local/lib/node_modules/textlint-rule-prh/lib/prh-rule.js:30:35)
    at reporter (/usr/local/lib/node_modules/textlint-rule-prh/lib/prh-rule.js:79:26)
    (略)

本当はもう少し長いエラーですが、注目すべきはたぶん1行目のこれで。

no such file or directory, open '/Users/kadomatsuhiroaki/sample/textlint/~/prh.yml'

「/Users/kadomatsuhiroaki」というのはぼくのMacのホームディレクトリ。そして「/sample/textlint」というのは今作業をしているディレクトリです。

そのつなぎ目がなんか変ですね。

/textlint/~/prh.yml

これは何かというと、おそらくtextlint-rule-prhの説明を見ながら書いた、textlintrc内の以下の記述が影響しているようです。

    "prh": {
      "rulePaths": [
        "~/prh.yml"
      ]
    },

ぼくとしては、「~/prh.yml」と書くことによって、「ホームディレクトリ直下にprh.ymlというファイルを置いたよ」とコンピューターに指示したつもりでしたが、上のエラーから察するに、そういう意味では受け取られておらず、コードを実行しているカレントディレクトリからの相対パスとして「~/prh.yml」が受け取られているようです。

しかしこの仕様だと、ファイルを実行するディレクトリごとに異なるprh.ymlを設置することになるので、本当にそうなのかなあ? としばらくハマりました。(使い方や理解の仕方が間違っているのかなと)

僕の感覚では、こういう多種多様なファイルを処理するツールの場合、作業場所もコロコロ変わると思うので、そのつどこういう必須データを生成するという発想はなかったのですが、ただあらためて考えてみると、校正ルールというのは文書の内容や方針によって変わるほうが自然とも言えて、毎回まったく同じというのも確かに不便なのかもしれません。

ということで、これについてはtextlintrcを以下のように直し、

    "prh": {
      "rulePaths": [
        "./prh.yml"  //<= 「~」を「.」に
      ]
    },

基本的にはつねに作業するディレクトリにprh.ymlを生成・設置することにしました。(生成方法については後出のtx.shをご参照)

追記: 絶対パスにも対応して頂きました

上記の「~/prh.yml と記述してもホームディレクトリだと認識してもらえない」という件に関して、作者の @azu さんが早々に対応してくださいました。

textlint-rule-prhの v3.1.0以降とのこと。

これにより、自分は基本的にprhの辞書はいつも同じでよい、という場合は絶対パスで以下のようにしておいて、

    "prh": {
      "rulePaths": [
        "~/path/to/prh.yml"
      ]
    },

プロジェクトごとに辞書を管理したい場合はこれまで通り、たとえば以下のようにしておけば大丈夫です。

    "prh": {
      "rulePaths": [
        "./prh.yml"
      ]
    },

なお、後出のコード「tx.sh」では、後者の設定を前提としていますのでご留意ください。

@azu さん、ありがとうございました!!

(追記ここまで)

prh を使ってみる(2) | 辞書を選ぶ

さて、そのprhですが、初期状態では以下のような辞書が用意されています。

prh.yml
review-ignore.yml
target.yml
techbooster.yml
WEB+DB_PRESS.yml

といっても、このすべてが同等に作られているということではなくて、とくに実用的なのは以下の二つのようです。

techbooster.yml
WEB+DB_PRESS.yml

前者の「techbooster」というのは、Androidを初めとする技術系のサークル「TechBooster(テックブースター)」の方針に沿って作られた辞書とのこと。

同サークルの名前は電子書籍の分野でもよく聞きますが、@vvakameさんとも関わりが深いようで、prh的にもこの辞書が一押しだそうです。

WEB+DB PRESS」は言わずと知れた技術誌の雄ですが、同誌編集者の稲尾さんが公開された用語統一ルールなどをもとに作られているようです。

複数の辞書をマージして使うこともできるようですが、素直なぼくとしては(そしてYAML力のないぼくとしては)、ひとまず一押しの「techbooster.yml」を使ってみます。

先の作業ディレクトリに prh.yml という名前でコピーして*1、実行。

$ textlint kusamakura.txt | less

結果。

/Users/kadomatsuhiroaki/sample/textlint/kusamakura.txt
3:57 ✓ error 出来る => できる prh
5:4 ✓ error 事 => こと prh
5:77 ✓ error 出来て => できて prh
6:293 ✓ error 故に => ゆえに prh
6:498 ✓ error ―― => ── spellcheck-tech-word
7:89 ✓ error ―― => ── spellcheck-tech-word
9:141 ✓ error 一つ => ひとつ prh
(略)

ふむ。いい感じにprh名義のエラー(というか指摘)が出てきましたね。だんだん使い方がわかってきた気がします。

結果をテキストファイルに書き出す

ここまでの作業を一旦ふり返ると、まずチェックしたいファイルと、準拠したいルールを用意して、そのチェックを通した「結果」を受け取る、ということまで出来ました。

しかしながら、この段階ではまだ毎回ターミナル上のlessを目視しては「ふむふむ」と確認しているだけなので、あまり実用的ではないですね。

ということで、次はこの出力されたエラーをテキストファイルに書き出してみようと考えました。

ただ、ちょっと冷静に考えてみると、上のような出力結果を受け取ったところで、結局それをまた元の文書とひとつひとつ突き合わせて、どこがどう間違っているのか確認していくという修行のような作業が待っているわけで、それはそれで大変そうです。

一方、textlintのオプション機能として、出力フォーマットを指定することができて、たとえば以下のようにすると……

$ textlint -f pretty-error kusamakura.txt | less

こんな感じに出てきます。

f:id:note103:20160612025026g:plain

ええと、例が悪くて何がいいんだかわかりづらいですが、行の中のどの辺がエラーなのか、というのが(本当は)見やすくなっています。

じゃあ、このフォーマットでテキストファイルに書き出してみようと、素朴なぼくとしては以下のコマンドを叩くわけですが、

$ textlint -f pretty-error kusamakura.txt > pretty-error.txt

こんな感じで出てきます。

[4m/Users/kadomatsuhiroaki/sample/textlint/kusamakura.txt[24m
[90m...[39m
[31m-  住みにくさが高こうじると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟さとった時、詩が生れて、画えが出来る。
[39m[32m+  住みにくさが高こうじると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟さとった時、詩が生れて、画えができる。
[39m[90m 人の世を作ったものは神でもなければ鬼でもない。やはり向う三軒両隣りょうどなりにちらちらするただの人である。ただの人が作った人の世が住みにくいからとて、越す国はあるまい。(略)

おぉぅ……。「[39m[32m」とか「[39m[31m」とかいう文字化け的な何かが出てきました。

ということで、これについてはPerl正規表現を使って整形していくことにしましょう。

追記: [39m[32m などを消す方法

こちらについても、その後 @azu さんから対応方法を教えて頂きました。

なるほど……じつは本記事には書かなかったのですが、この表示についてはけっこう試行錯誤しまして。一生懸命 less の設定をいじったりしていたのですが、まったく直らず諦めたのでした。
textlint コマンドのほうにこういったオプションを付ければ良かったのですね……。

ANSI escape code というものもこの機会に知ることができて大変勉強になりました。
これまたいろいろ検索したのですが、まったく引っかからなかったので……。

上記の方法を併用すれば、以下に出てくるコードもよりシンプルに書けそうですが、記事としてはこのオプションを知らない前提で話を進めていきますので、その旨ご留意ください。

あらためまして @azu さん、ありがとうございました!!

(追記ここまで)

コードを書く

あらためて、この段階で実現したいことを整理すると、

  1. ターミナルでコマンド+ファイル名を打ち込むと、
  2. 通常の結果と、pretty-errorフォーマットで出力された結果がそれぞれテキストファイルに書き出される

という状態を求めています。

そして、それを実現するために書いたシェルスクリプトPerlスクリプトが以下です。

tx.sh
#!/bin/sh

if [ ! -f "$1" ] ; then  # 引数で指定したファイルがカレントディレクトリになければ実行しない
    echo "No $1."
else
    if [ ! -f "prh.yml" ] ; then  # カレントディレクトリにprh.ymlがなければ元の置場からコピーしてくる
        cp /path/to/techbooster.yml ./prh.yml;
    fi

    textlint "$1" > textlint_error.txt  # 通常のtextlintをして結果をtextlint_error.txtに書き出し
    cat textlint_error.txt  # 結果をターミナルにも出力

    textlint -f pretty-error "$1" > textlint_pretty_error.txt  # pretty-errorフォーマットで出力し、一時ファイルに書き出し
    perl /path/to/textlint_pretty_error_tidy.pl textlint_pretty_error.txt > textlint_pretty_error_tidied.txt  # 後出のPerlスクリプトで整形し別ファイルへ書き出し
    rm -f textlint_pretty_error.txt  # 一時ファイルを削除
fi
textlint_pretty_error_tidy.pl
#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use File::Slurp;

my $data = read_file($ARGV[0]);  # 一時ファイルを読み込み
my @data = split/\n/, $data;

# 不要な文字列をカット
for (@data) {
    chomp $_;
    $_ =~ s/\\[90m//g;
    $_ =~ s/\\[31m//g;
    $_ =~ s/\\[0m//g;
    $_ =~ s/.*?([^\/]+\.[^\/]+)$/$1/g;  # ファイル名が絶対パスで出てくるのでbasenameのみにする
}

say for @data;

そしてすかさず、上記のシェルスクリプトを「tx」というコマンドで、マシンのどこからでも使えるようにしてみます。

$ chmod a+x tx.sh
$ ln -s /path/to/tx.sh /usr/local/bin/tx

シェルスクリプトでは引数から対象ファイルを受け取るようにしているので、以下のように入力すれば、

$ tx kusamakura.txt

あとはシェルスクリプトPerlと結託してあれこれやった挙句、作業ディレクトリ内に以下のファイルを生成してくれます。

textlint_error.txt
textlint_pretty_error_tidied.txt

Vimから使えるようにする

長くなりましたが、この勢いでもうひと山のぼります。

目当てのファイルを生成するところまで来ましたが、これでも今ひとつ不便です。
というのも、この方法だとtextlintを実行するたびにターミナルに行く必要がありますし、実行後には書き出したファイルをわざわざ開く手間が生じるからです。

では逆に考えろ、ということで、どうなったら満足なのか想像力を働かせてみると、

  1. Vimで文書を編集しているときにコマンド(マッピング)を叩くと、
  2. 上記の2ファイル(エラー結果を書き出したもの)が生成されて、
  3. 元バッファに加えて計3つのバッファが分割画面で表示される。

みたいな状況が良さそうなので、上のシェルスクリプトを活用して以下の関数をvimrcに書いてみました。

function! s:TextLintErrors()
  execute ":! tx %"
  execute ":sp textlint_error.txt"
  execute ":sp textlint_pretty_error_tidied.txt"
endfunction
nnoremap <silent> <Leader>tl :<C-u>call <SID>TextLintErrors()<CR><CR>

実行。

f:id:note103:20160612171920g:plain

できました。

動画だけだとわかりづらいので簡単に説明しますと、

  1. 最初に1画面まるまる「草枕」の状態
  2. 次に実行中の状況が流れていく様子(カクカク動きながら例の文字化けテキストも出てくる)
  3. 最後に画面が3分割された状態(下2つの出力結果をざっとスクロールしながら覗いていく)

という順に進んでいます。

展望とまとめ

ということで、かなり駆け足&欲張り&行き当たりばったりでしたが、自分なりにtextlintに触れた経過を書いてみました。

実質ほぼ1日半ぐらいの間に試したことなので、見落としや根本的な勘違いなどもあるかもしれませんし、実用性の面では、初めのほうで触れたブラウザ拡張などを用いたほうがずっと便利かもしれません。
io-monad.hatenablog.com

それでも、この機会にいろいろと学べたことは多く(とくにほとんど忘れていたシェルスクリプトの書き方とか)、個人的には良い勉強になりました。

今後の目標としては、上で紹介した設定やルールの使い方ではまだ不足が多いので、textlintには他にどんな機能があるのか、そしてそれをどう利用していけるのか、そのチューニングの方法なども含めてもう少し研究してみたいと思っています。

ちなみに、以下を見るとすでに少なからぬルールが揃いつつあるようなので、

この辺の状況も、自分なりに整理・把握していきたいところです。

ざっと触ってみた感触として、textlintは非常にプラガブルな思想にもとづくツールで、使い手が自由にカスタマイズしながら使えることが大きな魅力であると感じました。

こういうツールに対しては、「自然言語ってのはコードの正誤ほど簡単に白黒つけられないんだよな〜」といった否定的な見解が避けがたく出てくるものだと思いますが、そうであるからこそ、「唯一の正解」を設けない、誰もがそれぞれの用途に応じて一から設定していけるこのあり方には共感します。

職業としてこういう道具を必要とする人々(執筆家・編集者・校正者など)に届くまではもうしばらく時間が必要かもしれませんが、様々な専門領域の文書作成者たちが、それぞれのルールを一定の方式に沿って共有空間へ提供し、誰もがそれを使い、また自分なりにカスタマイズし……などという未来がくれば、それは大変喜ばしいことだと思います。

ぼくがその中でどのような役割を果たせるかはわかりませんが、そのような希望を抱いた数日でした。

*1:textlintrcのほうのファイル(辞書)名を書き換えてもいいと思います。

VimでMarkdownをカンタンにHTML化する

note103.hateblo.jp

以前に上の記事を書いたのは、もう1年以上前のこと。だから、というわけでもないのだけど、少し方法がアップデートされているので差分を記してみておきます。

まず、以前の方法を簡単にまとめると、

  1. Markdown記法で何か書く。
  2. 自作の md2html.pl で変換。
  3. HTML化された文書の出来上がり。

というもので、シェル上のコマンドで言うと、こんな感じ*1

$ perl md2html.pl in.md > out.html

それがその後、Perl界の先輩である @karupanerura さんから、それだったらこう書けるよ! と教えて頂きまして。

$ cat in.md | Markdown.pl > out.html

つまり、わざわざそういうスクリプトを使わなくても、Perlの Text::Markdown モジュールをインストール済みなら Markdown.pl コマンドを使えるので、それを通せば変換できるよ、と。

それで、その後はありがたく1年ほどその方法にて変換作業をしていたのですが、ふとこれ、Vim のほうにショートカットを仕込めるはずだなあ、と思いまして。

以下のようにマッピングしてみました。

nnoremap <Leader>mh :%! Markdown.pl %<CR>

使っているところ。

f:id:note103:20160605153705g:plain

変換後に、Vimプラグインの previm を使ってブラウザで確認しています。

で、とりあえずこれだけでもOKといえばOKなのですが、僕の性格的にはソースのファイル(バッファ)を上書きしてしまうのは結構抵抗があるので、変換後のHTMLは別のバッファに保存して、元のファイルは 残しておけるように以下のような関数を vimrc に設定してみました。

function! s:Markdown2HTML()
  execute ":%y"
  execute ":new"
  execute ":0put +"
  execute ":w ".strftime('%Y-%m-%d-%H-%M').".html"
  execute ":%! Markdown.pl %"
  execute ":w"
endfunction
nnoremap <Leader>mh :<C-u>call <SID>Markdown2HTML()<CR><CR>

ん〜む……なんだかむっちゃ冗長感溢れていますが、リファクタリングはまた学習の進度に沿ってということで……。
とりあえずやっていることとしては、

":%y" <= バッファ全体をコピー
":new" <= 新規バッファを水平分割で作成
":0put +" <= コピーしておいたテキストを新規バッファへペースト
":w ".strftime('%Y-%m-%d-%H-%M').".html" <= 新規バッファを現在日時分秒を元にしたファイル名で保存(この時はまだMarkdown書式)
":%! Markdown.pl %" <= 保存したバッファでテキストをMarkdownからHTMLへ変換
":w" <= HTMLファイルを保存

という感じです。

動いている様子は、こんな。

f:id:note103:20160605153635g:plain

便利!!

成果

さて上記でサンプル的に使ったMarkdownファイルですが、じつは本日パブリッシュしたこちらの記事でした。

perl-entrance.blog.jp

冒頭に示した記事でも書いたように、Perl入学式では各回のレポートを参加したサポーターさんに書いてもらって、その校正&ブログUPは可能な範囲でぼくが担当したりしています。

この新たに手に入れた Vim & Perl ワザを使って、今後ますます同ブログをカンタンに更新していけそうです。

*1:厳密にはちょっと当時の設定と違うのだけど、実際の構造を把握しやすいように少しアレンジした。