Programmare con la shell Bash impone necessariamente la conoscenza della sintassi del linguaggio impiegato ma, questa, non è certamente la difficoltà maggiore cui si va incontro. In effetti, la sintassi di Bash è abbastanza ricca da mostrare una potenza espressiva simile a molti linguaggi di programmazione strutturata di ultima generazione. Esiste, in realtà, un impiego criptico di alcuni simboli di controllo, in parte ereditato dal C, utili a velocizzare l'implementazione dello script e dargli una forma particolarmente compatta. L'acquisizione delle necessarie abilità implementative è frutto della pratica programmativa con questo linguaggio cosicchè, nel seguito, vengono proposti e discussi alcuni esempi, anche rivisitando esempi già visti.
Nel seguito vengono proposti alcuni esempi ragionati di shell script per la shell Bash. Si è volutamente cercato di utilizzare il più possibile la sintassi della Bourne shell che, come è noto, costituisce un sottoinsieme proprio della shell Bash. Le caratteristiche salienti sono l'utilizzo di parametri sia posizionali, per acquisire i valori passati dalla shell chiamante, sia variabile, per immagazzinare risultati intermedi dell'elaborazione. Vengono impiegati, inoltre, il ciclo for e il salto condizionato if-then-else. Altre caratteristiche impiegato sono il quoting e il back-quoting, quest'ultimo necessario quando si vuol assegnare il risultato di un comando, prodotto su stdout, ad un parametro variabile. Vengono utilizzati, inoltre, alcuni comandi di utilità generale il cui impiego e spiegazione dettagliata possono ottenersi consultando il manuale in linea con man <nomecomando>.
In questo esempio la determinazione dell'identificatore di processo PID, associato all'esecuzione del programma <progname> viene ottenuta analizzando opportunamente le informazioni prodotte in uscita dal comando ps che legge la tabella contenente i descrittori PCB dei processi correntemente attivi. L'analisi viene demandata al programma grep la cui azione è quella di estrarre, da un dato file di testo, tutte le righe che contengono un certo pattern passato come primo argomento.
L'impiego della pipe permette di trasferire l'output di ps a grep; il secondo filtraggio serve ad eliminare la riga che contiene il nome del processo passato come parametro a grep stesso. Infine, con il back-quote si assegna la riga cercata alla variabile A la quale, prima dell'assegnazione, viene trasformata in una lista di parole, interpretando il blank come separatore.
Si tenga presente che, in generale, è possibile che siano presenti più processi lanciati con lo stesso programma cosicchè A si presenta come un array di stringhe. Il ciclo for serve a scorrere questa lista e, nel caso specifico, ad estrarre il primo elemento, mediante il contatore K, che è proprio il primo PID cercato.
#!/bin/bash # Synopsis: fpid <progname> # # if [ "$#" -eq 0 ]; then echo "Usage: fpid <processname>" exit 1 fi # A=`ps -e | grep -e "\b$1\b" | grep -v grep` ; if [ "$A" == "" ] then echo "no such a process" ; exit 2 ; fi K=1 P=1 for ITEM in $A; do let "M = $K % 4" ; if [ $M -eq 1 ]; then echo "<$P> : $ITEM" ; let "P = $P + 1" ; fi K=`expr $K + 1` ; done exit 0 |
find the first running program |
In questo secondo esempio si procede in modo analogo al precedente con un ulteriore filtraggio per contare il numero di righe contenenti il nome del processo cercato; il comando utilizzato è wc con l'opzione -l. L'estrazione dei PID richiesti, su cui eseguire la terminazione forzata con il comando kill -9, è ottenuta dal ciclo while che scorre la lista TPID con l'avvertenza che il numero di item per ogni processo trovato è 4, conseguenza del fatto che il comando ps è stato eseguito con l'ozione -e.
Si noti, anche, come in questo caso venga utilizzato nel ciclo il comando read che serve a leggere la stringa introdotta dall'utente in risposta alla richiesta proposta col comando echo -n, in cui il flag -n notifica al comando di non avanzare alla riga successiva.
#!/bin/bash # gkill : terminazione selettiva dei processi # Synopsis : gkill <progname> if [ "$#" -eq 0 ] then echo "Usage: gkill <progname>" exit 1 fi PROG=$1 declare -a TPID declare -i N N=`ps -e | egrep -e "\b$PROG\b" | grep -v "grep" | wc -l` if [ $N -eq 0 ] then echo "No such a process" exit 2 fi TPID=(`ps -e | egrep -e "\b$PROG\b" | grep -v "grep"`) J=0; K=$N; while [ "$K" -gt 0 ] do echo -n "Vuoi terminare ${TPID[$J]}?(y/n) " read YES if [ "$YES" == "y" ] then kill -9 "${TPID[$J]}" fi J=`expr $J + 4` K=`expr $K - 1` done exit 0 |
Terminazione selettiva dei processi |
Il seguente programma ricerca, a partire da una lista di directory, quale di queste contiene il comando il cui nome è passato come argomento. Per effettuare la ricerca si analizza il contenuto della variabile d'ambiente PATH nella quale sono registrate tutti i possibili percorsi che raggiungono le directory in questione. Il programma si basa sulla particolare codifica utilizzata in questo caso, secondo la quale la separazione fra una directory e l'altra è ottenuta con il carattere :. Dunque, utilizzando il comando sed, viene effettuata una trasformazione della stringa PATH, sostituendo (comando s) tutte (flag g) le occorrenze di : con un blank.
La nuova stringa SEARCH_PATH è costituita da una serie di item che rappresentano directory per cui scorrendo la lista con la variabile DIR si verifica se "$DIR/$1" è il comando in questione che si trova nella directory correntemente analizzata.
#!/bin/bash # where cmd : where is (the path of) the cmd command # CURR_PATH=$PATH PATH=/bin:/usr/bin if [ "$#" -eq 0 ] then echo 'Usage: where <command>' ; exit 2 fi SEARCH_PATH="`echo ${CURR_PATH} | sed 's/:/ /g'`" K=0 for DIR in ${SEARCH_PATH} do if [ -f ${DIR}/$1 ] then echo ${DIR}/$1 K=`expr $K + 1` fi done if [ "$K" -eq 0 ] then echo "Command not found" exit 1 else exit 0 fi |
Dislocazione dei comandi |
Nel semplice esempio che segue viene messo in evidenza il ruolo posizionale dei parametri utilizzati per associare il valore degli argomenti passati dalla shell chiamante al comando da eseguire.
#!/bin/bash # wh-trans : Translate a sentence in a query form # Synopsis : wh-trans |
Trasformazione in forma interrogativa |
L'impiego delle istruzioni composte della shell Bash segue la falsa riga dei linguaggi di programmazione strutturata. In questo senso, le istruzioni disponibili riguardano la possibilità di costruire rami alternativi di esecuzione (salto condizionato), cicli a controllo diretto (for) e indiretto (while, until) oltre, naturalmente, alla selezione (case) e la definizione di funzione (function).
Lo script che segue è un esempio strutturato che mostra come costruire gli statement strutturati nelle varie forme. Si è predisposta una funzione per ciascuno delle forme che può essere eseguita e testata con opportuni dati di ingresso, secondo le indicazioni riportate dallo script stesso.
#!/bin/bash # xar: eXecute An example and Returm # Synopsis : xar [if | for | case | while | until | fcall] # function idiv() { local K=0; local A=$1; while [ "$A" -gt "$2" ]; do A=`expr $A - $2`; K=`expr $K + 1` ; done return $K;} function Until() { CMD="run"; K=0; until [ "$CMD" = "" ]; do K=`expr $K + 1`; echo -n "[$K]# "; read CMD; if [ "$K" -eq 1 ]; then HISTORY="$CMD"; else HISTORY="$HISTORY:$CMD"; fi done echo "$HISTORY" return 1;} function While() { PROMPT="sum> "; echo -n "$PROMPT"; read K if [ "$K" -eq 0 ]; then RetVal=1 else A=0 while [ $K -gt 0 ]; do A=`expr $A + $K` ; K=`expr $K - 1` ; done echo $A ; RetVal=0 fi return $RetVal;} function FCall() { PROMPT="> "; echo -n "$PROMPT"; read N if [ "$N" -eq 0 ]; then RetVal=1 else RetVal=0 fi return $RetVal;} function If() { PROMPT="if$T> "; echo -n "$PROMPT"; read N if [ "$N" -eq 0 ]; then T="" RetVal=1 else T=":$N" RetVal=0 fi return $RetVal;} function For() { PROMPT="for> "; echo -n "$PROMPT"; read N ; if [ "$N" -eq 0 ]; then RetVal=1 else N=`expr $N % 7` ; N=`expr $N + 1` ; WEEK="Foo Sun Mon Tue Wed Thu Fri Sat"; K=0 for DAY in $WEEK; do if [ "$K" -eq "$N" ]; then echo "today is $DAY" fi K=`expr $K + 1` done RetVal=0 fi return $RetVal;} function Case() { PROMPT="dc> "; echo -n "$PROMPT"; read ARG1 CMD ARG2 ; if [ "$ARG1" == "q" ]; then Q=1; T=0; else case $CMD in "+") T=`dc --expression="$ARG1 $ARG2 + p"`; Q=0 ;; "-") T=`dc --expression="$ARG1 $ARG2 - p"`; Q=0 ;; "x") T=`dc --expression="$ARG1 $ARG2 * p"`; Q=0 ;; ":") T=`dc --expression="$ARG1 $ARG2 / p"`; Q=0 ;; "%") T=`dc --expression="$ARG1 $ARG2 % p"`; Q=0 ;; esac fi echo "$T" ; return $Q;} if [ "$#" -eq 0 ]; then echo "Usage: xar [if | for | case | while | until | fcall]" exit 2 fi echo "sample bash scripts" echo case $1 in "if") echo "testing if statement:" ; echo "enter an integer; terminate with 0" ; T="" ;; "for") echo "testing for loop" ; echo "enter an integer; terminate with 0" ;; "case") echo "testing case statement" ; echo "enter an arithmetic operation (+,-,x,:,%) or q" ;; "while") echo "testing while loop" ; echo "enter an integer; terminate with 0" ;; "until") echo "testing until loop" echo "enter one string each line; terminate with null string" ;; "fcall") echo "testing function call" ;; *) echo "No such an example" ;; esac HaltF=0 until [ "$HaltF" -eq 1 ]; do case $1 in "if") If ; HaltF=$? ;; "for") For ; HaltF=$? ;; "case") Case ; HaltF=$? ;; "while") While ; HaltF=$? ;; "until") Until; HaltF=$? ;; "fcall") echo -n "enter two integers "; read N M; idiv $N $M; echo "$N:$M=$?" ; HaltF=1 ;; *) echo "Possible Commands: if, case, while, until, fcall" ;; esac done exit 0 |
Istruzioni Composte |
Nell'esempio proposto viene implementato un comando di cancellazione controllata di file e directory il cui effetto è di spostare tali file nella directory ~/.wastebasket che, per questo funge da cestino. Nella prima parte del comando viene fatto il parsing degli argomenti, distinguendo fra opzioni e argomenti, e che vengono posti in liste separate, contandone il numero per i secondi.
Superato il test di consistenza, si verifica se è presente il flag v che non può essere passato al comando mv e, perciò richiede un trattamento ad hoc. Dunque, mediante ciclo for si passa all'eliminazione selettiva dei file indicati, eventualmente richiedendo conferma all'utente.
#!/bin/bash # delete options file # delete options dir # NAMES="" K=0 OPTIONS="" for ITEM in $* do if [ `echo $ITEM | egrep -e "-"` ] > /dev/null; then OPTIONS="$OPTIONS `echo $ITEM | sed s/-//`" else K=`expr $K + 1` NAMES="$ITEM $NAMES" fi done if [ $K -eq 0 ]; then echo "insufficient parameters" exit 1 fi OPT="" MINUS="-" VERBOSE=0 for A in $OPTIONS do if [ $A = "v" ] > /dev/null; then VERBOSE=1 else OPT="$MINUS$A $OPT" fi done if [ ! -d "$HOME/.wastebasket" ] then mkdir "$HOME/.wastebasket" fi for NAME in $NAMES do if [ $VERBOSE -eq 0 ] > /dev/null; then mv $OPT $NAME $HOME/.wastebasket else echo -n "delete $NAME (y/n) " read YES if [ $YES = "y" ] > /dev/null; then mv $OPT $NAME $HOME/.wastebasket fi fi done exit 0 |
Cancellazione sicura di file |