¿Cuándo utilizarías un descriptor de archivo adicional?

74

Sé que puedes crear un descriptor de archivo y redirigir la salida a él. p.ej

exec 3<> /tmp/foo # open fd 3.
echo a >&3 # write to it
exec 3>&- # close fd 3.

Pero puede hacer lo mismo sin el descriptor de archivo:

FILE=/tmp/foo
echo a > "$FILE"

Estoy buscando un buen ejemplo de cuándo tendría que usar un descriptor de archivo adicional.

dogbane
fuente

Respuestas:

50

La mayoría de los comandos tienen un solo canal de entrada (entrada estándar, descriptor de archivo 0) y un solo canal de salida (salida estándar, descriptor de archivo 1) o bien operan en varios archivos que se abren solos (por lo que les pasa un nombre de archivo). (Además del error estándar (fd 2), que generalmente se filtra hasta el usuario). Sin embargo, a veces es conveniente tener un comando que actúe como filtro de varias fuentes o de varios objetivos. Por ejemplo, aquí hay un script simple que separa las líneas impares en un archivo de las líneas pares

while IFS= read -r line; do
  printf '%s\n' "$line"
  if IFS= read -r line; then printf '%s\n' "$line" >&3; fi
done >odd.txt 3>even.txt

Ahora suponga que desea aplicar un filtro diferente a las líneas de números impares y a las líneas de números pares (pero no volver a ponerlas juntas, eso sería un problema diferente, no factible desde el shell en general). En el shell, solo puede canalizar la salida estándar de un comando a otro comando; para canalizar otro descriptor de archivo, primero debe redirigirlo a fd 1.

{ while  done | odd-filter >filtered-odd.txt; } 3>&1 | even-filter >filtered-even.txt

Otro caso de uso más simple es filtrar la salida de error de un comando .

exec M>&Nredirige un descriptor de archivo a otro para el resto del script (o hasta que otro comando cambie los descriptores de archivo nuevamente). Existe cierta superposición en la funcionalidad entre exec M>&Ny somecommand M>&N. La execforma es más poderosa porque no tiene que estar anidada:

exec 8<&0 9>&1
exec >output12
command1
exec <input23
command2
exec >&9
command3
exec <&8

Otros ejemplos que pueden ser de interés:

Y para aún más ejemplos:

PD: ¡Esta es una pregunta sorprendente que proviene del autor de la publicación más votada en el sitio que utiliza la redirección a través de fd 3 !

Gilles 'SO- deja de ser malvado'
fuente
Prefiero decir que "la mayoría de los comandos tienen un canal de salida simple o doble : stdout (fd 1) y muy a menudo stderr (fd 2)".
rozcietrzewiacz
Además, ¿podrías explicar por qué lo usas while IFS= read -r line;? A mi modo de ver, IFS no tiene ningún efecto aquí ya que asigna valor a solo una variable ( línea ). Ver esta pregunta
rozcietrzewiacz
@rozcietrzewiacz He mencionado a stderr, y veo la primera parte de mi respuesta sobre por qué IFShace la diferencia incluso si estás leyendo una variable individual (es para retener el espacio en blanco principal).
Gilles 'SO- deja de ser malvado'
¿No podrías hacer lo mismo con sed -ne 'w odd.txt' -e 'n;w even.txt'?
Comodín el
1
@Wildcard Podrías hacer lo mismo con otras herramientas, seguro. Pero el objetivo de esta respuesta era ilustrar las redirecciones en el shell.
Gilles 'SO- deja de ser malvado'
13

Aquí hay un ejemplo del uso de FD adicionales como control de chattiness de script bash:

#!/bin/bash

log() {
    echo $* >&3
}
info() {
    echo $* >&4
}
err() {
    echo $* >&2
}
debug() {
    echo $* >&5
}

VERBOSE=1

while [[ $# -gt 0 ]]; do
    ARG=$1
    shift
    case $ARG in
        "-vv")
            VERBOSE=3
        ;;
        "-v")
            VERBOSE=2
        ;;
        "-q")
            VERBOSE=0
        ;;
        # More flags
        *)
        echo -n
        # Linear args
        ;;
    esac
done

for i in 1 2 3; do
    fd=$(expr 2 + $i)
    if [[ $VERBOSE -ge $i ]]; then
        eval "exec $fd>&1"
    else
        eval "exec $fd> /dev/null"
    fi
done

err "This will _always_ show up."
log "This is normally displayed, but can be prevented with -q"
info "This will only show up if -v is passed"
debug "This will show up for -vv"
Fordi
fuente
8

En el contexto de canalizaciones con nombre (fifos), el uso de un descriptor de archivo adicional puede habilitar el comportamiento de canalización sin bloqueo.

(
rm -f fifo
mkfifo fifo
exec 3<fifo   # open fifo for reading
trap "exit" 1 2 3 15
exec cat fifo | nl
) &
bpid=$!

(
exec 3>fifo  # open fifo for writing
trap "exit" 1 2 3 15
while true;
do
    echo "blah" > fifo
done
)
#kill -TERM $bpid

Ver: ¿La tubería con nombre se cierra prematuramente en el script?

Chad
fuente
1
desenterraste una de mis viejas preguntas :) chad tiene razón, te encontrarás con una condición de carrera.
n0pe
6

Un descriptor de archivo adicional es bueno para cuando desea capturar el stdout en una variable pero aún desea escribir en la pantalla, por ejemplo, en una interfaz de usuario de script bash

arg1 string to echo 
arg2 flag 0,1 print or not print to 3rd fd stdout descriptor   
function ecko3 {  
if [ "$2" -eq 1 ]; then 
    exec 3>$(tty) 
    echo -en "$1" | tee >(cat - >&3)
    exec 3>&- 
else 
    echo -en "$1"  
fi 
}
Adam Michael Danischewski
fuente
2
Sé que esta no es una respuesta nueva, pero tuve que mirar esto bastante tiempo para ver qué hace y pensé que sería útil si alguien agregara un ejemplo de esta función que se está utilizando. Este eco evoca y captura toda la salida de un comando - df, en este caso. dl.dropboxusercontent.com/u/54584985/mytest_redirect
Joe
1

Ejemplo: uso de flock para forzar que los scripts se ejecuten en serie con bloqueos de archivos

Un ejemplo es hacer uso del bloqueo de archivos para forzar que los scripts se ejecuten en serie en todo el sistema. Esto es útil si no desea que dos scripts del mismo tipo operen en los mismos archivos. De lo contrario, los dos scripts interferirían entre sí y posiblemente dañarían los datos.

#exit if any command returns a non-zero exit code (like flock when it fails to lock)
set -e

#open file descriptor 3 for writing
exec 3> /tmp/file.lock

#create an exclusive lock on the file using file descriptor 3
#exit if lock could not be obtained
flock -n 3

#execute serial code

#remove the file while the lock is still obtained
rm -f /tmp/file.lock

#close the open file handle which releases the file lock and disk space
exec 3>&-

Use el lote funcionalmente definiendo bloquear y desbloquear

También puede incluir esta lógica de bloqueo / desbloqueo en funciones reutilizables. El siguiente trapshell incorporado liberará automáticamente el bloqueo del archivo cuando salga el script (ya sea error o éxito). trapayuda a limpiar tus bloqueos de archivos. La ruta /tmp/file.lockdebe ser una ruta codificada para que varios scripts puedan intentar bloquearla.

# obtain a file lock and automatically unlock it when the script exits
function lock() {
  exec 3> /tmp/file.lock
  flock -n 3 && trap unlock EXIT
}

# release the file lock so another program can obtain the lock
function unlock() {
  # only delete if the file descriptor 3 is open
  if { >&3 ; } &> /dev/null; then
    rm -f /tmp/file.lock
  fi
  #close the file handle which releases the file lock
  exec 3>&-
}

La unlocklógica anterior es eliminar el archivo antes de liberar el bloqueo. De esta manera, limpia el archivo de bloqueo. Debido a que el archivo fue eliminado, otra instancia de este programa puede obtener el bloqueo del archivo.

Uso de funciones de bloqueo y desbloqueo en scripts

Puede usarlo en sus scripts como en el siguiente ejemplo.

#exit if any command returns a non-zero exit code (like flock when it fails to lock)
set -e

#try to lock (else exit because of non-zero exit code)
lock

#system-wide serial locked code

unlock

#non-serial code

Si desea que su código espere hasta que pueda bloquearse, puede ajustar el script como:

set -e

#wait for lock to be successfully obtained
while ! lock 2> /dev/null; do
  sleep .1
done

#system-wide serial locked code

unlock

#non-serial code
Sam Gleske
fuente
0

Como ejemplo concreto, acabo de escribir un script que necesita la información de tiempo de un subcomando. El uso de un descriptor de archivo adicional me permitió capturar el timestderr del comando sin interrumpir el stdout o stderr del subcomando.

(time ls -9 2>&3) 3>&2 2> time.txt

Lo que esto hace es apuntar lsstderr a fd 3, apuntar fd 3 a stderr del script y apuntar timestderr a un archivo. Cuando se ejecuta el script, su stdout y stderr son los mismos que los del subcomando, que se pueden redirigir como de costumbre. Solo timeel resultado se redirige al archivo.

$ echo '(time ls my-example-script.sh missing-file 2>&3) 3>&2 2> time.txt' > my-example-script.sh
$ chmod +x my-example-script.sh 
$ ./my-example-script.sh 
ls: missing-file: No such file or directory
my-example-script.sh
$ ./my-example-script.sh > /dev/null
ls: missing-file: No such file or directory
$ ./my-example-script.sh 2> /dev/null
my-example-script.sh
$ cat time.txt

real    0m0.002s
user    0m0.001s
sys 0m0.001s
Ben Blank
fuente