行を列に置き換えて文字数をカウントしたい時のsed, Perlスクリプト

Twitterで何か言おうとした時に、「これ140字に収まるかな?」と時々考える。

そんなことを考えなくても、そもそも多くのクライアントは入力欄に文字を入れた段階で何文字の過不足があるかわかりやすく教えてくれるし、仮にエディタでそれを書いていたとしても、大半のエディタには簡単に文字数を知らせる機能がついている。

たとえば今僕が使っているMacVim.appでも、知りたい部分を選択して

f:id:note103:20151011155913p:plain

「g」「<C-g>」の順番でキーを叩くと、このようにウィンドウ下部に表示が出て、

f:id:note103:20151014133340p:plain

選択した対象がエディタ全体の225文字中、81文字であることを教えてくれる。

あるいは、時々必要に応じて立ち上げるCotEdotorならこのように、

f:id:note103:20151013013222p:plain

キーなどを叩くまでもなく最初から文字数を表示してくれている。(なんて便利)

さてしかし、僕は心配性というか、物事を疑いやすい性格だから、「そんなこと言ってるけど、本当に81文字なの?」という心配が絶えない。(めんどくさい。自他共に)

そしてその心配を解消する最良の方法は、横に並んでいる文字たちをタテに並べ替えて、1文字=1行として、その「行数」を目でチェックすることではないか? と思ってしまったので、以下はそれを試してみるためのスクリプト行脚。

sedでやる

最初は、こういう操作こそsedでサクッとやるものでは、と思って、こんなのを書いたのだけど

$ echo この文字数を知りたい | sed 's/./&\n/g'

こんな風に出てくる。

f:id:note103:20151011160958p:plain

ようは「\n」が改行にならない。

結論的には、sedの部分を、以下の内容をもとに
shell - How to insert a newline in front of a pattern? - Stack Overflow

sed 's/./&\'$'\n/g'

という風にしたら、できるのはできたのだけど、

f:id:note103:20151011155956p:plain

第一に、なぜこれが実現しているのか理解できていないし、第二に、このままだと行番号が見えなくて「文字数をカウントしたい」というそもそもの目的を果たせていないので、いろいろ惜しい。

ひとまず、出力を適当にテキストファイルに書き出して……

$ echo この文字数を知りたい | sed 's/./&\'$'\n/g' > count.txt

それを見ると

$ vi count.txt

こんな。

f:id:note103:20151011160006p:plain

まあ、悪くない。
とりあえず、理解できていない部分は継続課題ということで……。

Perlワンライナーで作る

では同じものを、今度はPerlワンライナーでやってみたらどうなるか、という話。
最初に作ったのは、こんな。

$ echo この文字数を知りたい | perl -nle '$_ =~ s/(.)/$1\n/g; print $_'

実行。

f:id:note103:20151011160031p:plain

う、これはまさかEncoding沼……?

試しに、英数字で……

$ echo note103 | perl -nle '$_ =~ s/(.)/$1\n/g; print $_'

実行。

f:id:note103:20151011160047p:plain

できてる。ということはやはり、日本語問題。では、ありったけの知識を投入して……

$ echo この文字数を知りたい | perl -nle -MEncode '$_ = decode('utf8', $_); $_ =~ s/(.)/$1\n/g; $_ = encode('utf8', $_); print $_'

実行。

f:id:note103:20151011160101p:plain

エラー。

Can't open $_ =~ s/(.)/$1\n/g; encode(utf8, $_); print $_: No such file or directory.

なんかそもそもの使い方が違うっぽいですね。

ではモジュールの呼び方を若干変えて……

$ echo この文字数を知りたい | perl -nle 'use Encode; $_ = decode('utf8', $_); $_ =~ s/(.)/$1\n/g; $_ = encode('utf8', $_); print $_'

実行。

f:id:note103:20151011160115p:plain

できました。よかった。

しかしなんでさっきのは出来なかったんだろ?と思って、弾さんのリファレンスを確認しにいくと……
404 Blog Not Found:perl - ワンライナーの書き方入門

-MModuleでモジュールを利用

コマンドラインでモジュールを使うには、-e 'use Module; ...'としてもよいのですが、-MModuleとすることも出来ます。こちらの方が一般的です。

たとえば、以下のワンライナーhttp://www.dan.co.jp/の内容を標準出力に書き出します。(もちろんLWPがインストールされていれば)。

% perl -MLWP::Simple -le 'print get shift' http://www.dan.co.jp/

なるほど……ええと、以前にワンライナーを勉強した時には、
Perlワンライナーの学習帳 〜参考資料と実用例〜 - the code to rock

モジュールは使っていなかったので、モジュールまわりで間違いがあるのかも、とは思ったけど。もしかするとモジュールの宣言が最初じゃなきゃダメだったのか? と思ってやったら、

$ echo この文字数を知りたい | perl -MEncode -nle '$_ = decode('utf8', $_); $_ =~ s/(.)/$1\n/g; $_ = encode('utf8', $_); print $_'

f:id:note103:20151011160127p:plain

できた。

普通にPerlで書く

しかし上の場合、Perlワンライナーの理解という面ではある種の達成感を得られたものの、考えてみたら文字数をカウントしたい欲求が生じるたびにこんな長いワンライナーを打つのも大変というか、もうちょっと簡単にできないものか……と思って、とりあえずPerlで書いたスクリプトをどこかに置いておいて、それをつど呼び出して実行、というスタイルはどうか? と。

とりあえず、こんな。

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use utf8;
use Encode 'encode';

my $str = <DATA>;
my @array = split //, $str;
for (@array) {
    if ($_ =~ /(.)/) {
        my $str2 = encode('utf8', $_);
        say $str2;
    }
}
__DATA__
この文字数を知りたい

これをword-count.plなどとして保存して、実行。

f:id:note103:20151011160138p:plain

良さそうです。Quickrun.vimを使うとその場で行数とともに結果が見えるので、この「Vim上でQuickrun.vimを使ってPerlスクリプトを動かす」という方法がひとまず現状では最適かも。

しかしこのコードの場合、こんな風に、DATA以下に複数行を入れると、

my $str = <DATA>;
my @array = split //, $str;
for (@array) {
    if ($_ =~ /(.)/) {
        my $str2 = encode('utf8', $_);
        say $str2;
    }
}
__DATA__
この文字数を知りたい
この文字数も合わせて知りたい

最初の行しかカウントしてくれない。

f:id:note103:20151013174842p:plain

というかまあ、1行だけカウントする想定しかしてなかったので、それはそれで構わないのだけど、せっかくなので複数行でも対応してくれるように、1行目の

my $str = <DATA>;

my $str = do {local $/; <DATA>};

に変えてみる。

実行。

f:id:note103:20151011160225p:plain

できました。

では最後にひと押し。このスクリプトを簡単に開けるように、.bashrcに「wcp」とかでエイリアスを作って……

alias wcp='vi /path/to/word-count.pl'

はい。あとはターミナルで「wcp」と打てば、いつでも文字数をカウントできますね!

おつかれさまでした。