設計程序的用戶界面可能很困難而且耗時。Teodor Zlatanov 討論了如何使用 Parse::RecDescent 模塊來用簡單的英語創建用戶界面文法。他還展示了向程序添加功能或從程序除去功能時,更改文法是如何的方便。另外還與標准的 CLI 解析器和 GUI 進行了比較,討論了
設計程序的用戶界面可能很困難而且耗時。Teodor Zlatanov 討論了如何使用 Parse::RecDescent 模塊來用簡單的英語創建用戶界面文法。他還展示了向程序添加功能或從程序除去功能時,更改文法是如何的方便。另外還與標准的 CLI 解析器和 GUI 進行了比較,討論了這種方法的優缺點。
隨功能一起發展的很棒的用戶界面 因為用戶界面是程序最初的入口,所以它必須能夠用於多種目的。必須向用戶提供對程序所有功能的合適訪問。在向程序添加更多功能時(這幾乎是必然發生的情況),它必須是可擴展的。必須具備靈活性,可以接受常用命令的縮寫和快捷方式。它不應該有層疊的菜單或瀑布式單詞,這樣會讓用戶感到迷惑。無可否認,以上所有這些要求對
程序員來說都是復雜的約束,對此沒有一種很好的
解決方案能把它們全包括。許多軟件產品
開發人員到最後再解決用戶界面問題,把它作為一種事後來考慮的問題。另外一些開發人員則首先主要考慮用戶界面,讓功能僅僅成為界面設計選擇的結果。這些都不是理想的方法。用戶界面(UI)應該隨著程序功能的發展而發展,兩者就象一枚硬幣的正反面。
這裡我們將面向解析的方法用於用戶界面。雖然這種方法適合於 GUI 界面,但本文不討論 GUI 設計。我們將專門討論基於文本的 UI。首先,將簡要介紹標准的文本 UI 設計選擇,使您能熟悉該環境。然後,將展示 Parse::RecDescent 解決方案,事實證明它是靈活、直觀和易於編寫的!
注:為了運行我們所討論的某些程序,將需要 Parse::RecDescent CPAN 模塊。
用傳統的 Unix 方式創建的簡單用戶界面 Unix 用戶非常熟悉基於文本的 UI 模型。設想有一個 Perl 程序,讓我們先看一下這個模型用於該程序的簡單實現。標准的 Getopt::S
td 模塊簡化了命令行參數的解析。這個程序僅僅為了說明 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 <
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;
}
簡單的事件循環
當命令行參數不夠用時,下一步是編寫一個事件循環。在這種方案中,仍然可接受命令行參數,並且有時只使用命令行參數就足夠了。然而,事件循環支持用戶在不輸入任何參數的情形下調用程序,以及看到提示符。在此提示符下,通常可使用 help 命令,這將打印出更詳細的幫助。有時,這個 help 甚至可以是一個單獨的輸入提示符,有一個完整的軟件子系統來專門負責它。
帶有命令行開關的事件循環
#!/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 = ;
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 < 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;
}
這裡,通常會有三種選擇:
由於可能會有多個開關組合,所以程序的 UI 會復雜到不可忍受的程度。
UI 將發展為 GUI。
從頭重新開發 UI,使其至少帶有一些解析功能。
第一種選擇太可怕,難以去想象。這裡不討論第二種選擇,但它的確在向後兼容性和靈活性方面提出了有趣的挑戰。第三種選擇是本文下面要討論的內容。
Parse::RecDescent 的快速教程
Parse::RecDescent 是一個用於解析文本的模塊。通過幾個簡單構造就可以用它完成幾乎所有的解析任務。更高級的文法構造可能會讓人望而生畏,不過在大多數用途中不需要這麼高級的文法。
Parse::RecDescent 是一個面向對象的模塊。它圍繞著文法創建解析器對象。文法(grammar)是一組以文本形式表示的規則。下面這個示例是一條匹配單詞的規則:
word 規則
word: /\w+/
這條規則一次或多次匹配字符(\w)。跟在冒號後的部分稱為產品(production)。一條規則必須包含至少一個產品。一個產品可以包含其它規則或直接要匹配的內容。下面這個示例是一條規則,它可以匹配一個單詞、另一條規則(non-word)或一個錯誤(如果其它兩個都失敗):
另一些產品
token: word | non-word |
word: /\w+/
non-word: /\W+/
每個產品也可以包含一個用花括號括起的操作:
產品中的操作
print: /print/i { print_function(); }
如果操作是產品中的最後一項,則操作的返回碼決定產品是否成功。該操作是一種空產品,它將總是會匹配,除非它返回 0。
可以用 (s) 修飾符指定多個標記(token):
一個產品中帶一個或多個標記
word: letter(s)
letter: /\w/
也可以將 (?)(0 或 1)和 (s?)(0 到 N)修飾符作為可選關鍵字來使用。
可以通過 $item[position] 或 $item{name} 機制來訪問產品中的任何內容。請注意,在第二種情形中,兩個單詞的名稱是相同的,所以必須使用位置查找。在第三種情形中,單詞數組以數組引用的形式存儲在 $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 設計中常常會忽略的問題。在解析文法想要方便地接受一個命令的多個版本時,國際化會很方便。
Parse::RecDescent 可能不是一個優秀 UI 引擎
速度:啟動和解析速度不如簡單的匹配算法。在該模塊以後的發行版中,這將有所改善,與快速原型設計、開發和發布所節省的時間相比,應該仔細考慮速度上的代價是否值得。
模塊可用性:由於 OS 或系統管理問題,Parse::RecDescent 可能不可用。請咨詢您周圍的 Perl 專家。
使用 Parse::RecDescent 的簡單用戶界面
該腳本擴展了帶開關的簡單事件循環,將 Parse::RecDescent 用作解析引擎。該腳本最大好處是,不再必須執行匹配語句。而是由文法同時確定用戶輸入的格式和根據輸入所要采取的操作。usage() 函數得到了很大的改善,因為不再需要處理兩種獨立調用方式。
請注意將命令行參數直接傳遞給解析引擎的方式。這意味著不再需要 Getopts::Std 模塊,因為 Parse::RecDescent 模塊能很好地做這件事。如果配置文件十分復雜,可以類似地改寫 Parse::RecDescent 來解析它們(對於簡單到較復雜的配置文件,AppConfig CPAN 模塊可以很好地解析它們)。
在下一節,我們將進一步擴展所創建的簡單 UI。請注意,很容易理解和修改這個擴展,而且該擴展可以與前面的非解析示例一樣出色地完成工作(請參閱帶命令行開關的事件循環)。
下面所有的操作都以‘1;’結尾,因為操作中的最後代碼確定了該操作是成功(返回碼為 0)還是失敗(返回碼為 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 |
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 = ; # 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 < 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 的約定(以‘#’字開頭,一直到行尾,這之間都是注釋)。
set_type 規則將 $last_type 變量設置成與其參數相等。除非“set type”或“st”後跟一個單詞,否則將不匹配。
list 命令的可選參數意味著該命令可以列出具體文件或所有文件,這取決於調用命令的方式。由於直接將解除引用的參數單詞數組傳遞給‘/bin/ls’命令,因此如果數組為空,則不會出現問題。尤其要注意這種方法(以及使用 system() 函數、重音號或任何用戶提供的輸入來進行的文件操作)。極力推薦用帶 -T(taint)選項來運行 Perl。如果有可能直接將用戶輸入傳遞給 shell,則確實不可能對潛在的安全問題做仔細地檢查。關於這方面更多的信息,請參閱 perlsec 頁面(‘perldoc perlsec’)。
order/order_dairy 命令根據給定命令的參數來列出命令的可替代版本。由於 order_dairy 在 order 之前,所以將先嘗試用 order_dairy。否則,order 還將匹配任何 order_dairy。當設計復雜的文法時,請牢記命令的順序。還要注意,如何通過一條新規則,隨意檢測數字。在這裡,文法將兩種版本的命令(帶數字和不帶數字)壓縮成了能以兩種方式工作的單個版本。這裡,也可以通過指明參數是 dairy_product 還是 word 來將 order 和 order_dairy 命令合並成一條命令。
這就象在波士頓許多講英語的人稱(milk)shake 為“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 |
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 = ; # 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 < 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:
order [number] product: order a product, either dairy (milk, yogurt,
frappe, shake), or anything else.
set|s type|t word : set the current type word
show|s last|l type|t : show the current type word
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 解析引擎能夠比自創的方法具有更顯著的優勢。所有同 Parse::RecDescent 一樣功能強大的工具都存在速度問題。但在開發和測試上所節省的時間可以很好地平衡這一點。
Parse::RecDescent 大大簡化了復雜的參數列表和對用戶輸入的解析。使用它可以很容易地接受命令的可替換版本,這樣就具有了允許縮寫和國際化等優點。
實際上,GUI 在後端通常有一個 Parse::RecDescent 解析器。如果您設計象這樣的 GUI,則可以方便地將菜單命令轉化成文法規則,尤其是因為菜單已經具有了樹狀結構,這可以確保沒有重疊命令。可以在象這樣的 GUI 中使用來自命令行或單獨域(也許是“expert”模式)的用戶輸入,從實用性和定制的角度,這種做法甚至更好。
Parse::RecDescent 文法易於理解。不需懂得太多就可以理解擴展文法,在對付大項目時,這對您非常有幫助。可以在一個程序中用具有不同文法和用途的多個解析器。(正如我們所見,文法可以來自一個文件或來自一個內部的文本字符串。)
應該始終將 Parse:RecDescent 作為一種功能強大的工具。在小程序中,由於其速度太慢難以使用,所以難以顯示出其優越性。但對於較復雜的用戶輸入,其優越性會立即通過組織良好的代碼和功能而體現出來。將現有文法(命令行開關或自己開發的函數)移植到 Parse::RecDescent 非常容易,而編寫新的文法甚至會更容易。每個 UI 構建人員都應發現這一功能強大的工具是有用的。