Hlavní navigace

Ruby z rychlíku (3)

22. 2. 2002
Doba čtení: 10 minut

Sdílet

Dnes se podíváme na to, jak se v Ruby pracuje se soubory a sokety, na ošetřování výjimek, standardní knihovnu a chybět nebude ani jeden větší demonstrační příklad.

Komunikace se světem

Snad žádný proram se neobejde bez komunikace se svým okolím. V Ruby není těžké pracovat jak se standardním vstupem a výstupem, tak se soubory nebo síťovými sockety.

Standardní výstup

Uvedené ukázky zatím používaly pouze funkci puts, která vypíše parametr na standardní výstup a přidá znak konce řádku. Podobnou funkcí je print, který však znak konce řádku nepřidává. K dispozici je klasický printf, jehož formátovací specifikace je podobná té v jazyce C.

puts 'a'    # vypiš 'a' a odřádkuj
print 'b '  # vypiš 'b' a neodřádkuj
puts 'c'
printf "Číslo: %5.2f, řetězec: %s\n", 3.14, 'PI'    # \n bude konec řádku

Po spuštění uvidíme:

a
b c
Číslo:  3.14, řetězec: PI

Zjednodušeně řečeno se při volání těchto funkcí ve skutečnosti volá metoda stejného jména náležející objektu třídy IO. Konkrétně se v našem případě jednalo o objekt reprezentující standardní výstup programu, který je přístupný přes globální proměnnou $stdout.

$stdout.puts 'Tak tak'  # výpis na stdout

(Poznámka pro pozorné čtenáře: znak ‚$‘ není na začátku názvu proměnné náhodou. V Ruby existuje konvence pro pojmenování proměnných, která říká, že jména globálních proměnných začínají právě znakem ‚$‘. Ostatní proměnné, které používáme v příkladech, jsou lokální a jejich jména začínají malým písmenem.)

Standardní vstup

Ze standardního vstupu můžeme načíst řádek pomocí funkce gets. Opět se volá metoda objektu třídy IO – tentokrát $stdin. Pro čtení máme k dispozici i další metody. Podívejme se na příklady. Následující cyklus načítá řádky ze standardního vstupu a kopíruje je na standardní výstup:

while line=gets  # buď načte řádek a pokračuje, nebo vrací nil a cyklus končí
  print line     # vypiš řádek na stdout (gets ponechala znak konce řádku)
end

Pokud bychom chtěli nejdříve načíst všechny řádky a pak je zpracovávat, mohli bychom to udělat například takto:

a=$stdin.readlines  # načti řádky do pole řetězců
puts a.size         # počet prvků pole - tj. počet načtených řádků

Soubory

Viděli jsme, že vstup a výstup jsou realizovány pomocí metod třídy IO. Kromě standardního vstupu a výstupu můžeme využít také soubor nebo třeba síťový socket. Jak přečíst soubor?

f=File.new("soubor", "r")  # metoda new vytvoří objekt třídy File
while line=f.gets          # úplně stejně jako u standardního vstupu
  print line
end
f.close                    # zavřeme soubor

Pro účely přístupu k souboru se využívá třídy File, která je velmi podobná třídě IO – můžeme volat všechny metody pro vstup a výstup, které jsme dosud použili. Metoda new, která vytváří nový objekt a v našem případě otevírá soubor, má dva parametry. První určuje název souboru a druhý mód otevření souboru (např. ‚r‘ znamená read – čtení, ‚w‘ znamená write – zápis).

Ukažme si ještě obecnost některých principů v Ruby:

f=File.new("soubor", "r")   # metoda new vytvoří objekt třídy File
f.each_line { |line|        # iterátor volá blok a předává mu řádky
  print line
}
f.close                     # zavřeme soubor

Se souborem můžeme pohodlně pracovat pomocí iterátorů. V našem příkladu jsme zpracovávali soubor opět po řádcích, ale máme k dispozici i iterátor načítající soubor po bytech.

Argumenty a proměnné prostředí

Vstupními parametry programu mohou být samozřejmě také argumenty zadané z příkazové řádky a proměnné prostředí, ve kterém je program spuštěn. Argumenty jsou uloženy v globální proměnné ARGV, ke které přistupujeme stejnými metodami, jaké má třída Array. Oproti jazyku C není ARGV[0] cestou ke spuštěnému programu, ale skutečně prvním zadaným argumentem. Cesta ke spuštěnému programu se nalézá v globální proměnné $0.

Proměnné prostředí jsou uloženy v proměnné ENV, ke které lze přistupovat stejnými metodami, jako má třída Hash. Nejlépe je vše vidět na příkladu:

puts "Program: #{$0}"
puts "Argumenty z příkazové řádky: #{ARGV.join(', ')}"
puts "Proměnné prostředí:"
ENV.each { |key,value|
  puts "#{key} = #{value}"
}

Pokud uvedený kód bude uložen v souboru program.rb a spustíme jej pomocí příkazu ruby program.rb par1 par2 par3, obdržíme výpis podobný tomuto (výpis proměnných je krácen):

Program: test.rb
Argumenty z příkazové řádky: par1, par2, par3
Proměnné prostředí:
PWD = /home/dali/aoi/prg/ruby/tmp
PAGER = less
USER = dali
COLORTERM = Eterm
SHELL = /usr/local/bin/bash
HOSTTYPE = i386
OSTYPE = freebsd4.4

Ošetření výjimek

Jako moderní programovací jazyk nabízí Ruby solidní podporu ošetření výjimek. K pochopení jejího fungování nejlépe poslouží demonstrativní prográmek.

#!/usr/local/bin/ruby

begin                               # začátek bloku s výjimkami
  while line=gets                   # čti každý řádek až do konce souboru
    raise "Aaaa!" if line=~"^[aA]"  # způsob výjimku, když řádek začíná na 'a'
    puts line                       # vypiš řádek
  end                               # konec cyklu (zde končí běžný program)
rescue RuntimeError                 # odchyť výjimku třídy 'RuntimeError'
  puts "Chyba A!"                   # zobraz hlášku
rescue Interrupt                    # odchyť výjimku třídy 'Interrupt'
  puts "\nKonec = CTRL-D."          # zobraz hlášku
  retry                             # zpět na začátek bloku
else                                # jinak (tj. pokud nedošlo k výjimce)
  puts "\nNedošlo k výjimce."       # zobraz hlášku
ensure                              # tento kód se vykoná tak jako tak -
  puts "\nKonec!"                   # ať došlo k výjice, či nikoliv
end                                 # konec bloku s výjimkami

Tento program může skončit buď korektně na konci souboru, který na standardním vstupu vytvoříme stiskem CTRL-D, nebo vznikem výjimky. Jednu výjimku způsobujeme v kódu přímo příkazem raise (jedná se o výjimku třídy RuntimeError. Druhá výjimka třídy Interrupt vznikne stiskem CTRL-C (tedy požadavkem na přerušení provádění programu).

Zatímco RuntimeError pouze zachytíme, zobrazíme hlášku a program se ukončí, po zachycení Interrupt se pomocí příkazu retry vrátí řízení na začátek bloku označený klíčovým slovem begin. Za příkazem else následuje kód, který se vykoná, pokud k žádné výjimce nedošlo. Za příkazem ensure následuje kód, který se vykoná v každém případě, ať již v bloku k výjimce došlo, či ne. Příkazy retry, else a ensure samozřejmě nejsou k fungování mechanismu výjimek nutné. Eventuální nezachycená výjimka ukončí činnost programu.

Podle konkrétních podmínek může spuštění proběhnout různě. Při korektním ukončení takto (je vidět i vstup od uživatele):

^D
Nedošlo k výjimce.

Konec!

Na tomto příkladu vidíme zotavení po stisku CTRL-C a pak ukončení na výjimce vyvolané řádkem začínajícím na ‚a‘:

ppp
ppp
^C
Konec = CTRL-D.
aaa
Chyba A!

Konec!

V obou případech dojde k vypsání zprávy ‚Konec!‘.

Ošetření chyb a nestandardních situací v programech je zřejmě nejpracnější částí programování. Ačkoliv se jimi již nebudeme zabývat, nabízí Ruby ještě další cesty pro jejich zvládnutí.

Standardní knihovna

Veškeré příklady v tomto článku jsou založeny pouze na vestavěných objektových třídách a metodách jazyka Ruby. Pokud se dostaneme do situace, kdy si s těmito prostředky nevystačíme, čeká nás ještě velmi bohatá standardní knihovna. Jen namátkově naznačme, co lze od standardní knihovny Ruby očekávat:

  • další datové struktury a operace nad nimi: např. komplexní čísla a datumy (konverze, výpočty atd.)
  • podporu parsování složitých kombinací argumentů z příkazové řádky
  • podporu profilování programu na úrovni interpreteru
  • podporu serializace objektů a trvalého ukládání stavu objektů na disku
  • podporu internetových síťových protokolů od síťové vrstvy (IP) až po aplikační vrstvu (HTTP, FTP, IMAP atd.)
  • implementaci hashovacích funkcí
  • podporu knihovny Curses

Za všechny uvedené oblasti si ukažme alespoň jeden příklad. Jedná se o jednoduchý TCP server, který přijme (libovolný) jednořádkový požadavek na portu 8081 a vrací krátký HTML dokument s aktuálním časem. Po spuštění si lze činnost serveru ověřit nasměrováním prohlížeče na adresu http://localhos­t:8081.

#!/usr/local/bin/ruby

# Jednoduchý HTTP server naslouchající na portu 8081 vrací aktuální čas.

# načteme potřebný modul standardní knihovny
require 'socket'

# hlavička dokumentu zaslaná klientovi
head="HTTP/1.1 200/OK\r\nContent-type: text/html\r\n\r\n"

# text zaslaný klientovi - znaky xxx později nahradíme aktuálním časem
text=<<"EOS"
<html>
<body>
<h1>Právě je xxx</h1>
</body>
</html>\r\n
EOS

# vytvoříme instanci TCP serveru
server=TCPServer.new('localhost',8081)

begin                                  # budeme zachytávat výjimku
  puts "Start serveru..."
  while (session=server.accept)        # čekáme na spojení
    puts "Požadavek: #{session.gets}"  # přečteme data a vypíšeme je na stdout
    session.print head                 # vypíšeme hlavičku
    send=text.sub(/xxx/,Time.now.to_s) # nahradíme xxx za aktuální čas
    session.print send                 # vypíšeme připravený text
    session.close                      # uzavřeme spojení
  end
rescue Interrupt                       # stisk CTRL-C korektně ukončí server
  puts "\nStop serveru..."
end

Výpisy skriptu mohou vypadat třeba takto:

Start serveru...
Požadavek: GET / HTTP/1.0
Požadavek: GET / HTTP/1.0
^C
Stop serveru...

Kromě standardní knihovny se stále rozšiřuje množství knihoven vytvořených programátory pro všechny představitelné úlohy. Jejich patrně nejúplnější seznam je k dispozici na stránce The Ruby Application Archive.

Příklad programu

Abychom se nezabývali stále jen příklady přitaženými za vlasy, podívejte se na výpis krátkého, ale relativně smysluplného programu, který faktorizuje čísla zadaná jako parametry z příkazové řádky nebo načítaná ze standardního vstupu.

CS24_early

#!/usr/local/bin/ruby
#
# factor.rb - triviální faktorizace čísla, příklad ke článku Ruby z rychlíku
#

# Funkce factor vrací první nalezený dělitel čísla n, případně číslo n
# samotné, pokud je to prvočíslo.
def factor n
  f=2                              # dělitele začneme hledat od 2
  while f<=Math.sqrt(n)            # dělitel nebude větší než odmocnina čísla
    return f if n.remainder(f)==0  # pokud je zbytek 0, je f dělitel a končíme
    f+=1                           # jinak inkrementujeme f a opakujeme cyklus
  end
  return n                         # nebyl nalezen dělitel, vracíme n
end

# Funkce factorize hledá dělitele čísla n a ukládá je do pole, které je
# návratovou hodnotou. Pokud je číslo n prvočíslem, vrátí se v poli jen
# jeden prvek - samotné n.
def factorize n
  x,a=n,[]                # do x uložíme n a vytvoříme prázdné pole a
  while x!=(y=factor(x))  # dokud x není prvočíslo (x==factor x, viz fce factor)
    a.push(y)             # y je dělitelem x, uložíme y do pole faktorů
    x/=y                  # x vydělíme y a pokračujeme faktorizací výsledku
  end
  a.push(y)               # uložíme poslední výsledek fce factor
  return a                # vrací se pole faktorů
end

# Funkce printFactors vypíše na standardní výstup číslo n a jeho faktory.
def printFactors(n,a)
  print n.to_s.ljust(10)+': '  # n převedeme na string a doplníme mezerami
  if a.size>1                  # pokud jsou v poli faktory,
    puts a.join(', ')          # vypíšeme je oddělené čárkami,
  else                         # jinak
    puts 'je prvočíslo'        # je n prvočíslo
  end
end

help=<<EOS
  factor.rb - triviální faktorizace čísla, příklad ke článku Ruby z rychlíku
  použití: factor.rb [číslo1 číslo2 ...]
  (pokud nejsou zadány parametry, načítá ze standardního vstupu)
EOS

# Zde začíná hlavní část programu.
if ARGV.size>0                           # jsou-li parametry z příkazové řádky,
  if ARGV[0]=~/-h|--help/                # zkontroluj, zda není žádost o help
   puts help                             # výpis helpu
   exit                                  # normální ukončení programu
  end
  ARGV.each { |n|                        # jinak projdi parametry po jednom,
    n=n.to_i.abs                         # převeď na integer a absolutní hodnotu
    printFactors(n,factorize(n)) if n>0  # a faktorizuj ty větší než nula
  }
else                                 # když nejsou argumenty
  begin                              # budeme zachytávat výjimky
    while n=gets                     # načítej řádky ze standardního vstupu,
      n=n.to_i.abs                   # převeď na integer a absolutní hodnotu
      raise if n==0                  # pokud to bylo např. písmeno, výjimka,
      printFactors(n,factorize(n))   # jinak faktorizuj
    end
  rescue Interrupt                   # stisk CTRL-C
    puts "\n* Stiskněte CTRL-D!"     # potřebujeme " kvůli \n
    retry                            # opakujeme od začátku
  rescue RuntimeError                # 'ručně' vyvolaná výjimka
    puts '* Neplatný vstup!'
    retry                            # opakujeme od začátku
  end
end

Po spuštění bez parametrů může dialog s uživatelem vypadat například takto:

23456789
23456789  : je prvočíslo
123456789
123456789 : 3, 3, 3607, 3803
a
* Neplatný vstup!
^C
* Stiskněte CTRL-D!
as
* Neplatný vstup!

Na co místo nezbylo

Ruby má za sebou přeci jen několik let vývoje a jedná se o značně bohatý jazyk. Jak z hlediska svých vlastností, tak i vytvořených knihoven a aplikací. Stručný průřez v tomto článku nemůže být ani zdaleka kompletní. Z nejzajímavějších témat, o nichž nebyla řeč, vyberme například:

  • objektové programování – Ruby je jazyk od základu objektově orientovaný a zaslouží si samostatné povídání o práci s objekty
  • zabudovaná bezpečnostní opatření – každý objekt má například příznak, zda jsou pro něj povoleny určité operace: pro řetězec načtený ze souboru lze třeba stanovit, že nepůjde předat operačnímu systému jako parametr ke spuštění
  • thready – interpret Ruby má vlastní platformově nezávislou a přenositelnou implementaci threadů (v Ruby fungují thready dokonce i v DOSu)
  • interakce s grafickým uživatelským prostředím – pro Ruby existují knihovny umožňující vytvářet aplikace pro různá GUI včetně rozhraní pro GTK, Qt a MS Windows

Zdroje

  • Matsumoto, Y.: Ruby in a Nutshell, O'Reilly & Associates, 2001
  • Thomas, D. – Hunt, A.: Programming Ruby
Seriál: Ruby z rychlíku

Byl pro vás článek přínosný?