Ruby v příkladech (4) - Konzolová aplikace

29. 9. 2005
Doba čtení: 6 minut

Sdílet

V dnešním pokračování využijeme základ aplikace z minulého dílu k vytvoření konzolové aplikace, která bude umět vypsat jméno k zadanému datu a naopak. Požadované jméno či datum se bude předávat aplikaci pomocí parametrů z příkazové řádky.

Protože bychom chtěli, aby aplikace byla multiplatformní, musíme ještě přidat parametr pro kódovou stránku (data aplikace jsou kódována v UTF-8). Ačkoliv to není nezbytně nutné, aplikaci „zabalíme“ do třídy, která bude mít jen jednu instanci (návrhový vzor Jedináček – Singleton). Aplikace by měla také podporovat standardní možnost nápovědy (parametr -h resp. --help) a zobrazení verze ( -v resp.  --version).

 1:  require 'singleton'
 2:  require 'optparse'
 3:  require 'jmeniny'
 4:  require 'iconv'
 5:
 6:  class App
 7:    include Singleton
 8:
 9:    ID = 'Jmeniny CLI ver. 1.0.0'
10:
11:    def initialize
12:      get_options
13:    end

14:
15:    def get_options
16:      codes = %w|cp852 iso-8859-2 windows-1250|
17:      code_aliases = { "dos" => "cp852" }
18:
19:      ARGV.options do |opts|
20:        opts.banner = "Usage: #{File.basename $0} [options]"
21:
22:        opts.separator ""
23:        opts.separator "Specific options:"
24:
25:        code_list = (code_aliases.keys + codes).join(',')
26:        opts.on("-c", "--code-page CODE_PAGE", codes, code_aliases,
27:            "Select encoding:", "(#{code_list})") { |@code_page| }
28:        opts.on("-d", "--den DD.MM.", String, "") { |@den| }
29:        opts.on("-j", "--jmeno JMENO", String, "") { |@jmeno| }
30:
31:        opts.separator ""
32:        opts.separator "General options:"
33:
34:        opts.on('-h', '--help', 'Show this message and exit') { puts opts; exit }
35:        opts.on('-v', '--version', 'Print version info and exit') { puts ID; exit }
36:
37:        opts.parse!
38:      end
39:    end
40:
41:    def run
42:      if @den
43:        den, mesic = @den.split('.')
44:        jmeno = Jmeniny.jmeno(den.to_i, mesic.to_i)
45:        jmeno = Iconv.iconv(@code_page, 'utf-8', jmeno) if @code_page and jmeno
46:        puts jmeno if jmeno
47:      elsif @jmeno
48:        jmeno = @jmeno
49:        jmeno = Iconv.new('utf-8', @code_page).iconv(jmeno) if @code_page
50:        den, mesic = Jmeniny.den(jmeno)
51:        puts "#{den}.#{mesic}." if den
52:      end

53:    end
54:
55:  end
56:
57:  App.instance.run 

Na řádcích 1 až 4 vkládáme postupně prostředky pro vytvoření třídy – jedináčka (řádek 1), zpracování parametrů příkazového řádku (řádek 2), náš modul vytvořený minule (řádek 3) a interface na knihovnu Iconv pro převod mezi kódovými stránkami (řádek 4). Kromě našeho modulu je vše součástí standardní knihovny. Naše třída App pak na řádku 7 importuje modul Singleton, a stává se tak jedináčkem. ModulSingleton znepřístupní metodu new pro vytváření instancí a nahradí ji metodou instance, která vrací vždy tutéž instanci třídy App (řádek 57).

Metoda get_options definovaná na řádcích 15 až 39 zajišťuje vše kolem parametrů příkazového řádku. Definuje jednotlivé volby (krátkou i dlouhou verzi), jejich případné parametry, nápovědu k volbě a akci, kterou volba spouští. Například na řádku 35 je definována volba pro výpis verze programu. Volba se zadává jako -v nebo --version, nenásledují žádné hodnoty, v nápovědě vypisuje text „Print version info and exit“, a pokud je volba v příkazovém řádku použita, vykoná se blok na konci řádku (výpis obsahu konstanty ID na konzoli a ukončení aplikace). Výstup vypadá takto:

Jmeniny CLI ver. 1.0.0 

Nápověda programu (volba -h nebo --help) je zkonstruována automaticky a vypadá takto:

Usage: jmeniny-cli.rb [options]

Specific options:
    -c, --code-page CODE_PAGE        Select encoding:
                                     (dos,cp852,iso-8859-2,windows-1250)
    -d, --den DD.MM.
    -j, --jmeno JMENO

General options:
    -h, --help                       Show this message and exit
    -v, --version                    Print version info and exit 

Konstrukce na řádku 16 začínající %w je zkratka pro pole řetězců vytvořené ze slov. Pole definuje povolený výčet kódování. Konstrukce %w je jedním z dědictví Perlu. Ušetříme uvozovky okolo každého slova a čárky mezi nimi. Osobně ji sice docela rád používám (když jsem líný psát hodně uvozovek a čárek), ale standardní zápis by byl čitelnější. Řádek 16 zapsaný standardním způsobem by vypadal takto:

16:      codes = [ "cp852", "iso-8859-2", "windows-1250" ] 

Na řádku 17 je pak asociativní pole (Hash ), které definuje, že pro označení kódování „cp852“ lze použít i řetězec „dos“.

Na řádku 28 resp. 29 se zpracovává parametr datum resp. jméno. Při použití volby se nastaví proměnná @den resp. @jmeno (bloky na koncích řádků). Připomínám, že proměnné začínající zavináčem jsou proměnné instance (vnitřní proměnné objektu), a jsou tak přístupné ze všech metod instance.

Metoda run (řádky 41 až 53) se spouští jako hlavní vstupní bod aplikace (řádek 57). Je-li zadán datum, řetězec tvaru „DD.MM.“ se rozloží na den a měsíc pomocí metodysplit (řádek 43). Všimněte si vícenásobného přiřazení do proměnných den a měsíc. Ty se po převodu na číslo (metodou to_i ) použijí jako vstup metody Jmeniny.jmeno (řádek 44). Pokud je jméno nalezeno a byla zadána kódová stránka konzoly, proběhne ještě konverze (řádek 45) a jméno je vypsáno na standardní výstup (řádek 46). Řádky 48 až 51 pak obsahují kód pro opačný směr hledání. Použití vypadá takto:

CLI

Ne každému musí vyhovovat zde použitý způsob zpracování parametrů příkazové řádky pomocí optparse. V základní instalaci Ruby najdete i starší a jednoduššígetoptlong (obdoba funkce getopt_long známé z GNU knihovny pro jazyk C). Pokročilejší zpracování parametrů příkazového řádku pak naleznete v balíčku cmdparse , který ale není součástí standardní knihovny. Tento balíček je nadstavbou nadoptparse ze standardní knihovny a umožňuje konstrukovat například víceúrovňové příkazy (commands with subcommands – např. jako u CLI klientů CVS).

Co by se dalo na naší malé aplikaci ještě zlepšit? Bylo by možné před první řádek našeho skriptu s aplikací uvést „shebang“ (např. #!/usr/bin/ruby) a souboru přidat právo na spuštění. Pak bychom nemuseli na příkazovém řádku explicitně vypisovat volání interpretu Ruby. Přesněji řečeno, výše uvedené platí pro Linux, Unixy a podobné operační systémy. Šťastní majitelé MS Windows 2000 nebo XP, kteří si nainstalovali Ruby pomocí One-Click Ruby Installeru, se tím nemusejí trápit. Programy a skripty psané v Ruby by jim měly jít spouštět v „shellu“ (cmd) jen uvedením jména bez přípony – v našem případě tedy např.:

jmeno-cli -j petr -c dos 

Nepříjemná je také nutnost neustále zadávat kódovou stránku (kromě šťastlivců, kteří již mají pro konzolu nakonfigurovanou kódovou stránku UTF-8). Na možné řešení se ale podíváme příště, kdy si zkusíme vytvořit aplikaci s grafickým rozhraním.

Objekty a metody podruhé

Ve druhém dílu jsme si ukázali několik způsobů, jak zajistit, aby objekt nebo všechny objekty dané třídy rozuměly určité zprávě. Nebo, jinak řečeno, aby se na instanci (nebo všech instancích dané třídy) dala zavolat určitá metoda. Velmi zajímavý způsob „správy“ metod nabízí také metoda method_missing . Pokud se snažíme vyvolat metodu, která není definována, zavolá se právě metoda method_missing . Implicitní implementace zařídí vyvolání výjimky NoMethodError. My ale můžememethod_missing přepsat a předepsat jiné chování než vyvolání výjimky. V dokumentaci je například načrtnuta implementace třídy, která umožňuje zadávat čísla pomocí římských číslic takto:

r = Roman.new
r.iv      #=> 4
r.xxiii   #=> 23
r.mm      #=> 2000 

V následujícím příkladu přepišme tuto metodu standardní tříděHash ( return lze případně vynechat):

class Hash
  def method_missing(sym_name)
    return self[sym_name.to_s]
  end

end 

Pak můžeme získavat hodnoty z heše, i když zadáme klíč jako metodu, např.:

a = { 'key1' => 'val_a', 'key_2' => 3 }

# normalni zpusob:
a['key1']  #=> vysledek je 'val_a'
a['key_2'] #=> vysledek je 3

# pres metodu:
a.key1     #=> vysledek je 'val_a'
a.key_2    #=> vysledek je 3 

Ačkoliv se může zdát, že jde jen o hříčky, tento způsob umožňuje například velmi efektivní implementaci návrhového vzoru Proxy (nejen) pro distribuované aplikace. Pokud dojde k volání neznámé metody, v přepsané method_missing se serializují parametry, vyvolá vzdálená metoda a deserializují výsledky, které jsou vráceny volajícímu. Tomu se to pak jeví jako normální volání (lokální) metody.

Jinou možností aplikace method_missing je, pokud potřebujeme třídu s větším množstvím metod se stejnou (nebo podobnou) implementací. Typicky by se mohlo jednat o generátor well-formed XML:

ict ve školství 24

class XmlGen
  def method_missing(sym_name)
    print "<#{sym_name}>"
    print yield if block_given?
    print "</#{sym_name}>"
    return ""
  end

end 

Použití by pak bylo:

x = XmlGen.new

x.a                         #=> vytiskne: <a></a>
x.a { x.b }                 #=> vytiskne: <a><b></b></a>

x.a { x.b { "muj text" } }  #=> vytiskne: <a><b>muj text</b></a>

x.html {
  [
    x.head {
      x.title { "Nadpis" }
    },
    x.body {
      [
        x.h1 { "Taky nadpis" },
        x.p { "Prvni odstavec" }
      ]
    }
  ]
} 

Poslední použití XML generátoru vytvoří jednoduchou HTML stránku. Nakonec je potřeba varovat, že method_missing je velmi silný nástroj, který je ale nutno používat s rozmyslem.