歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Unix知識 >> 關於Unix

功能豐富的Perl:編寫說英語的Perl程序

設計程序的用戶界面可能很困難而且耗時。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::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 <  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 構建人員都應發現這一功能強大的工具是有用的。

Copyright © Linux教程網 All Rights Reserved