功能丰富的 Perl: 编写说英语的 Perl 程序_VMware, Unix及操作系统讨论区_Weblogic技术|Tuxedo技术|中间件技术|Oracle论坛|JAVA论坛|Linux/Unix技术|hadoop论坛_联动北方技术论坛  
网站首页 | 关于我们 | 服务中心 | 经验交流 | 公司荣誉 | 成功案例 | 合作伙伴 | 联系我们 |
联动北方-国内领先的云技术服务提供商
»  游客             当前位置:  论坛首页 »  自由讨论区 »  VMware, Unix及操作系统讨论区 »
总帖数
1
每页帖数
101/1页1
返回列表
0
发起投票  发起投票 发新帖子
查看: 3837 | 回复: 0   主题: 功能丰富的 Perl: 编写说英语的 Perl 程序        下一篇 
谁是天蝎
注册用户
等级:大元帅
经验:90210
发帖:106
精华:0
注册:2011-7-21
状态:离线
发送短消息息给谁是天蝎 加好友    发送短消息息给谁是天蝎 发消息
发表于: IP:您无权察看 2011-8-25 17:06:34 | [全部帖] [楼主帖] 楼主

随功能一起发展的很棒的用户界面

因为用户界面是程序最初的入口,所以它必须能够用于多种目的。必须向用户提供对程序所有功能的合适访问。在向程序添加更多功能时(这几乎是必然发生的情况),它必须是可扩展的。必须具备灵活性,可以接受常用命令的缩写和快捷方式。它不应该有层叠的菜单或瀑布式单词,这样会让用户感到迷惑。无可否认,以上所有这些要求对程序员来说都是复杂的约束,对此没有一种很好的解决方案能把它们全包括。许多软件产品开发人员到最后再解决用户界面问题,把它作为一种事后来考虑的问题。另外一些开发人员则首先主要考虑用户界面,让功能仅仅成为界面设计选择的结果。这些都不是理想的方法。用户界面(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;
}





简单的事件循环

当命令行参数不够用时,下一步是编写一个事件循环。在这种方案中,仍然可接受命令行参数,并且有时只使用命令行参数就足够了。然而,事件循环支持用户在不输入任何参数的情形下调用程序,以及看到提示符。在此提示符下,通常可使用 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 <<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;
}


这里,通常会有三种选择:

  • 由于可能会有多个开关组合,所以程序的 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 |
       <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 的约定(以‘#’字开头,一直到行尾,这之间都是注释)。

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 |
       <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:
                                                       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 构建人员都应发现这一功能强大的工具是有用的。




赞(0)    操作        顶端 
总帖数
1
每页帖数
101/1页1
返回列表
发新帖子
请输入验证码: 点击刷新验证码
您需要登录后才可以回帖 登录 | 注册
技术讨论