Shell Scripting

Bash e' una "Unix shell", vale a dire un'interfaccia a linea (o riga) di comando che permette l'interazione con sistemi operativi derivati da Unix.

In aggiunta alla modalita' interattiva, che permette all'utente di eseguire un comando alla volta e ricevere il risultato in tempo reale, Bash (cosi' come molte altre shell) ha la capacita' di eseguire interi script di comandi, conosciuti come "Bash shell script".

Per cominciare, il primo script che vedremo e' l'intramontabile "Hello World!", che ci dara' subito un'idea di massima della sintassi del linguaggio. Creiamo un file di testo vuoto, (chiamiamolo, ad esempio, hello.sh), contenente il codice seguente:

#!/bin/bash
# Questo e' un commento
echo Hello World          # Anche questo e' un commento!

La prima riga informa il sistema che il file deve essere eseguito da /bin/bash, che corrisponde al percorso standard della Bourne-Again Shell su qualsiasi sistema Unix based. La seconda riga inizia con il simbolo #, che identifica la riga come commento, e viene percio' ignorata dalla shell. L'unica eccezione di questa regola si verifica quando la prima riga del file comincia con #! (sequenza di caratteri nota come sha-bang), come nel nostro caso. Questa e' una direttiva che Unix tratta in maniera specifica: serve a specificare il percorso della shell che dovra' eseguire lo script.

In alternativa alla Bash

#!/bin/sh
#Questo e' un commento e non viene interpretato
echo "Lo script funziona..." 

Eseguiamo il comando chmod per rendere il file di testo eseguibile, utilizzando la sintassi seguente:

chmod +rx hello.sh

Infine eseguiamo il nostro script, direttamente dal terminale, come segue:

./hello.sh

Variabili

#!/bin/bash
location=World
echo "Hello ${location}"

La stringa $location nella terza riga e' stata sostituita da "World" prima dell'effettiva esecuzione del comando; questa operazione e' conosciuta come espasione di variable, ed e' molto piu' flessibile di come ci si aspetti. Ad esempio, si puo' anche memorizzare il nome del comando da eseguire:

cmd_to_run=echo
${cmd_to_run} "Hello World"

Se si cerca di leggere il contenuto di una variabile non dichiarata, si otterra' una stringa vuota

Lo scope delle variabili:

Il comando export ha effetto sullo scope delle variabili. Procediamo nello spiegare il suo funzionamento. Creiamo un piccolo script var.sh:

#!/bin/bash
echo "VAR e': $VAR"
VAR="Ciao!"
echo "VAR e': $VAR"

Eseguemdolo con e senza export si hanno ristultati diveversi:

$ VAR=Salve
$ ./var.sh
VAR e':
VAR e': Ciao!
$ export VAR=Salve
$ ./var.sh
VAR è: Salve
VAR è: Ciao!

Variabili built-in

#!/bin/bash
echo "Benvenuti $1 e $2!"

esegunendo si ottine:

$ ./welcome.sh Pippo Pluto
Benvenuti Pippo e Pluto!

E' possibile anche riferirsi a tutti gli argomenti in una volta utilizzando $@. Il numero di parametri posizionali e' memorizzato in $#. Altre due variabili principali che vengono impostate automaticamente dall'ambiente Bash sono $$ e $!. Entrambe rappresentano numeri di processi. Nella variabile $$ viene memorizzato il PID (Process IDentifier) della shell in esecuzione. Questo puo' essere utile per creare file temporanei, come /tmp/my-script.$$ Un'altra variabile interessante e' IFS, ovvero l'Internal Field Separator, che rappresenta il carattere (o i caratteri) che separa i diversi parametri fra di loro. Il valore di default e' SPACE TAB NEWLINE

Array

L'elemento in posizione x di un array memorizzato nella variabile arr puo' essere inizializzato utilizzando la notazione arr[x]. Quando invece si vuole accedere al valore memorizzato nel medesimo elemento e' sempre necessario utilizzare la notazione estesa, $arr[x]

#!/bin/bash
messaggio[0]="Ciao"
messaggio[1]="Mondo"
echo -n "messaggio[0] = "
echo ${messaggio[0]}

in alternativa

#!/bin/bash
messaggio=("Ciao" "Mondo!")
echo "${messaggio[0]} ${messaggio[1]}"

ancora in alternativa (con inserimento posizionale)

#!/bin/bash
messaggio=([2]="Ciao" [3]="Mondo!")
echo "${messaggio[2]} ${messaggio[3]}"

Per fare riferimento al suo intero contenuto si utilizza @ o * come indice; cosi' facendo, negli ultimi due esempi presentati, e' possibile sostituire l'ultima riga con la notazione piu' semplice echo "$messaggio[@]". Il simbolo # viene anteposto a quest'ultima notazione per conoscere la lunghezza dell'array :

#!/bin/bash
array=(2 3 4 5)
echo "array contiene ${#array[@]} elementi."

E' anche possibile accedere ad una porzione di un array specificandone la posizione di partenza e la lunghezza, come mostrato nell'esempio seguente (e' da tener presente che le posizioni partono da 0):

#!/bin/bash
array=(2 3 4 5)
echo "Il secondo e terzo elemento di array sono ${array[@]:1:2}"

Per concludere, si utilizza il comando unset per rimuovere un elemento di un array (indicandone percio' la posizione racchiusa in parentesi quadre) o per eliminare l'intero array. E' utile far presente che nel primo caso gli elementi ancora presenti nell'array manterranno le medesime posizioni anche dopo che l'elemento e' stato rimosso, come e' facile verificare con il prossimo esempio.

#!/bin/bash
array=(2 3 4 5)
unset array[1]
echo "Ecco il secondo e terzo elemento di array ${array[1]} ${array[2]}"

Gli operatori validi sull'intero contenuto di un array funzionano anche su di un singolo elemento, sostituendo il simbolo @ (o *) con la sua posizione.

#!/bin/bash
messaggio=("CiaoCiao" "Mondo!Mondo!")
echo "${messaggio[0]:4:4} ${messaggio[1]:6:6}"

ha come ristutato: Ciao Mondo! Cioe' messaggio[0] = CiaoCiao poi parto dalla 4 lettera (5 posizione) e prendo a seguire 4 lettere....

Operatori aritmetici e logici

Le operazioni aritmetiche in Bash seguono lo stile dei linguaggi derivati da C, percio' la loro sintassi e' molto simile a quella usata in C++, Java, Perl, JavaScript, C# o PHP. A differenza di questi ultimi, pero', Bash supporta soltanto l'aritmetica intera.

Per calcolare il risultato di un'operazione si utilizza la cosiddetta espansione aritmetica, denotata da $(()). Ad esempio, il comando echo $((3 + 4 * (5 - 1) )) stampa 19.

L'espansione aritmetica supporta tutti e quattro gli operatori classici (+, -, *, /), in aggiunta all'operatore di modulo (a % b produce il resto della divisione intera a/b) e quello di esponenziazione (2 ** 4 risulta 16); e' possibile anche utilizzare l'operatore - per invertire il segno di una espressione (-(3*4) viene valutato come -12).

In aggiunta al tradizionale operatore di assegnazione =, Bash supporta anche operatori composti come +=, -=, *=, /= e %=, che eseguono l'operazione seguita da un assegnamento; ad esempio, (( i *= 2 + 3 )) e' equivalente a (( i = i * (2 + 3) )).

Infine, Bash supporta anche l'operatore di incremento ++ e di decremento --, che rispettivamente aggiunge o sottrae 1 dal valore della variabile che lo segue o precede; se l'operatore precede il nome della variabile, l'espressione valuta la variabile con il suo nuovo valore, mentre se l'operatore lo segue, l'espressione valuta la variabile con il suo vecchio valore

0 sta per "falso" e tutti i valori diversi da 0 (specialmente 1) stanno per "vero".

Le espressioni aritmetiche supportano anche gli operatori di confronto intero , , =, =, == (che sta per =), != (che sta per ≠). Ciascuno viene valutato come 1 se "vero" o 0 se "falso".

Sono supportati anche i tradizionali operatori di logica booleana ! ("non" - che risulta vero solo se l'operando e' falso), && ("e" - che risulta vero solo se entrambi gli operandi sono veri), ||

Infine, Bash supporta anche l'operatore condizionale c ? e1 : e2. Questo operatore valuta innanzitutto c; se tale espressione e' vera, ritorna e1, altrimenti, valuta e2 e ritorna il suo risultato.

Aritmetica non-intera

Come abbiamo gia' visto, l'aritmetica di Bash supporta solo i numeri interi. E' possibile pero' utilizzare programmi esterni per estendere il supporto all'aritmetica non intera, includendo cioe' i numeri decimali. In particolare, l'utility bc viene spesso utilizzata per tale scopo. Il comando seguente:

echo "$(echo '3.4 + 2.2' | bc)"

stampa 5.6. La sintassi degli operatori supportati da bc non e' totalmente diversa da quella delle espressioni aritmetiche di Bash, ma vale la pena dare un'occhiata al manuale (digitando man bc) prima di utilizzarla nei propri script Bash.

Il comando test

Si possono anche usare le parentesi quadre in sostituzione di test: test expr e [ expr ] Il comando test ritorna 0 (vero) o 1 (falso) Il risultato della valutazione viene memorizzato nella variabile built-in $?, e lo si puo' usare con gli operatori booleani && e ||.

#!/bin/bash
i="abc"
j="def"
[ $j != $i ]
echo $?

Si possono confrontare valori aritmetici usando gli operatori -eq (uguale), -ne (diverso), -lt (minore di), -le (minore di o uguale a), -gt (maggiore di), -ge (maggiore di o uguale a).

L'operatore unario -z valuta se la stringa sia nulla, mentre -n o l'omissione di qualunque operatore risulta vero se la stringa non e' vuota. Esistono inoltre operatori per test su file, come gli operatori unari -d e -e che valutano rispettivamente l'esistenza del file, e se esso sia una directory, o l'operatore binario -nt, che valuta se il primo file e' stato modificato piu' recentemente del secondo. E' utile controllare il manuale (digitando man test) per ottenere la lista di tutte le opzioni di test disponibili.

Infine, e' possibile comporre condizioni piu' complicate tramite gli operatori della logica booleana che abbiamo introdotto nella sezione precedente: l'operatore -a corrisponde all'and logico, -o all'or mentre il ! alla negazione; per imporre la precedenza di un operatore rispetto ad un altro si possono utilizzare le parentesi, che essendo riservate tipicamente all'esecuzione di espressioni in sotto-shell, devono essere in questo caso precedute da \.

Le espressioni regolari vengono generalmente utilizzate per verificare se una stringa rispetta un determinato formato. Percio' si sfruttano all'interno del comando test mediante l'operatore =~.

$ [[ ciao =~ ^c ]] ; echo $?
$ [[ ciao =~ ^o ]] ; echo $?
$ [[ ciao =~ c$ ]] ; echo $?
$ [[ ciao =~ o$ ]] ; echo $?

Un meccanismo analogo alle espressioni regolari offerto da Bash per effettuare pattern matching su nomi di file e stringhe sono i cosiddetti globs, cioe' particolari metacaratteri che effettuano il matching quando vengono concatenati a caratteri tradizionali

I metacaratteri supportati di default da Bash sono *, che descrive qualsiasi stringa, ? che descrive un singolo carattere e [...] che descrive un qualunque carattere contenuto fra le parentesi

$ ls *
a abc b c
$ ls a*
a abc
$ ls a?c
abc
$ ls a[bd]c
abc
#!/bin/bash
filename="file.jpg"
 
if [[ $filename = *.jpg ]]; then
    echo "$filename è un file JPG!"
else
    echo "$email NON è un file JPG!"
fi

Costrutti condizionali: if e case

L'if in Bash e' un comando composto che valuta il risultato di un test o comando ($?) e si dirama in base al suo valore, vero (0) o falso (1)

#!/bin/bash
if [[ -e originale.txt ]] ; then
  cp originale.txt copia.txt
  echo "Copia riuscita."
elif [[ -e nuovo.txt ]] ; then
  echo "originale.txt non esiste, ma nuovo.txt sì."
  cp nuovo.txt copia.txt
  echo "Copia riuscita."
else
  echo "Non esiste né originale.txt né nuovo.txt."
fi    #if al contrario

Il costrutto case, che permette una piu' agile definizione di condizioni sul valore di una variabile, come si nota dall'esempio seguente:

#!/bin/bash
 
echo "Inserisci un numero da 1 a 3: "
read opt
 
case $opt in
1)
  echo "Hai inserito uno!" ;;
2)
  echo "Hai inserito due!" ;;
3)
echo "Hai inserito due!" ;;
*)
  echo "Non hai inserito un numero da 1 a 3..." ;;
esac   #case a contrario

La sintassi e' molto intuitiva e il risultato molto leggibile: viene letto e memorizzato l'input dell'utente nella variabile $opt per essere poi usata come argomento del costrutto case. Ogni opzione seguita da una parentesi chiusa ) determina il valore che deve assumere la variabile perche' i comandi che la seguono - terminati da ;; - vengano eseguiti.

Iterazioni: i cicli for, while ed until

Cicli for

for i in 1 2 topolino pippo 3 pluto
do
  echo "i ha valore $i"
done
#!/bin/bash
for (( i = 0 ; i <= 20 ; i += 2 )) ; do
  echo "i ha valore $i"
done

Cicli while ed until

while [[ -e attesa.txt ]] ; do
  sleep 3 # "dormi" per tre secondi
done

Un modo alternativo ma altrettanto valido e' quello di usare il ciclo until, che funziona in maniera simile al while, ma nel modo inverso: esso infatti, continua ad eseguire i comandi fintantoche' la condizione specificata e' false, e termina nel momento in cui essa risulta vera.

until [[ -e procedi.txt ]] ; do
  sleep 3 # "dormi" per tre secondi
done
echo "Inserisci un messaggio (esci per uscire)"
while [[ $INPUT != "esci" ]]
do
  read INPUT
  echo "Hai digitato: $INPUT"
done
#!/bin/bash
echo "Inserisci un messaggio (esci per uscire)"
while :
do
  read INPUT
  if [[ $INPUT = "esci" ]] ; then
    break
  fi
  echo "Hai digitato: $INPUT"
done

Funzioni

Una funzione e' uno speciale tipo di variabile che possiamo immaginare come uno script contenuto in un altro script. Questo permette di raggruppare una sequenza di comandi all'interno di un singolo comando

A differenza dei linguaggi di programmazione tradizionali, in cui e' comunemente possibile distinguere tra funzioni, che ritornano un singolo valore e non producono nessun output, e procedure, che non ritornano alcun valore ma possono produrre un output, una funzione in Bash puo' ascriversi ad entrambi questi comportamenti.

aggiungi_utente()
{
  ID=$1
  NOME=$2
  COGNOME=$3
  echo "Aggiunto l'utente $ID ($NOME $COGNOME)"
}
 
echo "Inizio dello script..."
aggiungi_utente 0 Albert Einstein
aggiungi_utente 1 Stephen Hawking
aggiungi_utente 2 Alan Turing
echo "Fine dello script..."

Visibilita' delle variabili e subshell

A differenza dei piu' comuni linguaggi di programmazione, lo scope delle variabili in Bash e' pressoche' inesistente

Le uniche eccezioni a quanto detto finora sullo scope delle variabili valgono per le subshell e il comando built-in local. In Bash, uno o piu' comandi racchiusi da parentesi vengono eseguiti in una subshell; questa riceve una copia dell'ambiente di esecuzione, che include, fra le altre cose, tutte le variabili dichiarate. Ogni modifica effettuata all'ambiente di esecuzione dalla subshell non ha pero' effetto sulla shell principale una volta completata l'esecuzione della subshell. Questo vale anche per le funzioni: una funzione dichiarata in una subshell non e' visibile all'esterno di essa. Usando invece la sintassi local var o local var=val si restringe lo scope della variabile var a ogni diversa chiamata della funzione in cui viene dichiarata; in effetti, utilizzare local equivale ad inserire la chiamata di funzione in una subshell, ma solo per una singola variabile. L'unica differenza fra queste due tecniche e' che laddove una subshell copia il valore delle proprie variabili dalla shell nella quale viene creata, un comando come local var nasconde immediatamente il valore precedente di val, cioe' la variabile diventa localmente non inizializzata. Se abbiamo bisogno di inizializzare la variabile con il suo valore esistente, e' necessario farlo esplicitamente, utilizzando local var="$var".

VEDI: http://tldp.org/LDP/abs/html/subshells.html#SUBSHELL

Valori di ritorno

Nello stesso modo in cui veniva utilizzato il valore di ritorno del comando test, e' possibile sfruttare il comando return per ritornare un valore specifico da una funzione allo script principale che ne ha effettuato la chiamata. Di seguito vediamo come e' possibile modificare facilmente il primo script che abbiamo introdotto sfruttando il valore di ritorno di una funzione.

aggiungi_utente()
{
  ((ID++))
  return $ID
}
 
ID=0
echo "Inizio dello script..."
aggiungi_utente Albert Einstein
echo "Aggiunto l'utente $? (Albert Einstein)"
aggiungi_utente Stephen Hawking
echo "Aggiunto l'utente $? (Stephen Hawking)"
aggiungi_utente Alan Turing
echo "Aggiunto l'utente $? (Alan Turing)"
echo "Fine dello script..."

Quoting: differenze tra apici e virgolette

L'uso delle virgolette ha effetto sulla valutazione degli spazi e dei caratteri di tabulazione

echo "Hello    World"
echo "Hello World"
echo "Hello * World"
echo Hello * World
echo Hello    World
echo "Hello" World
echo Hello "    " World
echo "Hello \"*\" World"
echo `hello` world
echo 'hello' world

Gli unici caratteri che vengono reinterpretati all'interno delle virgolette sono \, ` e $: il primo (\) viene anteposto ai caratteri speciali che non devono essere interpretati dalla shell, ma passati comunque al comando eseguito (ad esempio, quando si vuole che il comando echo stampi un messaggio contenente le virgolette); il secondo (`) viene utilizzato per racchiudere veri e propri comandi, il cui output viene inserito nella posizione del comando stesso; l'ultimo ($) e' infine il carattere che precede il nome di una variabile, il cui contenuto sara' sostituito ad esso.

$ comando="pwd"
$ echo "I comandi da eseguire all'interno delle \"\" sono racchiusi fra \`\`, come ad esempio \`$comando\`, che produce `$comando`"

echo "I comandi da eseguire all'interno delle \"\" sono racchiusi fra \`\`, come ad esempio \`$comando\`, che produce `$comando`"

Redirezione dell'I/O

Tutti i sistemi operativi basati su Unix forniscono almeno tre diversi canali input e output, chiamati rispettivamente stdin, stdout e stderr, che permettono la comunicazione fra un programma e l'ambiente in cui esso viene eseguito. In Bash ciascuno di questi canali viene numerato da 0 a 2, e prende il nome di file descriptor poiche' fa riferimento ad un particolare file: cosi' come avviene con un qualunque altro file memorizzato nel sistema, e' possibile manipolarlo, copiarlo, leggerlo o scrivere su di esso. Bash offre anche la possibilita' di aprirne di nuovi se le esigenze lo richiedono (utilizzando i numeri successivi al 2).

Quando un ambiente Bash viene avviato, tutti e tre i file descriptor di default puntano al terminale nel quale e' stata inizializzata la sessione: l'input (stdin 0) corrisponde a cio' che viene digitato nel terminale, ed entrambi gli output , stdout (1) per i messaggi tradizionali e stderr (2) per quelli di errore, vengono inviati al terminale. Infatti, un terminale aperto in un sistema operativo basato su Unix, e' in genere esso stesso un file, comunemente memorizzato in /dev/tty0; quando viene aperta una nuova sessione in parallelo ad una esistente, il nuovo terminale sara' /dev/tty1 e cosi' via. Percio', inizialmente i tre file descriptor puntano tutti al file che rappresenta il terminale in cui vengono eseguiti.

L'operatore > si utilizza per effettuare il redirect dell'output. Eseguendo:

  • comando >file
  • comando 1>file

output stream gli venga concatenato, l'operatore da utilizzare diventa >>.

E' anche possibile redirezionare sia stdout che stderr verso uno stesso file utilizzando l'operatore &> (o equivalentemente >&), cioe' scrivendo:

  • comando &> file

Lo stesso effetto si ottiene redirezionando i due stream in sequenza, come ad esempio:

  • comando >file 2>&1

Come e' facile immaginare, la redirezione di un canale di input (come quello predefinito stdin) avviene utilizzando l'operatore "<": volendo dare un file in input ad un comando o ad uno script, si utilizza quindi la notazione seguente:

  • comando <file

Ad esempio, se vogliamo leggere il contenuto della prima riga di un file e assegnarlo alla variabile riga, e' possibile semplicemente eseguire:

  • read -r riga < file

Tutte le redirezioni dei canali viste finora sono state effettuate per un singolo comando; ma come si fa a stabilire una redirezione permanente dei canali I/O da un certo momento in avanti tramite il comando exec:

exec 2>file
comando1
comando2

Un altro operatore molto utile per la manipolazione degli stream I/O e' quello di pipelining: |. Tramite questo simbolo, una sequenza di comandi separata da esso fa si' che ciascun comando venga eseguito allo stesso momento e che l'output di ognuno venga passato come input a quello successivo, da sinistra verso destra. Dobbiamo pero' fare attenzione: ogni comando viene eseguito in una subshell, percio' ogni variabile modificata da ciascuno non sara' poi letta dagli altri comandi della sequenza o nell'ambiente in cui viene eseguita l'intera pipeline, come si nota eseguendo il codice seguente:

echo "Prova" | (read var ; echo $var)

TEE

Per concludere, un comando molto utile, non fornito da Bash di default ma presente in pressoche' tutti i sistemi basati su Unix, e' tee: esso reindirizza lo stream input sia verso stdout sia verso un file specificato. Eseguire:

echo messaggio | tee file

Exit: terminare uno script

Uno script Bash ritorna un valore al processo chiamante tramite il comando built-in exit; il comando exit 4, percio', terminera' lo script (i comandi specificati dopo di esso non saranno mai eseguiti) e ritornera' un exit status di valore 4, indicando un certo tipo di errore. Omettendo l'exit status nel comando exit, oppure omettendo totalmente il comando, lo script ritorna l'exit status dell'ultimo comando eseguito all'interno di esso.

Per convenzione, si usa ritornare 0 se l'esecuzione e' terminata con successo (nel caso di un comando if, questo corrisponde al valore vero), oppure un valore intero positivo nel caso essa sia fallita con un errore

Un modo molto pratico e frequente di utilizzare gli exit status in Bash e' tramite gli operatori and (&&) , or (||) , e not (!) .

Ad esempio, supponiamo di voler eliminare il file esempio.txt e creare un nuovo file con lo stesso nome. Possiamo eliminarlo utilizzando il comando rm e crearlo di nuovo utilizzando touch; percio' potremmo scrivere in sequenza:

rm esempio.txt
touch esempio.txt

Ma nel caso in cui il comando di cancellazione fallisca, potremmo non volere che lo script procedesse con il secondo comando; possiamo percio' scrivere quanto segue:

rm esempio.txt && touch esempio.txt

Debugging: consigli utili

Esistono due modalita' offerte da Bash per l'esecuzione di uno script, che possono aiutare a scoprire facilmente gli errori: la modalita' verbose e quella xtrace. La prima si attiva aggiungendo l'opzione -v quando si esegue uno script (ad esempio sh -v script.sh); in questa modalita', ogni comando viene stampato (nel canale stderr) cosi' come deve essere eseguito, prima di effettuare ogni singola espansione. La seconda modalita', di gran lunga piu' utile, si attiva aggiungendo l'opzione -x. In modalita' xtrace, Bash stampa (sempre nel canale stderr) ogni comando come e' stato eseguito dopo ogni espansione, indicando inoltre il livello di ogni subshell in cui e' stato eseguito (utilizzando il prefisso + per ciascuna di esse).

Vediamo un semplice esempio: lo script bug.sh di seguito riportato genera un errore.

#!/bin/bash
hello="hello world"
[ $hello = hello ]

Eseguendo sh -x bug.sh possiamo facilmente individuarne la causa:

$ sh -x bug.sh
+ hello='hello world'
+ '[' hello world = hello ']'
bug.sh: line 3: [: too many arguments

Il comando test infatti riconosce hello e world come due parole separate; abbiamo quindi dimenticato di racchiudere la variabile $hello fra virgolette, come si nota dalla sua espansione effettuata da Bash.

Shell Script, un esempio completo

#!/bin/bash
 
cerca()
{
  local CONT=0
    for elem in `ls -d $1/*`
    do
    if [[ -f $elem ]]; then
      grep -Fxq $2 $elem
      if [[ $? == 0 ]]; then
        echo $elem >> $3
        ((CONT++))
      fi
    elif [[ -d $elem ]]; then
      cerca $elem $2 $3
      local SUB=$?
      CONT=$((SUB + CONT))
    fi
    done
  return $CONT
}
 
if [[ -z "$1" || -z "$2" || -z "$3" ]]; then
  echo "ERRORE: Parametri non corretti!" >&2
  exit 1
elif [[ ! -d $1 ]]; then
  echo "ERRORE: Directory $1 non esistente!" >&2
  exit 1
else
  cerca $1 $2 $3
  echo "Trovati $? file in $1 contenenti il pattern $2 e salvati in $3."
fi

Lo scopo della funzione e' quello di contare il numero di file contenenti il pattern nella directory fornita, e sommarlo a quello relativo alle sottodirectory in essa contenute.

La seconda porzione di script si occupa di controllare che i parametri forniti siano corretti e richiamare la funzione cerca, stampando un errore nel file descriptor di errore (stderr) in caso contrario.