LEZIONE del 19 Ottobre

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, infatti, 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.

ESEMPI DI SHELL SCRIPT

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>.

Identificazione di un Processo

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>
#
A=`ps ax | grep -e $1 | grep -v grep` ;
if [ "$A" == "" ]
   then
     echo "no such a process" ;
     exit 1 ;
fi
K=1
  for ITEM in $A
    do
      if [ $K -eq 1 ]
         then echo $ITEM ;
      fi
      K=`expr $K + 1` ;
    done
exit 0
find the first running program
Si noti, infine, l'impiego del comando exit che serve a terminare l'esecuzione del programma e produrre il risultato di uscita indipendentemente dalle regole utilizzate dalla shell a questo proposito.

Terminazione di un Processo

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 " $PROG" | grep -v "grep" | wc -l`
if [ $N -eq 0 ]
  then
    echo "No such a process"
    exit 2
fi
TPID=(`ps -e | egrep -e " $PROG" | 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
I contatori J e K servono, rispettivamente, a riposizionarsi al prossimo PID e a contare il numero di righe che rimangono da scandire.

Dislocazione dei Comandi

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/sh
#  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
Si noti come, anche in questo caso, i back-quota consentano di assegnare il risultato dell'editing sulla stringa CURR_PATH alla variabile SEARCH_PATH. Infine va notata la ridefinizione dei path di ricerca in modo da utilizzare i comandi originali per cui, prima di effettuare questa assegnazione, è necessario salvare il vecchio contenuto.

Trasformazione in Forma Interrogativa

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  [ ..]
#

if [ $# -eq 0 ]
  then
    echo "Synopsis : wh-trans  [ ..]" ;
    exit 2
fi

N="$#"
S1=${1}
if [ "${2}" == "รจ" ] || [ "${2}" == "ha" ]
  then
    S2="${2} ${3}"
    S3="${4}"
  else 
    S2="${2}"
    S3="${3}"
fi
case $S3 in
a) echo "Dove $S2 ${S1}?";;
alle) echo "Quando $S2 ${S1}?" ;;
con) echo "Con chi $S2 ${S1}?" ;;
esac
Trasformazione in forma interrogativa
Come si può notare facilmente l'output prodotto dall'esecuzione del comando ha come effetto quello di ricostruire una lista con gli stessi elementi, aggiungendo eventualmente elementi costanti, e modificandone eventualmente l'ordine.

Cancellazione di un file o una directory

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
In ogni caso il comando di cancellazione viene realizzato come mv -opt cosicchè i file sono soltanto trasferiti in altra directory.