¿Cómo capturo correctamente el código de salida / manejo de errores cuando uso la sustitución de procesos?

13

Tengo un script que analiza los nombres de archivo en una matriz utilizando el siguiente método tomado de un Q&A en SO :

unset ARGS
ARGID="1"
while IFS= read -r -d $'\0' FILE; do
    ARGS[ARGID++]="$FILE"
done < <(find "$@" -type f -name '*.txt' -print0)

Esto funciona muy bien y maneja perfectamente todo tipo de variaciones de nombre de archivo. A veces, sin embargo, pasaré un archivo no existente al script, por ejemplo:

$ findscript.sh existingfolder nonexistingfolder
find: `nonexistingfile': No such file or directory
...

En circunstancias normales, el script capturaría el código de salida con algo parecido RET=$?y lo usaría para decidir cómo proceder. Esto no parece funcionar con la sustitución del proceso anterior.

¿Cuál es el procedimiento correcto en casos como este? ¿Cómo puedo capturar el código de retorno? ¿Hay otras formas más adecuadas de determinar si algo salió mal en el proceso sustituido?

Glutanimato
fuente

Respuestas:

5

Puede obtener fácilmente el retorno de cualquier proceso subcalado haciendo eco de su retorno sobre su stdout. Lo mismo se aplica a la sustitución de procesos:

while IFS= read -r -d $'\0' FILE || 
    ! return=$FILE
do    ARGS[ARGID++]="$FILE"
done < <(find . -type f -print0; printf "$?")

Si ejecuto eso, entonces la última línea - (o \0sección delimitada según sea el caso) va a ser findel estado de retorno. readva a devolver 1 cuando obtiene un EOF, por lo que el único tiempo $returnestablecido $FILEes para el último bit de información leído.

Yo uso printfpara evitar la adición de un extra \newline - esto es importante porque incluso un readcabo regularmente - uno en el que no delimitan el \0NULs - se va a volver distinto de 0 en los casos en que los datos que acaba de leer en no termina en Una línea \nelectrónica. Entonces, si su última línea no termina con una línea \new, el último valor en su variable de lectura será su retorno.

Ejecutando el comando anterior y luego:

echo "$return"

SALIDA

0

Y si modifico la parte de sustitución del proceso ...

...
done < <(! find . -type f -print0; printf "$?")
echo "$return"

SALIDA

1

Una demostración más simple:

printf \\n%s list of lines printed to pipe |
while read v || ! echo "$v"
do :; done

SALIDA

pipe

Y, de hecho, siempre y cuando el retorno que desee sea lo último que escriba en stdout desde la sustitución del proceso, o cualquier proceso subconjunto del que lea de esta manera, $FILEsiempre será el estado de retorno que desee cuando es a través. Y, por lo tanto, la || ! return=...parte no es estrictamente necesaria: se usa solo para demostrar el concepto.

mikeserv
fuente
5

Los procesos en la sustitución de procesos son asíncronos: el shell los lanza y luego no da forma de detectar cuándo mueren. Por lo tanto, no podrá obtener el estado de salida.

Puede escribir el estado de salida en un archivo, pero esto es torpe en general porque no puede saber cuándo se escribe el archivo. Aquí, el archivo se escribe poco después del final del ciclo, por lo que es razonable esperarlo.

 < <(find …; echo $? >find.status.tmp; mv find.status.tmp find.status)
while ! [ -e find.status ]; do sleep 1; done
find_status=$(cat find.status; rm find.status)

Otro enfoque es utilizar una tubería con nombre y un proceso en segundo plano (que puede utilizar wait).

mkfifo find_pipe
find  >find_pipe &
find_pid=$!
 <find_pipe
wait $find_pid
find_status=$?

Si ninguno de los enfoques es adecuado, creo que tendrá que dirigirse a un lenguaje más capaz, como Perl, Python o Ruby.

Gilles 'SO- deja de ser malvado'
fuente
Gracias por esta respuesta Los métodos que describiste funcionan bien, pero debo admitir que son un poco más complicados de lo que había previsto. En mi caso, me decidí por un bucle antes del que se muestra en la pregunta que itera a través de todos los argumentos e imprime un error si uno de ellos no es un archivo o carpeta. Si bien esto no maneja otros tipos de errores que podrían ocurrir en el proceso sustituido, es lo suficientemente bueno para este caso específico. Si alguna vez necesito un método de manejo de errores más sofisticado en situaciones como esta, sin duda volveré a su respuesta.
Glutanimate
2

Usa un coproceso . Usando el coprocbuiltin puede iniciar un subproceso, leer su salida y verificar su estado de salida:

coproc LS { ls existingdir; }
LS_PID_=$LS_PID
while IFS= read i; do echo "$i"; done <&"$LS"
wait "$LS_PID_"; echo $?

Si el directorio no existe, wait saldrá con un código de estado distinto de cero.

Actualmente es necesario copiar el PID a otra variable porque $LS_PIDse desarmará antes de que waitse llame. Ver Bash desarma * _PID variable antes de que pueda esperar en coproc para más detalles.

Feuermurmel
fuente
1
Tengo curiosidad por saber cuándo se usarían <& "$ LS" frente a read -u $ LS? - gracias
Brian Chrisman
1
@BrianChrisman En este caso, probablemente nunca. read -udebería funcionar igual de bien. El ejemplo debía ser genérico y mostrar cómo la salida del coproceso podría canalizarse a otro comando.
Feuermurmel
1

Un enfoque es:

status=0
token="WzNZY3CjqF3qkasn"    # some random string
while read line; do
    if [[ "$line" =~ $token:([[:digit:]]+) ]]; then
        status="${BASH_REMATCH[1]}"
    else
        echo "$line"
    fi
done < <(command; echo "$token:$?")
echo "Return code: $status"

La idea es hacer eco del estado de salida junto con el token aleatorio después de que se haya completado el comando, y luego usar expresiones regulares bash para buscar y extraer el estado de salida. El token se usa para crear una cadena única para buscar en la salida.

Probablemente no sea la mejor manera de hacerlo en un sentido general de programación, pero podría ser la forma menos dolorosa de manejarlo en bash.

orev
fuente