Perl
debug
REPL
PerlDay 19

PerlでREPLをもっと使おう

REPL便利ですよね。

Webアプリケーションを動かしながらデバッグだったり

小さなクラス単位で開発・テストを進める際などにも

大変有用です。大好きです。

今回はもっと活用するための、ささやかなtipsをご紹介します。


古典的な方法 perl debugger

-dオプションでデバッグモードで起動するアレです。

見やすくないので、REPLのために積極的に利用するシーンは無いと思いますが、

どんなに古いperlでも、何もインストールせずにデフォルトのまま利用できるのが利点ですかね。

perl4でも使えたはず。

簡単な計算機代わりに利用する方法として、

イディオムとして perl -de0 で起動して、

xコマンドで式を評価した結果を展開表示する、

というのがあります

% perl -de0

Loading DB routines from perl5db.pl version 1.44
Editor support available.

Enter h or 'h h' for help, or 'man perldebug' for more help.

main::(-e:1): 0
DB<1> x 1 + 1
0 2
DB<2> x map { $_ * 10 } (1..3)
0 10
1 20
2 30
DB<3> x { a=> 1, b => 2}
0 HASH(0x7f951c94edb8)
'a' => 1
'b' => 2
DB<4> x { a=> 1, b => [ 2, 3, 4] }
0 HASH(0x7f951d0fffe8)
'a' => 1
'b' => ARRAY(0x7f951d862bb0)
0 2
1 3
2 4


REPLライブラリ利用

REPL用のライブラリを使いましょう。

カラー表示や、入力補完など便利なプラグインも多く、

利用しやすいです。

ライブラリはいくつかあるようですが、

Reply が依存モジュールが少なくインストールしやすいようです。

Replyをインストールすると reply コマンドが付属しています。

Replyを利用した、いくつかの派生モジュールが作成されています。

Carp::Reply

Pry

詳細は、諸先輩方の素晴らしい記事を参照ください。

perlで快適REPL生活! - sonots:blog

プラグインを入れてReplyを更に便利にする - Masteries

REPLでORMを使えるようにすると、めっちゃ便利だ、という話 - tsucchi の日記 2nd season

私は普段、タイプ数が少ない Pry を利用していますので、

以降はPryを例に説明しますが、

ご紹介するのは、ほとんどがReplyの機能ですので、

多くのReply派生環境で利用できるかと思います。


インストール

依存モジュールである Reply も同時にインストールされます。


## Pryをインストール
% cpanm Pry

## または、
## Carp::Replyをインストール
% cpanm Carp::Reply


REPL起動方法あれこれ


シンプルにREPLのみ起動

% perl -MPry -e pry

## または、
% reply


動かしたいプログラムの中から

例えば、Webアプリケーション開発などで、

デバッグしたいコードの任意の場所に以下の記述をしてから、

ブラウザでWebアプリを操作して、該当箇所を実行したときに

REPLが起動します。


任意のperlコード内

...

use Pry;
pry; # 任意の場所に挿入すると、そこでブレイクしてREPLを起動する。
...


Mojoliciousフレームワークで

Mojoliciousは、evalコマンドを提供しているので、

それを利用してREPLを起動します。

自作したModel, Controllerなどがそのまま見えるので、

動作確認や実験に便利です。

## script/myapp は mojoliciousのランチャスクリプト

% script/myapp eval 'use Pry; pry;'
0> app->db->member->find_by_id(100) ## myapp内のリソースが使える
...

私は、Railsのconsoleコマンドを参考に、

トランザクション周りの前処理を追加したり、--sandboxモードを追加した

Mojoliciousコマンドを作成して、普段の開発ではREPLを立ち上げっぱなしにしています。


ReplyのConfigを作成しよう

コンフィグとしてデフォルトで ~/.replyrc が参照されます。

まだ作成していない場合は、一度 reply コマンドを実行すれば、デフォルトのコンフィグファイルが作成されます。

中身は↓こんなかんじ。

% reply

/Users/xxx/.replyrc not found. Generating a default...
0>

% cat ~/.replyrc
script_line1 = use strict
script_line2 = use warnings
script_line3 = use 5.020000

[Interrupt]
[FancyPrompt]
[DataDumper]
[Colors]
[ReadLine]
[Hints]
[Packages]
[LexicalPersistence]
[ResultCache]
[Autocomplete::Packages]
[Autocomplete::Lexicals]
[Autocomplete::Functions]
[Autocomplete::Globals]
[Autocomplete::Methods]
[Autocomplete::Commands]


Replyプラグインを追加しよう

papixさんの記事を参照ください。

プラグインを入れてReplyを更に便利にする - Masteries

補完ができたり、useなしでモジュールをいきなり利用できたり、外部エディタが使えたりと、

大変捗ります。

私は↑を参考にして、デフォルトconfigから一部変更、追記しました。


~/.replyrc(抜粋)

;; ダンパーを DataDumper -> DataPrinter に変更

;[DataDumper]
[DataPrinter]

;; モジュールの自動ロードプラグイン LoadClass 追加
[LoadClass]



ReadLine

ReadLine プラグインで、コマンドライン編集や履歴が有効になりますが、

端末環境によっては、readlineライブラリをOSにインストールする必要があるかもしれません。

macの例

## readlineライブラリを確認

brew info readline

## インストール
brew install readline

また、コマンドライン履歴はヒストリファイルに記録されますが、

ヒストリファイルはmacの場合デフォルトで

${HOME}/Library/Application Support/.reply_historyとなるようです。

私の環境では、このファイルが肥大化していき、

400MBを超えたあたりでREPLを起動できなくなり、

ヒストリファイルを削除するなり適宜小さくすると改善した、という現象がありました。

定期的にヒストリファイルを削除するなり、保持履歴数を設定するなり、メンテナンスすると良さそうです。

ヒストリファイルのパス、保持履歴数は .replyrc で指定可能です。


.replyrc(ReadLine周り設定)

[ReadLine]

history_file = ~/.reply_history
history_length = 1000


いつもの前処理を自動化しよう

また、tsucchiさんの記事で紹介されているように、

REPLでORMを使えるようにすると、めっちゃ便利だ、という話 - tsucchi の日記 2nd season

script_line1〜の指定で任意の前処理を自動化できますので、

開発での定型的な前処理やヘルパの準備など、適宜追記すると良いでしょう。


Dumperを見やすくしよう

ダンパーを DataPrinterに変更しましたが、

デフォルトでは情報が多すぎる場合があります。

例えば、DateTimeオブジェクトなど、

よく使うデータクラスですが、ダンプすると以下の感じで非常に読みづらい。


冗長で読み辛い出力例

0> DateTime->now

$res[0] = DateTime {
public methods (109) : add, add_duration, am_or_pm, bootstrap, ce_year, christian_era, clone, compare, compare_ignore_floating, day_abbr, day_name, day_of_month, day_of_month_0, day_of_quarter, day_of_quarter_0, day_of_week, day_of_week_0, day_of_year, day_of_year_0, DefaultLocale, delta_days, delta_md, delta_ms, dmy, duration_class, epoch, era_abbr, era_name, format_cldr, formatter, fractional_second, from_day_of_year, from_epoch, from_object, hires_epoch, hms, hour, hour_1, hour_12, hour_12_0, is_dst, is_finite, is_infinite, is_leap_year, iso8601, jd, last_day_of_month, leap_seconds, local_day_of_week, local_rd_as_seconds, local_rd_values, locale, MAX_NANOSECONDS, mdy, microsecond, millisecond, minute, mjd, month, month_abbr, month_name, month_0, nanosecond, new, now, offset, quarter, quarter_abbr, quarter_name, quarter_0, second, SECONDS_PER_DAY, secular_era, set, set_day, set_formatter, set_hour, set_locale, set_minute, set_month, set_nanosecond, set_second, set_time_zone, set_year, STORABLE_freeze, STORABLE_thaw, strftime, subtract, subtract_datetime, subtract_datetime_absolute, subtract_duration, time_zone, time_zone_long_name, time_zone_short_name, today, truncate, utc_rd_as_seconds, utc_rd_values, utc_year, week, week_number, week_of_month, week_year, weekday_of_month, year, year_with_christian_era, year_with_era, year_with_secular_era, ymd
private methods (42) : __ANON__, _accumulated_leap_seconds, _add_overload, _adjust_for_positive_difference, _calc_local_components, _calc_local_rd, _calc_utc_components, _calc_utc_rd, _cldr_pattern, _compare, _compare_overload, _core_time, _day_has_leap_second, _day_length, _era_index, _format_nanosecs, _handle_offset_modifier, _is_leap_year, _maybe_future_dst_warning, _month_length, _new, _new_from_self, _normalize_leap_seconds, _normalize_nanoseconds, _normalize_seconds, _normalize_tai_seconds, _offset_for_local_datetime, _rd2ymd, _seconds_as_components, _set_locale, _space_padded_string, _string_compare_overload, _string_equals_overload, _string_not_equals_overload, _stringify, _subtract_overload, _time_as_seconds, _utc_hms, _utc_ymd, _weeks_in_year, _ymd2rd, _zero_padded_number
internals: {
formatter undef,
local_c {
day 19,
day_of_quarter 80,
day_of_week 3,
day_of_year 353,
hour 19,
minute 37,
month 12,
quarter 4,
second 4,
year 2018
},
local_rd_days 737047,
local_rd_secs 70624,
locale DateTime::Locale::en_US,
offset_modifier 0,
rd_nanosecs 0,
tz DateTime::TimeZone::UTC,
utc_rd_days 737047,
utc_rd_secs 70624,
utc_year 2019
}
}

DataPrinter周りを整備して見やすくしましょう。

DataPrinterは、多くのオプションや、フィルタプラグインなど多くのカスタマイズが可能です。

kiririmodeさんの以下の記事が参考になります。

Data::Dumper に代わる Data::Printer - 理系学生日記

DataPrinter用の外部フィルタをいくつかインストールして、

% cpanm Data::Printer::Filter::DateTime

% cpanm Data::Printer::Filter::JSON

以下のDataPrinter用コンフィグファイルを~/.dataprinterとして作成し、


~/.dataprinter

{

filters => {
-external => [ 'DateTime', 'JSON' ],
},
datetime => {
show_class_name => 1,
show_timezone => 1,
}
}

再度、REPLを見てみると、とても簡潔で読みやすいですね。


簡潔で読みやすい出力例

0> DateTime->now

$res[0] = 2018-12-19T19:53:12 [UTC] (DateTime)


DataPrinterフィルタを自作しよう

例えば、ORMのTengの行オブジェクト(Teng::Rowクラス)をそのままダンプすると、

以下のように大変冗長なものとなります。

データクラスであり通常は値だけが知りたいので、もう少し簡潔な表示にしたいところです。

一方、各カラムのinflate処理もされていないので、生のrow_dataが表示されるのも気になります。

0> my $row = app->model->find_by_id('member', 1)

$res[0] = MyApp::Model::Row::Member {
Parents MyApp::Model::Row
public methods (0)
private methods (0)
internals: {
_autoload_column_cache {},
_dirty_columns {},
_get_column_cached {},
row_data {
id 1,
name "foo",
updated_at "2018-12-01 00:00:00"
},
select_columns [
[0] "updated_at",
[1] "name",
[2] "id"
],
sql "SELECT * FROM member WHERE id = ?;",
table Teng::Schema::Table,
table_name "member",
teng MyApp::Model
}
}

こんな感じのフィルタを作成して、


lib/Data/Printer/Filter/Teng/Row.pm

package Data::Printer::Filter::Teng::Row;

use strict;
use warnings;
use utf8;
use Data::Printer::Filter;
use Term::ANSIColor;

filter -class => sub {
my ($obj, $p) = @_;
return unless ref($obj) && $obj->isa('Teng::Row');

my $hash = { map { $_ => $obj->get($_) } keys %{$obj->get_columns} };
my $str = colored(qq($obj), 'bright_green');
indent;
$str .= p($hash);
outdent;
return $str;
};

1;


~/.dataprinter の外部フィルタ定義に

-external => [ 'DateTime', 'JSON', 'Teng::Row' ]のような感じで指定してあげると

以下のような簡潔な出力にできますね。

オブジェクトが入れ子になった場合などは特に有益です。

少々雑ですが、十分としましょう。

0> my $r = app->model->find_by_id('member', 1)

$res[0] = MyApp::Model::Row::Member=HASH(0x7f7f9f848340)\ \ {
id 1,
name "foo",
updated_at 2018-12-01T00:00:00 [Asia/Tokyo] (DateTime),
}

フィルタの定義次第で、任意に値を加工し、また色やインデントの制御もできますので、

欲しい情報を欲しいフォーマットで出力することが可能になりますね。


DataPrinterフィルタ作成時の注意点

対象のデータ型が1つだけならフィルタ定義時に

filter 'Some::ClassName' => sub {} と定義できますが、

先程の実装例のように、

基底クラスに対するフィルタを書いて派生クラス全てに適用させる場合には

filter -class => sub {} と定義して、

関数内で isa()で判定してからフィルタを適用させる必要がある、ということに注意ください。

詳細は↓あたりを参照ください。

Data::Printer::Filter - Create powerful stand-alone filters for Data::Printer - metacpan.org


最後に

皆さん良いREPL生活を。

(記事公開が遅れて済みませんでした。)