Scripting Bash: le funzioni

Dopo i costrutti decisionali, i cicli e l’espansione dei parametri, è arrivato il momento di parlare di uno strumento molto potente che Bash ci mette a disposizione: le funzioni. Una funzione è un blocco di istruzioni a cui diamo un nome. Si può definire anche direttamente a riga di comando, prima ancora di parlare di script. In questo articolo vediamo come si scrive, come si passano gli argomenti e tre trabocchetti in cui si cade facilmente: le variabili globali, il return e il modo giusto di far restituire un valore.

La prima funzione

Creiamo una funzione che si chiama saluta. Le parentesi tonde dopo il nome dicono a Bash che stiamo definendo una funzione. Poi si apre un blocco con la graffa, si mettono i comandi che vogliamo (qui basta un semplice echo) e si chiude il blocco. Attenzione al punto e virgola dopo l’ultima istruzione: serve a separare più istruzioni fra loro ma anche, con una riga sola, a far capire a Bash che l’istruzione è finita. Se scriviamo tutto su una riga servono anche gli spazi dopo la graffa aperta e prima della chiusa.

saluta() { echo "Ciao dalla mia prima funzione"; }

Dando invio la funzione è registrata. La chiamiamo semplicemente con il suo nome, senza parentesi (funziona anche il completamento col tab):

saluta

L’output è:

Ciao dalla mia prima funzione

Per ridefinire una funzione basta riscriverla. Per esempio, prima del saluto facciamo pulire lo schermo:

saluta() { clear; echo "Ciao dalla mia prima funzione"; }
saluta

Ora viene eseguito il clear e poi l’echo. Una funzione serve a non dover riscrivere ogni volta una sequenza di istruzioni complicate.

Gli argomenti

Alle funzioni si passano gli argomenti esattamente come a qualsiasi comando, e dentro la funzione si leggono con le stesse variabili che abbiamo visto per gli script: $1, $2 e così via. Ridefiniamo saluta con un segnaposto per il primo argomento:

saluta() { echo "Ciao $1"; }
saluta Linux

L’output è:

Ciao Linux

Funzionano anche gli altri parametri speciali. Definiamo una funzione che si chiama conta e usa $# (il numero degli argomenti) e $@ (tutti gli argomenti):

conta() { echo "Mi hai passato $# argomenti: $@"; }
conta uno due tre quattro

L’output è:

Mi hai passato 4 argomenti: uno due tre quattro

Una funzione in uno script

Mettiamo la funzione dentro uno script. Qui il punto e virgola non serve, perché le istruzioni sono separate dagli a capo. Cosa importante: in uno script le funzioni vanno sempre dichiarate prima di usarle, quindi non possiamo chiamare saluta finché non l’abbiamo definita. Il file funzioni.sh:

#!/bin/bash

saluta() {
  echo "Ciao $1"
}

saluta Linux

Lo rendiamo eseguibile e lo lanciamo:

chmod +x funzioni.sh
./funzioni.sh

L’output è quello che ci aspettavamo:

Ciao Linux

Variabili globali e locali

Primo trabocchetto. In Bash le variabili sono globali di default, al contrario di quanto siete abituati se arrivate da altri linguaggi. Questo script, global.sh, definisce una variabile nome e una funzione cambia che la modifica:

#!/bin/bash

nome="Mario"

cambia() {
  nome="Luigi"
}

cambia
echo $nome

Se siete programmatori abituati a scope separati fra funzione e resto dello script vi aspettate Mario. Invece l’output è:

Luigi

La funzione ha modificato la stessa variabile globale: non c’è separazione di scope. Ma allora come si lavora su una variabile locale dentro una funzione? Esattamente al contrario di altri linguaggi: dove altrove si dichiara globale una variabile perché sia vista ovunque, in Bash la si dichiara locale se si vuole che resti dentro la funzione, con la parola chiave local. Nuovo script, local.sh:

#!/bin/bash

nome="Mario"

cambia() {
  local nome="Luigi"
  echo "dentro: $nome"
}

cambia
echo "fuori: $nome"

Con local abbiamo creato una copia locale della variabile, che non tocca quella globale. L’output è:

dentro: Luigi
fuori: Mario

Il return non restituisce valori

Secondo trabocchetto. Se siete affezionati alla parola chiave return, sappiate che in Bash non fa quello che vi aspettate: non serve a restituire un valore, serve a dare un codice di uscita, esattamente come gli exit code dei comandi e degli script. Zero vuol dire che è andato tutto bene, un altro valore vuol dire che qualcosa è andato storto. Facciamo la classica funzione che dice se un numero è pari:

pari() {
  if (( $1 % 2 == 0 ))
  then
    return 0
  else
    return 1
  fi
}

La funzione non stampa nulla, ma possiamo leggere il suo exit code con $? (l’exit code dell’ultima esecuzione):

pari 4
echo $?
pari 5
echo $?

Per il 4 l’exit code è 0, per il 5 è 1:

0
1

Proprio perché zero è “vero”, la funzione si può anche usare direttamente in un if.

Restituire un vero valore

Come si fa allora a restituire un valore e non solo un exit code? Semplicemente sfruttando quello che già sappiamo sulla cattura dell’output: la funzione scrive il risultato con echo e noi lo catturiamo con la sostituzione di comando. Una funzione che calcola il quadrato di un numero:

quadrato() {
  local n=$1
  echo $(( n * n ))
}

Se la lanciamo normalmente stampa il numero:

quadrato 3

L’output è:

9

Se invece catturiamo l’output in una variabile, il valore non viene stampato dalla funzione ma finisce in risultato, e lo usiamo dopo:

risultato=$(quadrato 5)
echo "Il quadrato di 5 è $risultato"

L’output è:

Il quadrato di 5 è 25

Le funzioni nelle pipe

Una funzione si comporta come qualunque altro programma, quindi può anche ricevere l’input da una pipe. Creiamo una funzione che fa un banalissimo tr, per rendere tutto maiuscolo:

maiuscolo() { tr 'a-z' 'A-Z'; }
echo ciao | maiuscolo

L’output è:

CIAO

La stringa è passata in pipe alla funzione, che con tr la rende tutta maiuscola.

Chiusura

Questo è quello che serve per cominciare: funzioni con nome, argomenti come negli script, local per non sporcare le variabili globali, return per gli exit code ed echo con la sostituzione di comando per i valori. Aprite il vostro editor preferito e giocateci un po’.

Link e risorse