Difference: TWikiBarChiacchiere006 ( vs. 1)

Revision 127 Feb 2013 - AlbertoTaddei

Line: 1 to 1
Added:
>
>
META TOPICPARENT name="BatePapos"

Chiacchiere da Bar Parte VI

Comandi di Loop o Anello (Continua)

     - Eccoti! Allora, hai imparato tutto quel che c'è da imparare sul comando for? Ti avevo assegnato un esercizio per fare un po' di pratica e, se non mi sbaglio, dovevi trovare il modo di contare il numero di parole presenti in un file... L'hai svolto?      - Certo! Questa roba mi prende un sacco! Ho fatto proprio come mi hai chiesto, cioè senza utilizzare il comando wc, altrimenti sarebbe stato ancor più semplice. Dagli un'occhiata...

     - Aspetta un attimo! Ti sei davvero innamorato della Shell, ma io ho la gola secca e devo bermi una birretta. Chico, due birre alla spina per favore. Una senza schiuma!

     - Come ti dicevo, guarda come ho risolto. È piuttosto semplice...

$ cat contpar.sh #!/bin/bash # Script meramente pedagogico la cui # funzione è contare il numero di parole # presenti in un file. Si suppone che le # parole siano separate tra loro da # uno spazio, oppure .

if [ $# -ne 1 ] then echo uso: $0 /percorso/del/file exit 2 fi Cont=0 for Parola in $(cat $1) do Cont=$((Cont+1)) done echo Il file $1 contiene $Cont parole.

Come sempre, il programma inizia verificando se l'inserimento dei parametri è avvenuta correttamente, quindi il comando for si incarica di prendere ogni parola (ricorda che la variabile $IFS di default è spazio, <TAB> e <ENTER>, proprio ciò che vogliamo per separare le parole) e di incrementare la variabile $Cont.

Ricordiamo com'è fatto FileDiDOS.txt.

$ cat FileDiDOS.txt Questo file è stato generato da DOS/Rwin ed è stato scaricato da un ftp mal fatto.

Adesso testiamo il programma passando questo file come parametro:

$ contpar.sh FileDiDOS.txt Il file FileDiDOS.txt contiene 16 parole

     - Che bello, funziona alla perfezione!

Ancora un po' di for e Matematica

Tornando a bomba, l'ultima volta che siamo stati qui al Bar abbiamo interrotto le nostre chiacchiere mostrando il seguente loop di for:

    for ((; i<=9;))
    do
        let i++
        echo -n "$i "
    done

Arrivati a questo punto, credo sia piuttosto interessante spiegare che la Shell segue il concetto di "Espansione Aritmetica" (Arithmetic Expansion), del quale parlerò brevemente perché nella sezione Stuzzichini per l'Aperitivo puoi trovare una spiegazione piuttosto esauriente.

L'espansione aritmetica si realizza tramite un costrutto del tipo:

     $((espressione))

o

     let espressione

Nell'ultimo for ho utilizzato l'espansione delle due forme, ma non potevamo procedere senza sapere che l'espressione può essere una di quelle elencate di seguito:

Espansione Aritmetica
||   O logico
  Espressione     Risultato  
id++ id--   post-incremento e post-diminuzione di variabili
++id -–id   pre-incremento e pre-diminuzione di variabili
**   elevamento a potenza
* / %   moltiplicazione, divisione, resto della divisione
+ -   addizione, sottrazione
<= >= < >   comparazione
== !=   uguaglianza, disuguaglianza
&&   E logico

     - Non penserai che il succo di loop (o anello) sia racchiuso nel comando for, vero? Ti sbagli amico mio! Adesso vedremo altri due comandi.

Il comando while

Ogni programmatore conosce questo comando perché è comune a tutti i linguaggi e ciò che in essi normalmente accade è che un blocco di comandi è eseguito finché (“finché” in inglese è while) una determinata condizione è vera. Beh, questo è ciò che accade nei linguaggi farlocchi! Nella programmazione Shell, il blocco di comandi è eseguito finché un comando è vero. E, ovviamente, se vuoi testare una condizione puoi utilizzare il comando while insieme a al comando test, proprio come hai imparato a fare con if, ricordi?

La sintassi del comando è:

    while comando
    do
        cmd1
        cmd2
        ...
        cmdn
    done

Il blocco di comandi formato dalle istruzioni cmd1, cmd2,..., cmdn, è eseguito fintanto che l'esecuzione dell'istruzione comando avviene con successo.

Ipotizziamo la situazione seguente: c'è un bel pezzo di ragazza che mi aspetta e io sono ancora incatenato al mio posto di lavoro senza potermene andare perché quel gran rompiscatole del capo (capo-palloso è una tautologia, no? :)), è ancora nella sua stanza che si trova esattamente tra il mio ufficio e l'uscita.

Il capo ha già drizzato le antenne (probabilmente piantategli in testa dalla moglie) dopo che, per la quinta volta, mi sono affacciato nel suo ufficio per vedere se se n'era andato. Quindi sono tornato alla mia scrivania e ho caricato nel server questo script:

$ cat logaut.sh #!/bin/bash

while who | grep capo do sleep 30 done echo Il rompipalle se n'è andato, non aspettare oltre, esci e buttati nella mischia

In questo piccolo script, il comando while testa la pipeline composta da who e grep e che sarà vera finché grep non incontrerà la parola capo nell'output di who. In questo modo, lo script dormirà per 30 secondi fintanto che il capo è loggato (Argh!). Non appena si scollegherà dal server, il flusso dello script uscirà dal loop e restituirà il tanto atteso messaggio di libertà.

Indovina cos'è accaduto quando l'ho mandato in esecuzione?

$ logaut.sh capo pts/0 Jan 4 08:46 (10.2.4.144) capo pts/0 Jan 4 08:47 (10.2.4.144) ... capo pts/0 Jan 4 08:52 (10.2.4.144)

Ogni 30 secondi l'output di grep è inviato allo schermo, e ciò non è un bene perché sporca continuamente lo schermo del mio pc e il messaggio tanto atteso potrebbe non essere visto. Per evitare che questo accada, sappiamo già che l'output della pipeline deve essere redirezionato in /dev/null (un altro modo per fare la stessa cosa è utilizzare l'opzione -q (quiet) di grep, ma funziona soltanto nello GNU grep e non in UNIX).

$ cat logaut.sh #!/bin/bash

while who | grep capo > /dev/null do sleep 30 done echo Il rompipalle se n'è andato, non aspettare oltre, esci e buttati nella mischia

Adesso voglio costruire uno script che riceva il nome (ed eventuali parametri) di un programma che sarà eseguito in background e che mi informi della fine di quest'ultimo. Tuttavia, affinché tu comprenda questo esempio, devo prima mostrarti una nuova variabile di sistema. Osserva questi comandi diretti nel prompt:

$ sleep 10& [1] 16317 $ echo $! 16317 [1]+ Done sleep 10 $ echo $! 16317

Ho generato un processo in background che dorme per 10 secondi soltanto per farti vedere che la variabile $! conserva il PID (Process IDentification) dell'ultimo processo in background, ma osserva che, dopo la riga contenente il comando done, la variabile ne ha mantenuto il valore subito dopo il termine del processo.

Sapendo ciò, è piuttosto facile controllare qualsiasi processo in background. Osserva attentamente:

$ cat monbg.sh #!/bin/bash

# Esegue e controlla un # processo in background

$1 & # Mette in backgroud while ps | grep -q $! do sleep 5 done echo Fine del Processo $1

Questo script è piuttosto simile al precedente, ma contiene qualche trucchetto in più: esso deve essere eseguito in background per non impegnare il prompt, ma $! conterrà il PID del programma passato come parametro poiché è stato messo in background dopo il monbg.sh vero e proprio. Osserva anche l'opzione -q (quiet) di grep: essa serve a trasformarlo in un comando “sotterraneo”, cioè a far sì che grep "lavori in silenzio". Otterremmo lo stesso risultato se la riga fosse while ps | grep $! > /dev/null, come negli esempi visti finora.

Pinguim com placa de dica (em inglês) Non dimenticare: Bash rende disponibile la variabile $! che contiene il PID (Process IDentification) dell'ultimo processo eseguito in background.

Adesso proviamo a migliorare insercanz, il nostro programma per l'inserimento di record nel file canzoni, ma prima ho bisogno di insegnarti a catturare un dato dallo schermo. Però ti avviso: ti darò solo un'infarinatura del comando read (che è esattamente il comando che cattura i dati dallo schermo) sufficiente a risolvere il nostro problema. Ti insegnerò tutto al riguardo, compreso come formattare lo schermo, in un altro giro di bevute, ma adesso parliamo di loop.

La sintassi del comando read che oggi ci interessa è la seguente:

$ read -p "prompt di lettura" var

Prompt di lettura è il testo che vuoi che appaia sullo schermo e, quando l'utente immetterà il dato, questo sarà immagazzinato nella variabile var. Per esempio:

$ read -p "Titolo dell'Album: " Tit

Detto questo, definiamo il nostro problema: creeremo un programma che inizialmente leggerà il titolo dell'album, quindi eseguirà un loop di lettura catturando il titolo della canzone e il nome dell'artista. Il loop terminerà quando sarà inserito un titolo vuoto, cioè quando l'operatore digiterà semplicemente <ENTER> alla richiesta di un titolo. Per rendere più semplice la vita all'operatore, proporremo come default lo stesso artista della canzone precedente (poiché è normale che l'album sia tutto dello stesso artista) fino a quando non voglia modificarlo. Vediamo come si presenta:

$ cat insercanz #!/bin/bash # Registra CD (versione 4) # clear read -p "Titolo dell'Album: " Tit [ "$Tit" ] || exit 1 # Fine dell'esecuzione se il titolo è vuoto if grep "^$Tit\^" canzoni > /dev/null then echo Questo album è già stato registrato exit 1 fi Reg="$Tit^" Cont=1 oArt= while true do echo Dati della traccia $Cont: read -p "Canzone: " Can [ "$Can" ] || break # Esce se vuoto read -p "Artista: $oArt // " Art [ "$Art" ] && oArt="$Art" # Se vuoto Artista precedente Reg="$Reg$oArt~$Can:" # Creazione del record Cont=$((Cont + 1)) # La riga precedente poteva anche essere ((Cont++)) done echo "$Reg" >> canzoni sort canzoni -o canzoni

L'esempio inizia con la lettura del titolo dell'album che, se non inserito, farà terminare l'esecuzione del programma. Subito dopo, il comando grep cerca l'inizio (^) di ogni record di canzoni e il titolo seguito dal separatore (^) (il quale è preceduto da una barra inversa (\) per proteggerlo dall'interpretazione della Shell).

Per leggere i nomi degli artisti e le canzoni dell'album è stato creato semplice un loop di while , la cui unica particolarità è il fatto che il nome dell'artista della canzone precedente è immagazzinato nella variabile $oArt il cui contenuto varierà soltanto quando sarà immesso un dato nella variabile $Art, cioè quando non si digiterà semplicemente <ENTER> per mantenere l'artista precedente.

Quanto abbiamo visto finora su while è assai poco. Questo comando è molto utilizzato, specialmente per la lettura di file, tuttavia ci mancano informazioni per proseguire. Una volta imparato a eseguire una lettura, vederemo questa istruzione più a fondo.

Pinguim com placa de dica (em inglês) Lettura di un file significa leggerne i record uno per uno, operazione sempre assai lenta. Attento a non utilizzare while quando non è necessario. La Shell possiede strumenti come sed e la famiglia grep che setacciano i file in modo ottimizzato senza che sia necessario l'uso di comandi di loop per l'analisi record per record (o addirittura parola per parola).

Il comando until

Il comando until funziona esattamente come while, ma al contrario. Ho detto tutto ma non ho detto niente, vero? Allora: entrambi testano comandi, entrambi hanno la stessa sintassi ed entrambi funzionano in loop, tuttavia, mentre while esegue il blocco di istruzioni di loop finché un comando è eseguito con successo, until esegue il blocco di loop fino a quando il comando non è eseguito con successo. Sembra una cosa da niente, ma la differenza è fondamentale.

La sintassi del comando è praticamente la stessa di while. Osserva:

    until comando
    do
        cmd1
        cmd2
        ...
        cmdn
    done

In tal modo il blocco di comandi formato dalle istruzioni cmd1, cmd2,..., cmdn, è eseguito fino a quando l'esecuzione dell'istruzione comando non ha successo.

Come ti ho detto, while e until funzionano in modo opposto e ciò è molto facile da dimostrare: in guerra, ogni volta che si inventa una nuova arma, il nemico cerca la soluzione per neutralizzarla. In base a questo principio bellico, il mio capo ha sviluppato, sullo stesso serve in cui girava logaut.sh, uno script per controllare il mio orario di arrivo.

Un giorno si è verificato un problema di rete e il capo mi ha chiesto di dare un'occhiata al suo computer lasciandomi solo nella sua stanza. Mi sono messo subito a curiosare tra i suoi file – la guerra è guerra – e guarda un po' cosa ho scoperto:

$cat arrivo.sh #!/bin/bash

until who | grep julio do sleep 30 done echo $(date "+ Il %d/%m alle %H:%M") >> recidivo.log

Che carogna! Il maledetto stava generando un log con i miei orari d'arrivo e, in più, ha chiamato il file che mi monitorava recidivo.log! Che cosa avrà voluto dire?

In questo script, la pipeline who | grep julio, sarà eseguita con successo solo quando comparirà julio all'interno del comando who, cioè quando mi “loggherò” nel server. Fin quando ciò non accadrà, il comando sleep, che forma il blocco di istruzioni di until, metterà il programma in attesa per 30 secondi. Quando il loop si chiuderà, sarà inviato un messaggio a recidivo.log (ARGHH!). Supponendo che il giorno 20/01 mi sia loggato alle 11:23, il messaggio sarà il seguente:

     Il 20/01 alle 11:23

Quando registriamo delle canzoni, l'ideale sarebbe poter inserire diversi CD, ma nell'ultima versione di insercanz ciò non accade e a ogni CD registrato il programma termina. Vediamo come migliorarlo:

$ cat insercanz #!/bin/bash # Registra CD (versione 5) # Ferma= until [ "$Ferma" ] do clear read -p "Titolo dell'Album: " Tit if [ ! "$Tit" ] # Se titolo è vuoto... then Ferma=1 # Inserito flag di uscita else if grep "^$Tit\^" canzoni > /dev/null then echo Questo album è già stato registrato exit 1 fi Reg="$Tit^" Cont=1 oArt= while [ "$Tit" ] do echo Dati della Traccia $Cont: read -p "Canzone: " Canz [ "$Canz" ] || break # Esce se vuoto read -p "Artista: $oArt // " Art [ "$Art" ] && oArt="$Art" # Se vuoto Artista precedente Reg="$Reg$oArt~$Canz:" # Creazione del record Cont=$((Cont + 1)) # La riga precedente poteva anche essere ((Cont++)) done echo "$Reg" >> canzoni sort canzoni -o canzoni fi done

In questa versione, è stato aggiunto un grande loop prima della lettura del titolo e che terminerà soltanto quando la variabile $Ferma non sarà più vuota. Nel caso in cui il titolo dell'album non sia digitato, la variabile $Ferma sarà valorizzata (ho messo 1 ma avrei potuto mettere qualsiasi cosa, l'importante è che la variabile non sia vuota) per uscire dal loop, terminando così il programma. Per il resto, lo script è identico alla sua versione precedente.

Scorciatoie nei loop

Non sempre un ciclo di programma compreso tra un do e un done esce dalla porta principale. In certi casi può essere necessario inserire un comando che interrompa in modo controllato il loop. All'opposto, a volte vorremmo che il flusso di esecuzione del programma rientrasse prima di arrivare al done. Per fare ciò abbiamo, rispettivamente, i comandi break (che già abbiamo visto rapidamente negli esempi del comando while) e continue. Il loro funzionamento è il seguente:

Ciò che prima non ho detto è che, nella loro sintassi generica, essi hanno la seguente forma:

     break [qtd loop]

e

     continue [qtd loop]

In essa, qtd loop rappresenta la quantità di loop interni sui quali i comandi agiranno. Il suo valore di default è 1.

Fluxograma

Dubito che tu non abbia mai cancellato un file e subito dopo ti sia mangiato le mani lamentandoti perché non avresti dovuto eliminarlo. Dopo aver fatto questa bestialità per la decima volta, ho creato uno script per simulare un cestino, cioè, quando cancello uno o più file, il programma "finge" di rimuoverli, ma in realtà li spedisce nella directory /tmp/LoginNome_Utente. Ho chiamato il programma erreemme e in /etc/profile ho inserito questa riga:

     alias rm=erreemme

Il programma è il seguente:

$ cat erreemme #/bin/bash # # Salvataggio Copia File Prima di Rimuoverlo #

if [ $# -eq 0 ] # Devono esserci uno o più file da rimuovere then echo "Errore -> Uso: erreemme file [file] ... [file]" echo " L'uso di metacaratteri è; permesso. Es. erreemme file*" exit 1 fi

MiaDir="/tmp/$LOGNAME" # Variabile di sist. Contiene il nome dell'utente. if [ ! -d $MiaDir ] # Se la mia directory in /tmp non esiste... then mkdir $MiaDir # ...la creo! fi

if [ ! -w $MiaDir ] # Se non posso scrivere nella directory... then echo Impossibile salvare file in $MiaDir. Modifica i permessi... exit 2 fi

Errore=0 # Variabile per indicare il codice di ritorno del prog for File # For senza "in" riceve i Parametri passati do if [ ! -f $File ] # Se questo file non esiste... then echo $File non esiste. Errore=3 continue # Ritorna al comando for fi

DirOrig=`dirname $File` # Il comando dirname restituisce il nome della directory di $File if [ ! -w $DirOrig ] # Verifica permesso di scrittura nella directory then echo Non si hanno i permessi di scrittura nella directory di $File Erro=4 continue # Torna al comando for fi

if [ "$DirOrig" = "$MiaDir" ] # Se sto "svuotando il cestino"... then echo $File non ha alcuna copia di sicurezza rm -i $File # Domanda prima della rimozione [ -f $File ] || echo $File rimosso # L'utente lo avrà forse già rimosso? continue fi

cd $DirOrig # Salvo alla fine del File la sua directory pwd >> $File # originaria per utilizzarla in uno script di anti-cancellazione mv $File $MiaDir # Salvo e rimuovo echo $File rimosso done exit $Errore # Passo l'eventuale numero di errore al codice di ritorno

Come puoi vedere, la maggior parte dello script è formata da piccole verifiche ai parametri inseriti, ma poiché lo script può aver ricevuto diversi file da rimuovere, per ogni file che non rientra nelle specifiche c'è un continue affinché la sequenza torni al loop di for in modo da ricevere altri file.

Quando traffichi con Windows (chiedo scusa per l'orrenda parola) e tenti di rimuovere quella montagna di robaccia dai nomi strani come HD04TG.TMP, se uno di essi genera un errore, gli altri non vengono rimossi, giusto? Invece è stato utilizzato continue proprio per evitare che accada una mostruosità del genere, cioè, anche se si verifica un errore nella rimozione di un file, il programma andrà avanti rimuovendo tutti gli altri.

     - Credo che adesso tu sia curioso di vedere il programma che ripristina il file rimosso, vero? Allora ti propongo una sfida: scrivilo a casa e portamelo la prossima volta qui al bar così ne parliamo.

     - Diamine, stavolta sono nei guai, non so neanche come iniziare...

     - Ascolta, questo programma è come ogni altra cosa che si fa nella Shell, estremamente facile e al massimo in 10 righe. Non dimenticare che il file è salvato in /tmp/$LOGNAME e che la sua ultima riga è la directory nella quale si trovava prima di essere “rimosso". Non dimenticare di verificare se è stato inserito in nome del file che deve essere rimosso.

     - Ci provo, ma non so se...

     - Abbi fede amico mio, ti dico che è facile facile! Se hai dubbi inviami una email all'indirizzo julio.neves@gmail.com. Adesso basta chiacchiere, ho la gola secca per il tanto parlare. Mi fai compagnia con una birra o corri a fare lo script che ti ho assegnato?

     - Fammi pensare...

     - Chico, una birra mentre il mio amico ci pensa su!

Come puoi vedere, la maggior parte dello script è formata da piccole verifiche ai parametri inseriti, ma poiché lo script può aver ricevuto diversi file da rimuovere, per ogni file che non rientra nelle specifiche c'è un continue affinché la sequenza torni al loop di for in modo da ricevere altri file.

Quando traffichi con Windows (chiedo scusa per l'orrenda parola) e tenti di rimuovere quella montagna di robaccia dai nomi strani come HD04TG.TMP, se uno di essi genera un errore, gli altri non vengono rimossi, giusto? Invece è stato utilizzato continue proprio per evitare che accada una mostruosità del genere, cioè, anche se si verifica un errore nella rimozione di un file, il programma andrà avanti rimuovendo tutti gli altri.

     - Credo che adesso tu sia curioso di vedere il programma che ripristina il file rimosso, vero? Allora ti propongo una sfida: scrivilo a casa e portamelo la prossima volta qui al bar così ne parliamo.

     - Diamine, stavolta sono nei guai, non so neanche come iniziare...

     - Ascolta, questo programma è come ogni altra cosa che si fa nella Shell, estremamente facile e al massimo in 10 righe. Non dimenticare che il file è salvato in /tmp/$LOGNAME e che la sua ultima riga è la directory nella quale si trovava prima di essere “rimosso". Non dimenticare di verificare se è stato inserito in nome del file che deve essere rimosso.

     - Ci provo, ma non so se...

     - Abbi fede amico mio, ti dico che è facile facile! Se hai dubbi inviami una email all'indirizzo julio.neves@gmail.com. Adesso basta chiacchiere, ho la gola secca per il tanto parlare. Mi fai compagnia con una birra o corri a fare lo script che ti ho assegnato?

     - Fammi pensare...

     - Chico, una birra mentre il mio amico ci pensa su!

 
This site is powered by FoswikiCopyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding Wiki-SL? Send feedback