paper.gifioProgrammo N°20, Dicembre 1998 ©Copyright DIEMME Editori

fire.gif

PRIMI PASSI CON TCL
parte 2
Ci mancano ancora pochi comandi per concludere l'introduzione al linguaggio tcl. Nel prossimo articolo metteremo in pratica le nostre conoscenze programmando con il toolkit tk. Nel frattempo via libera a comandi di controllo, iterazioni, espressioni regolari, procedure ed I/O su file
I comandi fondamentali per controllare il flusso di esecuzione di un programma tcl sono if e switch. La sintassi di if è molto simile a quella di altri linguaggi, ovvero:
if {condizione1} {
  corpo1
} elseif {condizione2} {
  corpo2
} else {
  corpo3
}

Il comando if controlla il valore ritornato da condizione1. Condizione1 deve essere una espressione valida e come tale deve restituire un valore booleano: 0 (falso) o un qualsiasi numero diverso da 0 (vero). Può restituire anche una stringa ed in questo caso sarà true/yes (vero) o false/no (falso). Se condizione1 ritorna vero, verrà eseguito corpo1 (per corpo si intende uno o più comandi in successione). E' anche permessa la presenza o meno della clausola elseif che verifica una nuova condizione ed esegue un nuovo corpo. Elseif può essere ripetuto più volte a seconda di come dovrà essere controllato il programma. La clausola else finale è facoltativa e serve per eseguire l'ultimo blocco di comandi se la verifica di tutte le altre condizioni precedenti ha avuto esito negativo. Qualche esempio?

puts a=
set a [gets stdin]
# attende un input da tastiera
puts b=
set b [gets stdin]
if {$a == $b} {
  puts "a e b sono uguali"
} elseif {$a > $b} {
  puts "a e' maggiore di b"
} else {
  puts "b e' maggiore di a"
}

Il comando switch invece è usato per controllare l'esecuzione dello script in base al risultato (sempre booleano) del confronto di due stringhe o di una stringa ed una porzione dell'altra che chiameremo modello. Switch è spesso usato in combinazione con le espressioni regolari che consentono la creazione di modelli di confronto molto flessibili. A qualcuno potrà ricordare il case del Pascal o lo switch del C, ma in realtà è abbastanza diverso. Le forme generali di questo comando sono:
switch ?opzioni? stringa modello1 corpo1 ?modello2 corpo2 ...?
oppure
switch ?opzioni? stringa {modello1 corpo1 ?modello2 corpo2 ...?}
Le opzioni disponibili sono quattro: -exact, -glob , -regex e -- La prima, -exact, significa che il confronto avrà esito positivo solo se stringa e modello coincidono perfettamente. -glob è usata per eseguire confronti particolari usando gli stessi modelli costruiti con il comando "string match" illustrato nel numero precedente. L'opzione -regex permette l'uso delle espressioni regolari, mentre i due trattini -- segnano la fine delle opzioni in modo da impedire che un' eventuale stringa da confrontare venga scambiata per un'opzione del comando.

puts -nonewline "Scrivi un valore booleano: "
gets stdin boole
# attende un input da tastiera
# e lo assegna alla variabile boole
switch -exact -- $boole {
  1 {puts vero}
  yes {puts vero}
  true {puts vero}
  # se immetto 1 yes o true stampa vero
  0 {puts falso}
  no {puts falso}
  false {puts falso}
  # se immetto 0 no o false stampa falso
  defalut {puts "Non booleano"}
  # se scrivo qualsiasi altra cosa stampa non booleano
}
switch -glob pippo {
  p* {puts 1}
  ?ippo {puts 1}
  [1-0] {puts 0}
}
# stampa 1
switch -regexp aaab {
  ^a.*b$ -
  b {puts 1}
  a* {puts 2}
  default {puts 3}
}
# stampa 1
switch xyz {
  a  -
  b {puts 1}
  a* {puts 2}
  default {puts 3}
}
# stampa 3

Vi sarete accorti che negli esempi compare la clausola default. Ebbene, ciò significa che verrà eseguito il blocco di comandi preceduto da default se tutti i confronti precedenti falliscono. Inoltre se un corpo è costituto da un singolo trattino - ad esso verrà sostituito il corpo successivo. In questo modo, quindi, è possibile condividere un unico corpo di codice per più confronti.
linea.gif
COMANDI DI ITERAZIONE
Per permettere l'esecuzione di uno o più comandi fino a che una certa condizione rimane vera (cioè non diventa falsa) si usano i comandi for, while e foreach. Il comando for è ormai un classico.
for {inizializzazione} {condizione} {incremento} {
  corpo
}

set somma 0
for {set i 0} {$i <= 10} {incr i} {
  set somma [expr $somma+$i]
}
puts "La somma e': $somma"
# stampa La somma e' 55

Lo stesso risultato può essere ottenuto con il comando while
while {condizione} {
  corpo
}

set i 0
set somma 0
while {$i <= 10} {
  set somma [expr $somma+$i]
  incr i
}
puts "La somma e': $somma"
# stampa La somma e' 55

ma anche con foreach, usato soprattutto per eseguire uno o più comandi per ogni elemento di una lista
foreach variabile lista {
  corpo
}

set somma 0
foreach i {1 2 3 4 5 6 7 8 9 10} {
  set somma [expr $somma+$i]
}
puts "La somma e': $somma"
# stampa La somma e' 55

Vi ricordate gli array associativi del Perl? In tcl, una simile implementazione può essere fatta con le liste ed il comando foreach.

set lista {rosso #ff0000 verde #00ff00 blu #0000ff}
foreach {colore valore} $lista {
  puts "$colore: $valore"
}
# stampa\ 
rosso: ff0000\
verde: 00ff00\
blu: 0000ff

linea.gif
ESPRESSIONI REGOLARI
Trattandosi di un linguaggio rivolto alla gestione dei dati in formato testuale, anche tcl dispone di un comando che permette l'uso delle espressioni regolari. In Sun si sta ancora lavorando per migliorare il set di opzioni a tale comando e ciò fa supporre, non a torto, che c'è ancora un po' di strada da fare per raggiungere i livelli di compattezza e potenza offerti dal Perl. Comunque se fosse necessario riscontrare le occorrenze di una stringa in una variabile, oppure in una riga di un file di testo, si deve usare il comando regexp con la seguente sintassi
regexp ?opzioni? espressione stringa ?var? ?subvar subvar ...?
dove un possibile valore per opzioni può essere -nocase (effettua il matching senza distinguere minuscole e maiuscole, espressione è l'espressione regolare, stringa è la variabile o il letterale su cui si esegue il confronto, var è il nome di una variabile in cui memorizzare la stringa riscontrata dall'espressione regolare mentre in subvar sono contenute le parti della stringa riscontrate da ogni singolo "atomo" (vedremo tra poco cosa si intende per atomo). In tcl una regular expression è una sequenza di caratteri racchiusa tra parentesi graffe. Al loro interno possiamo definire uno più atomi seguiti (opzionalmente) da uno dei caratteri * + ?. L'asterisco riscontra zero o più volte il contenuto dell'atomo, il + riscontra una o più volte, mentre il ? riscontra un'unica volta l'atomo oppure la stringa nulla. Il contenuto di un atomo, che deve essere racchiuso tra parentesi tonde, può essere formato da:
- una sequenza di caratteri alfanumerici (riscontra esattamente tale sequenza)
- un intervallo di valori racchiuso tra due quadre ([0-9] riscontra ogni singolo numero tra zero e nove, [aA-zZ] riscontra ogni singola lettera dell'alfabeto, []abc-] riscontra ], a, b, c, -. Se nell'intervallo compare il carattere ], questo va scritto all'inizio della sequenza. La stessa precauzione va usata per il carattere - il quale può essere scritto anche alla fine dell'intervallo)
- il segno di punto . (riscontra ogni singolo carattere)
- il segno ^ (riscontra la stringa nulla all'inizio della stringa da confrontare)
- il segno $ (riscontra la stringa nulla alla fine della stringa da confrontare)
- il backslash \ seguito da un qualsiasi carattere (riscontra quel carattere)
- un singolo carattere privo di significato speciale (riscontra quel carattere)
Ecco un breve esempio riassuntivo.

set url http://www.sun.com:80/index.html
regexp {([^:]+)://([^:/]+)(:([0-9]+))?(/.*)} $url \
	match protocol server x port path
puts $match
# http://www.sun.com:80/index.html
puts $protocol
# http
puts $server
# www.sun.com
puts $x
# :80
puts $port
# 80
puts $path
# /index.html

linea.gif
LE PROCEDURE
Anche tcl è un linguaggio che consente una programmazione di tipo strutturato. Il comando proc infatti ci permette di creare blocchi di codice che eseguono un determinato compito e che a seconda delle necessità possono ricevere informazioni dal comando di chiamata della procedura. In questo modo lo script acquista leggibilità e velocità di esecuzione. Il metodo più semplice per definire una procedura è il seguente
proc nomeprocedura {} {
  comandi
}
Abbiamo definito una semplicissima procedura che non riceve nessun parametro e non restituisce alcun valore. Se fosse necessario usare all'interno della procedura una variabile globale definita nel corpo principale dello script, si deve ricorrere al comando global. Per defaul, infatti, una procedura "vede" solo le variabili definite all'interno di essa a meno che non le vengano passate in modo esplicito.

set a 3
set b 5
proc somma1 {} {
  set a 1
  set b 9
  set somma [expr $a+$b]
  puts $somma
}
proc somma2 {} {
  global a b
  set somma [expr $a+$b]
  puts $somma
}
proc somma3 {a b} {
  set somma [expr $a+$b]
  puts $somma
} 
somma1; # stampa 10
somma2; # stampa 8
somma3 $a $b; # stampa 8

Per default una procedura restituisce il valore ritornato dall'ultimo comando. E' tuttavia possibile forzare la restituzione di un determinato valore usando il comando return. Vediamone un'applicazione per il calcolo del fattoriale di un numero

proc fattoriale {x} {
  set i 1; set prodotto 1
  while {$i <= $x} {
    set prodotto [expr $prodotto * $i]
    incr i
  }
  return $prodotto
}
puts [fattoriale 10]
# stampa 3628800

Se in fase di dichiarazione della procedura uso l'argomento args, tale procedura potrà ricevere un numero variabile di argomenti che verranno memorizzati all'interno della lista args

proc prova {a b args} {
  puts $a
  puts $b
  puts $args
}
prova 1 2 3 4
# stampa \
1\
2\
3 4

A conclusione di questo paragrafo è utile ricordare che il passaggio dei parametri ad una procedura avviene per valore. Se fosse necessario modificare il valore di una variabile il cui nome viene passato come argomento ad una procedura, si deve far ricorso al comando upvar abilitando il cosiddetto passaggio per riferimento

set a 5
proc rif {} {
  upvar a b
  set b 9
} 
rif
puts $a; # stampa 9
set x 0
proc cambio {x} {
  upvar x y
  set y 1
}
cambio $x
puts $x; # stampa 1

linea.gif
ACCEDERE AI FILE E AL SISTEMA OPERATIVO
Il comando open di tcl abilita la lettura o la scrittura dei file. Per leggere ed elaborare un file di testo, in generale si usa il comando open con l'opzione r associato ad un ciclo while. Quando il file non verrà più usato conviene chiuderlo con il comando close. Ecco un esempio

set idfile [open "/etc/passwd" r]
while {[gets $idfile riga] != -1} {
  set item [split $riga ":"]
  puts [lindex $item 0]
}
close $idfile

Per scrivere un file si utilizza sempre open ma in questo caso l'opzione fondamentale è w. Vediamo

set idfile [open "./prova.txt" w]
puts $idfile "Prima riga del file"
close $idfile

E se avessimo bisogno di leggere dei file binari? In questo caso si usa il comando read che permette la lettura del file in blocchi chiamati buffer. Un buffer è in sostanza una porzione del file espressa in byte. Cambia anche la condizione di fine file, ottenuta con il comando eof che ritorna 1 se si è raggiunto l'end of file.

set idfile [open "./prova" r]
while {![eof $idfile]} {
  set buffer [read $idfile 1024]
  . . .
}
close $idfile

Un altro comando molto utile, usato per compiere operazioni su un gruppo di file, è glob, il quale restituisce una lista i cui elementi sono i nomi dei file individuati dallo stesso glob. Per esempio, se si volesse elaborare tutti i file html della directory corrente si dovrebbe usare il seguente codice

foreach html [glob *.html] {
  set idfile [open $html r]
  while {[gets $idfile riga] != -1} {
    puts $riga
  }
  close $idfile
}

Per concludere vediamo come tcl offra al programmatore la possibilità di eseguire comandi del sistema operativo. Un primo approccio consiste nell'aprire dei pipe con il comando open

set input [open "|sort /etc/passwd" r]
set contents [split [read $input] \n]
close $input
foreach item $contents {
  puts $item
}

Il secondo metodo, forse più naturale, consiste nel ricorrere al comando exec. In questo caso però ci si deve ricordare che exec non espande i caratteri jolli * ? sottomessi al comando di sistema

exec df; # esegue il comando df
exec ls -l *.tcl; # errore
eval exec ls -l [glob *.tcl]; # ok
# esegue ls -l *.tcl

linea.gif
TEORIA E TECNICA
Un compilatore per Tcl/Tk

Nelle ultime due versioni di tcl/tk, precisamente la 8.0 e la 8.1, Sun Microsystems ha fatto molto per migliorare la velocità del codice prodotto dagli interpreti tclsh e wish, a tal punto che forse non è più corretto definirli interpreti puri. Entrambe le release, infatti, sono in grado di trasformare lo script in byte-code prima di passarlo al sistema operativo per l'esecuzione vera e propria. Non dimentichiamoci che questa particolare caratteristica sta pian piano contribuendo al successo di Java. Una software-house americana, la ICEM CFD Engineering, ha tuttavia superato le intenzioni di Sun, proponendo agli sviluppatori tcl/tk un vero compilatore. Il suo nome è ICE tcl/tk compiler. La prima versione ufficiale di questo compilatore è la 1.3, pienamente compatibile con le versioni 7.6/4.2 di tcl/tk. Attualmente è disponibile la versione 2.0, completa di debugger, e che dà il meglio di se con le versioni 8.0 dei due linguaggi. Si tratta di un software commerciale, che può essere scaricato liberamente da Internet, ma del quale viene concesso l'uso a titolo valutativo per sole due settimane. La procedura di installazione del compilatore prevede infatti la generazione di una chiave afanumerica, il cui valore deve essere comunicato via e-mail direttamente alla ICEM. Sarà poi compito di quest'ultima abilitare l'utente all'uso del compilatore fornendolo di un codice di "sprotezione". Se qualcuno fosse interessato, può trovare tutte le informazione all'indirizzo ftp://ftp.dnai.com/users/i/icemcfd/tclc

fire.gif

Data creazione HTML: Marzo 1999
Autore: Francesco Munaretto
E-mail: NoSpam@thank.you
exclaim.gif