Bash scripting - espansioni non quotabili
Bash scripting |
Sommario |
Le espansioni trattate nelle sezioni successive avvengono unicamente in stringhe non quotate, almeno limitatamente ai caratteri speciali che le attivano, e sono attivabili soltanto da caratteri diversi dal $.
Le loro priorità sono tutte diverse: l'espansione di parentesi è quella maggiore, seguita dalla tilde, da tutte le espansioni attivabili con $ (quelle quotabili) e infine dall'espansione di percorso.
Inoltre l'espansione di tilde, che è sempre espansa in una singola stringa, è l'unica possibile in un'assegnazione tra le espansioni non quotabili.
Espansione di tilde
Sintassi:
~
(per digitarlo con tastiera con layout italiano:Alt-Gr+ì
) si espande alla home, se non è quotata (equivalente all'uso di ${HOME}, che può essere quotata);~utente
si espande alla home di un dato utente, se esiste, ma la stringa non può essere quotata né essere una variabile.
Si distingue dall'espansione di percorso perché:
- si espande sempre a una singola stringa;
- può essere espansa in un'assegnazione, se non è quotata;
- ha priorità maggiore delle espansioni quotabili; per cui, se assegnata quotata a una variabile, non sarà espansa quando si accede alla variabile.
Esempi:
var=~ # assegno la home dell'utente a $var var=$HOME # equivalente (ma più chiaro) var="~" # assegno ~ a $var printf %s\\n "$var" # stampo ~ printf %s\\n $var # equivalente (nessuna espansione) var=~root # assegno a var la home di root printf %s\\n ~ # stampo la home dell'utente printf %s\\n "$HOME" # equivalente printf %s\\n "~" # stampo ~ printf %s\\n ~root # stampo la home di root printf %s\\n ~fdsfd # stampo ~fdsfd (l'utente fdsfd non esiste)
È possibile indicare un percorso a partire dalla home, in tal caso può essere quotata la parte successiva a /
:
printf %s\\n ~/"percorso quotato" # può contenere anche variabili printf %s\\n ~utente/"percorso" # come sopra printf %s\\n ~"/percorso" # ERRORE: nessuna espansione!
Si noti che il percorso può anche non esistere, infatti soltanto le stringhe ~/
e ~utente/
sono espanse. Non è infatti un'espansione di percorso.
Espansione di percorso
Le espansioni di percorso sono possibili solo se i caratteri speciali che la consentono (* ? [ ]
) non sono racchiusi tra virgolette, apici o preceduti da /
. È sempre consigliabile racchiudere tutto il resto tra virgolette, per non permettere espansioni accidentali.
L'espansione non è possibile, direttamente, in un'assegnazione. Avendo la priorità più bassa, contrariamente all'espansione di tilde può avvenire anche in seguito all'espansione di una variabile (e con ogni altra espansione), se non è quotata:
var="./*" # assegno ./* a $var var=./* # come sopra (nessuna espansione in un'assegnazione) printf %s\\n "$var" # stampa letteralmente ./* printf %s\\n $var # stampa la lista di tutti i file non nascosti # nella directory corrente, oppure ./* se è vuota
Sintassi (prefisso e suffisso possono essere omessi, o essere variabili da espandere):
prefisso?suffisso
sostituisce un singolo carattere di un nome di file, con tutti quelli possibili che combaciano con le due stringhe date, ma di default tranne il . iniziale se manca il prefisso;prefisso*suffisso
può sostituire tutti i caratteri nei nomi di file, ma di default tranne quelli nascosti se manca il prefisso (ossia quelli inizianti con .);prefisso[classe]suffisso
sostituisce un singolo carattere di un nome di file, con tutti quelli possibili appartenenti alla classe data e che combaciano con le due stringhe (tranne il carattere . dei file nascosti, perfino se contenuto nella classe). La classe può contenere:- una lista di caratteri, tutti attaccati, per sostituirne uno qualsiasi della lista;
- un intervallo composto da due caratteri separati da un trattino
-
, per sostituirne uno qualsiasi dell'intervallo in base al loro valore ASCII; - un
!
iniziale per farne il complemento, ossia sostituirne uno qualsiasi non presente nella classe; - il carattere
!
può essere contenuto per il suo valore letterale in una classe purché non in prima posizione, mentre il carattere-
è considerato letteralmente soltanto in prima posizione (o seconda se dopo!
) e in ultima posizione.
Esistono inoltre delle classi già predefinite, utilizzabili all'interno di [ ] con altre parentesi quadre e la seguente sintassi:
- [:lower:], per caratteri alfabetici minuscoli (a-z);
- [:upper:], per caratteri alfabetici maiuscoli (A-Z);
- [:alpha:], per caratteri alfabetici minuscoli e maiuscoli (a-zA-Z);
- [:digit:], per caratteri numerici (0-9);
- [:xdigit:], per caratteri numerici in base esadecimale (0-9A-F);
- [:alnum:], per caratteri alfabetici e numerici (a-zA-Z0-9);
- [:word:], per caratteri alfabetici, numerici e underscore (a-zA-Z0-9_);
- [:punct:], per caratteri di punteggiatura, accenti, virgolette, parentesi, underscore, operatori e tutti i simboli presenti tra i primi 127 caratteri ASCII;
- [:graph:], per caratteri che hanno una rappresentazione grafica visibile (niente spazi), ossia tutto ciò che appartiene alle classi [:word:] e [:punct:]. Corrisponde a tutti i caratteri ASCII compresi tra 21 e 126;
- [:print:], corrisponde alla classe [:graph:] con l'aggiunta del carattere spazio (non tabulazioni o "a capo"), ossia ai caratteri ASCII compresi tra 20 e 126;
- [:blank:], per caratteri di spaziatura orizzontale che lasciano spazi vuoti, ossia spazio e tabulazione (orizzontale);
- [:space:], per tutti i caratteri di spaziatura, ossia spazio, tabulazione orizzontale e verticale, interruzioni (break), ritorno a inizio riga (carriage return) e "a capo";
- [:cntrl:], per tutti i caratteri di controllo, ossia i primi 128 caratteri ASCII che non appartengono alla classe [:print:]. Non include lo spazio, ma include tutti gli altri caratteri presenti in [:space:]; e in generale i caratteri ASCII da 0 a 19, e il carattere ASCII n. 127;
- [:ascii:], per tutti i caratteri ASCII da 0 a 127, ed è equivalente alle due classi [:print:] e [:cntrl:].
Esempi di espansioni di percorso
Se un nome di file non include il percorso assoluto (iniziante con la directory radice /) o relativo (iniziante con ./ oppure ../, dalla directory corrente e da quella superiore rispettivamente), di default si assume che sia nella directory corrente. Tuttavia per evitare ambiguità con i nomi delle opzioni di alcuni comandi, in presenza di possibili nomi di file inizianti con il trattino -, in particolare se la parte iniziale del file è generata dall'espansione di percorso, è sempre bene rendere esplicito il percorso relativo premettendo ./ al nome del file.
Esempi (nella directory corrente):
./file.???
si espande a tutti i file con nome "file" e con una qualsiasi estensione di tre caratteri;./???.ext
si espande a tutti i file con nomi di tre caratteri (salvo i file nascosti, ossia con . iniziale) ed estensione ext;./*
si espande a tutti i file non nascosti nella directory corrente;./*.txt
espande a tutti i file con estensione .txt (NOTA: anche directory e qualsiasi file non regolare avente tale estensione);./*.[tT][xX][tT]
espande a tutti i file con estensione txt (ignorando maiuscole e minuscole);./*."${estensione}"
espande a tutti i file con l'estensione indicata dalla variabile quotata;"./${nome}"*
espande a tutti i file inizianti con il prefisso indicato dalla variabile quotata;./*/
espande a tutte le directory non nascoste;./[a-zA-Z]*
espande a tutti i file inizianti con una lettera qualsiasi;./[[:alpha:]]*
equivalente a sopra;./[[:word:]]*
espande a tutti i file inizianti con lettere maiuscole, minuscole, numeri e underscore;./.*
espande a tutti i file nascosti (ATTENZIONE: comprese "." e "..", ossia directory corrente e superiore);./.[!.]*
espande a tutti i file nascosti di almeno due caratteri in cui il secondo non è un punto (non espande a . e .., ma nemmeno a possibili file nascosti inizianti con ..);./..?*
espande a tutti i file nascosti di almeno tre caratteri in cui il secondo è un punto (tutti i file nascosti saltati dal precedente, ma sempre escludendo . e ..);./.[!.]* ./..?*
espande a tutti i file nascosti, esclusi . e .. (POSIX).
È importante sapere che, se nessun file combacia con un dato pattern, allora l'espansione non viene effettuata e i caratteri mantengono il loro valore letterale. E inoltre *
, ?
, [
e ]
sono caratteri validi per un nome di file.
L'esistenza di file ottenuti da tali espansioni va pertanto sempre controllata, impiegando l'espansione per generare una lista di stringhe all'interno di un ciclo for
, ed effettuando poi il controllo di esistenza su ognuna:
for file in ./*; do if [ -e "$file" ]; then ... fi done
Il pattern *
è l'unico in grado di sostituire un numero qualsiasi di caratteri (zero o più), mentre ?
e le classi sempre e soltanto un singolo carattere. Si faccia attenzione però che tutti i pattern, e non solo quelli composti da *
, possono generare liste di percorsi, in presenza di fili multipli con lo stesso prefisso e/o suffisso. Per esempio il pattern ./a?c può espandersi alla lista ./abc ./aBC ./acc, se esistono questi tre file nella directory corrente (e nessun altro di tre caratteri che inizi con a e termini con c).
Espansione di percorso con nuovi file
L'espansione di percorso può fallire anche se non trova corrispondenze nella parte non riguardante i caratteri speciali, perché i caratteri speciali sono espansi in funzione di tutta la stringa. Questo significa che deve esistere il risultato dell'espansione, considerando il percorso nella sua interezza, prima dell'esecuzione di un qualsiasi comando.
Ne consegue che è sempre sbagliato usare l'espansione di percorso direttamente con comandi di creazione di file:
# crea un file in ogni directory, se non esiste già touch -- ./*/"file" # SBAGLIATO! (per come avviene l'espansione) # aggiorna il tempo di accesso e modifica dei file (se esistenti) in ogni directory touch -- ./*/"file" # SBAGLIATO! (sarebbe corretto SOLO se almeno un file esiste) # forma corretta, sempre e solo con ciclo for e if [ ... ] for file in ./*/"file" # espande ai file GIÀ esistenti do # il confronto serve nel caso in cui l'espansione resti ./*/file # e la directory * non esista, ma non serve a nient'altro # nuovi file non verrebbero creati perché l'espansione avviene # solo con le corrispondenze trovate! if [ -e "$file" ]; then touch -- "$file" fi done # forma corretta del comando iniziale per creare un file in ogni directory for dir in ./*/ # SOLO la parte già esistente nell'espansione! do if [ -e "$dir" ]; then touch -- "${dir}/file" fi done
Cambiare i risultati dell'espansione
Il comportamento di default dell'espansione può essere cambiato in bash (non POSIX), tramite shopt -s
(set):
- nullglob espande a "niente" se non trova nessun file con un dato pattern, rendendo superfluo il controllo sull'esistenza;
- dotglob espande ai file nascosti (ma non a . e ..);
- nocaseglob espande il percorso a tutte le corrispondenze trovate, ignorando maiuscole e minuscole (case-insensitive).
Per esempio per espandere a tutti i file, compresi quelli nascosti:
shopt -s dotglob nullglob for file in ./*; do ... done
Per disabilitare un'opzione, ripristinando il default, si può utilizzare shopt -u
(unset).
Esempio: cambiare l'estensione ai file regolari
Rinomina tutti i file regolari *.txt
della directory corrente in *.log
, tramite il comando esterno mv
:
for f in ./*.txt; do if [ -f "$f" ]; then mv -- "$f" "${f%txt}log" fi done
Si noti che utilizzando [ -f ... ]
in luogo di [ -e ... ]
, si saltano anche tutti i file che non sono regolari, e che potrebbero essere restituiti dall'espansione di percorso.
Espansione di parentesi (graffa)
In bash (non POSIX) se i caratteri {
e }
non sono quotati, e non sono preceduti dal carattere di escape \
, possono essere espansi con due diverse sintassi per generare una lista di stringhe. E più espansioni di parentesi possono essere annidate.
Questa espansione avviene prima di tutte le altre, e il risultato può passare per tutte le altre espansioni. Non può avvenire in un'assegnazione, se non all'interno di altre espansioni.
Con indici di intervallo
Sintassi: prefisso{x..y[..z]}suffisso
L'espansione avviene per tutte le stringhe nell'intervallo compreso da "prefissoxsuffisso" fino a "prefissoysuffisso", con incrementi di 1 (o z se specificato). Le stringhe prefisso e suffisso possono essere omesse, o essere variabili (anche quotate, purché le graffe non lo siano), mentre x e y (e z, se presente) devono essere determinati valori e non possono essere variabili:
{x..y}
dove x e y sono due interi;{x..y..z}
dove x, y e z sono tre interi;{a..b}
dove a e b sono due caratteri;{a..b..z}
dove a e b sono due caratteri, e z è un intero.
Per esempio:
# crea un file temporaneo, associa il percorso a $tmp_file tmp_file=$(tempfile) # crea altri dieci file temporanei (.0, .1, .., .9) con lo stesso nome touch -- "$tmp_file".{0..9}
Con lista di stringhe
Sintassi: prefisso{stringa1,stringa2,...}suffisso
L'espansione avviene per tutte le stringhe nella lista, racchiudendole tra il prefisso e il suffisso dati, se presenti. Il prefisso, il suffisso e tutte le stringhe possono essere variabili, anche quotate, purché non siano quotate le graffe e le virgole interne.
Questa espansione è effettuata prima di tutte le altre, e il risultato dell'espansione se non quotato può quindi subire ulteriori espansioni. Per esempio per effettuare un'operazione sui file nella cartella corrente che hanno una data estensione, si può scrivere:
for file in ./*.{odt,abw,txt,rtf,doc}; do if [ -e "$file" ]; then ... fi done
e la prima riga è equivalente a:
for file in ./*.odt ./*.abw ./*.txt ./*.rtf ./*.doc; do
Un altro esempio particolarmente utile potrebbe essere la copia di un file in un'altra directory, il cui percorso assoluto è molto lungo:
cp -- /percorso/assoluto/decisamente/lungo/del/file/nomefile /percorso/assoluto/decisamente/lungo/del/file/nomefile.nuovo
che si traduce in:
cp -- /percorso/assoluto/decisamente/lungo/del/file/{nomefile,nomefile.nuovo}
senza bisogno di spostarsi dalla directory corrente. E lo stesso si applica a mv
per spostare o rinominare un file.
Differenze con l'espansione di percorso
L'espansione di parentesi graffe:
- non è POSIX, mentre l'espansione di percorso sì;
- espande delle stringhe, non ha importanza a cosa si riferiscono, mentre l'espansione di percorso si espande solo a percorsi esistenti (al tempo dell'espansione, ossia prima dell'esecuzione del comando);
- avviene per prima e può contenere altre espansioni (anche quotate, purché non siano quotate le graffe e le virgole), mentre l'espansione di percorso avviene per ultima e può solo essere il risultato di altre espansioni (se non quotate);
- ha una forma con indici di intervallo che è completamente diversa dall'intervallo permesso in una classe dell'espansione di percorso.
Riguardo l'ultimo punto si consideri per esempio file{1..20}
: si espande alla lista (di stringhe!) file1 file2 ... file20, a prescindere che esistano.
All'opposto file[1-20]
non è possibile, nel senso che ha tutt'altro significato. Infatti l'espansione di percorso [1-20]
significa: tutti i caratteri tra 1 e 2, e lo 0, ossia equivale alla forma (più comprensibile) [0-2]
, perché l'intervallo è possibile solo tra due singoli caratteri all'interno di una classe. E inoltre, al solito con le espansioni di percorso, file[0-2]
sarebbe espanso ai soli file esistenti tra file0, file1 e file2, e solo se almeno uno dei tre esiste oppure resterebbe letteralmente file[0-2]
.