Perl Hackers Hub

第45回Perlで作るコマンドラインツール―オプション、サブコマンド、設定ファイルへの対応(2)

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

設定ファイルの利用

ここでは、Perlを使って設定ファイルの読み込みを行う手法について紹介していきます。

設定ファイルはなぜ必要か

コマンドラインツールでは、実行するたびに変わってくるものがあります。たとえば実行結果をメールで送信するツールなら、メールアドレスは実行ユーザーごとに変わりますし、同じユーザーでも常用しているメールアドレスが変更になった場合は変える必要があります。ファイルを変換するツールなら、どのファイルを変換するのかやファイルの保存場所が毎回変わります。こういった値は、コードに直接書くのではなく、設定値としてプログラムの外部から渡せるようになっているのが良いプログラムです。

設定値を渡す方法として、すでにコマンドライン引数を使ってオプションを渡す方法を説明しました。これは標準的な方法でユーザーも使い慣れているためわかりやすいのですが、多数の設定項目があったり、それらの項目の組み合わせを複数セット使い分けたりする必要がある場合、煩雑になります。

それに対して設定値を設定ファイルとして持つようにすると、設定値の確認や編集をテキストエディタで行えます。また、よく使う設定値のパターンを複数のファイルとして用意しておけば、設定ファイルを切り替えるだけでコマンドラインツールの動作を変えられます。作っているコマンドラインツールの設定項目が多くなってきたと感じたら、設定ファイルを使った動作設定を行えるようにすることを検討するとよいでしょう。

設定ファイルの形式

Perlで書かれたコマンドラインツールの設定ファイルには、JSONやYAMLYAML Ain't Markup Languageなどの広く使用されている形式の文書ファイルを使うのが一般的です。また、Perlのコードそのものを設定ファイルとして使う手法も、手軽かつ強力です。よく使われる形式を表1にまとめました。

表1設定ファイルによく使われる形式
形式特徴
JSONプログラムによる解釈が簡単。人間が読みづらくなる場合がある
YAMLインデントといくつかの記号で構造を表す。比較的人間が読みやすい
TOMLシンプルな記法。複雑な構造の記述には向いていない
PerlPerlコードそのもの。データの表現力は高いがPerlコードを書ける人でないと使えない

Perlコードを設定ファイルとして使う方法

Perlコードを設定ファイルとして使う場合、設定ファイル内では、Perlのプログラムでハッシュリファレンスを定義します。

config.pl
+{
    dbi => ["dbi:mysql:database=foo", "user", "pass"],
    filepath => '/path/to/file',
};

設定ファイルを利用するプログラムでは、次のように記述して設定ファイルを読み込みます。

my $config = do 'config.pl';

$config->{filepath}
# => /path/to/file

Perlコードを設定ファイルとして使う利点は、読み込む側で依存モジュールを必要としないことと、記述力の高さにあります。設定ファイルがPerlのプログラムですので、ループや分岐で設定値を組み立て/変更したり、環境変数を使ったりも自在です。

my $conf = {
    path => '/path/',
};

# 分岐と環境変数の利用
if ($ENV{TEST}) {
    $conf->{debug} = 1;
}
$conf;

ただし逆に言えば、なんでもできすぎて危険な処理を設定ファイル内で実行される可能性があります。また、プログラマーではないユーザーが設定ファイルを作成/編集するのは難しいでしょう。そのため、小規模かつプログラマーが運用するコマンドラインツールに向いていると言えそうです。

JSON/YAML/TOMLを設定ファイルとして使う方法

設定ファイルとして何らかの記法で記述されたファイルを使う場合、対応するモジュールを使用します。JSON、YAMLなど、たいていの記法には同名のCPANモジュールが存在しています。

例として、コマンドライン引数で渡したファイル名のJSONファイルを読み込むプログラムを示します。

read_json.pl
my $opt = Smart::Options->new
       ->coerce('File' => 'Str', sub { path($_[0]) })
       ->type('conf' => 'File')->parse();

unless ($opt->{conf}) {
    my $default_conf = +{
        file => '/path/to/file',
        rows => 10,
    };
    path('.config')->spew(encode_json($default_conf));
    print "write default config => .config\n";
    exit;
}

my $config = decode_json($opt->{conf}->slurp());

# 以下、コマンドラインツールの実際の処理

このコードでは、コマンドライン引数でconfオプションが指定されなかった場合、.configという名前で設定ファイルを作成し、その旨を表示してプログラムを終了します。このように、設定ファイルの指定なしにプログラムを起動したら、所定の場所に初期値を埋めた設定ファイルのひな型を出力する機能が付いていると親切です。

コマンドラインツールの入出力

ここまで、コマンドラインツールでよく使われるコマンドライン引数と設定ファイルについて説明しました。それ以外に重要なのは、データの入出力です。本節ではコマンドラインツールのデータ入出力にあたってのTipsや、利用すると便利なモジュールを紹介していきます。

IO::Prompt::Simpleを使ったインタラクティブな入力

コマンドラインツールを実行する際、途中でユーザーからの入力を受け付けたい場合があります。たとえば復旧の難しい処理(ファイル削除など)を行う前に、本当に実行してよいかユーザーに確認するなどです。IO::Prompt::Simpleを使うと、こういったケースに対応するコードをシンプルに記述できます。

use IO::Prompt::Simple;

# 前処理

my $answer =
    prompt 'ファイルの削除を実施します。よろしいですか? (y/N)',
    'N';

if ($answer eq 'y') {
    # ファイル削除
} elsif ($answer eq 'N') {
    print "ファイルの削除をキャンセルしました\n";
    exit;
} else {
    die 'invalid input';
}

prompt関数の第1引数にはユーザーへのメッセージ、第2引数にはデフォルト値を指定します。ユーザーが何も入力せずに[Enter]を押した場合は、デフォルト値がpromptの戻り値として返ってきます。

prompt関数の第2引数がスカラ値の場合はデフォルト値として扱われますが、ハッシュリファレンスを渡して細かな設定を行うこともできます。たとえば先ほどのコードのように確認メッセージにyes/noで答えてもらうようなケースでは、入力するデータを制限して次のように書くことができます。

use IO::Prompt::Simple;

my $answer =
     prompt 'ファイル削除を実施します。よろしいですか?',
            { anyone => [qw/y N/], ignore_case => 1 };

詳しくは、IO::Prompt::Simpleのドキュメントを参照してください。

環境変数からの入力

本項では環境変数から入力を受け付ける方法を説明します。ここまで紹介したコマンドライン引数を使う方法、ファイルから設定を読み込む方法、標準入力からデータを読み込む方法を使い分けることで、ほとんどのユースケースに対応できます。そのうえであえて環境変数を使うメリットはなんでしょうか。一例としては、TERM変数やLANG変数など、OS環境の情報が入っている環境変数を取得できることが挙げられます。また、crontabで複数のコマンドの実行計画を定義している場合、冒頭で環境変数の設定を定義しておけば、すべてのコマンドでその設定を参照できます。

MAILTO=report@example.com

0 * * * * /path/to/cmd1.pl
30 1 * * * /path/to/cmd2.pl

上記のように記述すれば、cmd1.plでもcmd2.plでも$ENV{MAILTO} でレポート送信先メールアドレスreport@example.comを参照できます。

ほかに、HerokuやGoogle App EngineなどのPaaSPlatform as a Service環境では、実行するプログラムへ設定を渡す方法が環境変数に限られるケースがあります。この場合、否応なしに環境変数を使うしかありません。こういったケースに対応するため、最近のコマンドラインオプション解析ライブラリでは、環境変数からオプション値を取得する機能がサポートされているものが多く、Smart::Optionsでも対応しています。

use Smart::Options;

$ENV{TEST_FLAG} = 'FLAG VALUE';

Smart::Options->new->env_prefix('TEST')
              ->env('flag')->parse();
# => { 'flag' => 'FLAG VALUE' }

envメソッドを使うと、オプション値が指定されなかった場合に、env_prefixで指定した接頭辞_大文字化したオプション名」の環境変数(上記で言うとTEST_FLAGをオプション値として取得するようになります。

なお、Smart::Optionsでは、コマンドライン引数と環境変数の両方で値が指定されていた場合、コマンドライン引数で渡されたもののほうを優先します。

色付きの出力

diffコマンドなどが良い例ですが、出力結果に適切に色を付けると見分けやすくなります。Perlでターミナルへの出力に色を付けるにはカラー変更のエスケープシーケンスを出力すればよいのですが、Term::ANSIColorを使えばエスケープシーケンスを使わずに色名の指定で同じことをできます。

use Term::ANSIColor;

print color('red') . "この文字は赤く表示されます\n";
print color('red') . "この文字も赤く表示されます\n" . colo
r('reset');
print "この文字は普通の色で表示されます\n";

print color('on_blue white');
print "この文字は青地に白で表示されます\n";
print color('reset');

color関数に色を渡すことで、以降に出力する文字列がその色になります。color('reset')を出力するまで色は変わったままになります。

color('on_blue')のようにon_を色名に付けると背景色の指定になります。それ以外にもbold(強調⁠⁠、blink(点滅⁠⁠、under(下線)などを指定したり、256色に対応した色指定(ansi255)をしたりもできますが、これらは実行される環境によって機能しないこともあります。詳細はTerm::ANSIColorのドキュメントを参照してください。

<続きの(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

おすすめ記事

記事・ニュース一覧