¿Cómo se determina el estado de retorno de una asignación variable?

10

He visto construcciones en scripts como este:

if somevar="$(somecommand 2>/dev/null)"; then
...
fi

¿Está esto documentado en alguna parte? ¿Cómo se determina el estado de retorno de una variable y cómo se relaciona con la sustitución de comandos? (Por ejemplo, ¿obtendría el mismo resultado con if echo "$(somecommand 2>/dev/null)"; then?)

Comodín
fuente

Respuestas:

12

Está documentado (para POSIX) en la Sección 2.9.1 Comandos simples de las especificaciones básicas del grupo abierto. Hay un muro de texto allí; Dirijo su atención al último párrafo:

Si hay un nombre de comando, la ejecución continuará como se describe en Búsqueda y ejecución de comandos . Si no hay un nombre de comando, pero el comando contenía una sustitución de comando, el comando se completará con el estado de salida de la última sustitución de comando realizada. De lo contrario, el comando se completará con un estado de salida cero.

Así por ejemplo,

   Command                                         Exit Status
$ FOO=BAR                                   0 (but see also the note from icarus, below)
$ FOO=$(bar)                                Exit status from "bar"
$ FOO=$(bar) baz                            Exit status from "baz"
$ foo $(bar)                                Exit status from "foo"

Así es como funciona bash también. Pero vea también la sección "no tan simple" al final.

phk , en su pregunta ¿Las asignaciones son como comandos con un estado de salida, excepto cuando hay sustitución de comandos? , sugiere

... parece que una asignación en sí cuenta como un comando ... con un valor de salida cero, pero que se aplica antes del lado derecho de la asignación (por ejemplo, una llamada de sustitución de comando ...)

Esa no es una forma terrible de verlo. Un esquema de crudo para determinar el estado de retorno de un simple comando (uno que no contenga ;, &, |, &&o ||) es:

  • Escanee la línea de izquierda a derecha hasta llegar al final o una palabra de comando (generalmente un nombre de programa).
  • Si ve una asignación variable, el estado de retorno de la línea podría ser 0.
  • Si ve una sustitución de comando, es decir, $(…)tome el estado de salida de ese comando.
  • Si llega a un comando real (no en una sustitución de comando), tome el estado de salida de ese comando.
  • El estado de retorno de la línea es el último número que encontró.
    Las sustituciones de comandos como argumentos del comando, por ejemplo, foo $(bar)no cuentan; obtienes el estado de salida de foo. Parafraseando la notación de phk , el comportamiento aquí es

    temporary_variable  = EXECUTE( "bar" )
    overall_exit_status = EXECUTE( "foo", temporary_variable )
    

Pero esto es una ligera simplificación excesiva. El estado general de devolución de

A = $ ( cmd 1 ) B = $ ( cmd 2 ) C = $ ( cmd 3 ) D = $ ( cmd 4 ) E = mc 2
es el estado de salida de . La asignación que ocurre después de la asignación no establece el estado general de salida en 0.cmd4E=D=

icarus , en su respuesta a la pregunta de phk , plantea un punto importante: las variables se pueden establecer como de solo lectura. El penúltimo párrafo de la Sección 2.9.1 del estándar POSIX dice:

Si alguna de las asignaciones de variables intenta asignar un valor a una variable para la cual el atributo de solo lectura se establece en el entorno de shell actual (independientemente de si la asignación se realiza en ese entorno), se producirá un error de asignación de variable. Consulte las Consecuencias de los errores de Shell para conocer las consecuencias de estos errores.

entonces si dices

readonly A
C=Garfield A=Felix T=Tigger

el estado de retorno es 1. No importa si las cadenas Garfield, Felixy / o Tigger se reemplazan con sustituciones de comando, pero vea las notas a continuación.

La sección 2.8.1 Consecuencias de los errores de Shell tiene otro grupo de texto y una tabla, y termina con

En todos los casos que se muestran en la tabla donde se requiere que un shell interactivo no salga, el shell no realizará ningún procesamiento adicional del comando en el que ocurrió el error.

Algunos de los detalles tienen sentido; algunos no:

  • La A=asignación a veces aborta la línea de comando, ya que esa última oración parece especificar. En el ejemplo anterior, Cse establece en Garfield, pero Tno se establece (y, por supuesto, tampoco se establece  A).
  • Del mismo modo, se ejecuta pero no . Sin embargo, en mis versiones de fiesta (que incluyen 4.1.xy 4.3.x), que hace ejecutar . (Por cierto, esto impugna aún más la interpretación de phk de que el valor de salida de la asignación se aplica antes del lado derecho de la asignación).C=$(cmd1) A=$(cmd2) T=$(cmd3)cmd1cmd3
    cmd2

Pero aquí hay una sorpresa:

En mis versiones de bash,

solo lectura A
C = algo A = algo T = algo  cmd 0

no ejecutar. En particular,cmd0

C = $ ( cmd 1 ) A = $ ( cmd 2 ) T = $ ( cmd 3 )    cmd 0
ejecuta y , pero no . (Tenga en cuenta que esto es lo contrario de su comportamiento cuando no hay un comando). Y se establece (así como ) en el entorno de . Me pregunto si esto es un error en bash.cmd1cmd3cmd2TCcmd0


No es tan simple:

El primer párrafo de esta respuesta se refiere a "comandos simples".  La especificación dice:

Un "comando simple" es una secuencia de asignaciones y redirecciones de variables opcionales, en cualquier secuencia, opcionalmente seguida de palabras y redirecciones, terminadas por un operador de control.

Estas son declaraciones como las de mi primer bloque de ejemplo:

$ FOO=BAR
$ FOO=$(bar)
$ FOO=$(bar) baz
$ foo $(bar)

los primeros tres incluyen asignaciones variables y los últimos tres incluyen sustituciones de comandos.

Pero algunas asignaciones variables no son tan simples.  bash (1) dice:

Las sentencias de asignación también pueden aparecer como argumentos a la alias, declare, typeset, export, readonly, y localÓRDENES INTERNAS ( declaración de comandos).

Para export, la especificación POSIX dice:

ESTADO DE SALIDA

    0 0
      Todos los operandos de nombre se exportaron con éxito.
    > 0
      No se pudo exportar al menos un nombre, o -pse especificó la opción y se produjo un error.

Y POSIX no es compatible local, pero bash (1) dice:

Es un error usar localcuando no está dentro de una función. El estado de retorno es 0 a menos que localse use fuera de una función, se proporcione un nombre no válido o el nombre sea ​​una variable de solo lectura.

Leyendo entre líneas, podemos ver que los comandos de declaración como

export FOO=$(bar)

y

local FOO=$(bar)

son más como

foo $(bar)

la medida en que ignoran el estado de salida desde bar y le dan un estado de salida basándose en la orden principal ( export, localo foo). Entonces tenemos rarezas como

   Command                                           Exit Status
$ FOO=$(bar)                                    Exit status from "bar"
                                                  (unless FOO is readonly)
$ export FOO=$(bar)                             0 (unless FOO is readonly,
                                                  or other error from “export”)
$ local FOO=$(bar)                              0 (unless FOO is readonly,
                                                  statement is not in a function,
                                                  or other error from “local”)

que podemos demostrar con

$ export FRIDAY=$(date -d tomorrow)
$ echo "FRIDAY   = $FRIDAY, status = $?"
FRIDAY   = Fri, May 04, 2018  8:58:30 PM, status = 0
$ export SATURDAY=$(date -d "day after tomorrow")
date: invalid date ‘day after tomorrow’
$ echo "SATURDAY = $SATURDAY, status = $?"
SATURDAY = , status = 0

y

myfunc() {
    local x=$(echo "Foo"; true);  echo "x = $x -> $?"
    local y=$(echo "Bar"; false); echo "y = $y -> $?"
    echo -n "BUT! "
    local z; z=$(echo "Baz"; false); echo "z = $z -> $?"
}

$ myfunc
x = Foo -> 0
y = Bar -> 0
BUT! z = Baz -> 1

Afortunadamente, ShellCheck detecta el error y levanta SC2155 , que informa que

export foo="$(mycmd)"

debe cambiarse a

foo=$(mycmd)
export foo

y

local foo="$(mycmd)"

debe cambiarse a

local foo
foo=$(mycmd)
G-Man dice 'restablecer a Mónica'
fuente
1
Genial, gracias! Para puntos de bonificación, ¿sabes cómo se localvincula esto? Por ejemplo local foo=$(bar)?
Comodín
1
Para el segundo ejemplo (solo FOO=$(bar)) vale la pena señalar que, teóricamente, tanto el estado de salida como la asignación podrían desempeñar un papel, consulte unix.stackexchange.com/a/341013/117599
phk
1
@Wildcard: Me alegra que te guste. Acabo de actualizarlo nuevamente; Una gran parte de la versión que acaba de leer estaba mal. Mientras estés aquí, ¿qué te parece? ¿Es esto un error en bash, que se A=foo cmdejecuta cmdincluso si Aes de solo lectura?
G-Man dice 'reinstalar a Mónica' el
1
@phk: (1) Teoría interesante, pero no estoy seguro de cómo tiene sentido. Eche otro vistazo al ejemplo justo antes de mi título de "sorpresa". Si Aes de solo lectura, el comando se C=value₁ A=value₂ T=value₃establece Cpero no T(y, por supuesto, Ano se establece): el shell finalizó el procesamiento de la línea de comando, ignorando T=value₃, porque A=value₂es un error. (2) Gracias por el enlace a la pregunta de desbordamiento de pila : he publicado comentarios al respecto.
G-Man dice 'Reincorporar a Monica'
1
@Wildcard "Para puntos de bonificación, ¿sabes cómo los lazos locales en esto?". Sí ... local=$(false)tiene un valor de salida 0debido a que (desde la página del manual): It is an error to use local when not within a function. The return status is 0 unless local is used outside a function, an invalid name is supplied, or name is a readonly variable.. Esto no es suficiente trollface en el mundo para el genio que lo diseñó de esa manera.
David Tonhofer
2

Está documentado en Bash ( LESS=+/'^SIMPLE COMMAND EXPANSION' bash):

Si queda un nombre de comando después de la expansión .... De lo contrario, el comando sale. ... Si no hubo sustituciones de comandos, el comando sale con un estado de cero.

En otras palabras (mis palabras):

Si no queda ningún nombre de comando después de la expansión, y no se ejecutaron sustituciones de comando, la línea de comando sale con un estado de cero.

phk
fuente