レベル: 中級 Teodor Zlatanov (tzz@bu.edu), Programmer, Gold Software Systems
2000年 8月 01日 プログラムのユーザー・インターフェースを設計するのは、難しくて時間のかかる作業となることがあります。本稿では、Parse::RecDescent を使用した平易な英語によるユーザー・インターフェースの文法を作成する方法を説明します。また、プログラムの機能の追加や削除があった場合に、文法の変更が容易である点についてもご紹介します。この方法の利点と欠点、および標準的な CLI パーサーおよび GUI との比較も行っていきます。
ユーザー・インターフェースの機能性を伴った進化
ユーザー・インターフェースはプログラムへの入り口になる部分ですから、複数の目的に資するものとならなければなりません。ユーザー・インターフェースは、エンド・ユーザーがプログラムのすべての機能を使用するための手段とならなければなりませんし、プログラムにさらに機能が追加される場合に備えて、拡張可能でなければなりません。また、一般的なコマンドの省略形やショートカットを受け入れる柔軟性が必要な一方で、混乱をまねかないために、メニューのカスケードの乱用や言葉の洪水は避けなければなりません。こうした事柄のすべてを考慮しようとすると、これらは複雑な制約となってしまいます。開発の工程では、ユーザー・インターフェースの設計は最後にまわされたり、後から思い付きのように追加されることも多いのが現状です。また、インターフェースに集中するあまり、機能性がインターフェースの付随物になってしまうことも多く見受けられます。このどちらも好ましい現象とは言えません。ユーザー・インターフェース (UI) は、あたかも同じ硬貨の両面のように、プログラムの機能性と一緒に進化していかなければなりません。
ここでは、ユーザー・インターフェースに構文解析指向の方法を取り入れてみます。この方法は GUI インターフェースにも適用可能ですが、この記事では GUI 設計については説明しません。テキスト・ベースの UI だけに話を絞ります。まず最初に、環境を理解していただけるように、標準的なテキスト UI の設計について簡単に説明します。次に、Parse::RecDescent ソリューションのデモをお見せします。このデモを見ていただければ、柔軟性、直観性、作成の容易さをご理解いただけるに違いありません。
注: ここで説明するプログラムの中には、実行するために Parse::RecDescent CPAN モジュールを必要とするものがあります。
従来の UNIX 方式による単純なユーザー・インターフェース
一般的に UNIX ユーザーは、テキスト・ベースの UI モデルに精通しています。最初に、このモデルの単純なインプリメンテーションを、架空の Perl プログラムを例に見てみましょう。標準の Getopt::Std モジュールは、コマンド行引数の構文解析を単純化します。このプログラムはあくまでも Getopt::Std モジュールのデモであって、実用性は考慮されていないことを予めお断りさせていただきます。
Getopt::Std を使用したコマンド行スイッチ
#!/usr/bin/perl -w
use strict; # always use strict, it's a good habit
use Getopt::Std; # see "perldoc Getopt::Std"
my %options;
getopts('f:hl', \%options); # read the options with getopts
# uncomment the following two lines to see what the options hash contains
#use Data::Dumper;
#print Dumper \%options;
$options{h} && usage(); # the -h switch
# use the -f switch, if it's given, or use a default configuration filename
my $config_file = $options{f} || 'first.conf';
print "Configuration file is $config_file\n";
# check for the -l switch
if ($options{l})
{
system('/bin/ls -l');
}
else
{
system('/bin/ls');
}
# print out the help and exit
sub usage
{
print <<EOHIPPUS;
first.pl [-l] [-h] [-f FILENAME]
Lists the files in the current directory, using either /bin/ls or
/bin/ls -l. The -f switch selects a different configuration file.
The -h switch prints this help.
EOHIPPUS
exit;
}
|
単純なイベント・ループ
コマンド行引数では不十分な場合、次に踏むべきステップは、イベント・ループを書くということです。この方式でも依然としてコマンド行引数は受け入れ可能ですし、それだけで十分な場合もあります。イベント・ループを使用すると、ユーザーはパラメーターを何も指定しないでプログラムを起動してから、プロンプトを見ることができるようになります。通常、プロンプトからヘルプ・コマンドを実行して、より詳細なヘルプ情報をプリントすることができます。ヘルプを、それ専用のソフトウェア・サブシステムを持つ、独立した入力プロンプトとすることさえできます。
コマンド行スイッチ付きのイベント・ループ
#!/usr/bin/perl -w
use strict; # always use strict, it's a good habit
use Getopt::Std; # see "perldoc Getopt::Std"
my %options;
getopts('f:hl', \%options); # read the options with getopts
# uncomment the following two lines to see what the options hash contains
#use Data::Dumper;
#print Dumper \%options;
$options{h} && usage(1); # the -h switch, with exit option
# use the -f switch, if it's given, or use a default configuration filename
my $config_file = $options{f} || 'first.conf';
print "Configuration file is $config_file\n";
# check for the -l switch
if ($options{l})
{
system('/bin/ls -l');
}
else
{
my $input; # a variable to hold user input do
{
print "Type 'help' for help, or 'quit' to quit\n-> ";
$input =<stdin>;
print "You entered $input\n"; # let the user know what we got
# note that 'listlong' matches /list/, so listlong has to come first
# also, the i switch is used so upper/lower case makes no difference
if ($input =~ /listlong/i)
{
system('/bin/ls -l');
}
elsif ($input =~ /list/i)
{
system('/bin/ls');
}
elsif ($input =~ /help/i)
{
usage();
}
elsif ($input =~ /quit/i)
{
exit;
}
}
while (1); # only 'quit' or ^C can exit the loop
}
exit; # implicit exit here anyway
# print out the help and exit
sub usage
{
my $exit = shift @_ || 0; # don't exit unless explicitly told so
print <<EOHIPPUS;
first.pl [-l] [-h] [-f FILENAME]
The -l switch lists the files in the current directory, using /bin/ls -l.
The -f switch selects a different configuration file. The -h
switch prints this help. Without the -l or -h arguments, will show
a command prompt.
Commands you can use at the prompt:
list: list the files in the current directory
listlong: list the files in the current directory in long format
help: print out this help
quit: quit the program
EOHIPPUS
exit if $exit;
}
|
通常、この時点で、以下に挙げる 3 つの事柄のうちの 1 つが生じます。
- 生じ得るスイッチの組み合わせがあまりに多くなり過ぎて、プログラムの UI が非常に複雑になる。
- UI が GUI に進化する。
- 最低でも構文解析機能を追加して、UI を一から作り直さなければならなくなる。
1 番目については、ひどすぎて考慮に値しません。2 番目については、ここでは論じませんが、逆方向の互換性と柔軟性の点で興味深い課題となります。3 番目については、この記事の残りの部分で説明します。
Parse::RecDescent の簡単なチュートリアル
Parse::RecDescent は、テキストの構文解析モジュールです。少数の単純な構成を使用するだけで、ほとんどすべての構文解析タスクに使用することができます。より進んだ文法の構造は難題となることがありますが、それが必要となるような使用目的はほとんどないと言っていいでしょう。
Parse::RecDescent はオブジェクト指向のモジュールです。このモジュールは、文法に則したパーサー・オブジェクトを作成します。文法とは、テキスト形式で記述した規則の集合です。次の例は、word のマッチングに関する 1 つの規則を示しています。
word の規則
この規則では、word の文字 (\w) に 1 回以上マッチングします。コロンに続く部分をプロダクションといいます。規則は少なくとも 1 つのプロダクションを含んでいなければなりません。プロダクションは、他の規則から構成される場合と、直接マッチングするものから構成される場合とがあります。次の例は、word、別の規則 (word 以外の場合)、またはエラー (上記の 2 つが失敗した場合) にマッチングできる規則です。
代わりのプロダクション
token: word | non-word |<error>
word: /\w+/
non-word: /\W+/
|
各プロダクションにはアクションを含めることもできます。アクションは中括弧で囲みます。
プロダクションの中のアクション
print: /print/i { print_function(); }
|
アクションがプロダクションの最後に位置する場合、アクションの戻りコードは、プロダクションが成功したかどうかを決定します。アクションは、0 を戻さない限り必ずマッチングする、一種の NULL のプロダクションです。
複数のトークンは、(s) 修飾子を使って指定することができます。
1 つのプロダクションの中の 1 つ以上のトークン
word: letter(s)
letter: /\w/
|
オプション・キーワードとして、(?) (0 か 1) および (s?) (0 ? N) 修飾子も使用可能です。
プロダクションの中に含まれるものはすべて、$item[position] または $item{name} 機構を介してアクセスできます。2 番目の例の場合、2 つの語の名前が同じなので、位置に基づくアドレッシングを使用しなければならないことに注意してください。3 番目の例では、語の配列は $item{word} の中に配列参照として格納されます。プロダクションの中で任意指定項目を使用する場合には、配列の位置決めの体系はまったく機能しなくなります。この体系は一般に、どのような場合でも使用を控えるべきです。名前によるアドレッシングの方が常に簡単で単純だからです。
%item および @item 変数の使用
print: /print/i word { print_function($item{word}); }
print2: /print2/i word word { print_function($item[1], $item[2]); }
print3: /print3/i word(s) { print_function(@{$item{word}}); }
|
このトピックに関する詳細な情報が必要な場合は、Parse::RecDescent perldoc ページ、およびモジュールに付属のチュートリアルをご覧ください。
なぜ Parse::RecDescent は良いユーザー・インターフェース・エンジンなのか
- 柔軟性: 規則を容易に追加および除去することができるばかりでなく、他の規則を微調整する必要がありません。
- 能力: 規則はどのようなコードでも起動することができ、どのようなテキスト・パターンでも認識することができます。
- 使用が容易: 5 分あれば、単純な文法をまとめることができます。
- フロントエンドを選ばない: パーサーは、正規の Perl 関数として利用できます。また、パーサーから他の正規の Perl 関数およびモジュールを利用することもできます。
- 国際化対応: これは、しばしば見過ごされがちな UI 設計上の問題です。構文解析の文法が、1 つのコマンドの複数のバージョンを容易に受け入れることができるよう意図されていれば、国際化対応は容易になります。
なぜ Parse::RecDescent は良い UI エンジンではないのか
- 速度: 始動と構文解析の速度は、単純なマッチング・アルゴリズムに比べて劣ります。この点は、モジュールの将来のリリースで改善される予定であり、プロトタイピング、開発、リリースが迅速に行えるという長所と注意深く比較考慮すべき点です。
- モジュールの可用性: OS またはシステム管理の問題により、Parse::RecDescent が使用できない場合があります。Perl に詳しい身近な方にお尋ねください。
Parse::RecDescent を使用した単純なユーザー・インターフェース
以下のスクリプトは、Parse::RecDescent を構文解析エンジンとして使用するよう、スイッチ付きの単純なイベント・ループを拡張したものです。このスクリプトの最も注目すべき利点は、マッチング・ステートメントを実行する必要がなくなったことです。その代わりに文法が、入力を検出した時点で、ユーザー入力のフォーマットと取るべきアクションを判別します。usage() 関数は著しく改善されました。2 種類の別々の起動モードを扱う必要がなくなったからです。
コマンド行引数がどのように構文解析エンジンに直接渡されるかに注意してください。これは、Getopts::Std モジュールがまったく必要なくなったということを意味します。Parse::RecDescent がその役目を十分果たすからです。相当に複雑な構成ファイルであれば、Parse::RecDescent を同じように調整して、構文解析することができます (単純な構成ファイルや、複雑さが中程度の構成ファイルであれば、AppConfig CPAN モジュールを使って上手に処理できます)。
次のセクションでは、この単純な UI をさらに拡張します。拡張が理解しやすく、修正しやすいこと、および先に挙げた構文解析の伴わない例 (「コマンド行スイッチ付きのイベント・ループ」を参照) とまったく同様に機能することに注意してください。
以下のすべてのアクションは '1;' を戻して終了します。アクションの最後のコードは戻りコードとして、アクションが成功したか (0)、失敗したか (1) を決定するからです。この点でアクションは関数に非常によく似ています。1 つのアクションが失敗すると、プロダクション全体が失敗します。したがって、アクションが終了時に '1;' を戻すと、成功したことが保証されることになります。
Parse::RecDescent を使用した単純な UI
#!/usr/bin/perl -w
use strict; # always use strict, it's a good habit
use Parse::RecDescent; # see "perldoc Parse::RecDescent"
my $global_grammar = q{
input: help | helpquit | quit | listlong | list | fileoption |
<error>
help: /help|h/i { ::usage(); 1; }
helpquit: /-h/i { ::usage(); exit(0); }
list: /list|l/i { system('/bin/ls'); 1; }
listlong: /-l|listlong|ll/i { system('/bin/ls -l'); 1; }
fileoption: /-f/i word { print "Configuration file is $item{word}\n"; 1; }
quit: /quit|q/i { exit(0) }
word: /\S+/
};
{ # this is a static scope! do not remove!
# $parse is only initialized once...
my $parse = new Parse::RecDescent ($global_grammar);
sub process_line
{
# get the input that was passed from above
my $input = shift @_ || '';
# return undef if the input is undef, or was not parsed correctly $parse->input($input)
or return undef; # return 1 if everything went OK
return 1;
}
}
# first, process command-line arguments
if (@ARGV)
{
process_line(join ' ', @ARGV);
}
do
{
print "Type 'help' for help, or 'quit' to quit\n-> ";
my $input = <STDIN>; # a variable to hold user input print "You entered $input\n"; # let the user know what we got
process_line($input);
} while (1); # only 'quit' or ^C can exit the loop
exit; # implicit exit here anyway
# print out the help and exit
sub usage
{
print <<EOHIPPUS;
first.pl [-l] [-h] [-f FILENAME]
The -l switch lists the files in the current directory, using /bin/ls -l.
The -f switch selects a different configuration file. The -h
switch prints this help. Without the -l or -h arguments, will show
a command prompt.
Commands you can use at the prompt:
list | l : list the files in the current directory
listlong | ll | -l : list the files in the current directory in long format
help | h : print out this help
quit | q : quit the program
EOHIPPUS
}
|

 |
Parse::RecDescent を使用した複雑なユーザー・インターフェース
ここでは、単純なイベント・ループと単純なユーザー・インターフェースの UI 機能に追加を行うことにより、Parse::RecDescent の文法機能をご覧に入れます。これから見ていく新しい機能は、オプションのコマンド・パラメーター、パラメーターに基づく可変アクション、および内部の文法状態変数です。
コメントが文法の中に置かれていますが、コメントが Perl の規則に準じている限り、まったく問題ありません (1 個の '#' 以降に置かれているものは、行末まですべてコメントです)。
set_type 規則は、$last_type 変数をそのパラメーターと同じ値に設定します。これは、"set type" または "st" の後に語が伴わない限り、マッチングしません。
リスト・コマンド用のオプション・パラメーターは、起動方法に応じて、コマンドが特定のファイルまたはすべてのファイルをリストできることを暗示しています。パラメーターの語の間接参照された配列を '/bin/ls' コマンドに渡すので、配列が空でも問題とはなりません。この方法 (および、system() 関数、backtick、または何らかのユーザー提供の入力を使用してファイル操作を行うすべての方法) には、特別な注意を払う必要があります。-T (taint) オプションを指定して Perl を実行することを強くお勧めします。ユーザー入力が直接シェルに渡される可能性がある場合には、潜在的なセキュリティー・ブリーチ (抜け穴) についての十分な検査が行えないことになります。この点についての詳細は、perlsec ページ ('perldoc perlsec') を参照してください。
order コマンドと dairy_order コマンドは、コマンドに指定されるパラメーターに基づく、1 つのコマンドの別個のバージョンです。dairy_order は order の先に来るので、最初に試行されます。そうでない場合、order があらゆる dairy_order にマッチングすることになります。複雑な文法を設計する場合は、コマンドの順序を銘記しておくようにしてください。また、新しい規則により、オプションで数字がどのように検出されるのかにも注意してください。この例では、文法により、1 つのコマンドの 2 つのバージョン (数字ありと数字なし) が、どちらの方法でも機能する 1 つのバージョンにまとめられています。パラメーターが dairy_product または語のどちらかになると指定すれば、ここで order コマンドと dairy_order コマンドを組み合わせることも可能でした。
ボストンでは、他の地域に住む英語を話す人々が (milk) shakes (ミルクセーキ) と呼ぶものを、"frappes (フラッペ)" と呼びます。実体が同じでも、異なる呼び方がされるものもあるのです。
Parse::RecDescent を使用した複雑な UI
#!/usr/bin/perl -w
use strict; # always use strict, it's a good habit
use Parse::RecDescent; # see "perldoc Parse::RecDescent"
my $global_grammar = q{
{ my $last_type = undef; } # this action is executed when the # grammar is created
input: help | helpquit | quit | listlong | list | fileoption |
show_last_type | set_type | order_dairy | order |
<error>
help: /help|h/i { ::usage(); 1; }
helpquit: /-h/i { ::usage(); exit(0); }
list: /list|l/i word(s?) { system('/bin/ls', @{$item{word}}); 1; }
listlong: /-l|listlong|ll/i { system('/bin/ls -l'); 1; }
fileoption: /-f/i word { print "Configuration file is $item{word}\n"; 1; }
show_last_type: /show|s/i /last|l/i /type|t/ { ::show_last_type($last_type); 1; }
set_type: /set|s/i /type|t/i word { $last_type = $item{word}; 1; }
order_dairy: /order/i number(?) dairy_product
{ print "Dairy Order: @{$item{number}} $item{dairy_product}\n"; 1; }
order: /order/i number(?) word
{ print "Order: @{$item{number}} $item{word}\n"; 1; }
# come to Boston and try our frappes...
dairy_product: /milk/i | /yogurt/i | /frappe|shake/i
quit: /quit|q/i { exit(0) }
word: /\S+/
number: /\d+/
};
{ # this is a static scope! do not remove!
# $parse is only initialized once...
my $parse = new Parse::RecDescent ($global_grammar);
sub process_line
{
# get the input that was passed from above
my $input = shift @_ || '';
# return undef if the input is undef, or was not parsed correctly $parse->input($input)
or return undef; # return 1 if everything went OK
return 1;
}
}
# first, process command-line arguments
if (@ARGV)
{
process_line(join ' ', @ARGV);
}
do
{
print "Type 'help' for help, or 'quit' to quit\n-> ";
my $input = <STDIN>; # a variable to hold user input print "You entered $input\n"; # let the user know what we got
process_line($input);
} while (1); # only 'quit' or ^C can exit the loop
exit; # implicit exit here anyway
# print out the help and exit
sub usage
{
print <<EOHIPPUS;
first.pl [-l] [-h] [-f FILENAME]
The -l switch lists the files in the current directory, using /bin/ls -l.
The -f switch selects a different configuration file. The -h
switch prints this help. Without the -l or -h arguments, will show
a command prompt.
Commands you can use at the prompt:
list | l : list the files in the current directory
listlong | ll | -l : list the files in the current directory in long format
help | h : print out this help
quit | q : quit the program
EOHIPPUS
}
sub show_last_type
{
my $type = shift;
return unless defined $type; # do nothing for an undef type word
print "The last type selected was $type\n";
}
|

 |
Parse::RecDescent: 強力で分かりやすく、簡単に使用できて調整が容易
Parse::RecDescent の構文解析能力には、かなり柔軟性があります。ここでは、この構文解析能力を使って UI 構文解析エンジンを作成できること、およびその方法には UI を自作する方法に比べて大きな利点が伴うことを示しました。Parse::RecDescent のような強力なツールにはすべて言えることですが、スピードに関しては問題になり得るかもしれません。しかし、開発とテストに要する時間が節約されるので、実質的にはプラスマイナスゼロと言えるかもしれません。
Parse::RecDescent は、複雑なパラメーターのリストと、ユーザー入力の構文解析を大いに単純化してくれます。コマンドの代替バージョンを容易に受け入れることができます。これは、特に省略形と国際化対応を可能にする面で有用です。
GUI がバックグラウンドで Parse::RecDescent パーサーを使用していることがよくあります。このように GUI を設計する場合、メニュー・コマンドを文法の規則に変換するのは容易です。それは特に、メニューがすでに木のような構造を持っており、コマンド同士が重複しないようになっているためです。このような GUI では、コマンド行または別個のフィールドからのユーザー入力という方法も使用できます (「エキスパート」モードなどの形で)。これは、使用可能度やカスタマイズの観点からすると、よりよい方法かもしれません。
Parse::RecDescent の文法は分かりやすいものです。文法を理解して拡張するのに深い知識はいりません。これは、大規模なプロジェクトに取り組んでいる場合、大いに助けとなり得る点です。1 つのプログラムの中で、文法と目的に応じて、複数のパーサーを使い分けることもできます。(既に見たとおり、文法はファイルから供給することも、内部テキスト・ストリングから供給することも可能です。)
Parse:RecDescent は、常にパワー・ツールとして扱うべきです。非常に低速で手に余るため、小さなプログラムの中で使用しても効果がありません。しかし、ユーザー入力がかなり複雑な場合には、その効果は、コードの編成や機能性の点で顕著に現れます。既存の文法 (コマンド行スイッチまたは自作の機能など) を Parse::RecDescent に移植するのは容易ですし、新しい文法を作成することも一層容易になります。UI を開発する際には、この類のツールの有用性を認識しているべきです。
参考文献
- Perl モジュールに関しては、「CPAN」をご覧ください。Comprehensive Perl Archive Network は、Perl モジュールの入手先としては最も優れたものです。自動インストールもサポートされているので、モジュールの追加が迅速かつ効果的に行えます。
- Perl についての情報が必要な場合は、「PERL.com」をご覧ください。言語、その背後にいる人々、トレーニング、雇用、または Perl のニュースなどに関心がある場合は、まずここをご覧ください。
- Programming Perl, 3rd Edition (Larry Wall、Tom Christiansen、Jon Orwant 共著、O'Reilly Associates、2000) は、現時点で最も優れた Perl のガイドです。最近更新されて、5.005 と 5.6.0 の説明が追加されました。第 3 版は最近発行されたもので、大変優れた書籍です。大いにお勧めします。
- Getopt::Std と Getopt::Long の perldoc ページを参照してください。プロンプトから 'perldoc Getopt::Std' または 'perldoc Getopt::Long' と入力すると、これらのモジュールに関する文書を表示できます。これを読むと、コマンド行引数の構文解析が容易になります。
-
「わたしが以前に書いた Parse::RecDescent に関する記事」は、Parse::RecDescent の使用を始めるにあたって十分な情報となります。しかし、モジュールのすべての機能を引き出したい場合は、資料を読んで、それらの機能を利用する方法を理解する必要があります。
- Perl の最初の権威者、「Larry Wall のホーム・ページ」をご覧ください。
-
「Republic of Perl のホーム・ページ」では、他の Perl 使用者が紹介されています。
- Perl に関する会議などについての情報については、「Perl Conference のホーム・ページ」をご覧ください。
著者について  | 
|  | Teodor Zlatanov 氏は、1999 年にボストン大学を卒業し、コンピューター・エンジニアリングの分野で理学修士号を取得しました。1992 年以来、Perl、Java、C、および C++ を使用して、プログラマーとして働いています。興味の対象は、オープン・ソースのテキスト分析処理、3 層クライアント・サーバー・データベース・アーキテクチャー、UNIX システム管理、CORBA、およびプロジェクト管理です。メール・アドレスは
tzz@bu.edu です。 |
記事の評価
|