Bash scripting - file descriptor

Da Guide@Debianizzati.Org.
Vai alla navigazione Vai alla ricerca
Bash scripting

Sommario

  1. Introduzione
  2. Comandi essenziali
  3. Variabili (stringhe)
  4. Caratteri di escape, apici e virgolette
  5. Espansioni in stringhe quotate
  6. Espansioni non quotabili
  7. Istruzioni composte
  8. Funzioni
  9. File descriptor e redirezioni
  10. Segnali

File descriptor

Su Unix e Unix-like ogni processo che non è avviato in background ha di default tre file descriptor, nella forma di identificativi interi:

  • 0 (stdin), lo standard input, da cui si leggono gli input (di default quanto scritto dalla tastiera sul terminale associato);
  • 1 (stdout), lo standard output, a cui si inviano tutti i messaggi prodotti (di default sul terminale associato), tranne quelli relativi a messaggi d'errore;
  • 2 (stderr), lo standard error, a cui si inviano tutti i messaggi di errore prodotti (di default sul terminale associato).

Lo standard input è chiuso per i processi avviati in background.

Si chiamano file descriptor perché sono identificativi che fanno riferimento a un file; e infatti perfino i dispositivi, anche virtuali come il terminale, sono considerati dei file. Questo permette la possibilità di associare in modo trasparente questi file descriptor standard anche a file qualsiasi, oltre che ad altri file descriptor, in modo da ridirigerne il contenuto. E disporre di due diversi file descriptor per l'output prodotto da un comando e i messaggi di errore, permette di disabilitare anche solo uno dei due, o di salvarli su due file diversi.

Redirezioni

Alcune comuni redirezioni, da scriversi dopo un comando (la stringa file può essere anche una variabile quotata):

  • < file collega lo standard input al file, in modo da leggerne il contenuto. Il file descriptor è implicito, ma sarebbe equivalente scrivere 0< file;
  • > file dopo un comando ne scrive lo standard output sul file (troncandolo, se esiste). Si noti che il file descriptor è implicito, ma sarebbe equivalente scrivere 1> file;
  • >> file (append) aggiunge il contenuto dello standard output al file (creandolo, se non esiste). Il file descriptor è implicito, ma sarebbe equivalente scrivere 1>> file;
  • 2> file è simile a > file, ma ridirige lo standard error anziché lo standard output sul file (con append: 2>> file);
  • >&2 scrive lo standard output sullo standard error. Il file descriptor dello standard output è implicito, mentre quello alla destra dev'essere preceduto da &, ma sarebbe equivalente scrivere 1>&2;
  • 2>&1 scrive lo standard error sullo standard output;
  • &> file (non POSIX, abbreviazione per: > file 2>&1) invia standard output ed error sul file (con append: &>> file);
  • <&- chiude lo standard input. Il file descriptor è implicito, ma sarebbe equivalente scrivere 0<&-;
  • >&- chiude lo standard output. Il file descriptor è implicito, ma sarebbe equivalente scrivere 1>&-;
  • 2>&- chiude lo standard error.

Per evitare la scrittura a schermo standard output e/o standard error non vanno chiusi, oppure l'operazione genererebbe un errore (perché il file descriptor relativo non esisterebbe più), ma ridiretti su /dev/null, un file dispositivo la cui esistenza è specificata da POSIX che non produce mai output e può essere usato per assorbire qualsiasi cosa.

Esempi:

comando &> /dev/null     # non stampa niente a schermo, neanche gli errori
comando > /dev/null 2>&1 # equivalente (POSIX)

# si noti la differenza con:
comando >&- 2>&-         # standard output ed error sono chiusi, quindi se
                         # venissero usati, il comando fallirebbe e il suo
                         # exit status sarebbe diverso da zero

Una pipe (nella forma: comando1 | comando2) è anch'essa una forma di redirezione, già trattata in una sezione sulle istruzioni composte, in cui lo standard output di comando1 è rediretto sullo standard input di comando2.

Redirezioni "here document/string"

Delle redirezioni particolari dallo standard input sono possibili anche nelle forme:

  • <<parola (here-document): legge tutte le righe che seguono, finché ne incontra una corrispondente alla parola scelta, e le invia allo standard input di un comando. Nel testo inviato si applicano le espansioni delle stringhe quotate, e come se tutto il testo fosse quotato, ma le virgolette non sono considerate un carattere speciale (restano: $ \ `, con \ a fare da carattere di escape). Se invece la parola è racchiusa tra apici o virgolette (per esempio: <<"parola"), si annullano tutte le espansioni fino alla chiusura di here-document. Il file descriptor è implicito, ma è equivalente scrivere 0<<parola;
  • <<<stringa (here-string, non POSIX): invia una stringa e una riga vuota allo standard input di un comando. La stringa può essere espansa e racchiusa tra apici, virgolette o nulla come una stringa qualsiasi, in funzione delle espansioni da attivare. Il file descriptor è implicito, ma è equivalente a scrivere 0<<<stringa'.

Per esempio:

cat <<EOF
Testo su più righe da inviare a cat
che si limiterà a stamparlo...

Sono permesse anche $VARIABILI, che saranno
espanse come tutte le espansioni attivabili
da $ e permesse in una stringa quotata.
EOF

cat <<'EOF'
Testo su più righe da inviare a cat
che si limiterà a stamparlo...

NON sono permesse $VARIABILI, che se presenti
saranno stampate letteralmente.
EOF

Ricorda che, a prescindere dall'indentazione del comando, il testo e la parola (negli esempi: EOF) usata come linea conclusiva devono iniziare sempre a inizio riga. Esiste una sintassi alternativa con <<- per ignorare le tabulazioni iniziali del testo, e in tal caso solo la parola dev'essere a inizio riga, ma è sconsigliabile, in quanto alcuni editor potrebbero non preservare il carattere tabulazione e trasformarlo in spazio, e visivamente non si nota differenza tra i due casi.

Pipe e altre concatenazioni da aggiungere al comando utilizzante la redirezione here-document vanno scritte sulla stessa riga del comando. E, se è possibile scrivere il comando su più righe, quella successiva al comando è considerata la riga dopo la parola che chiude tutto il testo usato per la redirezione here-document.

Scope della redirezione

Si noti che lo scope (il raggio d'azione) della redirezione non serve soltanto per evitare di scrivere più volte il percorso del file, ma ne influenza anche il significato. In particolare con la redirezione dello standard input (lettura), per garantire che tutte le istruzioni continuino la lettura da dove era rimasta invece che riprenderla sempre dall'inizio, e dello standard output, per garantire che il file non venga troncato da ogni comando ma che i successivi continuino a scrivere in seguito (come se usassero la redirezione con append).

Per esempio in genere è sbagliato scrivere:

while read riga < file
do
   # "$riga" contiene *SEMPRE* la prima riga del file
   # quindi il ciclo può essere eseguito per sempre se ha più di una riga!

done

Mentre quello che si vuole è probabilmente:

while read riga
do
    # "$riga" contiene sempre una nuova linea del file
    # il ciclo è eseguito per ogni linea del file

done < file

Si noti che leggere in questo modo un file ha senso solo per file non binari, che potrebbero contenere anche il carattere ASCII n. 0, che non può essere memorizzato in una variabile. Inoltre l'istruzione read fallisce se non trova un "a capo", per cui l'ultima riga del file è letta all'interno del ciclo soltanto se termina con un "a capo", altrimenti è necessario controllare se è vuota la variabile dopo il ciclo.

Allo stesso modo:

{
   printf %s\\n "$var"
   printf %s\\n "$var2"
   printf %s\\n "$var3"
} > file

scrive il contenuto delle variabili nel file, facendole seguire da una riga vuota. È equivalente a:

printf %s\\n "$var" > file
printf %s\\n "$var2" >> file # append!
printf %s\\n "$var3" >> file # append!

Si noti invece che utilizzando sempre la redirezione > per tre volte di fila, si scriverebbe il file sempre dall'inizio, con il risultato che solo l'ultima variabile sarebbe presente nel file al termine delle istruzioni.

Nuovi file descriptor

In aggiunta ai tre file descriptor standard, è possibile aprirne di nuovi, in lettura (FD<......), in scrittura (FD>..., append: FD>>...) e in lettura/scrittura (FD<>...), dove FD è un intero rappresentante il nuovo file descriptor e al posto dei puntini può esserci sia un file sia un file descriptor già esistente preceduto dal carattere &.

È necessario quindi determinare a quale file (o file descriptor, se preceduto da &) il nuovo file descriptor va associato. Ci sono due modi:

  • specificarlo dopo un'istruzione composta, in modo che il file descriptor sia utilizzabile normalmente all'interno;
  • specificarlo con l'istruzione exec senza altri argomenti (o avrebbe un significato diverso).

Per usarlo invece, una volta aperto, il file descriptor va scritto subito dopo la redirezione, preceduto dal carattere &.

Per esempio:

{
   # il file descriptor 3 è definito dentro il blocco (in scrittura)
   comando1 >&3  # ridirige lo standard output sul nuovo file descriptor
   comando2
   comando3 >&3  # come per comando1
} 3> file # apre in scrittura il file descriptor per il contenuto del blocco
# NOTA: fuori dal blocco il nuovo file descriptor non esiste più

Sul file scelto sono ridiretti lo standard output di comando1 e comando3. È equivalente a:

comando1 > file  # scrittura
comando2
comando3 >> file # scrittura con append! (nel blocco è implicito)

Equivalentemente, con exec:

exec 3> file     # apre il file descriptor in scrittura
comando1 >&3     # usa il file descriptor
comando2
comando3 >&3     # usa il file descriptor
exec 3>&-        # chiude il file descriptor

Finché un file descriptor non è chiuso, non è garantita la scrittura sul file, ma la scrittura tramite file descriptor è più efficiente.

Salvare i file descriptor

Si noti che exec è equivalente a una redirezione in un blocco, ma la chiusura dei file descriptor aperti dev'essere gestita manualmente. Prima di cambiare quelli standard, per consentirne il successivo ripristino al loro valore originale, vanno salvati in un nuovo file descriptor temporaneo:

exec 3<&0   # apre un nuovo file descriptor per salvare lo standard input
exec < file # sovrascrive lo standard input
read riga   # legge la prima riga (se esiste)
read riga   # legge la seconda riga (se esiste)! (e non di nuovo la prima)
exec <&3    # assegna nuovamente lo standard input al suo valore precedente
exec 3<&-   # chiude il nuovo file descriptor

si ricordi che invece, senza exec e senza un blocco, una redirezione (di lettura o scrittura senza append) su un singolo comando parte sempre dall'inizio:

read riga < file # legge la prima riga del file (se esiste)
read riga < file # legge sempre la prima riga del file (se esiste)!