Guide avancé d'écriture des scripts Bash: | ||
---|---|---|
Précédent | Chapitre 12. Filtres externes, programmes et commandes | Suivant |
Décompose un entier en nombre premiers.
bash$ factor 27417 27417: 3 13 19 37 |
Bash ne peut traiter les calculs en virgule flottante et n'intègre pas certaines fonctions mathématiques importantes. Heureusement, bc est là pour nous sauver.
bc n'est pas simplement une calculatrice souple à précision arbitraire, elle offre aussi beaucoup de facilités disponibles dans un langage de programmation.
La syntaxe de bc ressemble vaguement à celle du C.
bc est devenu un outil UNIX assez puissant pour être utilisé via un tube et est manipulable depuis des scripts.
Ceci est un simple exemple utilisant bc pour calculer la valeur d'une variable. Il utilise lasubstitution de commande.
variable=$(echo "OPTIONS; OPERATIONS" | bc) |
Exemple 12-32. Paiement mensuel sur une hypothèque
#!/bin/bash # monthlypmt.sh: Calculates monthly payment on a mortgage. # This is a modification of code in the "mcalc" (mortgage calculator) package, # by Jeff Schmidt and Mendel Cooper (yours truly, the author of this document). # http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz [15k] echo echo "Given the principal, interest rate, and term of a mortgage," echo "calculate the monthly payment." bottom=1.0 echo echo -n "Enter principal (no commas) " read principal echo -n "Enter interest rate (percent) " # If 12%, enter "12", not ".12". read interest_r echo -n "Enter term (months) " read term interest_r=$(echo "scale=9; $interest_r/100.0" | bc) # Convert to decimal. # "scale" determines how many decimal places. interest_rate=$(echo "scale=9; $interest_r/12 + 1.0" | bc) top=$(echo "scale=9; $principal*$interest_rate^$term" | bc) echo; echo "Please be patient. This may take a while." let "months = $term - 1" # ==================================================================== for ((x=$months; x > 0; x--)) do bot=$(echo "scale=9; $interest_rate^$x" | bc) bottom=$(echo "scale=9; $bottom+$bot" | bc) # bottom = $(($bottom + $bot")) done # -------------------------------------------------------------------- # Rick Boivie pointed out a more efficient implementation #+ of the above loop, which decreases computation time by 2/3. # for ((x=1; x <= $months; x++)) # do # bottom=$(echo "scale=9; $bottom * $interest_rate + 1" | bc) # done # And then he came up with an even more efficient alternative, #+ one that cuts down the run time by about 95%! # bottom=`{ # echo "scale=9; bottom=$bottom; interest_rate=$interest_rate" # for ((x=1; x <= $months; x++)) # do # echo 'bottom = bottom * interest_rate + 1' # done # echo 'bottom' # } | bc` # Embeds a 'for loop' within command substitution. # ==================================================================== # let "payment = $top/$bottom" payment=$(echo "scale=2; $top/$bottom" | bc) # Use two decimal places for dollars and cents. echo echo "monthly payment = \$$payment" # Echo a dollar sign in front of amount. echo exit 0 # Exercises: # 1) Filter input to permit commas in principal amount. # 2) Filter input to permit interest to be entered as percent or decimal. # 3) If you are really ambitious, # expand this script to print complete amortization tables. |
Exemple 12-33. Conversion de base
: ########################################################################## # Script shell: base.sh - affiche un nombre en différentes bases (Bourne Shell) # Auteur : Heiner Steven (heiner.steven@odn.de) # Date : 07-03-95 # Categorie : Desktop # $Id: base.sh,v 1.4 2003/12/08 20:57:22 guillaume Exp $ ########################################################################## # Description # # Modifications # 21-03-95 stv correction d'une erreur arrivant avec 0xb comme entrée (0.2) ########################################################################## # ==> Utilisé dans ce document avec la permission de l'auteur du script. # ==> Commentaires ajoutés par l'auteur du document. NOARGS=65 PN=`basename "$0"` # Nom du programme VER=`echo '$Revision: 1.4 $' | cut -d' ' -f2` # ==> VER=1.2 Usage () { echo "$PN - Affiche un nombre en différentes bases, $VER (stv '95) usage: $PN [nombre ...] Si aucun nombre n'est donné, les nombres sont lus depuis l'entrée standard. Un nombre peut être binaire (base 2) commençant avec 0b (i.e. 0b1100) octal (base 8) commençant avec 0 (i.e. 014) hexadécimal (base 16) commençant avec 0x (i.e. 0xc) décimal autrement (c'est-à-dire 12)" >&2 exit $NOARGS } # ==> Fonction pour afficher le message d'usage. Msg () { for i # ==> [liste] manquante. do echo "$PN: $i" >&2 done } Fatal () { Msg "$@"; exit 66; } AfficheBases () { # Détermine la base du nombre for i # ==> [liste] manquante... do # ==> donc opère avec le(s) argument(s) en ligne de commande. case "$i" in 0b*) ibase=2;; # binaire 0x*|[a-f]*|[A-F]*) ibase=16;; # hexadécimal 0*) ibase=8;; # octal [1-9]*) ibase=10;; # décimal *) Msg "nombre illégal $i - ignoré" continue;; esac # Suppression du préfixe, conversion des nombres hexadécimaux en #+ majuscule (bc a besoin de cela) number=`echo "$i" | sed -e 's:^0[bBxX]::' | tr '[a-f]' '[A-F]'` # ==> Utilise ":" comme séparateur sed, plutôt que "/". # Conversion des nombres en décimal dec=`echo "ibase=$ibase; $number" | bc` # ==> 'bc' est un utilitaire de #+ calcul. case "$dec" in [0-9]*) ;; # nombre ok *) continue;; # erreur: ignore esac # Affiche toutes les conversions sur une ligne. # ==> le 'document en ligne' remplit la liste de commandes de 'bc'. echo `bc <<! obase=16; "hex="; $dec obase=10; "dec="; $dec obase=8; "oct="; $dec obase=2; "bin="; $dec ! ` | sed -e 's: : :g' done } while [ $# -gt 0 ] do case "$1" in --) shift; break;; -h) Usage;; # ==> Message d'aide. -*) Usage;; *) break;; # premier nombre esac # ==> Plus de vérification d'erreur pour des entrées illégales #+ serait utile. shift done if [ $# -gt 0 ] then AfficheBases "$@" else # lit à partir de l'entrée standard #+ stdin while read ligne do PrintBases $ligne done fi |
Une autre façon d'utiliser bc serait d'utiliser des documents en ligne embarqués dans un bloc de substitution de commandes. Cette méthode est très intéressante lorsque le script passe un grand nombre de commandes à bc
variable=`bc >> CHAINE_LIMITE options instructions operations CHAINE_LIMITE ` ...or... variable=$(bc >> CHAINE_LIMITE options instructions operations CHAINE_LIMITE ) |
Exemple 12-34. Une autre façon d'invoquer bc
#!/bin/bash # Appelle 'bc' en utilisant la substitution de commandes # en combinaison avec un 'document en ligne'. var1=`bc << EOF 18.33 * 19.78 EOF ` echo $var1 # 362.56 # La notation $( ... ) fonctionne aussi. 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 ) # Renvoie le sinus de 1,7 radians. # L'option "-l" appelle la bibliothèque mathématique de 'bc'. echo $var3 # .991664810 # Maintenant, essayez-la dans une fonction... hyp= # Déclarez une variable globale. hypotenuse () # Calculez l'hypoténuse d'un triangle à angle droit. { hyp=$(bc -l << EOF scale = 9 sqrt ( $1 * $1 + $2 * $2 ) EOF ) # Malheureusement, on ne peut pas renvoyer de valeurs à virgules flottantes à #+ partir d'une fonction Bash. } hypotenuse 3.68 7.31 echo "hypoténuse = $hyp" # 8.184039344 exit 0 |
Exemple 12-35. Calculer PI
#!/bin/bash # cannon.sh: Approximation de PI en tirant des balles de canon. # C'est une très simple instance de la simulation "Monte Carlo", un modèle #+ mathématique d'un événement réel, en utilisant des nombres pseudo-aléatoires #+ pour émuler la chance. # Considérez un terrain parfaitement carré, de 10000 unités par côté. # Ce terrain comprend un lac parfaitement circulaire en son centre d'un #+ diamètre de 10000 units. # Ce terrain ne comprend donc que de l'eau sauf dans ses quatre coins. # (un carré comprenant un cercle.) # # Tirons des balles de canon à partir d'un vieux canon situé sur un des côtés #+ du terrain. # Tous les tirs créent des impacts quelque part sur le terrain, soit dans le #+ lac soit dans un des coins secs. # Comme le lac prend la majorité de l'espace disponible sur ce terrain, la #+ plupart des tirs va tomber dans l'eau. # Seuls quelques tirs tomberont sur un sol rigide compris dans les coins du #+ terrain. # # Si nous prenons assez de tirs non visés et au hasard, #+ alors le ratio des coups dans l'eau par rapport au nombre total sera #+ approximativement de PI/4. # Exercice: Expliquez pourquoi. # # Théoriquement, plus de tirs sont réalisés, plus cela correspondra. # Néanmoins, un script shell, contrairement à un langage compilé avec un #+ support des calculs à virgule flottante, nécessite quelques compromis. # Ceci tend à rendre la simulation moins précise malheureusement. DIMENSION=10000 # Longueur de chaque côté du terrain. # Initialise aussi le nombre d'entiers générés au hasard. NB_TIRS_MAX=1000 # Tire ce nombre de fois. # 10000 ou plus serait mieux mais prendrait bien plus de temps. PMULTIPLIEUR=4.0 # Facteur d'échelle pour l'approximation de PI. au_hasard () { RECHERCHE=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }') HASARD=$RECHERCHE # Du script d'exemple "seeding-random.sh" let "rnum = $HASARD % $DIMENSION" # Echelle plus petite que 10000. echo $rnum } distance= # Déclaration de la variable globale. hypotenuse () # Calcule de l'hypoténuse d'un triangle à angle droit. { # A partir de l'exemple "alt-bc.sh". distance=$(bc -l << EOF scale = 0 sqrt ( $1 * $1 + $2 * $2 ) EOF ) # Initiale "scale" à zéro fait que le résultat sera une valeur entière, un #+ compris nécessaire dans ce script. # Ceci diminue l'exactitude de la simulation malheureusement. } # main() { # Initialisation des variables. tirs=0 dans_l_eau=0 sur_terre=0 Pi=0 while [ "$tirs" -lt "$NB_TIRS_MAX" ] # Boucle principale. do xCoord=$(au_hasard) # Obtenir les coordonnées X et Y au # hasard. yCoord=$(au_hasard) hypotenuse $xCoord $yCoord # Hypoténuse du triangle = #+ distance. ((tirs++)) printf "#%4d " $tirs printf "Xc = %4d " $xCoord printf "Yc = %4d " $yCoord printf "Distance = %5d " $distance # Distance à partir du centre #+ du lac if [ "$distance" -le "$DIMENSION" ] then echo -n "Dans l'eau ! " ((dans_l_eau++)) else echo -n "Sur terre ! " ((sur_terre++)) fi Pi=$(echo "scale=9; $PMULTIPLIEUR*$dans_l_eau/$tirs" | bc) # Multipliez le ratio par 4.0. echo -n "PI ~ $Pi" echo done echo echo "Après $tirs tirs, PI ressemble approximativement à $Pi." # Tend à être supérieur. # Probablement dû aux erreurs d'arrondi et au hasard perfectible de $RANDOM. echo # } exit 0 # On peut se demander si un script shell est approprié pour une application #+ aussi complexe et aussi intensive en calcul. # # Il existe au moins deux justifications. # 1) La preuve du concept: pour montrer que cela est possible. # 2) Pour réaliser un prototype et tester les algorithmes avant de le réécrire #+ dans un langage compilé de haut niveau. |
L'utilitaire dc (desk calculator) est orienté pile et utilise la << notation polonaise inversée >> (RPN). Commebc, Comme bc, il possède les bases d'un langage de programmation.
La plupart des gens évitent dc, parce qu'il nécessite de saisir les entrées en RPN, ce qui n'est pas très intuitif. Toutefois, cette commande garde son utilité.
Exemple 12-36. Convertir une valeur décimale en hexadecimal
#!/bin/bash # hexconvert.sh: Convertit un nombre décimal en héxadecimal. BASE=16 # Héxadecimal. if [ -z "$1" ] then echo "Usage: $0 nombre" exit $E_SANSARGS # A besoin d'un argument en ligne de commande. fi # Exercice: ajouter une vérification de la validité de l'argument. hexcvt () { if [ -z "$1" ] then echo 0 return # "Renvoie" 0 si aucun argument n'est passé à la fonction. fi echo ""$1" "$BASE" o p" | dc # "o" demande une sortie en base numérique. # "p" Affiche le haut de la pile. # Voir 'man dc' pour plus d'options. return } hexcvt "$1" exit 0 |
L'étude de la page d'info de la commande dc donne une idée de sa complexité. Il semble cependant qu'une poignée de connaisseurs de dc se délectent de pouvoir exiber leur maîtrise de cet outil puissant mais mystérieux.
Exemple 12-37. Factoring
#!/bin/bash # factr.sh: Factorise un nombre MIN=2 # Ne fonctionnera pas pour des nombres plus petits que celui-ci. E_SANSARGS=65 E_TROPPETIT=66 if [ -z $1 ] then echo "Usage: $0 nombre" exit $E_SANSARGS fi if [ "$1" -lt "$MIN" ] then echo "Le nombre à factoriser doit être supérieur ou égal à $MIN." exit $E_TROPPETIT fi # Exercice: Ajouter une vérification du type (pour rejeter les arguments non #+ entiers). echo "Les facteurs de $1:" # --------------------------------------------------------------------------------- echo "$1[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr[dli%0=1lrli2+dsi!>.]ds.xd1<2" | dc # --------------------------------------------------------------------------------- # La ligne de code ci-dessus a été écrit par Michel Charpentier <charpov@cs.unh.edu>. # Utilisé avec sa permission (merci). exit 0 |
Une autre façon d'utiliser les nombres à virgule flottante est l'utilisation des fonctions internes de la commande awk dans un emballage shell .
Exemple 12-38. Calculer l'hypoténuse d'un triangle
#!/bin/bash # hypotenuse.sh: Renvoie l'"hypoténuse" d'un triangle à angle droit, # (racine carrée de la somme des carrés des côtés) ARGS=2 # Le script a besoin des côtés du triangle. E_MAUVAISARGS=65 # Wrong number of arguments. if [ $# -ne "$ARGS" ] # Teste le nombre d'arguments du script. then echo "Usage: `basename $0` cote_1 cote_2" exit $E_MAUVAISARGS fi SCRIPTAWK=' { printf( "%3.7f\n", sqrt($1*$1 + $2*$2) ) } ' # commande(s) / paramètres passés à awk echo -n "Hypoténuse de $1 et $2 = " echo $1 $2 | awk "$SCRIPTAWK" exit 0 |
Précédent | Sommaire | Suivant |
Commandes de contrôle du terminal | Niveau supérieur | Commandes diverses |