Guida avanzata di scripting Bash: Un'approfondita esplorazione dell'arte dello scripting di shell | ||
---|---|---|
Indietro | Capitolo 15. Filtri, programmi e comandi esterni | Avanti |
Scompone un intero in fattori primi.
bash$ factor 27417 27417: 3 13 19 37 |
Bash non è in grado di gestire i calcoli in virgola mobile, quindi non dispone di operatori per alcune importanti funzioni matematiche. Fortunatamente viene in soccorso bc.
Non semplicemente una versatile utility per il calcolo in precisione arbitraria, bc offre molte delle potenzialità di un linguaggio di programmazione.
bc possiede una sintassi vagamente somigliante al C.
Dal momento che si tratta di una utility UNIX molto ben collaudata, e che quindi può essere utilizzata in una pipe, bc risulta molto utile negli script.
Ecco un semplice modello di riferimento per l'uso di bc per calcolare una variabile di uno script. Viene impiegata la sostituzione di comando.
variabile=$(echo "OPZIONI; OPERAZIONI" | bc) |
Esempio 15-42. Rata mensile di un mutuo
#!/bin/bash # monthlypmt.sh: Calcola la rata mensile di un mutuo (prestito). # Questa è una modifica del codice del pacchetto "mcalc" (mortgage calculator), #+ di Jeff Schmidt e Mendel Cooper (vostro devotissimo, autore di #+ questo documento). # http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz [15k] echo echo "Dato il capitale, il tasso d'interesse e la durata del mutuo," echo "calcola la rata di rimborso mensile." denominatore=1.0 echo echo -n "Inserisci il capitale (senza i punti di separazione)" read capitale echo -n "Inserisci il tasso d'interesse (percentuale)" # Se 12% inserisci "12", #+ non ".12". read t_interesse echo -n "Inserisci la durata (mesi)" read durata t_interesse=$(echo "scale=9; $t_interesse/100.0" | bc) # Lo converte #+ in decimale. # "scale" determina il numero delle cifre decimali. tasso_interesse=$(echo "scale=9; $t_interesse/12 + 1.0" | bc) numeratore=$(echo "scale=9; $capitale*$tasso_interesse^$durata" | bc) echo; echo "Siate pazienti. È necessario un po' di tempo." let "mesi = $durata - 1" # ==================================================================== for ((x=$mesi; x > 0; x--)) do den=$(echo "scale=9; $tasso_interesse^$x" | bc) denominatore=$(echo "scale=9; $denominatore+$den" | bc) # denominatore = $(($denominatore + $den")) done # ==================================================================== # -------------------------------------------------------------------- # Rick Boivie ha indicato un'implementazione più efficiente del #+ ciclo precedente che riduce di 2/3 il tempo di calcolo. # for ((x=1; x <= $mesi; x++)) # do # denominatore=$(echo "scale=9; $denominatore * $tasso_interesse + 1" | bc) # done # Dopo di che se n'è uscito con un'alternativa ancor più efficiente, una che #+ abbatte il tempo di esecuzione di circa il 95%! # denominatore=`{ # echo "scale=9; denominatore=$denominatore; tasso_interesse=$tasso_interesse" # for ((x=1; x <= $mesi; x++)) # do # echo 'denominatore = denominatore * tasso_interesse + 1' # done # echo 'denominatore' # } | bc` # Ha inserito il 'ciclo for' all'interno di una #+ sostituzione di comando. # -------------------------------------------------------------------------- # In aggiunta, Frank Wang suggerisce: # denominatore=$(echo "scale=9; ($tasso_interesse^$mesi-1)/\ # ($tasso_interesse-1)" | bc) # Perché . . . # L'algoritmo che segue il ciclo è, #+ in realtà, la somma di una serie geometrica. # La formula è e0(1-q^n)/(1-q), #+ dove e0 è il primo elemento, mentre q=e(n+1)/e(n) #+ ed n il numero degli elementi. # -------------------------------------------------------------------------- # let "rata = $numeratore/$denominatore" rata=$(echo "scale=2; $numeratore/$denominatore" | bc) # Vengono usate due cifre decimali per i centesimi di Euro. echo echo "rata mensile = Euro $rata" echo exit 0 # Esercizi: # 1) Filtrate l'input per consentire l'inserimento del capitale con i # punti di separazione. # 2) Filtrate l'input per consentire l'inserimento del tasso # d'interesse sia in forma percentuale che decimale. # 3) Se siete veramente ambiziosi, implementate lo script per visualizzare # il piano d'ammortamento completo. |
Esempio 15-43. Conversione di base
#!/bin/bash ################################################################################ # Shellscript: base.sh - visualizza un numero in basi differenti (Bourne Shell) # Autore : Heiner Steven (heiner.steven@odn.de) # Data : 07-03-95 # Categoria : Desktop # $Id : base.sh,v 1.2 2000/02/06 19:55:35 heiner Exp $ # ==> La riga precedente rappresenta l'ID RCS. ################################################################################ # Descrizione # # Changes # 21-03-95 stv fixed error occuring with 0xb as input (0.2) ################################################################################ # ==> Utilizzato in questo documento con il permesso dell'autore dello script. # ==> Commenti aggiunti dall'autore del libro. NOARG=65 NP=`basename "$0"` # Nome del programma VER=`echo '$Revision: 1.2 $' | cut -d' ' -f2` # ==> VER=1.2 Utilizzo () { echo "$NP - visualizza un numero in basi diverse, $VER (stv '95) utilizzo: $NP [numero ...] Se non viene fornito alcun numero, questi vengono letti dallo standard input. Un numero può essere binario (base 2) inizia con 0b (es. 0b1100) ottale (base 8) inizia con 0 (es. 014) esadecimale (base 16) inizia con 0x (es. 0xc) decimale negli altri casi (es. 12)" >&2 exit $NOARG } # ==> Funzione per la visualizzazione del messaggio di utilizzo. Msg () { for i # ==> manca in [lista]. do echo "$NP: $i" >&2 done } Fatale () { Msg "$@"; exit 66; } VisualizzaBasi () { # Determina la base del numero for i # ==> manca in [lista] ... do # ==> perciò opera sugli argomenti forniti da riga di comando. case "$i" in 0b*) ibase=2;; # binario 0x*|[a-f]*|[A-F]*) ibase=16;; # esadecimale 0*) ibase=8;; # ottale [1-9]*) ibase=10;; # decimale *) Msg "$i numero non valido - ignorato" continue;; esac # Toglie il prefisso, converte le cifre esadecimali in caratteri #+ maiuscoli (è richiesto da bc) numero=`echo "$i" | sed -e 's:^0[bBxX]::' | tr '[a-f]' '[A-F]'` # ==> Si usano i ":" come separatori per sed, al posto della "/". # Converte il numero in decimale dec=`echo "ibase=$ibase; $numero" | bc` # ==> 'bc' è l'utility di calcolo. case "$dec" in [0-9]*) ;; # numero ok *) continue;; # errore: ignora esac # Visualizza tutte le conversioni su un'unica riga. # ==> 'here document' fornisce una lista di comandi a 'bc'. echo `bc <<! obase=16; "esa="; $dec obase=10; "dec="; $dec obase=8; "ott="; $dec obase=2; "bin="; $dec ! ` | sed -e 's: : :g' done } while [ $# -gt 0 ] # ==> "Ciclo while" che qui si rivela veramente necessario # ==>+ poiché in ogni caso, o si esce dal ciclo # ==>+ oppure lo script termina. # ==> (Grazie, Paulo Marcel Coelho Aragao.) do case "$1" in --) shift; break;; -h) Utilizzo;; # ==> Messaggio di aiuto. -*) Utilizzo;; *) break;; # primo numero esac # ==> Sarebbe utile un'ulteriore verifica d'errore per un input #+ non consentito. shift done if [ $# -gt 0 ] then VisualizzaBasi "$@" else # legge dallo stdin while read riga do VisualizzaBasi $riga done fi exit 0 |
Un metodo alternativo per invocare bc comprende l'uso di un here document inserito in un blocco di sostituzione di comando. Questo risulta particolarmente appropriato quando uno script ha la necessità di passare un elenco di opzioni e comandi a bc.
variabile=`bc << STRINGA_LIMITE opzioni enunciati operazioni STRINGA_LIMITE ` ...oppure... variabile=$(bc << STRINGA_LIMITE opzioni enunciati operazioni STRINGA_LIMITE ) |
Esempio 15-44. Invocare bc usando un here document
#!/bin/bash # Invocare 'bc' usando la sostituzione di comando # in abbinamento con un 'here document'. var1=`bc << EOF 18.33 * 19.78 EOF ` echo $var1 # 362.56 # $( ... ) anche questa notazione va bene. v1=23.53 v2=17.881 v3=83.501 v4=171.63 var2=$(bc << EOF scale = 4 a = ( $v1 + $v2 ) b = ( $v3 * $v4 ) a * b + 15.35 EOF ) echo $var2 # 593487.8452 var3=$(bc -l << EOF scale = 9 s ( 1.7 ) EOF ) # Restituisce il seno di 1.7 radianti. # L'opzione "-l" richiama la libreria matematica di 'bc'. echo $var3 # .991664810 # Ora proviamolo in una funzione... ip= # Dichiarazione di variabile globale. ipotenusa () # Calcola l'ipotenusa di un triangolo rettangolo. { ip=$(bc -l << EOF scale = 9 sqrt ( $1 * $1 + $2 * $2 ) EOF ) # Sfortunatamente, non si può avere un valore di ritorno in virgola mobile #+ da una funzione Bash. } ipotenusa 3.68 7.31 echo "ipotenusa = $ip" # 8.184039344 exit 0 |
Esempio 15-45. Calcolo del pi greco
#!/bin/bash # cannon.sh: Approssimare il PI a cannonate. # È un esempio molto semplice di una simulazione "Monte Carlo": #+ un modello matematico di un evento reale, utilizzando i numeri #+ pseudocasuali per simulare la probabilità dell'urna. # Consideriamo un appezzamento di terreno perfettamente quadrato, di 10000 #+ unità di lato. # Questo terreno ha, al centro, un lago perfettamente circolare con un #+ diametro di 10000 unità. # L'appezzamento è praticamente tutta acqua, tranne per il terreno ai #+ quattro angoli (Immaginatelo come un quadrato con inscritto un cerchio). # # Spariamo delle palle con un vecchio cannone sul terreno quadrato. Tutti i #+ proiettili cadranno in qualche parte dell'appezzamento, o nel lago o negli #+ angoli emersi. # Poiché il lago occupa la maggior parte dell'area, la maggior #+ parte dei proiettili CADRA' nell'acqua. # Solo pochi COLPIRANNO il terreno ai quattro angoli del quadrato. # # Se le cannonate sparate saranno sufficientemente casuali, senza aver #+ mirato, allora il rapporto tra le palle CADUTE IN ACQUA ed il totale degli #+ spari approssimerà il valore di PI/4. # # La spiegazione sta nel fatto che il cannone spara solo al quadrante superiore #+ destro del quadrato, vale a dire, il 1 Quadrante del piano di assi #+ cartesiani. (La precedente spiegazione era una semplificazione.) # # Teoricamente, più alto è il numero delle cannonate, maggiore #+ sarà l'approssimazione. # Tuttavia, uno script di shell, in confronto ad un linguaggio compilato che #+ dispone delle funzione matematiche in virgola mobile, richiede un po' di #+ compromessi. # Naturalmente, questo fatto tende a diminuire la precisione della #+ simulazione. DIMENSIONE=10000 # Lunghezza dei lati dell'appezzamento di terreno. # Imposta anche il valore massimo degli interi #+ casuali generati. MAXSPARI=1000 # Numero delle cannonate. # Sarebbe stato meglio 10000 o più, ma avrebbe #+ richiesto troppo tempo. # PMULTIPL=4.0 # Fattore di scala per approssimare PI. genera_casuale () { SEME=$(head -n 1 /dev/urandom | od -N 1 | awk '{ print $2 }') RANDOM=$SEME # Dallo script di esempio #+ "seeding-random.sh". let "rnum = $RANDOM % $DIMENSIONE" # Intervallo inferiore a 10000. echo $rnum } distanza= # Dichiarazione di variabile globale. ipotenusa () # Calcola l'ipotenusa di un triangolo rettangolo. { # Dall'esempio "alt-bc.sh". distanza=$(bc -l <<EOF scale = 0 sqrt ( $1 * $1 + $2 * $2 ) EOF ) # Impostando "scale" a zero il risultato viene troncato (vengono eliminati i #+ decimali), un compromesso necessario in questo script. # Purtroppo. questo diminuisce la precisione della simulazione. } # main() { # Inizializzazione variabili. spari=0 splash=0 terra=0 Pi=0 while [ "$spari" -lt "$MAXSPARI" ] # Ciclo principale. do xCoord=$(genera_casuale) # Determina le #+ coordinate casuali X e Y. yCoord=$(genera_casuale) ipotenusa $xCoord $yCoord # Ipotenusa del triangolo #+ rettangolo = distanza. ((spari++)) printf "#%4d " $spari printf "Xc = %4d " $xCoord printf "Yc = %4d " $yCoord printf "Distanza = %5d " $distanza # Distanza dal centro del lago - #+ "origine" degli assi - #+ coordinate 0,0. if [ "$distanza" -le "$DIMENSIONE" ] then echo -n "SPLASH! " ((splash++)) else echo -n "TERRENO! " ((terra++)) fi Pi=$(echo "scale=9; $PMULTIPL*$splash/$spari" | bc) # Moltiplica il rapporto per 4.0. echo -n "PI ~ $Pi" echo done echo echo "Dopo $spari cannonate, $Pi sembra approssimare PI." # Tende ad essere un po' più alto . . . # Probabilmente a causa degli arrotondamenti e dell'imperfetta casualità di #+ $RANDOM. echo # } exit 0 # Qualcuno potrebbe ben chiedersi se uno script di shell sia appropriato per #+ un'applicazione così complessa e ad alto impiego di risorse qual'è una #+ simulazione. # # Esistono almeno due giustificazioni. # 1) Come prova concettuale: per dimostrare che può essere fatto. # 2) Per prototipizzare e verificare gli algoritmi prima della #+ riscrittura in un linguaggio compilato di alto livello. |
L'utility dc (desk calculator) è orientata allo stack e usa la RPN ("Reverse Polish Notation" - notazione polacca inversa). Come bc, possiede molta della potenza di un linguaggio di programmazione.
La maggior parte delle persone evita dc perché richiede un input RPN non intuitivo. Viene, comunque, ancora utilizzata.
Esempio 15-46. Convertire un numero decimale in esadecimale
#!/bin/bash # hexconvert.sh: Converte un numero decimale in esadecimale. E_ERR_ARG=65 # Argomento da riga di comando mancante BASE=16 # Esadecimale. if [ -z "$1" ] then echo "Utilizzo: $0 numero" exit $E_ERR_ARG # È necessario un argomento da riga di comando. fi # Esercizio: aggiungete un'ulteriore verifica di validità dell'argomento. esacvt () { if [ -z "$1" ] then echo 0 return # "Restituisce" 0 se non è stato passato nessun argomento alla #+ funzione. fi echo ""$1" "$BASE" o p" | dc # "o" imposta la radice (base numerica) dell'output. # "p" visualizza la parte alta dello stack. # Vedi 'man dc' per le altre opzioni. return } esacvt "$1" exit 0 |
Lo studio della pagina info di dc fornisce alcuni chiarimenti sulle sue difficoltà. Sembra esserci, comunque, un piccolo, selezionato gruppo di maghi del dc che si deliziano nel mettere in mostra la loro maestria nell'uso di questa potente, ma arcana, utility.
bash$ echo "16i[q]sa[ln0=aln100%Pln100/snlbx]sbA0D68736142snlbxq" | dc" Bash |
Esempio 15-47. Fattorizzazione
#!/bin/bash # factr.sh: Fattorizza un numero MIN=2 # Non funzionerà con con un numero inferiore a questo. E_ERR_ARG=65 E_INFERIORE=66 if [ -z $1 ] then echo "Utilizzo: $0 numero" exit $E_ERR_ARG fi if [ "$1" -lt "$MIN" ] then echo "Il numero da fattorizzare deve essere $MIN o maggiore." exit $E_INFERIORE fi # Esercizio: Aggiungete una verifica di tipo (per rifiutare un argomento #+ diverso da un intero). echo "Fattori primi di $1:" # ----------------------------------------------------------------------------- echo "$1[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr[dli%0=1lrli2+dsi!>.]ds.xd1<2"\ | dc # ----------------------------------------------------------------------------- # La precedente riga di codice è stata scritta da Michel Charpentier # <charpov@cs.unh.edu>. # Usata con il permesso dell'autore (grazie). exit 0 |
Un altro modo ancora per eseguire calcoli in virgola mobile in uno script, è l'impiego delle funzioni matematiche built-in di awk in uno shell wrapper.
Esempio 15-48. Calcolo dell'ipotenusa di un triangolo
#!/bin/bash # hypotenuse.sh: Calcola l'"ipotenusa" di un triangolo rettangolo. # (radice quadrata della somma dei quadrati dei cateti) ARG=2 # Lo script ha bisogno che gli vengano passati i cateti #+ del triangolo. E_ERR_ARG=65 # Numero di argomenti errato. if [ $# -ne "$ARG" ] # Verifica il numero degli argomenti. then echo "Utilizzo: `basename $0` cateto_1 cateto_2" exit $E_ERR_ARG fi SCRIPTAWK=' { printf( "%3.7f\n", sqrt($1*$1 + $2*$2) ) } ' # comando/i / parametri passati ad awk # Ora passiamo, per mezzo di una pipe, i parametri a awk. echo -n "Ipotenusa di $1 e $2 = " echo $1 $2 | awk "$SCRIPTAWK" # ^^^^^^^^^^^^ # echo-e-pipe: un modo facile per passare dei parametri di shell ad awk. exit 0 # Esercizio: riscrivete lo script usando 'bc' al posto di awk. # Qual'è il metodo più intuitivo? |