Difference: TWikiBarChiacchiere008 ( vs. 1)

Revision 127 Feb 2013 - AlbertoTaddei

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

Chiacchiere da Bar Parte VIII



     - Allora, tutto a posto?

     - Meglio di così si muore! Vorrei farti vedere quello che ho fatto ma so già che prima vuoi bagnarti l'ugola, sbaglio?

     - Tanto per fare il bastian contrario, oggi ti permetterò di farmi vedere subito il tuo "accrocco". Avanti, mostramelo.

     - Beh, l'esercizio che mi hai assegnato è piuttosto corposo. Guarda come l'ho svolto:

$ cat inscanz5 #!/bin/bash # Registra CD (versione 5) # clear RigaMesg=$((`tput lines` - 3)) # Riga in cui compariranno i messaggi per l'operatore TotColonne=$(tput cols) # Numero di colonne a schermo per centrare i messaggi echo "   Inserimento Canzoni     ======== == =======       Titolo dell'Album:   | Questo campo è stato   Fascia: < creato solamente come   | guida per l'inserimento   Titolo della Canzone:     Interprete:" # Schermo montato con un unico echo while true do tput cup 5 38; tput el # Posiziona e pulisce la riga read Album [ ! "$Album" ] && # L'operatore ha premuto <ENTER> { Msg="Vuoi Interrompere? (S/n)"         LungMsg=${#Msg} Col=$(((TotColonne - LungMsg) / 2)) # Centra msg nella riga tput cup $RigaMesg $Col echo "$Msg" tput cup $RigaMesg $((Col + LungMsg + 1)) read -n1 SN tput cup $RigaMesg $Col; tput el # Cancella il messaggio dallo schermo [ $SN = "N" -o $SN = "n" ] && continue # $SN è uguale a N oppure (-o) no? clear; exit # Fine dell'esecuzione } grep "^$Album\^" canzoni > /dev/null && { Msg="Questo album è già stato inserito"         LungMsg=${#Msg} Col=$(((TotColonne - LungMsg) / 2)) # Centra il messaggio nella riga tput cup $RIgaMesg $Col echo "$Msg" read -n1 tput cup $RIgaMesg $Col; tput el # Cancella il messaggio dallo schermo continue # Torna a leggere un altro album } Reg="$Album^" # $Reg riceverà i dati per registrare l'album oArtista= # Variabile che contiene l'artista precedente while true do ((Fascia++)) tput cup 7 38 echo $Fascia tput cup 9 38 # Posizionamento per la lettura della Canzone read Canzone [ "$Canzone" ] || # Se l'operatore ha premuto <ENTER>... { Msg="Fine dell'Album? (S/n)"             LungMsg=${#Msg} Col=$(((TotColonne - LungMsg) / 2)) # Centra il messaggio nella riga tput cup $RigaMesg $Col echo "$Msg" tput cup $RigaMesg $((Col + LungMsg + 1) read -n1 SN tput cup $RIgaMesg $Col; tput el # Cancella il messaggio dallo schermo [ "$SN" = N -o "$SN" = n ]&&continue # $SN è uguale a N oppure (-o) no? Break # Esce dal loop per registrare } tput cup 11 38 # Posizionamento per la lettura dell'Artista [ "$oArtista" ]&& echo -n "($oArtista) " # L'Artista precedente è di default read Artista [ "$Artista" ] && oArtista="$Artista" Reg="$Reg$oArtista~$Canzone:" # Costruzione del registro tput cup 9 38; tput el # Cancella Canzone dallo schermo tput cup 11 38; tput el # Cancella Artista dallo schermo done echo "$Reg" >> canzoni # Salva il record alla fine del file sort canzoni -0 canzoni # Classifica il file done

     - Il programma è a posto, davvero ben fatto, ma mi piacerebbe qualche commento riguardo a ciò che hai fatto:

  • Solo come promemoria, i seguenti costrutti: [ ! $Album ] && e [ $Canzone ] || rappresentano la stessa cosa, cioè, nel caso della prima, verifichiamo se la variabile $Album non (!) contiene niente, quindi (&&) ... Nel caso della seconda, verifichiamo se $Canzone contiene dati, altrimenti (||) ...
  • Se ti lamenti della sua lunghezza, è perché non ti ho dato ancora nessuna dritta. Osserva che la maggior parte dello script serve per mostrare dei messaggi centrati nella penultima riga dello schermo. Osserva anche che alcuni messaggi richiedono la digitazione di S o N, altri sono solo avvertimenti. Questo è il tipico caso dell'uso di funzioni scritte solo una volta e richiamate da diversi punti dello script. Costruirò due funzioni per risolvere questi casi e le inseriremo poi nel tuo programma per vedere il risultato finale.

Funzioni

     - Chico! Due birre alla spina, una senza schiuma per darmi l'ispirazione.

    Domanda ()
        {
        #  La funzione riceve 3 parametri in questo ordine:
        #  $1 &#8211; Il messaggio da mostrare sullo schermo
        #  $2 &#8211; Il valore accettato come default
        #  $3 - L'altro valore accettato
        #  Supponendo che $1=Accetti?, $2=s e $3=n, la riga seguente
        #  inserirà in Msg il valore "Accetti? (S/n)"
        local Msg="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)"
        local LungMsg=${#Msg}
        local Col=$(((TotColonne - LungMsg) / 2))  # Centra il msg nella riga
        tput cup $RigaMesg $Col
        echo "$Msg"
        tput cup $RigaMesg $((Col + LungMsg + 1))
        read -n1 SN
        [ ! $SN ] && SN=$2                     # Se vuota inserisce il valore di default in SN
        echo $SN | tr A-Z a-z                  # L'output di SN sarà in caratteri minuscoli
        tput cup $RigaMesg $Col; tput el      # Cancella il msg dallo schermo
        return                                 # Esce dalla funzione
        }

Come possiamo vedere, una funzione è definita quando immettiamo nome_della_funzione () e il suo corpo si trova racchiuso tra parentesi graffe ({}). Così come noi conversiamo qui al Bar sul passaggio di parametri, le funzioni li ricevono nello stesso modo, cioè sono parametri posizionali ($1, $2, ..., $n) e tutte le regole che si applicano al passaggio di parametri ai programmi valgono anche per le funzioni, ma è molto importante sottolineare che i parametri passati a un programma non si confondono con quelli che il programma passa alle sue funzioni. Ciò significa, per esempio, che il parametro $1 di uno script è diverso dal parametro $1 di una delle sue funzioni.

Osserva che le variabili $Msg, $LungMsg e $Col sono per uso esclusivo da parte di questa routine e per questo sono state create come local. Lo scopo di tutto ciò è semplicemente risparmiare memoria poiché, nel momento in cui usciremo dalla routine, le variabili saranno opportunamente annullate e, nel caso in cui non avessi usato questo escamotage, rimarrebbero in memoria.

La riga di codice che genera local Msg fa seguire al testo ricevuto ($1) una parentesi aperta, la risposta di default ($2) in maiuscolo, una barra, l'altra risposta ($3) in minuscolo e una parentesi aperta. Uso questa convenzione per mostrare le opzioni disponibili e, allo stesso tempo, per evidenziare la risposta offerta come default.

Quasi alla fine della routine, la risposta ricevuta ($SN) è trasformata in minuscolo per evitare di eseguire una verifica in tal senso all'interno del programma.

Osserva adesso come modificare la funzione affinché compaia un messaggio sullo schermo:

    function InviaMsg
        {
        # La funzione riceve solamente un parametro
        # con il messaggio che si vuole visualizzare,
        # per non obbligare il programmatore a passare
        # il messaggio tra virgolette useremo $* (tutti
        # i parametri, ricordi?) e non $1.
        local Msg="$*"
        local LungMsg=${#Msg}
        local Col=$(((TotColonne - LungMsg) / 2)) # Centra il messaggio nella riga
        tput cup $RigaMesg $Col
        echo "$Msg"
        read -n1
        tput cup $RigaMesg $Col; tput el     # Cancella il messaggio dallo schermo
        return                                # Esce dalla funzione
        }

Questo è un altro modo di definire una funzione: non la nominiamo, come nell'esempio precedente, usando una costruzione con sintassi nome_della_funzione (), ma come function nome_della_funzione. Non ci sono differenze con la prima modalità, tranne per il fatto che, come si evince dai commenti, si utilizza la variabile $* che, come sappiamo, è l'insieme di tutti i parametri passati, così che il programmatore non sia costretto a usare le virgolette per racchiudere il messaggio che vuole passare alla funzione.

Per concludere il discorso, vediamo le modifiche da apportare al programma quando utilizziamo il concetto di funzioni:

$ cat insercanz6 #!/bin/bash # Registra CD (versione 6) #

# Area delle variabili globali RigaMessaggi=$((`tput righe` - 3)) # Riga in cui compariranno i messaggi per l'operatore TotColonne=$(tput colonne) # Numero di colonne per centrare i messaggi # Area delle funzioni Domanda () { # La funzione riceve 3 parametri in quest'ordine: # $1 – Messaggio da mostrare sullo schermo # $2 - Valore accettato come default # $3 – Altro valore accettato # Supponendo che $1=Accetti?, $2=s e $3=n, la riga # sottostante collocherebbe in Msg il valore "Accetti? (S/n)" local Msg="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)" local LungMsg=${#Msg} local Col=$(((TotColonne - LungMsg) / 2)) # Centra il messaggio nella riga tput cup $RigaMesg $Col echo "$Msg" tput cup $RigaMesg $((Col + LungMsg + 1)) read -n1 SN [ ! $SN ] && SN=$2 # Se vuoto inserisce il valore default in SN echo $SN | tr A-Z a-z # L'output di SN sarà in carattere minuscolo tput cup $RigaMesg $Col; tput el # cancella il messaggio dallo schermo return # Esce dalla funzione } function InviaMsg { # La funzione riceve soltanto un parametro # con il messaggio che si vuole mostrare e # per non obbligare il programmatore a passare # il messaggio tra virgolette, useremo $* (tutti # i parametri, ricordi?) invece di $1. local Msg="$*" local LungMsg=${#Msg} local Col=$(((TotColonne - LungMsg) / 2)) # Centra il messaggio nella riga tput cup $RigaMesg $Col echo "$Msg" read -n1 tput cup $RigaMesg $Col; tput el # Cancella il messaggio dallo schermo return # Esce dalla funzione }

# Il corpo del programma vero e proprio inizia qui clear echo " Inserimento Canzoni     ======== == =======       Titolo dell'Album:   | Questo campo è stato   Traccia: < creato solamente come   | guida nell'inserimento   Titolo della Canzone:     Interprete:" # Schermo montato con un unico echo while true do tput cup 5 38; tput el # Posiziona e pulisce la riga read Album [ ! "$Album" ] && # L'operatore ha premuto <ENTER> { Domanda "Vuoi Uscire dal Programma?" s n [ $SN = "n" ] && continue # Adesso solo testo minuscolo clear; exit # Fine dell'esecuzione } grep -iq "^$Album\^" canzoni 2> /dev/null && {     InviaMsg Questo album è già stato registrato continue # Torna a leggere un altro album } Reg="$Album^" # $Reg riceverà i dati della registrazione oArtista= # Salverà l'artista precedente while true do ((Traccia++)) tput cup 7 38 echo $Traccia tput cup 9 38 # Posizionamento per leggere la canzone read Canzone [ "$Canzone" ] || # Se l'operatore ha premuto <ENTER>... { Domanda "Fine dell'Album?" s n [ "$SN" = n ] && continue # Adesso solo testo minuscolo break # Esce dal loop per scrivere i dati } tput cup 11 38 # Posizionamento per legger l'Artista [ "$oArtista" ]&& echo -n "($oArtista) " # L'Artista precedente è il valore di default read Artista [ "$Artista" ] && oArtista="$Artista" Rec="$Rec$oArtista~$Canzone:" # Creazione del record tput cup 9 38; tput el # Cancella la Canzone dallo schermo tput cup 11 38; tput el # Cancella l'Artista dallo schermo done echo "$Rec" >> canzoni # Salva il record alla fine del file sort canzoni -o canzoni # Aggiorna il file done

Osserva che la struttura dello script è conforme al grafico che segue:

  Corpo del Programa  
Variabili Globali
Funzioni

Una simile struttura è dovuta al fatto che la Shell è un linguaggio interpretato, per cui il programma è letto da sinistra verso destra e dall'alto verso il basso e, affinché una variabile sia vista simultaneamente dallo script e dalle sue funzioni, deve essere dichiarata (o inizializzata) prima di ogni altra cosa. Le funzioni, a loro volta, devono essere dichiarate prima del corpo del programma propriamente detto poiché, nel momento in cui il programmatore le menziona, l'interprete Shell ha già localizzato il programma e lo ha registrato come una funzione.

Una cosa divertente dell'uso di funzioni è farle più generiche possibile così che esse possano essere utilizzate in altre applicazioni senza doverle riscrivere. Le due funzioni che abbiamo appena visto hanno un uso generico poiché è difficile creare uno script che abbia come input dei dati dallo schermo e che sia privo di una routine del tipo di InviaMsg o che non interagisca con l'operatore tramite qualcosa di simile a Domanda.

Un consiglio da amico: genera un file e aggiungi ad esso ogni nuova funzione che crei. Dopo un po' di tempo avrai una bella biblioteca di funzioni che ti farà risparmiare tempo.

Il comando source

Guarda se noti qualcosa di diverso nel seguente output di ls:

$ ls -la .bash_profile -rw-r--r-- 1 Julio unknown 4511 Mar 18 17:45 .bash_profile

Non guardare la risposta e presta attenzione! Beh, visto che non ti va di arrovellarti e preferisci leggere la risposta, ti do un suggerimento: penso che tu sappia che .bash_profile è uno dei programmi “eseguiti” automaticamente quando ti logghi (ARRGGHH! Odio questa parola). Adesso che ti ho dato questo suggerimento, osserva di nuovo l'output di ls e dimmi cosa ci trovi di diverso.

Come ti ho detto, .bash_profile è "eseguito" al momento del logon e nota bene che non ha alcun diritto di esecuzione. Ciò accade perché se tu lo mandassi in esecuzione come qualsiasi altro script, una volta terminata la sua esecuzione tutto l'ambiente da esso generato morirebbe assieme alla Shell dentro la quale è stato eseguito (ricordi che tutti gli script sono eseguiti in una sottoshell, vero?).

Bene: è proprio per questo che esiste il comando source, conosciuto anche come . (punto). Questo comando fa sì che non sia creata una nuova Shell (una sottoshell) per eseguire il programma che gli viene passato come parametro.

È meglio un esempio di mille parole. Osserva il piccolo script che segue:

$ cat script_scemo cd .. ls

Questo script ci riporta nella directory superiore rispetto a quella in cui ci troviamo. Proviamo a eseguire alcuni comandi che richiamano script_scemo e analizziamone i risultati:

$ pwd /home/jneves $ script_scemo jneves juliana paula silvie $ pwd /home/jneves

Se gli ho imposto di salire nella directory superiore, perché non l'ha fatto? Ma certo che l'ha fatto! La sottoshell creata per eseguire lo script è salita tanto che ha elencato le directory dei quattro utenti presenti in /home, soltanto che, una volta terminato lo script, la sottoshell è scomparsa assieme all'ambiente da esso generato. Adesso guarda come possiamo cambiare le cose:

$ source script_scemo jneves juliana paula silvie $ pwd /home $ cd - /home/jneves $ . script_scemo jneves juliana paula silvie $ pwd /home

Ahh! Adesso sì che va bene! Essendo passato come parametro del comando source o . (ponto), lo script è stato eseguito nella Shell corrente lasciando in essa tutto l'ambiente generato. Adesso ritorniamo all'inizio della spiegazione di questo comando. Allora abbiamo parlato del .bash_profile, e a questo punto dovresti sapere che il suo compito, subito dopo il login, è quello di lasciare l'ambiente di lavoro pronto per l'utente: adesso possiamo capire che che è proprio per questo che è eseguito utilizzando questo artificio.

A questo punto ti starai chiedendo se il comando serve soltanto a questo, e la mia risposta è sì, ma ciò ci dà un sacco di vantaggi, il più utilizzato dei quali è quello di trattare le funzioni come routine esterne. Guarda come possiamo costruire il nostro programma di inserimento dei CD nel file canzoni in modo diverso.

$ cat insercanz7 #!/bin/bash # Registra CD (versione 7) # # Area delle variabili globali RigaMesg=$((`tput lines` - 3)) # Riga in cui compariranno i messaggi per l'operatore TotColonne=$(tput colonne) # Numero di colonne a schermo par centrare i messaggi # Il corpo del programma propriamente detto inizia qui cleatr echo " Inserimento Canzoni     ======== == =======       Titolo dell'Album:   | Questo campo è stato   Traccia: < creato solamente come   | guida nell'inserimento   Titolo della Canzone:     Interprete:" # Schermo montato con un unico echo while true do tput cup 5 38; tput el # Posiziona e pulisce la riga read Album [ ! "$Album" ] && # L'operatore ha premuto { source domanda.funz "Vuoi uscire dal programma" s n [ $SN = "n" ] && continue # Adesso solo testo minuscolo clear; exit # Fine dell'esecuzione } grep -iq "^$Album\^" canzoni 2> /dev/null && { . inviamsg.funz Questo album è già stato registrato continue # Torna a leggere un altro album } Reg="$Album^" # $Reg riceverà i dati della registrazione oArtista= # Salverà l'artista precedente while true do ((Traccia++)) tput cup 7 38 echo $Traccia tput cup 9 38 # Posizionamento per leggere la Canzone read Canzone [ "$Canzone" ] || # Se l'operatore ha premuto ... { . domanda.funz "Fine dell'Album?" s n [ "$SN" = n ] && continue # Adesso solo testo minuscolo break # Esce dal loop per scrivere i dati } tput cup 11 38 # Posizionamento per leggere l'Artista [ "$oArtista" ] && echo -n "($oArtista) " # L'Artista precedente è il valore di default read Artista [ "$Artista" ] && oArtista="$Artista" Reg="$Reg$oArtista~$Canzone:" # Creazione del record tput cup 9 38; tput el # Cancella la Canzone dallo schermo tput cup 11 38; tput el # Cancella l'Artista dallo schermo done echo "$Reg" >> canzoni # Salva il record alla fine del file sort canzoni -o canzoni # Aggiorna il file done

Adesso il programma è più snello e le chiamate di funzione sono state sostituite da file esterni chiamati domanda.funz e inviamsg.funz che possono essere richiamati da altri programmi. In questo modo è possibile riutilizzarne il codice.

Per motivi meramente didattici l'esecuzione di domanda.funz e inviamsg.funz sono governate indifferentemente da source e . (ponto), sebbene io preferisca source poiché ha una struttura più lineare che permette una migliore leggibilità del codice e una manutenzione più semplice.

Adesso osserva i due file:

$ cat domanda.funz # La funzione riceve 3 parametri nel seguente ordine: # $1 – Messaggio da mostrare sullo schermo # $2 – Valore da accettare come risposta di default # $3 – L'altro valore accettato # Supponendo che $1=Accetti?, $2=s e $3=n, la riga # sottostante inserirà in Msg il valore "Accetta? (S/n)" Msg="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)" LungMsg=${#Msg} Colonne=$(((TotColonne - LungMsg) / 2)) # Centra msg nella riga tput cup $RigaMesg $Colonne echo "$Msg" tput cup $RigaMesg $((Colonne + RigaMsg + 1)) read -n1 SN [ ! $SN ] && SN=$2 # Se vuota inserisce il valore di default in SN echo $SN | tr A-Z a-z # L'output di SN sarà in minuscolo tput cup $RigaMesg $Col; tput el # Cancella msg dallo schermo

$ cat inviamsg.funz # La funzione riceve soltanto un parametro # con il messaggio che si vuole mostrare, # per non obbligare il programmatore a passare # il messaggio tra virgolette useremo $* (tutti # i parametri, ricordi?) e non $1. Msg="$*" LungMsg=${#Msg} Colonne=$(((TotColonne - LungMsg) / 2)) # Centra msg nella riga tput cup $RigaMesg $Colonne echo "$Msg" read -n1 tput cup $RigaMesg $Col; tput el # Cancella msg dallo schermo

In ognuno dei file ho introdotto soltanto due variazioni che ora spiegherò, ma ho altre tre osservazioni da fare:

  1. Le variabili non sono più dichiarate come local perché questa è una direttiva che può essere utilizzata soltanto all'interno di funzioni: se le dichiarassimo resterebbero nell'ambiente Shell e lo sporcherebbero;
  2. Il comando return non è più presente ma potrebbe esserlo senza alterare in alcun modo la logica poiché esso servirebbe soltanto a indicare un eventuale errore per mezzo di un codice di ritorno stabilito in precedenza (a esempio return 1, return 2, …): tieni presente che return e return 0 sono identici e il loro significato è “routine eseguita senza errori;
  3. Il comando che usiamo di solito per generare un codice di retorno è exit, ma l'output di una routine esterna non può essere creato in questo modo poiché, essendo essa eseguita nella stessa Shell dello script che la richiama, il comando exit chiuderebbe la Shell interrompendo l'esecuzione dell'intero script;
  4. Da dove è spuntata la variabile RigaMesg? Essa proviene da insercanz7, poiché essa era stata dichiarata prima della chiamata delle routine (non scordare che la Shell che sta interpretando lo script e queste routine è la stessa);
  5. Se decidi di utilizzare routine esterne, non andare di fretta, abbonda con i commenti (principalmente sul passaggio dei parametri) per renderne più semplice la manutenzione e il loro uso da parte di altri programmatori.

     - Bene, adesso che hai un sacco di strumenti nuovi per migliorare gli script che abbiamo creato, ricordi il programma elencartista nel quale inserivi il nome di un artista come parametro e ottenevi le sue canzoni? Era fatto così:

$ cat elencartista #!/bin/bash # Dato um artista, mostra le sue canzoni # versione 2

if [ $# -eq 0 ] then echo Devi inserire almeno un parametro exit 1 fi

IFS=" :" for ArtCanz in $(cut -f2 -d^ canzoni) do echo "$ArtCanz" | grep -i "^$*~" > /dev/null && echo $ArtCanz | cut -f2 -d~ done

     - Certo che mi ricordo!...

     - Allora, tanto per consolidare i concetti che ti ho appena spiegato, riscrivilo con una formattazione dello schermo in loop, così che esso termini soltanto quando riceve <ENTER> al posto del nome dell'artista. Ahhh! Quando l'elenco raggiungerà la terzultima riga dello schermo, il programma dovrà fermarsi per permettere all'operatore di leggere i titoli delle canzoni. Per capirci meglio, supponiamo che lo schermo contenga 25 righe: ebbene, ogni 22 titoli elencati (numero delle righe diminuito di 3) il programma attenderà che l'operatore prema un tasto per continuare. Eventuali messaggi di errore devono essere trattati per mezzo della routine inviamsg.funz che abbiamo appena sviluppato.

     - Chico, altre due birre, la mia ormai è sgasata...

 
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