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.