¿Es esto un error en bash? `return` no sale de la función si se llama desde una tubería

16

Últimamente he tenido algunos problemas extraños con bash. Mientras intentaba simplificar mi script, se me ocurrió este pequeño fragmento de código:

$ o(){ echo | while read -r; do return 0; done; echo $?;}; o
0
$ o(){ echo | while read -r; do return 1; done; echo $?;}; o
1

returndebería haber salido de la función sin imprimir $?, ¿no? Bueno, luego verifiqué si puedo regresar solo de una tubería:

$ echo | while read -r; do return 1; done
bash: return: can only `return' from a function or sourced script

Lo mismo sucede sin un whilebucle:

$ foo(){ : | return 1; echo "This should not be printed.";}
$ foo
This should not be printed.

¿Hay algo que me falta aquí? ¡Una búsqueda en Google no trajo nada sobre esto! Mi versión bash es la versión 4.2.37 (1) en Debian Wheezy.

Teresa e Junior
fuente
¿Hay algún problema con la configuración que sugerí en mi respuesta que permite que su script se comporte de la manera intuitiva que esperaba?
jlliagre
@jlliagre Es un script bastante complejo en las miles de líneas. Con la preocupación de romper algo más, prefiero evitar ejecutar una tubería dentro de la función, por lo que la reemplacé con una sustitución de proceso. ¡Gracias!
Teresa e Junior
¿Por qué no eliminar los dos primeros ejemplos, si whileno es necesario para la reproducción? Se distrae del punto.
Lightness compite con Monica el
@LightnessRacesinOrbit Un whilebucle es un uso muy común para una tubería con return. El segundo ejemplo es más directo al punto, pero es algo que no creo que nadie pueda usar ...
Teresa e Junior
1
Lamentablemente, mi respuesta correcta ha sido eliminada ... Estás en una zona gris mientras haces algo que no está especificado. El comportamiento depende de cómo el shell interpreta las tuberías y esto es incluso diferente entre el Bourne Shell y el Korn Shell a pesar de que ksh se derivó de las fuentes sh. En Bourne Shell, el ciclo while está en una subshell, por lo tanto, verá el eco como con bash, en ksh el ciclo while es el proceso en primer plano y, por lo tanto, ksh no llama a echo con su ejemplo.
schily

Respuestas:

10

Relacionado: /programming//a/7804208/4937930

No es un error que no pueda salir de un script o regresar de una función por exito returnen subcapas. Se ejecutan en otro proceso y no afectan el proceso principal.

Además de eso, supongo que está viendo comportamientos indocumentados de bash en (probablemente) especificaciones indefinidas. En una función, no se afirman errores returnen el nivel superior de los comandos de subshell y simplemente se comporta como exit.

En mi humilde opinión, es un error bash por el comportamiento inconsistente de returndepender de si la declaración principal está en una función o no.

#!/bin/bash

o() {
    # Runtime error, but no errors are asserted,
    # each $? is set to the return code.
    echo | return 10
    echo $?
    (return 11)
    echo $?

    # Valid, each $? is set to the exit code.
    echo | exit 12
    echo $?
    (exit 13)
    echo $?
}
o

# Runtime errors are asserted, each $? is set to 1.
echo | return 20
echo $?
(return 21)
echo $?

# Valid, each $? is set to the exit code.
echo | exit 22
echo $?
(exit 23)
echo $?

Salida:

$ bash script.sh 
10
11
12
13
script.sh: line 20: return: can only `return' from a function or sourced script
1
script.sh: line 22: return: can only `return' from a function or sourced script
1
22
23
yaegashi
fuente
La falta de verbosidad de error puede ser indocumentada. Pero el hecho de que returnno funciona desde una secuencia de comandos de nivel superior en una subshell, y en particular no sale de la subshell, es lo que los documentos existentes ya me han hecho esperar. El OP podría usar exit 1 || return 1donde está tratando de usar return, y luego debería obtener el comportamiento esperado. EDITAR: la respuesta de @ herbert indica que toplevel returnen subshell funciona como exit(pero solo desde el subshell).
dubiousjim
1
@dubiousjim Actualicé mi script. Quiero decir que returnen una secuencia de subshell simple se debe afirmar como un error de tiempo de ejecución en cualquier caso , pero en realidad no es cuando ocurre en una fucntion. Este problema también se ha discutido en gnu.bash.bug , pero no hay conclusión.
yaegashi
1
Su respuesta no es correcta, ya que no se especifica si el ciclo while está en una subshell o es el proceso en primer plano. Independientemente de la implementación del shell real, la returndeclaración está en una función y, por lo tanto, es legal. Sin embargo, el comportamiento resultante no está especificado.
schily
No debe escribir que es un comportamiento indocumentado, mientras que los componentes de la tubería de hecho se encuentran en una subshell se documenta en la página de manual de bash. Probablemente no debería escribir el comportamiento probablemente se basa en especificaciones indefinidas mientras POSIX especifica los comportamientos permitidos. No debe sospechar un error de bash mientras bash sigue el estándar POSIX al permitir el retorno en una función pero no fuera.
jlliagre
17

No es un error bashsino su comportamiento documentado :

Cada comando en una tubería se ejecuta en su propia subshell

La returninstrucción es válida al estar dentro de una definición de función pero al estar también en una subshell, no afecta a su shell principal, por lo que la siguiente instrucción echo, se ejecuta independientemente. Sin embargo, es una construcción de shell no portátil ya que el estándar POSIX permite que los comandos que componen una tubería se ejecuten en un subshell (el predeterminado) o el superior (una extensión permitida).

Además, cada comando de una tubería de comandos múltiples está en un entorno de subshell; como extensión, sin embargo, cualquiera o todos los comandos en una tubería pueden ejecutarse en el entorno actual. Todos los demás comandos se ejecutarán en el entorno de shell actual.

Con suerte, puede decir bashque se comporte de la manera que espera con un par de opciones:

$ set +m # disable job control
$ shopt -s lastpipe # do not run the last command of a pipeline a subshell 
$ o(){ echo | while read -r; do return 0; done; echo $?;}
$ o
$          <- nothing is printed here
jlliagre
fuente
1
Dado returnque no saldrá de la función, ¿no tendría más sentido si el shell acaba de imprimirse bash: return: can only `return' from a function or sourced script, en lugar de darle al usuario una falsa sensación de que la función podría haber regresado?
Teresa e Junior
2
No veo en ninguna parte de la documentación que se diga que el retorno dentro de la subshell es válido. Apuesto a que esta característica se copió de ksh, la declaración de devolución fuera de la función o el script de origen se comportan como salida . No estoy seguro sobre el shell Bourne original.
Cuonglm
1
@jlliagre: Tal vez Teresa está confundida acerca de la terminología de lo que está pidiendo, pero no veo por qué sería "complicado" que bash emita un diagnóstico si ejecuta returnun subshell. Después de todo, sí sabe que está en una subshell, como lo demuestra la $BASH_SUBSHELLvariable. El mayor problema es que esto podría conducir a falsos positivos; un usuario que entiende cómo funcionan los subshells podría haber escrito scripts que se usan returnen lugar de exitterminar un subshell. (Y, por supuesto, hay casos válidos en los que uno podría querer establecer variables o hacer una cden una subshell.)
Scott
1
@ Scott Creo que entiendo bien la situación. Una tubería crea una subshell y returnestá regresando de la subshell en lugar de fallar, ya que está dentro de una función real. El problema es que help returnestablece específicamente: al Causes a function or sourced script to exit with the return value specified by N.leer la documentación, cualquier usuario esperaría que al menos fallara o imprimiera una advertencia, pero que nunca se comportara de esa manera exit.
Teresa e Junior
1
Me parece que cualquiera que espere que una función return en una subshell en una función regrese de la función (en el proceso de shell principal) no entiende muy bien las subcapas. Por el contrario, esperaría que un lector que entiende las subcapas espere return en una subshell en una función terminar la subshell, tal como lo exitharía.
Scott
6

Según la documentación de POSIX, el uso returnfuera de la función o el script de origen no está especificado . Entonces, depende de su shell para manejar.

El shell de SystemV informará un error, mientras que dentro ksh, returnfuera de la función o el script de origen se comporta como exit. La mayoría de los otros shells POSIX y schily's osh también se comportan así:

$ for s in /bin/*sh /opt/schily/bin/osh; do
  printf '<%s>\n' $s
  $s -c '
    o(){ echo | while read l; do return 0; done; echo $?;}; o
  '
done
</bin/bash>
0
</bin/dash>
0
</bin/ksh>
</bin/lksh>
0
</bin/mksh>
0
</bin/pdksh>
0
</bin/posh>
0
</bin/sh>
0
</bin/yash>
0
</bin/zsh>
</opt/schily/bin/osh>
0

kshy zshno salió porque la última parte de la tubería en estos shells se ejecutó en el shell actual en lugar del subshell. La declaración return afectó el entorno actual del shell que llamó a la función, ya que la función regresa inmediatamente sin imprimir nada.

En la sesión interactiva, bashsolo informe el error pero no finalizó el shell, schily's oshinformó el error y finalizó el shell:

$ for s in /bin/*sh; do printf '<%s>\n' $s; $s -ci 'return 1; echo 1'; done
</bin/bash>
bash: return: can only `return' from a function or sourced script
1
</bin/dash>
</bin/ksh>
</bin/lksh>
</bin/mksh>
</bin/pdksh>
</bin/posh>
</bin/sh>
</bin/yash>
</bin/zsh>
</opt/schily/bin/osh>
$ cannot return when not in function

( zshen la sesión interactiva y la salida es terminal, no terminó bash, yashe schily's oshinformó el error pero no terminó el shell)

Cuonglm
fuente
1
Se puede argumentar que returnse usa dentro de una función aquí.
jlliagre
1
@jlliagre: No estoy seguro de lo que quieres decir, returnera su uso dentro de subnivel dentro de la función , excepto kshy zsh.
Cuonglm
2
Me refiero a que estar dentro de un subshell que está dentro de una función no necesariamente significa estar fuera de esa función, es decir, nada en los componentes de la tubería de estados estándar debe considerarse que está fuera de la función donde se encuentran. Esto merecería ser aclarado por el Grupo Abierto.
jlliagre
3
Creo que no. Eso está fuera de la función. El shell que llamó a la función y el subshell que ejecutó return son diferentes.
Cuonglm
Entiendo su razonamiento que explica correctamente el problema, mi punto es de acuerdo con la gramática de shell descrita en el estándar POSIX, la tubería es parte de la lista compuesta que forma parte del comando compuesto que es el cuerpo de la función. En ninguna parte se dice que los componentes de la tubería deben considerarse fuera de la función. Al igual que si estoy en un automóvil y ese automóvil está estacionado en un garaje, puedo asumir que también estoy en ese garaje ;-)
jlliagre
4

Creo que obtuvo el comportamiento esperado, en bash, cada comando en una tubería se ejecuta en una subshell. Puede ser usted mismo tratando de modificar una variable global de su función:

foo(){ x=42; : | x=3; echo "x==$x";}

Por cierto, el retorno está funcionando pero regresa desde la subshell. De nuevo puedes comprobar que:

foo(){ : | return 1; echo$?; echo "This should not be printed.";}

Producirá lo siguiente:

1
This should not be printed.

Entonces, la declaración de retorno salió correctamente de la subshell

.

Herbert
fuente
2
Por lo tanto, para salir de la función, use foo(){ : | return 1 || return 2; echo$?; echo "This should not be printed.";}; foo; echo $?y obtendrá un resultado de 2. Pero para mayor claridad, haría el return 1ser exit 1.
dubiousjim
Por cierto, ¿hay alguna prueba del hecho de que todos los miembros de una tubería (no todos menos uno) se ejecutan en subcapas?
Incnis Mrsi
@IncnisMrsi: Ver la respuesta de jlliagre .
Scott
1

La respuesta más general es que bash y algunos otros shells normalmente colocan todos los elementos de una tubería en procesos separados. Esto es razonable cuando la línea de comando es

programa 1 | programa 2 | programa 3

ya que los programas normalmente se ejecutan en procesos separados de todos modos (a menos que usted lo indique ) Pero puede ser una sorpresa paraexec program

comando 1 | comando 2 | comando 3

donde algunos o todos los comandos son comandos integrados. Los ejemplos triviales incluyen:

$ a=0
$ echo | a=1
$ echo "$a"
0
$ cd /
$ echo | cd /tmp
$ pwd
/

Un ejemplo un poco más realista es

$ t=0
$ ps | while read pid rest_of_line
> do
>     : $((t+=pid))
> done
$ echo "$t"
0

donde todo while... do... el donebucle se coloca en un subproceso, por lo que sus cambios tno son visibles para el shell principal después de que finaliza el bucle. Y eso es exactamente lo que estás haciendo: conectando un whilebucle, haciendo que el bucle se ejecute como una subshell, y luego tratando de regresar de la subshell.

Scott
fuente