Come salvare la posizione di destinazione / dev / stdout in uno script bash?


12

Ho un certo script bash, che vuole preservare la /dev/stdoutposizione originale prima di sostituire il primo descrittore di file con un'altra posizione.

Quindi, naturalmente, ho scritto qualcosa del genere

old_stdout=$(readlink -f /dev/stdout)

E non ha funzionato. Molto rapidamente capisco quale fosse il problema:

test@ubuntu:~$ echo $(readlink -f /dev/stdout)
/proc/5175/fd/pipe:[31764]
test@ubuntu:~$ readlink -f /dev/stdout
/dev/pts/18

Ovviamente, $()viene eseguito in una subshell, che viene reindirizzata alla shell padre.

Quindi la domanda è: esiste un modo affidabile (nell'ambito della portabilità tra le distribuzioni Linux) per salvare la /dev/stdoutposizione come stringa in uno script bash?


Questo suona un po 'come un problema XY . Qual è il problema di fondo?
Kusalananda

Il problema di fondo è un certo script di installazione che viene eseguito in due modalità: silenzioso, in cui registra tutto l'output su file e dettagliato, in cui non solo registra su file, ma stampa anche tutto sul terminale. Ma in entrambe le modalità lo script vuole interagire con l'utente, ovvero stampare sul terminale e leggere la risposta dell'utente. Quindi ho pensato che il salvataggio /dev/stdoutavrebbe risolto il problema con la stampa di messaggi in modalità silenziosa. L'alternativa è il reindirizzamento di ogni altra azione che produce output e ce ne sono parecchi. Circa 100 volte più dei messaggi di interazione dell'utente.
alexey.e.egorov,

Il modo standard di interagire con l'utente è stampare su stderr. Questo è, ad esempio, il motivo per cui i prompt verranno visualizzati stderrdi default.
Kusalananda

Sfortunatamente, stderrdeve anche essere reindirizzato e salvato, poiché lo script chiama un numero di programmi esterni e tutti i possibili messaggi di errore devono essere raccolti e registrati.
alexey.e.egorov,

Risposte:


14

Per salvare un descrittore di file, lo duplicate su un altro fd. Il salvataggio di un percorso nel file corrispondente non è sufficiente, è necessario salvare la modalità di apertura, i flag di apertura, la posizione corrente all'interno del file e così via. E, naturalmente, per pipe o socket anonimi, che non funzionerebbero poiché quelli non hanno percorso. Ciò che si desidera salvare è la descrizione del file aperto a cui fa riferimento il file fd e la duplicazione di un file fd in realtà restituisce un nuovo file fd alla stessa descrizione del file aperto .

Per duplicare un descrittore di file su un altro, con shell tipo Bourne, la sintassi è:

exec 3>&1

Sopra, fd 1 è duplicato su fd 3.

Qualunque fd 3 fosse già aperto prima sarebbe chiuso, ma nota che fds da 3 a 9 (di solito più, fino a 99 con yash) sono riservati a tale scopo (e non hanno alcun significato speciale in contrasto con 0, 1 o 2), il shell sa di non usarli per il proprio business interno. L'unico motivo per cui fd 3 sarebbe stato aperto in anticipo è perché l'hai fatto nello script 1 o è stato trapelato dal chiamante.

Quindi, puoi cambiare stdout in qualcos'altro:

exec > /dev/null

E più tardi, per ripristinare stdout:

exec >&3 3>&-

( 3>&-essendo per chiudere il descrittore di file di cui non abbiamo più bisogno).

Ora, il problema è che, tranne in ksh, ogni comando che esegui dopo exec 3>&1erediterà quel fd 3. Questa è una perdita fd. Generalmente non è un grosso problema, ma ciò può causare problemi.

kshimposta il flag close-on-exec su quei fds (per fds oltre 2), ma non altre shell e altre shell non hanno modo di impostarlo manualmente.

Il lavoro per altre shell è chiudere fd 3 per ogni singolo comando, come:

exec 3>&-

exec > file.log

ls 3>&-
uname 3>&-

exec >&3 3>&-

Ingombrante. Qui, il modo migliore sarebbe non usare execaffatto, ma reindirizzare i gruppi di comandi:

{
  ls
  uname
} > file.log

Lì, è la shell che si occupa di salvare stdout e ripristinarlo in seguito (e lo fa internamente duplicandolo su un fd (sopra 9, sopra 99 per yash) con il flag close-on-exec impostato.

Nota 1

Ora, la gestione di questi fds da 3 a 9 può essere ingombrante e problematica se li usi ampiamente o nelle funzioni, specialmente se il tuo script utilizza un codice di terze parti che a sua volta può usare quei fds.

Alcune conchiglie ( zsh, bash, ksh93, tutto aggiunto la funzione ( suggerito da Oliver Kiddle dizsh ) nello stesso periodo nel 2005, dopo che è stato discusso tra loro sviluppatori) hanno una sintassi alternativa per assegnare il primo fd libero al di sopra 10, invece che aiuta in questo caso:

myfunction() {
  local fd
  exec {fd}>&1
  # stdout was duplicated onto a new fd above 10, whose actual value
  # is stored in the fd variable
  ...
  # it should even be safe to re-enter the function here
  ...
  exec >&"$fd" {fd}>&-
}

Inoltre, il tuo codice è errato, nel senso che fd 3 potrebbe essere già preso, come accade quando uno script viene eseguito da un rc.localservizio, ad es. Quindi dovresti davvero usare qualcosa di simile exec {FD}>&1o qualcosa. Ma questo è supportato solo in bash 4, il che è davvero triste. Quindi questo non è davvero portatile.
alexey.e.egorov,

@ alexey.e.egorov, vedi modifica.
Stéphane Chazelas,

Bash 3. * non supporta questa funzione e questa versione è utilizzata in Centos 5, che è ancora supportata e ancora utilizzata. E trovare un descrittore gratuito e quindi eval "exec $i>&1"è una cosa che vorrei evitare, a causa della sua ingombro. Posso davvero fare affidamento sul fatto che i fds superiori a 9 sarebbero quindi gratuiti?
alexey.e.egorov,

@ alexey.e.egorov, no, lo stai guardando all'indietro. le fds da 3 a 9 sono gratuite (e spetta a te gestirle come desideri) e sono destinate a tale scopo. fds sopra 9 può essere usato dalla shell internamente e chiuderle potrebbe avere conseguenze spiacevoli. La maggior parte delle shell non ti consente di usarle. bashti lascerà sparare al piede.
Stéphane Chazelas,

2
@ alexey.e.egorov, se all'avvio lo script ha alcuni fds in (3..9) aperti, è perché il tuo chiamante ha dimenticato di chiuderli o impostare il flag close-on-exec su di essi. Questo è ciò che chiamo una perdita fd. Ora, forse il chiamante ha intenzione di passarti quei fds, così puoi leggere e / o scrivere dati da / a loro, ma poi lo sapresti. Se non li conosci, non ti interessa, quindi puoi chiuderli liberamente (nota che chiude semplicemente il processo dello script fd, non quello del tuo chiamante).
Stéphane Chazelas,

3

Come puoi vedere, bash scripting non è come un normale linguaggio di programmazione in cui è possibile assegnare descrittori di file.

La soluzione più semplice consiste nell'utilizzare una sotto-shell per eseguire ciò che si desidera reindirizzare in modo che l'elaborazione possa essere ripristinata nella top-shell che ha il suo I / O standard intatto.

Una soluzione alternativa sarebbe quella ttydi identificare il dispositivo TTY e controllare l'I / O nello script. Per esempio:

dev=$(tty)

e poi puoi ..

echo message > $dev

> Una soluzione alternativa sarebbe quella di utilizzare tty per identificare il dispositivo TTY e controllare l'I / O nello script. Come si fa?
alexey.e.egorov,

1
Ho appena incluso un esempio nella mia risposta.
Julie Pelletier,

1

$$ ti procurerebbe l'attuale PID di processo, in caso di shell interattiva o script il PID di shell pertinente.

Quindi puoi usare:

readlink -f /proc/$$/fd/1

Esempio:

% readlink -f /proc/$$/fd/1
/dev/pts/33

% var=$(readlink -f /proc/$$/fd/1)

% echo $var                       
/dev/pts/33

1
Sebbene sia funzionale, fare affidamento su una /procstruttura specifica causa problemi di portabilità, così come l'utilizzo /dev/stdoutcome indicato nella domanda.
Julie Pelletier,

1
@JuliePelletier Fare affidamento su una /procstruttura specifica? procfs
Funzionerebbe

1
Bene, quindi possiamo generalizzare per Linux come procfsè quasi sempre presente, ma spesso vediamo domande sulla portabilità e una buona metodologia di sviluppo include la considerazione della portabilità ad altri sistemi. bashpuò funzionare su una moltitudine di sistemi operativi.
Julie Pelletier,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.