Index of UNOFFICIAL Circumvesuviana Home Page La mia tesi di laurea main index Vi avevo già parlato del linguaggio di programmazione Ruby?

In questa pagina presento alcune cosucce che ho scritto in questi ultimi giorni. No, non mi interessa l'aver duplicato qualcosa già esistente nella libreria standard; mi interessa mostrare come ho imparato senza troppa fatica.

Suggerimento: provate a salvare questi programmini in file di testo con estensione .rb (come da tradizione Ruby) e poi ad eseguirli col solito metodo:
ruby nomeprogramma.rb argomento1 argomento2 ...

Suggerimento ancora più potente: stàmpati questa pagina e mettila nel bagno - così, al primo Momento di Concentrazione ti libererai di un peso e avrai imparato il linguaggio di programmazione che ti farà deprecare tutti gli altri! :-)


Brevissima introduzione

Ruby è un linguaggio a oggetti. Talmente a oggetti, che tutto è un oggetto; per esempio, per farsi calcolare i quadrati dei numeri da 0 a 14 è lecito scrivere:

15.times { |x| puts x*x }

Dato che "15" è un oggetto della classe Fixnum (numeri senza decimali), allora gli si può applicare il metodo times (che è un "iteratore") e passargli un blocco di codice da eseguire quindici volte. Nell'iterazione, il valore corrente verrà chiamato x (ecco il significato di quel |x|): la prima volta che viene chiamato il blocco di codice x varrà 0, la seconda 1, la terza 2, etc, l'ultima volta varrà 14.

C'è poi il metodo puts (manda in output gli argomenti e poi vai a capo) a cui passiamo un'espressione numerica (x*x) per averne la stampa a video (più esattamente "su standard output").

Di questo programma Ruby di una sola riga vediamone il tragico equivalente in linguaggio C:

#include <stdio.h>
main()
{
  int x;
  for(x=0; x<15; x++) printf("%d\n", x*x);
}

Secondo voi quale dei due listati è più leggibile, più facile da scrivere, più facile da manutenere?


Brevissima introduzione: stringhe e array

Come ho già detto qui, varrebbe la pena di usare Ruby per la sola comodità di avere stringhe e array manipolabili con estrema semplicità. Per esempio, per creare un array contenente le parole di una data stringa:

"le parole di una data stringa".split.each do |x|
  print "*", x.capitalize, "* "
end

Cosa abbiamo fatto:

Nota: se avessimo voluto recuperare la stringa dagli argomenti della command-line avremmo usato ARGV.first.split.each... (cioè avremmo preso dalla command-line ARGV la prima stringa utile con first e poi l'avremmo divisa con split e quindi su ogni elemento avremmo ciclato con each).
Risultato a video dell'esempio di cui sopra: *Le* *Parole* *Di* *Una* *Data* *Stringa*

Tre righe di programma! Di cui una contiene solo end (per marcare la fine del blocco di codice). E i blocchi si possono creare con le parentesi graffe o con do ed end.

D'accordo, anche il C++ ha nella sua libreria il supporto per stringhe ed array. Ma quante righe di codice C++ ti occorrono per scrivere un programma del genere? Quante righe di VB.Net ti servono? Quante righe di Java? (ahò, mica scherzo, eh! abbiamo ridotto una stringa a un array di stringhe, e poi ci abbiamo applicato un iteratore!)

Esercizio (per i più facinorosi): riscrivere il sopracitato programma in diversi altri linguaggi di programmazione e poi stilare una classifica ordinando sul numero di parentesi utilizzate e sul numero di byte del sorgente indentato... ;-)

Per esempio, il Messaggiero mi scrive la sua versione ultra-"elegante" e super-ridotta in Java:
class IlMessaGGiero
{
  public static void main(String[] s)
  {
    for (int i=0; (s.length>0 && i<s[0].split(" ").length); i++)
      System.out.println("*"+(""+s[0].split(" ")[i].charAt(0)).
      toUpperCase() + s[0].split(" ")[i].substring(1) +"*");
  }
}


È evidente la terribile complicazione di Java su un problema risolto in maniera così egregiamente semplice con Ruby...! ;-)

Gianpaolo mi propone una versione PHP in una sola riga:
echo "*".str_replace(" ","* *",ucwords("le parole di una data stringa"))."*";

Che è un pochino più corta della versione telegrafica in Ruby (ma solo per la presenza di quella ucwords nella sua libreria di sistema!):
print "le parole di una data stringa".split.collect {|p| "*#{p.capitalize}*"}.join(" ")

Continuo con gli esempi (volutamente semplici)...


Sostituzione di keyword

Problema: "per ogni linea a presa dallo standard input, sostituire un pattern con un altro (entrambi dati sulla linea di comando) e mandarla in output".

Traduzione in Ruby:

$stdin.each_line { |a| print a.gsub(ARGV[0], ARGV[1]) }
(e la vasta libreria di Ruby non ha richiesto nessun equivalente dell'#include dei vari C/C++).

Scrivere una cosa del genere in C costa un po' di fatica: #includere almeno stdio.h e string.h, fare un ciclo di fgets su un buffer controllando l'end-of-file per capire quando uscire, etc.

Sì, sì, lo so che anche il C++ ha i suoi bravi oggetti stringa e stream, ma vogliamo scommettere che sarà ancora più complicato che non col linguaggio "C"...?

Partiamo dalla gsub: è un metodo della classe delle stringhe che effettua una sostituzione globale; il primo argomento può essere una stringa (o espressione regolare) e il secondo è la sostituzione da applicare ovunque venga "trovato" il primo argomento.

Da notare che in Ruby tutto è a oggetti:

Scriviamo quella riga di codice Ruby in un file sostituisci.rb e proviamo a farlo eseguire passandogli come argomenti a ed AA:
ruby sostituisci.rb a AA
123 abc def ABC DEF
123 AAbc def ABC DEF
111 aaa bbb
111 AAAAAA bbb

Risultato? abbiamo cambiato ogni a minuscola con una doppia A maiuscola. Provare per credere!


"Estrarre" un metodo

Problema: definire una procedura per triplicare un numero. Traduzione elegante in Ruby, "estraendo" il metodo:

triplica = 3.method(:*)

Ecco fatto! L'oggetto triplica contiene il metodo della moltiplicazione associato all'oggetto 3 per cui possiamo chiamarlo per triplicare 15 e ottenere x uguale a 45:

x = triplica.call(15)

Nota: il termine :* significa "metodo *", dove "*" è il metodo della moltiplicazione associato alla classe Fixnum cui apparteneva l'oggetto "3" di cui sopra. In Ruby tutto è "a oggetti"!


Aggiungere il numero di riga

Problema: "per ogni linea a dello standard input, aggiungere all'inizio il numero di linea (con un formato specificato da linea di comando), e mandare in output".

Traduzione in Ruby:

$stdin.each_line { |a| printf ARGV.first+"%s", $stdin.lineno, a }

Note

L'array ARGV contiene gli argomenti passati da linea di comando; a differenza di C e C++ (e similarmente a quanto avviene in Perl) il primo argomento è ARGV[0] (oops: era più elegante scrivere ARGV.first)

Piccola digressione: in Ruby si può comunque ottenere il nome del programma con... $0 (come nelle Unix shell), senza rincitrullirsi come con gli argomenti della main dei programmi in C. Nel caso di Ruby, infatti, se eseguite in un programma prova.rb questo assegnamento:
prg = $0

avreste creato una stringa prg contenente il testo "prova.rb".

A proposito del metodo lineno dell'oggetto $stdin (standard input): lo so, lo so che chiunque può crearsi una variabile intera ed incrementarla ad ogni lettura. Ma è bello avere una funzione già pronta per una cosa che si usa così spesso. Se ripenso a tutte le volte che in vita mia ho dovuto "parsàre" un file e tener conto del numero di linea per indicare in output la posizione di eventuali errori...

La funzione each_line è un "iteratore": gli si passa come parametro (!!!) un blocco di codice che verrà eseguito su ogni elemento (di volta in volta chiamato a), come nell'esempio precedente.

La funzione printf è ripresa pari pari dal linguaggio C.

E infine, il programma va eseguito per esempio con:
ruby aggiunginumero.rb "%3d) " < filediprova.txt

Da notare che alle funzioni occorre aggiungere le parentesi solo nei casi di ambiguità (debitamente segnalati con un warning dall'interprete Ruby): questo semplifica enormemente la creazione e modifica e la leggibilità e manutenibilità dei programmi in Ruby. Se vi sembra convincente già adesso, immaginate quanto lo troverete convincente dopo averne fatto massiccio uso. Alla faccia di tutte le parentesi dei vari C, C++, Java, Nice, D, C#, etc!


Mescolare le linee di input

Problema: "prelevare le linee dello standard input (fino ad un massimo di centomila) e ripresentarle in output in modo casuale".

Traduzione in Ruby, versione elegante e didattica:

class Array                # vado ad estendere la classe Array...
  def random               # ...e vi aggiungo la funzione "random"
    return self if size<2  # ritorna se ci sono meno di 2 elementi
    srand Time.now.to_i    # numeri casuali con seed dall'orologio
    a = []
    while size > 1         # finche` ci sono elementi da prendere:
      x = rand size        # ...ne pesca uno a caso (x e' l'indice)
      a << self[x]         # ...lo aggiunge ad un array temporaneo
      delete_at x          # ...ed elimina quello appena pescato
    end                    # ...e cosi' via, fino a svuotare questo
    concat a               # aggiunge "a" all'array ormai vuoto
  end                      # fine: ritorna l'ultima espressione
end                        #   (cioe' l'array appena calcolato)

a = []                     # main: inizializza un array
while a.size < 100000      # finche` non diventa di 100mila elementi
  l = gets                 # prende una linea da standard input
  break unless l           # esce dal loop a meno che non sia valida
  a << l                   # la aggiunge all'array corrente
end                        # e continua

puts a.random              # randomizza l'array e lo manda in output

Note

Prima di tutto, cominciamo col dire questa bella cosa: che se volete tutto il file di input in un array in un sol colpo, bastava utilizzare l'istruzione:
a = $stdin.readlines

...oops: potevate ulteriormente semplificare con:
a=readlines

Ruby ha un discreto malloppo di classi e oggetti già pronti per l'uso (come la Array di cui sopra) senza aver bisogno di citare riferimenti esterni ("include-files") come si fa negli altri linguaggi. Le classi predefinite sono ovviamente estendibili a piacere (vi si possono aggiungere le proprie personalizzazioni) e addirittura modificabili (al punto che si può cambiare il significato di "+" per i numeri interi per far significare qualcosa di diverso dall'addizione).

Per separare i comandi c'è il punto e virgola (come in C) oppure semplicemente non aggiungere altri comandi sul resto della linea. I commenti cominciano dal carattere "#" fino alla fine della riga.

Notare come le parentesi tonde siano necessarie solo nei casi di ambiguità: anziché scrivere x=random(size()); come avremmo fatto in C o C++, abbiamo scritto x = rand size e basta.

Notare come la puts, nel caso riceva un array anziché una stringa, mandi in output l'intero array, un elemento alla volta. Molte funzioni di libreria sanno già cosa fare in base al tipo dei parametri che arriva loro: nel nostro caso, se arriva un array, la puts esegue la stessa operazione per ogni elemento dell'array; se arriva un intero, manda l'intero; se arriva una stringa, o un numero in virgola mobile, etc, sa già cosa fare (e c'è anche il modo per "specializzare" le funzioni con altri tipi di parametri, compresa la puts).

Naturalmente, Ruby si preoccupa di spostare puntatori agli oggetti anziché gli oggetti stessi, per cui il sort di centomila righe di diversa lunghezza non abbisogna di tempi biblici per l'ordinamento.

Altra nota importante: con l'esempio di sopra abbiamo aggiunto dinamicamente il metodo chiamato random alla classe di sistema Array (che prima non lo aveva); tale metodo utilizza due sue variabili locali a ed x (per definirla come variabile d'istanza bastava usare @a mentre per darla come globale occorreva usare $a: così non rischiate di confondere le globali con le locali!) e pertanto le variabili "locali" verranno eliminate al termine della funzione stessa. Visto? Non c'è bisogno di tutte quelle complesse dichiarazioni e controdichiarazioni!

Quando è necessario ridurre un oggetto ad un tipo esplicito, ci sono i metodi per convertirli ad intero, stringa, array; come sopra, Time.now restituirebbe un oggetto di tipo Time inizializzato con il metodo now (l'ora esatta di quel momento) e come potete vedere vi abbiamo anche applicato il metodo to_i per ridurre l'oggetto da formato "Time" ad un numero intero (come da standard Unix, il numero di secondi passati dal 1' gennaio 1970). Fra parentesi, non era neppure necessario inizializzare il "seed" del generatore dei numeri casuali, visto che all'avvio è inizializzato casualmente (in modo dipendente dall'orario e dal process-id) ma io l'ho fatto per mania di chiarezza (e di grandezza: W la modestia)...


Oops: mescolare gli elementi di un array...

Che ve ne pare di questo?

class Array
  def random!
    a = []
    each { a << delete_at(rand(size)) }
    self << a
  end
end

Crea un array vuoto a, e per ogni elemento rimasto nell'array originale ne pesca uno a caso, lo aggiunge all'array temporaneo a e lo fa sparire dall'array originale. Alla fine l'array originale sarà vuoto, e quello temporaneo conterrà gli elementi in ordine casuale, e basta aggiungerli a... sé stesso.

Notare come definendo un metodo all'interno della classe Array non ci sia bisogno di specificare "chi" e "cosa" (c'è al più self che indica ciò che this indicava nel C++). Non c'è bisogno di ritornare self perché il risultato dell'ultima espressione è self stesso.

Stavolta gli ho messo il punto esclamativo nel nome, perché il metodo cambia "in loco" l'array, e non fa male ricordarlo al programmatore con tal piccolo tocco di eleganza nel nome.

Da notare che in altri linguaggi (anche "a oggetti") avremmo dovuto lottare con puntatori e strutture. Qui invece no. Finché un oggetto è referenziato da qualcuno, resta in memoria. Viene "copiato" solo se è davvero il caso (l'istruzione self << a non provocherà una copia di elementi, ma di puntatori). Quando non è più usato, allora viene deallocato. È la magia della garbage collection, gente! E quella di Ruby è una mark-&-sweep fatta alla perfezione!


Oops: mescolare un array...

Ritorno - per motivi "didattici" - ancora sullo stesso argomento.

Per esempio, dato un array di n oggetti, si può eseguire n volte uno scambio casuale tra due oggetti. Ve lo lascio come esercizio (e i più bravi dovranno dimostrare che in caso di numero dispari di elementi l'ordinamento è un po' meno "casuale"), perché qui volevo solo presentare un altro modo "alla Ruby", col metodo sort_by al quale si passa il nome di una funzione:

a = %w{ ho una gran fame e voglio tornare a casa }
p a.sort_by { rand }

Oops: guardate come ho definito l'array a: la sequenza "%w" seguita da un separatore permette di dargli un elenco di parole e di considerarle tutte stringhe. L'assegnamento di a è in pratica la stessa cosa del farlo con:
a = [ "ho", "una", "gran", "fame", ...

Notate come a ripetere il comando di stampa "quotata" (il comando p) si ottiene sempre una sequenza casuale. Già che ci siete, provate questo e andate a nanna (le spiegazioni sono nelle prossime "lezioni"):

%w( a b c d ).sort { rand <=> rand }

(esatto: abbiamo usato l'operatore "astronave", cioè <=>)


Quicksort in sei righe

Problema: "implementare il quicksort".


def qsort l                      # in input prende un array
  return [] if l.size == 0       # ritorna vuota se era vuoto
  x, r = *l                      # x=pivot, r=resto della lista
  a, b = r.partition{|i| i < x}  # partiziona quelli minori di x
  qsort(a) + [x] + qsort(b)      # sort lista sinistra + lista[x] + destra
end                              # ritorna il risultato ottenuto

Note

Sì, sì, lo so, che la prima riga di codice poteva essere ottimizzata con: return l if l.size <= 1 (cioè non ordinare l'array se è vuoto o se contiene un solo elemento: in tal caso ritornalo subito), ma non c'entrava tutto il commento in poche parole :-)

Le parentesi non sono indispensabili, e non c'è da dichiarare i tipi di funzioni e parametri. In C++ si sarebbe scritto: Lista *qsort(Lista *l) (con appropriato return finale). Il C++ non ha la garbage collection per cui dovrà arrangiarsi diversamente (magari sfruttanto la qsort della libreria C, fornendole una complicata funzione di controllo, con casting da void* alla classe in uso, e tutte le tragedie del caso).

In Ruby è possibile fare assegnamenti multipli:
a, b, c = 1, "prova", -5.5
e quando sulla destra c'è un array, la prima variabile ottiene il primo elemento, e la seconda ottiene il resto (è il caso di x,r=*list su cui però ci sarebbe da spendere qualche altra parola per le altre features del passaggio dati tra array e variabili).

Notare che qsort ritorna un array, quindi è lecito "sommare" tre array (traduzione: esiste un metodo "+" per la classe Array per aggregare più array in uno solo), cioè "sommare" qsort(a), più un array che contiene il solo elemento pivot x, più qsort(b) per ottenere l'array risultante.

Notare anche come alla funzione partition della classe Array sia stato passato come parametro un blocco di codice, che significa "quando vieni chiamato con un parametro i allora restituisci il confronto tra questo i e il pivot x che apparteneva a noi". Quel blocco verrà eseguito da un iteratore (comodissimo! confrontatelo con i patetici iteratori di certe librerie C++ e attenti a non sbavare sulla tastiera...).

Da notare che Ruby è un altro di quei linguaggi che ha la sua bravissima garbage collection, per cui ci si può divertire a creare e usare oggetti a tutto spiano: non appena l'interprete Ruby si accorge che un oggetto non è più referenziato da nessuno, allora lo deinizializza e libera l'area di memoria allocata (uno dei problemi più rognosi del linguaggio C è quella maledetta legge del: quando lo metti in memoria, ricordati dove lo metti).


Sort "decorato"

Per i più fanatici, ritorniamo al problema del mescolamento causale degli elementi di un array:

class Array
  def shuffle
    b = []
    each      { |x|    b << [ rand, x ]    }   # decorazione
    b.sort!   { |x,y|  x.first <=> y.first }   # sort
    b.collect { |x|    x.last              }   # recupero
  end
end

puts [ a","b","c","d","e","f" ].shuffle

Abbiamo aggiunto alla classe Array una funzione shuffle che crea un array b che conterrà delle coppie (ad ogni elemento associa un numero casuale).

Sull'array b viene fatto il sort "in loco" confrontando solo il primo elemento di tali coppie (il simbolo <=> in Ruby significa "restituisci 0 se sono uguali, -1 se il primo è minore del secondo, +1 se il primo è maggiore del secondo": quello che sullo Zx Spectrum si chiamava SGN e nell'NBC si chiamava sign - utilissimo per confronti e ordinamenti), che guarda caso era un numero casuale.

Alla fine si recupera (collect) da ogni coppia solo l'elemento finale. E dato che è l'ultima istruzione della funzione, allora il suo valore verrà ritornato al chiamante.

Ora sono stanco, la collect ve la spiego più avanti...


Aggiungere una stringa alla fine di ogni riga

Problema: "creare uno script lanciabile da shell che mandi in output le righe ricevute in input, appendendovi la stringa indicata sulla linea di comando (protestando se non è stata fornita)".

Traduzione in Ruby, scritta in /usr/local/bin/appendstring in modo da avere il comando "appendstring" già utilizzabile dal terminale senza neppure sapere che era stato scritto in Ruby:

#!/usr/bin/env ruby

fail "uso: #{$0} stringa"  if ARGV.size != 1
$stdin.each_line { |a| print a.chomp, ARGV[0], "\n" }

Note

Esci con messaggio di errore se il numero di parametri da command-line non era uno solo. Notare che all'interno della stringa passata al comando fail (pardon, al metodo "fail") c'è un'espressione che verrà espansa al momento dell'uso (ricordate $0 di cui sopra?).

Se scrivo infatti puts "3 + 4 = #{3+4}" otterrò in output 3 + 4 = 7 (comodo, no?). Le stringhe possono essere delimitate da singoli apici (e allora vi funzionano solo le sequenze col backslash: \n, \r, etc) oppure da doppi apici (e allora funziona anche l'espansione delle espressioni). Da notare che le espressioni possono essere combinate a piacere, per esempio:
puts "il tuo numero fortunato è: #{Time.now.to_i / 17}"

La print è come la puts - entrambe mandano in output tutti gli argomenti, ma la seconda va a capo dopo ogni argomento.

Il metodo chomp della classe String si mangia l'eventuale sequenza di caratteri \r \n a fine stringa... ah, se solo penso a quante volte ho dovuto (ouch!!) scrivere in C qualcosa del tipo:
char *p = str + strlen(str) -1; if(*p == '\n') *p='\0';
mi viene l'angoscia...


Manipolazioni di stringhe in stile linguaggio C

Data una stringa, cambiarne ogni carattere col suo valore xor 1.

Traduzione in Ruby:

def cambiami str
  b = []
  str.each_byte { |x| b << (x^1) }
  b.pack "c*"
end

Note

Questo è il tipico problema che rallegra i fautori del linguaggio C, poiché a scrivere in Java una roba del genere c'è da arrabbiarsi. Ma come abbiamo appena notato, in Ruby è - tanto per cambiare - anche più facile del linguaggio C.

Per ogni byte della stringa, aggiungiamo il valore numerico "manipolato" ad un array temporaneo. Alla fine "impacchiamo" l'array ottenuto in una stringa di caratteri. Provatelo con:
puts cambiami("prova")

Notate che le funzioni Array.pack e String.unpack conoscono anche base64, UTF8, quoted-printable, uuencode, formati binari e floating point, etc.

Se il metodo vi piace parecchio, allora aggiungetevelo alla classe stringa:
class String
  def cambiami
    str = self   # parte dal valore corrente
    b = []
    str.each_byte { |x| b << (x^1) }
    b.pack "c*"
  end
end

puts "Funziona!?".cambiami

Nota: dato che "Funziona!?" è un oggetto String, allora ci si può applicare il metodo cambiami e passare l'oggetto risultante alla puts che lo manderà in output. I fautori di C e C++ avrebbero dovuto allocare una nuova stringa calcolando dimensioni sufficienti, copiarla, effettuare la passata di xor, mandarla alla puts e infine deallocare il malloppo... Quanta fatica, eh?


Lettura di un file .INI

Dato un file .INI nel solito formato (chiave=valore, commenti inizianti per '#', etc), prelevarne i valori ed usare il valore di una chiave.

Traduzione in Ruby:
def fileini cfgfile
  fail "file non trovato: #{cfgfile}"  unless FileTest.exists? cfgfile
  cfg = Hash.new

  File.open(cfgfile).each_line do |x|  # per ogni riga x del file: 
    x.chomp!                           # elimina CR/LF
    x.gsub! /\s*#.*$/, ""              # elimina i commenti
    x.sub! /\s*=\s*/, "="              # elimina eventuali spazi attorno a '='
    ch = x.split "=",2                 # splitta chiave e valore attorno a '='
    cfg.store *ch  if ch.length == 2   # aggiungi chiave e valore se entrambe
  end

  cfg
end

Note

La struttura più adatta è l'Hash. La funzione legge ogni riga del file di input, ne elimina CR/LF alla fine, sostituisce i commenti (un'espressione regolare formata da eventuali spazi, più un '#', più eventuali caratteri fino a fine riga) con una stringa vuota, sostituisce poi eventuali spazi prima e dopo il primo carattere '=' (abbiamo usato la substitution perché non c'era bisogno di fare una global substitution che avrebbe tentato di cambiare anche sul resto); la stringa viene divisa in un massimo di due elementi attorno al primo carattere '=' trovato e diventa un array di stringhe ch, di cui si può controllare che abbia due elementi (chiave e valore) da aggiungere all'hash cfg che verrà ritornato al termine.

Notare come in una sola riga, senza contorsionismi, io abbia potuto dire "esci con errore file non trovato: nomefile" a meno che non esista davvero il nomefile". Ruby ti fa concentrare su come risolvere il problema, gli altri linguaggi di programmazione ti fanno concentrare su come spiegare al compilatore un modo per risolvere il problema!

Le funzioni di libreria che terminano col punto esclamativo lavorano "in loco" (anziché creare un nuovo oggetto con le modifiche in questione). Per scrivere i commenti le ho messe su tre righe, ma si potevano anche accodare (x.chomp!.sub!...).

All'hash si accede per chiavi; se eseguo puts fileini("FILE_1") e nel file di input c'era una riga FILE_1 = prova.txt allora otterrò come risultato la stringa "prova.txt". Nel caso non c'era nessuna riga FILE_1 = ... allora otterrò il valore nullo (che in Ruby si chiama nil).

Se vi spaventano le espressioni regolari vuol dire che non avete mai usato Perl e che forse non avete mai usato neppure il grep. In Ruby le espressioni regolari sono delimitate tra due caratteri '/' e hanno quasi la stessa sintassi di grep. Per esempio, interpretiamo il pattern passato alla gsub di cui sopra:
\s -- spazi o caratteri tab
* -- ripetuti 0 o più volte
# -- seguiti dal carattere '#'
.* -- poi qualsiasi carattere, 0 o più volte
$ -- il tutto terminato da un "fine riga" (LF o CR/LF)

Quindi il pattern che la gsub andrà a sostituire ovunque nella stringa x che l'ha invocata (la stringa è un oggetto e pertanto può invocare le "sue" funzioni) è: "eventuali spazi o tab seguiti da un carattere '#' e poi, eventualmente, da altri caratteri tra lì ed il fine-riga".

Esercizio: convertire le chiavi in minuscolo durante il caricamento, e protestare in caso di chiave non prevista da noi.

Piccola nota: cos'è l'Hash

È una struttura dati che contiene coppie di oggetti (generalmente chiamati "chiave" e "valore"). Per esempio, in Ruby, si fa così:

prezz = { "primo" => 3.15, "secondo" => 4.21, "contorno" => "assente" }
prezz.default=0.00


A questo punto, se chiedo prezz["secondo"] ottengo 4.21 e se chiedo prezz["antipasto"] ottengo 0.00 (quando il default non è indicato, allora viene usato nil, cioè il valore "nullo" di Ruby).

Da notare che in Ruby non ci sono limiti sul tipo degli oggetti che si possono mettere in un Array o un Hash: per esempio, eseguendo:
x = prezz["contorno"]
otterrò che x="assente" (guardate che "default=", compreso il carattere uguale, è il nome di un metodo della classe Hash): al posto di un numero floating-point ho ottenuto una stringa. Scrivendo puts x otterrò in entrambi i casi l'effetto desiderato; se poi voglio fare lo schizzinoso, posso controllare il tipo dell'oggetto x con:
if x.class == String
...

Tutto ciò permette un'elasticità colossale (un array può contenere oggetti di diversi tipi, un Hash può contenere chiavi di diversi tipi associate a oggetti di diversi tipi). Ovviamente l'Hash contiene i soliti utilissimi metodi each_key (per ogni chiave), each (per ogni coppia), each_value (per ogni valore).

Recupero di tag all'interno di file HTML

Problema: reperire il titolo di una pagina HTML.

class String
  def to_rx
    Regexp.new( Regexp.quote(self), true )
  end

  def tag tg
    return nil  unless self =~ ("<#{tg}>".to_rx)
    return nil  unless   $' =~ ("</#{tg}>".to_rx)
    $`
  end
end

puts `cat index.html`.tag("title")

Alla classe String ho aggiunto due metodi: il primo è il to_rx, che converte un oggetto stringa in un oggetto espressione regolare (si limita a crearlo "quotando" sé stessa; il secondo parametro significa che l'espressione sarà da valutare case-insensitive).

Il secondo metodo (tag) applicato ad una stringa contenente l'intero file HTML, accetta in input una stringa tg contenente il tag da cercare. Nel nostro caso per ottenere il valore compreso tra "<title>...</title>" ci basterà indicare "title".

Come vedete, la tag ritorna nil a meno che la stringa contenga un tag "<tiTLE>", oppure "<TITLe>", etc. L'operatore =~ fa il controllo con un'espressione regolare e lascia pronte per l'uso le stringhe "$'" (ciò che c'era dopo il pattern trovato) e "$`" (ciò che c'era prima del pattern trovato) e restituisce nil quando non trova niente.

Dunque:

  1. ritorniamo nil a meno che non trovi il tag iniziale;
  2. ritorniamo nil a meno che nella parte trovata non trovi il tag di chiusura;
  3. dopo quest'ultima ricerca, restituiamo $` (cioè quello che aveva trovato prima di "</Title>").

Infine: eseguiamo il comando Unix (Linux, Mac OS X, etc) cat nomefile e tutto il suo output lo convertiamo in una stringa a cui applicare il metodo tag per recuperare il "title" ed avremo come risultato nil (se non c'era nel file una sequenza "<title>...</title>") oppure il titolo cercato.

Provare per credere! Dedicato a tutti quelli che per recuperare un titolo da un file HTML devono complicarsi la vita con parecchie decine di righe di programma (sempreché bastino).

Nota: nel caso la sequenza "<title>...</title>" sia su più righe, è sufficiente aggiungere un flag per extèndere l'espressione regolare su più righe.


"Umanizzazione" del file di log di Apache

Problema da risolvere: al mio server HTTP accedono per lo più i miei colleghi di lavoro (tutti debitamente classificati nel mio hosts) ed io sto sempre col tail -f sul log in una finestra di terminale per vedere se qualcuno abusa troppo delle mie doppiamente arcipotenziatissime pagine. Solo che non mi ricordo mai chi è tizio e chi è caio a partire dal solo numero IP ("10.173.134.130"?!?) e così...

Subito vado a riciclare l'esempio dell'Hash, parsando stavolta il file /etc/hosts e poi presentando l'access_log di Apache sostituendo agli indirizzi IP conosciuti (messi nell'hash che per pigrizia ho lasciato chiamato cfg come prima) i nomi, ed incolonnando. Yeah!

#!/usr/bin/env ruby

cfg = Hash.new

File.open('/etc/hosts').each_line do |x|
s = x.chomp.strip.sub(/\s*#.*$/, "").split
s.delete_at 2
cfg.store *s  if s.size==2
end

File.popen('tail -100 -f /var/log/httpd/access_log').each_line do |l|
i = h = l.split.first    # per default la split va sugli spazi...!
i = cfg[h]  if cfg[h]
r = l.chomp.sub(h, "")
puts "%16s  %s" % [ i,r ]
end

Dunque:

Secondo blocco di codice:


Lavoro su migliaia di files...

Problema: c'è una directory con un paio di decine di migliaia di files in formato testo, ognuno dei quali nella prima riga contiene un filename ed una chiave separati da una virgola.

Dato un file di input contenente una chiave per riga, dire in output quale chiave non è presente in archivio.

In Ruby è più facile a farsi che a dirsi (avete letto bene):

lst = Dir["*.txt"].collect { |f| File.open(f).gets.chomp.split(",").last.strip }
puts "-- #{lst.size} chiavi totali"

while kwd = gets
print kwd unless lst.include?(kwd.chomp)
end

La prima riga fa una cosa che i programmatori Perl m'invidieranno (nel peggiore dei casi a causa della chiarezza), e i programmatori C/C++/VB/.NET/C#/etc rosicheranno, per non parlare della stizza dei tifosi di Python/awk/Java/Nice/etc:

Con una sola riga di codice abbiamo dunque raccolto nella lista lst tutte le chiavi che ci servivano. Dopo aver usato il metodo strip il valore stringa ottenuto viene restituito alla collect ed il file aperto non è più necessario e dunque Ruby lo chiude senza nostro intervento (alé!!) e ci possiamo permettere di passare comodamente a stampare a video il numero delle chiavi.

Subito dopo, per ogni keyword presa dallo standard input (presa con il CR/LF) la mandiamo in output "a meno che" (unless) la sopracitata lista la contenga (il chomp serve perché la gets ci ritorna la riga compresi i CR/LF finali).

Notare che i metodi che interrogano hanno tipicamente un punto interrogativo finale (include?(elemento)) mentre i metodi che cambiano "in loco" qualche valore hanno un punto esclamativo finale (qualchestringa.chomp! usato in qualcuno dei precedenti esempi).

Notare che c'è anche una open di sistema per cui negli esempi precedenti - e in questo - non c'era bisogno di dire File.open(... ma bastava open(... (l'ho scoperto solo adesso!) :-)

Insomma, varrebbe la pena imparare ad usare Ruby solo a causa del comodissimo uso di array, stringhe e iteratori. Ma Ruby è ovviamente molto di più...


Codice automodificante

Tenetevi forte:
#!/usr/bin/env ruby

s="aaa"

x=open(__FILE__).readlines
x[2]="s=#{s.next.inspect}\n"
open(__FILE__, "w").print(x)
puts s

In questo listato, creo una stringa s col valore "aaa".

Il metodo next degli interi incrementa di uno il valore, e c'è anche la versione per le stringhe, per cui "pf".next varrà "pg" e "pg".next varrà "ph" e così via. Quando si arriva alla zeta, si aumenta di una cifra ("zz".next varrà "aaa"). Se nella stringa c'era una cifra, allora anziché dalla a alla z la vedremo ruotare da 0 a 9 (per esempio "e9z".next varrà "f0a").

Torniamo al listato di cui sopra. Ho appena assegnato s e apro in lettura un file (guardacaso lo stesso listato) e ne leggo tutte le linee in un array x di cui vado a modificare il terzo elemento (x[2]) per scriverci al posto di s="aaa" un s= seguìto dal prossimo valore di s.

Il metodo inspect di un oggetto produce una versione "quotata" e stampabile (escaping dei caratteri, virgolette, etc), per cui quando vado a riscrivere il file (come nella successiva open in modo "w", cioè scrittura) e mandarci dentro l'array x appena modificato, il programma si sarà... "automodificato"!!!

Al termine del programma mando la versione corrente di s in output. In questo modo, dalla shell Unix, potrò eseguire cose del tipo: cp file1 file1-`seq` (per assegnare un nome temporaneo e univoco a nomi di file).

Esatto: codice automodificante... come nei bei tempi dei primi anni ottanta. Ed ancora non ho discusso il metodo eval che apre a scenari deliziosi. Guardate questo pezzo di codice:
a = "def turz ; $$*10 ; end"
eval a
puts turz

Prima riempio in qualche modo la stringa a e poi gliela faccio "evalutare". L'interprete Ruby esegue il contenuto della stringa (che è codice, regolarissimo codice: definizione di un metodo turz che restituisce un numero intero - nel nostro caso il process-id moltiplicato per dieci); all'istruzione successiva, siamo già in grado di utilizzare il metodo turz (che prima della eval non esisteva!).


Insisto ancora: recuperare i link da una pagina

Problema: recuperare i link presenti in una pagina HTML (fornita sullo standard input) e presentarli ordinatamente su standard output.

Basterebbe una sola riga di codice:
puts STDIN.readlines.join.split(/<a href="/i).slice(1..-1).collect { |x| x =~ //"//; $`.strip }.sort.uniq.delete_if { |y| y=="" || y=~//javascript\:/ }

Visto che qualcuno sarà arrivato a questo punto senza leggere almeno tre quarti di questa pagina, sarà bene che io spieghi un po' più in dettaglio...

puts STDIN.                                  # stream "standard input"
     readlines.                              # recupera un array di righe
     join.                                   # incollale in un'unica stringa
     split(/<a href="/i).                    # separale su "href"
     slice(1..-1).                           # scarta la prima
     collect { |x|  x =~ /"/; $`.strip }.    # togli spazi e immondizia
     sort.                                   # ordina
     uniq.                                   # scarta i duplicati
     delete_if { |y|  y =~ /javascript\:/ }  # elimina spazzatura

La puts manderà in output l'array risultante dall'applicazione di tutti questi metodi sull'oggetto STDIN passato all'inizio. Da STDIN caviamo un array di righe (tutto ciò che è letto da standard input), a cui applichiamo la join per ottenere in un unica stringa l'intero input (cioè l'intero file HTML).

A questo punto sbrogliamo il malloppo HTML separando sui tag <a href=" (quella i dopo l'espressione regolare tra i due slash "/" significa che non deve far differenza tra maiuscole e minuscole).

La slice prende l'array a partire dal secondo elemento fino all'ultimo (l'ultimo è sempre richiedibile all'indice -1 mentre il primo elemento ha indice 0 e il secondo ha indice 1, etc, come in C e C++). Abbiamo insomma eliminato il primo elemento, perché contiene certamente gli header HTML: il primo link verrà solo dopo la prima sequenza a-href.

A questo punto ricollezioniamo i link, poiché dobbiamo eliminare ciò che c'era dopo la fine del link (la chiusura del tag, il titolo, il testo che ne segue): è sufficiente dirgli di cercare le doppie virgolette e di restituire nell'array solo la parte della stringa precedente alle virgolette trovate (questo è il significato di $`), spazzolando via eventuali spazi.

Applichiamo all'array il metodo sort per ordinare i link e poi uniq per eliminare i duplicati. Ed infine eliminiamo dall'array gli eventuali link che anziché una pagina chiamano una funzione javascript.

Questo esempio funziona solo per le pagine che contengono i link tra i doppi apici: se vogliamo aggiornarlo per le pagine in HTML scritte con i piedi (che contengono anche i singoli apici oppure addirittura ne mancano, separando i tag con gli spazi) allora basta rifinire le espressioni regolari usate nella split e nella collect.


Nota bene: ho scritto anche un'altra paginetta a proposito di manipolazioni di bitmap in Ruby!


Ruby "avanzato"

Beh, ci sarebbe ancora parecchio da dire su Ruby: multithreading, singleton, librerie web, transazioni, ereditarietà... e il grandioso "Ruby on Rails" (framework per applicazioni web)... Credo però che quel che ho presentato in questa pagina sia già sufficiente a rimpiazzare un gran numero di tool (shell scripts, awk e perl, tanto per cominciare).

Presto (si spera) scriverò qualcosa di ancor più dettagliato, ad uso e consumo di quelli che dopo aver gustato questa pagina vogliono ancora di più... :-)


Cigno e anatre sul lago di Como

home page - send e-mail