105.2 Lezione 2
Certificazione: |
LPIC-1 |
---|---|
Versione: |
5.0 |
Argomento: |
105 Shell e Script di Shell |
Obiettivo: |
105.2 Personalizzare o scrivere semplici script |
Lezione: |
2 di 2 |
Introduzione
Gli script di shell sono generalmente destinati ad automatizzare le operazioni relative a file e directory, le stesse operazioni che potrebbero essere eseguite manualmente dalla riga di comando. Tuttavia, la copertura degli script di shell non è limitata ai soli documenti di un utente, poiché anche la configurazione e l’interazione con molti aspetti di un sistema operativo Linux vengono eseguite tramite file di script. La shell Bash offre molti utili comandi builtin per scrivere script di shell, ma la piena potenza di questi script si basa sulla combinazione dei comandi incorporati di Bash con le numerose utilità della riga di comando disponibili su un sistema Linux.
Test Estesi
Bash, come linguaggio di script, è principalmente orientato a lavorare con i file, quindi il comando integrato di Bash test
ha molte opzioni per valutare le proprietà degli oggetti del filesystem (essenzialmente file e directory). I test incentrati su file e directory sono utili, per esempio, per verificare se i file e le directory necessari per eseguire una determinata attività sono presenti e possono essere letti. Quindi, associato a un costrutto condizionale if, il set di azioni appropriato viene eseguito se il test ha esito positivo.
Il comando test
può valutare espressioni usando due diverse sintassi: le espressioni di prova possono essere fornite come argomento del comando test
oppure possono essere inserite tra parentesi quadre, dove il comando test
è dato implicitamente. Quindi, il test per valutare se /etc
è una directory valida può essere scritto come test -d /etc
o come [-d /etc]
:
$ test -d /etc $ echo $? 0 $ [ -d /etc ] $ echo $? 0
Come confermato dai codici di stato di uscita nella variabile speciale $?
— un valore di 0 significa che il test ha avuto successo — entrambe le forme hanno valutato /etc
come una directory valida. Supponendo che il percorso di un file o di una directory sia stato memorizzato nella variabile $VAR
, le seguenti espressioni possono essere utilizzate come argomenti per test
o racchiuse tra parentesi quadre:
-a "$VAR"
-
Valuta se il percorso in
VAR
esiste nel filesystem ed è un file. -b "$VAR"
-
Valuta se il percorso in
VAR
è un file speciale a blocchi. -c "$VAR"
-
Valuta se il percorso in
VAR
è un file speciale di caratteri. -d "$VAR"
-
Valuta se il percorso in
VAR
è una directory. -e "$VAR"
-
Valuta se il percorso in
VAR
esiste nel filesystem. -f "$VAR"
-
Valuta se il percorso in
VAR
esiste ed è un file normale. -g "$VAR"
-
Valuta se il percorso in
VAR
ha il permesso SGID. -h "$VAR"
-
Valuta se il percorso in
VAR
è un collegamento simbolico. -L "$VAR"
-
Valuta se il percorso in
VAR
è un collegamento simbolico. (come-h
). -k "$VAR"
-
Valuta se il percorso in
VAR
ha il permesso sticky bit. -p "$VAR"
-
Valuta se il percorso in
VAR
è un file pipe. -r "$VAR"
-
Valuta se il percorso in
VAR
è leggibile dall’utente corrente. -s "$VAR"
-
Valuta se il percorso in
VAR
esiste e non è vuoto. -S "$VAR"
-
Valuta se il percorso in
VAR
è un file socket. -t "$VAR"
-
Valuta se il percorso in
VAR
è aperto in un terminale. -u "$VAR"
-
Valuta se il percorso in
VAR
ha il permesso SUID. -w "$VAR"
-
Valuta se il percorso in
VAR
è scrivibile dall’utente corrente. -x "$VAR"
-
Valuta se il percorso in
VAR
è eseguibile dall’utente corrente. -O "$VAR"
-
Valuta se il percorso in
VAR
è di proprietà dell’utente corrente. -G "$VAR"
-
Valuta se il percorso in
VAR
appartiene al gruppo effettivo dell’utente corrente. -N "$VAR"
-
Valuta se il percorso in
VAR
è stato modificato dall’ultimo accesso. "$VAR1" -nt "$VAR2"
-
Valuta se il percorso in
VAR1
è più recente del percorso inVAR2
, in base alle date di modifica. "$VAR1" -ot "$VAR2"
-
Valuta se il percorso in
VAR1
è più vecchio diVAR2
. "$VAR1" -ef "$VAR2"
-
Questa espressione restituisce True se il percorso in
VAR1
è un hardlink aVAR2
.
Si consiglia di utilizzare le virgolette doppie attorno a una variabile testata perché, se la variabile risulta essere vuota, potrebbe causare un errore di sintassi per il comando test
. Le opzioni di test richiedono un argomento e una variabile vuota non messa tra virgolette causerebbe un errore a causa di un argomento obbligatorio mancante. Esistono anche test per variabili di testo arbitrarie, descritti come segue:
-z "$TXT"
-
Valuta se la variabile
TXT
è vuota (dimensione zero). -n "$TXT"
ortest "$TXT"
-
Valuta se la variabile
TXT
non è vuota. "$TXT1" = "$TXT2"
o"$TXT1" == "$TXT2"
-
Valuta se
TXT1
eTXT2
sono uguali. "$TXT1" != "$TXT2"
-
Valuta se
TXT1
eTXT2
non sono uguali. "$TXT1" < "$TXT2"
-
Valuta se
TXT1
viene prima diTXT2
, in ordine alfabetico. "$TXT1" > "$TXT2"
-
Valuta se
TXT1
viene dopoTXT2
, in ordine alfabetico.
Lingue diverse possono avere regole diverse per l’ordine alfabetico. Per ottenere risultati coerenti, indipendentemente dalle impostazioni di localizzazione del sistema in cui viene eseguito lo script, si consiglia di impostare la variabile d’ambiente LANG
su C
, come in LANG=C
, prima di eseguire operazioni che implicano l’ordine alfabetico . Questa definizione manterrà anche i messaggi di sistema nella lingua originale, quindi dovrebbe essere utilizzata solo nell’ambito dello script.
I confronti numerici hanno il proprio set di opzioni di test:
$NUM1 -lt $NUM2
-
Valuta se
NUM1
è minore diNUM2
. $NUM1 -gt $NUM2
-
Valuta se
NUM1
è maggiore diNUM2
. $NUM1 -le $NUM2
-
Valuta se
NUM1
è minore o uguale aNUM2
. $NUM1 -ge $NUM2
-
Valuta se
NUM1
è maggiore o uguale aNUM2
. $NUM1 -eq $NUM2
-
Valuta se
NUM1
è uguale aNUM2
. $NUM1 -ne $NUM2
-
Valuta se
NUM1
non è uguale aNUM2
.
Tutti i test possono ricevere i seguenti modificatori:
! EXPR
-
Valuta se l’espressione
EXPR
è falsa. EXPR1 -a EXPR2
-
Valuta se sia
EXPR1
cheEXPR2
sono veri. EXPR1 -o EXPR2
-
Valuta se almeno una delle due espressioni è vera.
Un altro costrutto condizionale, case
, può essere visto come una variazione del costrutto if. L’istruzione case
eseguirà un elenco di comandi dati se un elemento specificato — il contenuto di una variabile, per esempio — può essere trovato in un elenco di elementi separati da pipes e terminato da )
. Il seguente script di esempio mostra come il costrutto case
pussa essere usato per indicare il formato di pacchettizzazione del software corrispondente per una data distribuzione Linux:
#!/bin/bash DISTRO=$1 echo -n "Distribution $DISTRO uses " case "$DISTRO" in debian | ubuntu | mint) echo -n "the DEB" ;; centos | fedora | opensuse ) echo -n "the RPM" ;; *) echo -n "an unknown" ;; esac echo " package format."
Ogni elenco di pattern e comandi associati deve terminare con ;;
, ;&
, o ;;&
. L’ultimo pattern, un asterisco, corrisponderà se nessun altro pattern precedente corrispondeva in precedenza. L’istruzione esac
(case al contrario) termina il costrutto case
. Supponendo che lo script di esempio precedente fosse chiamato script.sh
e che venga eseguito su OpenSuse come primo argomento, verrà generato il seguente output:
$ ./script.sh opensuse Distribution opensuse uses the RPM package format.
Tip
|
Bash ha un’opzione chiamata |
L’elemento ricercato e gli schemi subiscono l’espansione della tilde (~
), l’espansione dei parametri, la sostituzione dei comandi e l’espansione aritmetica. Se l’elemento cercato è specificato tra virgolette, verranno rimossi prima che venga tentata la corrispondenza.
Costrutti di Loop
Gli script vengono spesso utilizzati come strumento per automatizzare attività ripetitive, eseguendo lo stesso set di comandi fino a quando non viene verificato un criterio di interruzione. Bash ha tre istruzioni di ciclo — for
, until
e while
— progettate per specifici costrutti di loop.
Il costrutto for
percorre un dato elenco di elementi — di solito un elenco di parole o qualsiasi altro segmento di testo separato da spazi — eseguendo lo stesso insieme di comandi su ciascuno di quegli elementi. Prima di ogni iterazione, l’istruzione for
assegna l’elemento corrente a una variabile, che può quindi essere utilizzata dai comandi inclusi. Il processo viene ripetuto fino a quando non ci sono più elementi rimasti. La sintassi del costrutto for
è:
for VARNAME in LIST do COMMANDS done
VARNAME
è un nome di variabile di shell arbitrario e LIST
è una qualsiasi sequenza di termini separati. I caratteri di delimitazione validi che dividono gli elementi nell’elenco sono definiti dalla variabile d’ambiente IFS
, e sono i caratteri space, tab e newline per impostazione predefinita. L’elenco dei comandi da eseguire è delimitato dalle istruzioni do
e done
, quindi i comandi possono occupare tutte le righe necessarie.
Nell’esempio seguente, il comando for
prenderà ogni elemento dall’elenco fornito — una sequenza di numeri — e lo assegnerà alla variabile NUM
, un elemento alla volta:
#!/bin/bash for NUM in 1 1 2 3 5 8 13 do echo -n "$NUM is " if [ $(( $NUM % 2 )) -ne 0 ] then echo "odd." else echo "even." fi done
Nell’esempio, un costrutto if
nidificato viene utilizzato insieme a un’espressione aritmetica per valutare se il numero nella variabile NUM
corrente è pari o dispari. Supponendo che lo script di esempio precedente sia denominato script.sh
e si trovi nella directory corrente, verrà generato il seguente output:
$ ./script.sh 1 is odd. 1 is odd. 2 is even. 3 is odd. 5 is odd. 8 is even. 13 is odd.
Bash supporta anche un formato alternativo ai costrutti for
, con la notazione delle doppie parentesi. Questa notazione ricorda la sintassi dell’istruzione for
del linguaggio di programmazione C ed è particolarmente utile per lavorare con gli array:
#!/bin/bash SEQ=( 1 1 2 3 5 8 13 ) for (( IDX = 0; IDX < ${#SEQ[*]}; IDX++ )) do echo -n "${SEQ[$IDX]} is " if [ $(( ${SEQ[$IDX]} % 2 )) -ne 0 ] then echo "odd." else echo "even." fi done
Questo script di esempio genererà lo stesso identico output dell’esempio precedente. Tuttavia, invece di usare la variabile NUM
per memorizzare un elemento alla volta, la variabile IDX
viene impiegata per tracciare l’indice dell’array corrente in ordine crescente, partendo da 0 e aggiungendo continuamente ad esso il numero di elementi nell’array SEQ
. L’elemento effettivo viene recuperato dalla sua posizione nell’array con ${SEQ[$IDX]}
.
Allo stesso modo, il costrutto until
esegue una sequenza di comandi fino a quando un comando di test — come il comando test
stesso — termina con lo stato 0 (successo). Per esempio, la stessa struttura di loop dell’esempio precedente può essere implementata con until
come segue:
#!/bin/bash SEQ=( 1 1 2 3 5 8 13 ) IDX=0 until [ $IDX -eq ${#SEQ[*]} ] do echo -n "${SEQ[$IDX]} is " if [ $(( ${SEQ[$IDX]} % 2 )) -ne 0 ] then echo "odd." else echo "even." fi IDX=$(( $IDX + 1 )) done
Il costrutto until
può richiedere più istruzioni rispetto ai costrutti for
, ma può essere più adatto a criteri di arresto non numerici forniti dalle espressioni test
o da qualsiasi altro comando. È importante includere azioni che assicurino un criterio di arresto valido, come l’incremento di una variabile contatore, altrimenti il ciclo potrebbe essere eseguito indefinitamente.
L’istruzione while
è simile all’istruzione until
, ma while
continua a ripetere l’insieme di comandi se il comando di test termina con lo stato 0 (successo). Pertanto, l’istruzione until [ $IDX -eq ${#SEQ[*]} ]
dell’esempio precedente è equivalente a while [ $IDX -lt ${#SEQ[*]} ]
, come ciclo che dovrebbe ripetersi mentre l’indice dell’array è minore del il totale degli elementi nell’array.
Un Esempio più Elaborato
Immagina che un utente desideri sincronizzare periodicamente una raccolta dei propri file e directory con un altro dispositivo di archiviazione, montato in un punto di montaggio arbitrario nel filesystem, e che un sistema di backup completo sia considerato eccessivo. Poiché si tratta di un’attività che deve essere eseguita periodicamente, uno script di shell è una buon candidato per una simile automazione.
Il compito è semplice: sincronizzare ogni file e directory contenuto in un elenco, da una directory di origine come primo argomento dello script a una directory di destinazione come secondo argomento dello script. Per semplificare l’aggiunta o la rimozione di elementi dall’elenco, verrà conservato in un file separato, ~/sync.list
, un elemento per riga:
$ cat ~/.sync.list Documents To do Work Family Album .config .ssh .bash_profile .vimrc
Il file contiene una combinazione di file e directory, alcuni con spazi vuoti nei nomi. Questo è uno scenario adatto per il comando Bash builtin mapfile
, che analizzerà qualsiasi contenuto di testo e creerà una variabile di array da esso, posizionando ogni riga come un singolo elemento di array. Il file di script sarà chiamato sync.sh
, contenente il seguente codice:
#!/bin/bash set -ef # List of items to sync FILE=~/.sync.list # Origin directory FROM=$1 # Destination directory TO=$2 # Check if both directories are valid if [ ! -d "$FROM" -o ! -d "$TO" ] then echo Usage: echo "$0 <SOURCEDIR> <DESTDIR>" exit 1 fi # Create array from file mapfile -t LIST < $FILE # Sync items for (( IDX = 0; IDX < ${#LIST[*]}; IDX++ )) do echo -e "$FROM/${LIST[$IDX]} \u2192 $TO/${LIST[$IDX]}"; rsync -qa --delete "$FROM/${LIST[$IDX]}" "$TO"; done
La prima azione che lo script esegue è ridefinire due parametri della shell con il comando set
: l’opzione -e
fa terminare immediatamente l’esecuzione se un comando esce con uno stato diverso da zero e l’opzione -f
disabilita il globbing del nome del file. Entrambe le opzioni possono essere abbreviate come -ef
. Questo non è un passaggio obbligatorio, ma aiuta a diminuire la probabilità di comportamenti imprevisti.
Le istruzioni effettive orientate all’applicazione del file di script possono essere suddivise in tre parti:
-
Raccoglie e controlla i parametri dello script
La variabile
FILE
è il percorso del file contenente l’elenco degli elementi da copiare:~/.sync.list
. Le variabiliFROM
eTO
sono rispettivamente i percorsi di origine e di destinazione. Poiché questi ultimi due parametri sono forniti dall’utente, passano attraverso un semplice test di convalida eseguito dal costruttoif
: se uno dei due non è una directory valida, valutato dal test[ ! -d "$FROM" -o ! -d "$TO" ]
, lo script mostrerà un breve messaggio di aiuto e poi terminerà con uno stato di uscita di 1. -
Carica l’elenco di file e directory
Dopo che tutti i parametri sono stati definiti, viene creato un array contenente l’elenco degli elementi da copiare con il comando
mapfile -t LIST < $FILE
. L’opzione-t
dimapfile
rimuoverà il carattere di nuova riga finale da ogni riga prima di includerlo nella variabile array denominataLIST
. Il contenuto del file indicato dalla variabileFILE
—~/.sync.list
— viene letto tramite il reindirizzamento dell’input. -
Esegue la copia e informa l’utente
Un ciclo
for
che utilizza la notazione con doppie parentesi attraversa l’array di elementi, con la variabileIDX
che tiene traccia dell’incremento dell’indice. Il comandoecho
informerà l’utente di ogni elemento che viene copiato. Il carattere Unicode di escape —\u2192
— per il carattere freccia destra è presente nel messaggio di output, quindi deve essere utilizzata l’opzione-e
del comandoecho
. Il comandorsync
copierà selettivamente solo i file modificati dall’origine, quindi il suo utilizzo è raccomandato per tali attività. Le opzionirsync
-q
e-a
, condensate in-qa
, inibiranno i messaggirsync
e attiveranno la modalità archive, dove vengono preservate tutte le proprietà dei file. L’opzione--delete
farà in modo chersync
cancelli un elemento nella destinazione che non esiste più nell’origine, quindi dovrebbe essere usato con attenzione.
Supponendo che tutti gli elementi nell’elenco esistano nella directory home dell’utente carol
, /home/carol
, e la directory di destinazione /media/carol/backup
punta a un dispositivo di archiviazione esterno montato, il comando sync. sh /home/carol /media/carol/backup
genererà il seguente output:
$ sync.sh /home/carol /media/carol/backup /home/carol/Documents → /media/carol/backup/Documents /home/carol/"To do" → /media/carol/backup/"To do" /home/carol/Work → /media/carol/backup/Work /home/carol/"Family Album" → /media/carol/backup/"Family Album" /home/carol/.config → /media/carol/backup/.config /home/carol/.ssh → /media/carol/backup/.ssh /home/carol/.bash_profile → /media/carol/backup/.bash_profile /home/carol/.vimrc → /media/carol/backup/.vimrc
L’esempio presuppone anche che lo script venga eseguito da root o dall’utente carol
, poiché la maggior parte dei file sarebbe illeggibile da altri utenti. Se script.sh
non è all’interno di una directory elencata nella variabile d’ambiente PATH
, allora dovrebbe essere specificato con il suo percorso completo.
Esercizi Guidati
-
Come potrebbe essere usato il comando
test
per verificare se il percorso del file memorizzato nella variabileFROM
è più recente di un file il cui percorso è memorizzato nella variabileTO
? -
Il seguente script dovrebbe stampare una sequenza numerica da 0 a 9, ma invece stampa indefinitamente 0. Cosa si dovrebbe fare per ottenere l’output atteso?
#!/bin/bash COUNTER=0 while [ $COUNTER -lt 10 ] do echo $COUNTER done
-
Supponiamo che un utente abbia scritto uno script che genera un elenco ordinato di nomi utente. L’elenco ordinato risultante viene presentato come il seguente sul suo computer:
carol Dave emma Frank Grace henry
Tuttavia, lo stesso elenco è ordinato come segue sul computer del suo collega:
Dave Frank Grace carol emma henry
Cosa potrebbe spiegare le differenze tra i due elenchi ordinati?
Esercizi Esplorativi
-
Come possono essere usati tutti gli argomenti della riga di comando dello script per inizializzare un array Bash?
-
Perché, controintuitivamente, il comando
test 1 > 2
viene valutato come vero? -
In che modo un utente potrebbe cambiare temporaneamente il separatore di campo predefinito solo con il carattere di nuova riga, pur essendo ancora in grado di ripristinarlo al suo contenuto originale?
Sommario
Questa lezione dà uno sguardo più approfondito ai test disponibili del comando test
e ad altri costrutti condizionali e di ciclo, necessari per scrivere script di shell più elaborati. Un semplice script di sincronizzazione dei file viene fornito come esempio di una pratica applicazione di script di shell. La lezione segue i seguenti passaggi:
-
Test estesi per i costrutti condizionali
if
ecase
. -
Costrutti di loop della shell:
for
,until
ewhile
. -
Iterazione attraverso array e parametri.
I comandi e le procedure affrontati sono stati:
test
-
Esegue un confronto tra gli elementi forniti al comando.
if
-
Un costrutto logico utilizzato negli script per valutare qualcosa come vero o falso, quindi l’esecuzione del comando successivo in base ai risultati.
case
-
Valuta diversi valori rispetto a una singola variabile. L’esecuzione del comando script viene quindi eseguita a seconda del risultato del comando
case
. for
-
Ripete l’esecuzione di un comando in base a un dato criterio.
until
-
Ripete l’esecuzione di un comando finché un’espressione non restituisce false.
while
-
Ripete l’esecuzione di un comando mentre una data espressione restituisce true.
Risposte agli Esercizi Guidati
-
Come potrebbe essere usato il comando
test
per verificare se il percorso del file memorizzato nella variabileFROM
è più recente di un file il cui percorso è memorizzato nella variabileTO
?Il comando
test "$FROM" -nt "$TO"
restituirà un codice di stato 0 se il file nella variabileFROM
è più recente del file nella variabileTO
. -
Il seguente script dovrebbe stampare una sequenza numerica da 0 a 9, ma invece stampa indefinitamente 0. Cosa si dovrebbe fare per ottenere l’output atteso?
#!/bin/bash COUNTER=0 while [ $COUNTER -lt 10 ] do echo $COUNTER done
La variabile
COUNTER
dovrebbe essere incrementata, cosa che potrebbe essere eseguita con l’espressione aritmeticaCOUNTER=$(( $COUNTER + 1 ))
, per raggiungere i criteri di stop e terminare il ciclo. -
Supponiamo che un utente abbia scritto uno script che genera un elenco ordinato di nomi utente. L’elenco ordinato risultante viene presentato come il seguente sul suo computer:
carol Dave emma Frank Grace henry
Tuttavia, lo stesso elenco è ordinato come segue sul computer del suo collega:
Dave Frank Grace carol emma henry
Cosa potrebbe spiegare le differenze tra i due elenchi ordinati?
L’ordinamento è basato sulle impostazioni internazionali del sistema corrente. Per evitare incongruenze, le attività di ordinamento dovrebbero essere eseguite con la variabile d’ambiente
LANG
impostata suC
.[[sec.105.2_02-AEE]] == Risposte agli Esercizi Esplorativi
-
Come possono essere usati tutti gli argomenti della riga di comando dello script per inizializzare un array Bash?
I comandi
PARAMS=( $* )
oPARAMS=( "$@" )
creeranno un array chiamatoPARAMS
con tutti gli argomenti. -
Perché, controintuitivamente, il comando
test 1 > 2
viene valutato come vero?L’operatore
>
deve essere utilizzato con i test di stringa, non con i test numerici. -
In che modo un utente potrebbe cambiare temporaneamente il separatore di campo predefinito solo con il carattere di nuova riga, pur essendo ancora in grado di ripristinarlo al suo contenuto originale?
Una copia della variabile
IFS
può essere memorizzata in un’altra variabile:OLDIFS=$IFS
. Quindi il nuovo separatore di riga è definito conIFS=$'\n'
e la variabile IFS può essere ripristinata conIFS=$OLDIFS
.