Precedencia de los operadores lógicos de shell &&, ||

126

Estoy tratando de entender cómo funciona la precedencia del operador lógico en bash. Por ejemplo, hubiera esperado que el siguiente comando no haga eco de nada.

true || echo aaa && echo bbb

Sin embargo, contrario a lo que esperaba, bbbse imprime.

¿Alguien puede explicar, cómo puedo dar sentido a los compuestos &&y los ||operadores en bash?

Martin Vegter
fuente

Respuestas:

127

En muchos lenguajes de computadora, los operadores con la misma precedencia son asociativos a la izquierda . Es decir, en ausencia de estructuras de agrupación, las operaciones más a la izquierda se ejecutan primero. Bash no es una excepción a esta regla.

Esto es importante porque, en Bash, &&y ||tienen la misma precedencia.

Entonces, lo que sucede en su ejemplo es que la operación más a la izquierda ( ||) se realiza primero:

true || echo aaa

Como truees obviamente cierto, el ||operador hace un cortocircuito y toda la declaración se considera verdadera sin la necesidad de evaluar echo aaacomo era de esperar. Ahora queda por hacer la operación más correcta:

(...) && echo bbb

Como la primera operación se evaluó como verdadera (es decir, tenía un estado de salida 0), es como si estuviera ejecutando

true && echo bbb

así &&que no se cortocircuitará, razón por la cual ves bbbeco.

Obtendría el mismo comportamiento con

false && echo aaa || echo bbb

Notas basadas en los comentarios.

  • Debe tener en cuenta que la regla de asociatividad izquierda solo se sigue cuando ambos operadores tienen la misma precedencia. Este no es el caso cuando usa estos operadores junto con palabras clave como [[...]]o ((...))o usa los operadores -oy -acomo argumentos para los comandos testo [. En tales casos, AND ( &&o -a) tiene prioridad sobre OR ( ||o -o). Gracias al comentario de Stephane Chazelas por aclarar este punto.
  • Parece que en los lenguajes C y C &&tiene mayor prioridad que la ||que probablemente sea la razón por la que esperaba que su construcción original se comportara como

    true || (echo aaa && echo bbb). 

    Sin embargo, este no es el caso con Bash, en el que ambos operadores tienen la misma precedencia, por lo que Bash analiza su expresión utilizando la regla de asociatividad izquierda. Gracias al comentario de Kevin por mencionar esto.

  • También puede haber casos en los que se evalúan las 3 expresiones. Si el primer comando devuelve un estado de salida distinto de cero, ||no se cortocircuitará y ejecutará el segundo comando. Si el segundo comando regresa con un estado de salida cero, entonces &&tampoco se cortocircuitará y se ejecutará el tercer comando. Gracias al comentario de Ignacio Vázquez-Abrams por mencionar esto.

Joseph R.
fuente
26
Una pequeña nota para agregar un poco más de confusión: mientras que los operadores &&y ||shell como en cmd1 && cmd2 || cmd3tienen la misma precedencia, los &&in ((...))y [[...]]tienen precedencia sobre ||( ((a || b && c))is ((a || (b && c)))). Lo mismo va para -a/ -oen test/ [y finde &/ |en expr.
Stéphane Chazelas
16
En lenguajes tipo c, &&tiene mayor prioridad que ||, por lo que el comportamiento esperado del OP ocurriría. Los usuarios incautos acostumbrados a tales lenguajes pueden no darse cuenta de que en bash tienen la misma precedencia, por lo que vale la pena señalar más explícitamente que sí tienen la misma precedencia en bash.
Kevin
También tenga en cuenta que hay situaciones en las que se pueden ejecutar los tres comandos, es decir, si el comando del medio puede devolver verdadero o falso.
Ignacio Vazquez-Abrams
@ Kevin Por favor revise la respuesta editada.
Joseph R.
1
Hoy, para mí, el principal éxito de Google para "precedencia de operador bash" es tldp.org/LDP/abs/html/opprecedence.html ... que afirma que && tiene una precedencia más alta que ||. Sin embargo, @JosephR. es claramente correcto de facto y de jure también. Dash se comporta igual y buscando "precedencia" en pubs.opengroup.org/onlinepubs/009695399/utilities/… , veo que es un requisito POSIX, por lo que podemos confiar en él. Todo lo que encontré para la notificación de errores fue la dirección de correo electrónico del autor, que seguramente ha sido enviada por correo no deseado en los últimos seis años. Lo intentaré de todos modos ...
Martin Dorey
62

Si desea que varias cosas dependan de su condición, agrúpelas:

true || { echo aaa && echo bbb; }

Eso no imprime nada, mientras

true && { echo aaa && echo bbb; }

imprime ambas cadenas.


La razón por la que esto sucede es mucho más simple de lo que Joseph está haciendo. Recuerda lo que Bash hace con ||y &&. Se trata del estado de retorno del comando anterior. Una forma literal de ver su comando sin procesar es:

( true || echo aaa ) && echo bbb

El primer comando ( true || echo aaa) está saliendo con 0.

$ true || echo aaa; echo $?
0
$ true && echo aaa; echo $?
aaa
0

$ false && echo aaa; echo $?
1
$ false || echo aaa; echo $?
aaa
0
Oli
fuente
77
+1 Para lograr el resultado previsto del OP. Tenga en cuenta que los paréntesis que colocó (true || echo aaa) && echo bbbes exactamente lo que estoy haciendo.
Joseph R.
1
Es bueno ver a @Oli en este lado del mundo.
Braiam
77
Tenga en cuenta la semi-columna ;al final. ¡Sin esto, no funcionará!
Serge Stroobandt
2
Estaba teniendo problemas con esto porque me faltaba el punto y coma final después del último comando (por ejemplo, echo bbb;en los primeros ejemplos). Una vez que descubrí que me estaba perdiendo eso, esta respuesta fue exactamente lo que estaba buscando. ¡+1 por ayudarme a descubrir cómo lograr lo que quería!
Doktor J
55
Vale la pena leer para cualquier persona confundida (...)y con {...}notación: manual de agrupación de comandos
ANTARA
16

Los operadores &&y no|| son reemplazos en línea exactos para if-then-else. Aunque si se usan con cuidado, pueden lograr lo mismo.

Una sola prueba es sencilla e inequívoca ...

[[ A == A ]]  && echo TRUE                          # TRUE
[[ A == B ]]  && echo TRUE                          # 
[[ A == A ]]  || echo FALSE                         # 
[[ A == B ]]  || echo FALSE                         # FALSE

Sin embargo, intentar agregar múltiples pruebas puede producir resultados inesperados ...

[[ A == A ]]  && echo TRUE   || echo FALSE          # TRUE  (as expected)
[[ A == B ]]  && echo TRUE   || echo FALSE          # FALSE (as expected)
[[ A == A ]]  || echo FALSE  && echo TRUE           # TRUE  (as expected)
[[ A == B ]]  || echo FALSE  && echo TRUE           # FALSE TRUE   (huh?)

¿Por qué se hacen eco FALSO y VERDADERO?

Lo que sucede aquí es que no nos hemos dado cuenta de eso &&y ||somos operadores sobrecargados que actúan de manera diferente dentro de los corchetes de prueba condicionales [[ ]]que en la lista AND y OR (ejecución condicional) que tenemos aquí.

Desde la página de manual de bash (editado) ...

Liza

Una lista es una secuencia de una o más tuberías separadas por uno de los operadores;, &, &&, o ││, y opcionalmente terminadas por uno de;, &, o. De estos operadores de lista, && y ││ tienen la misma precedencia, seguidos de; y &, que tienen igual precedencia.

Puede aparecer una secuencia de una o más líneas nuevas en una lista en lugar de un punto y coma para delimitar los comandos.

Si el operador de control termina un comando &, el shell ejecuta el comando en segundo plano en un subshell. El shell no espera a que termine el comando y el estado de retorno es 0. Comandos separados por a; se ejecutan secuencialmente; el shell espera que cada comando termine a su vez. El estado de retorno es el estado de salida del último comando ejecutado.

Las listas AND y OR son secuencias de uno o más canales separados por los operadores de control && y ││, respectivamente. Las listas AND y OR se ejecutan con asociatividad izquierda.

Una lista AND tiene la forma ...
command1 && command2
Command2 se ejecuta si, y solo si, command1 devuelve un estado de salida de cero.

Una lista OR tiene la forma ...
command1 ││ command2
Command2 se ejecuta si y solo si command1 devuelve un estado de salida distinto de cero.

El estado de retorno de las listas AND y OR es el estado de salida del último comando ejecutado en la lista.

Volviendo a nuestro último ejemplo ...

[[ A == B ]]  || echo FALSE  && echo TRUE

[[ A == B ]]  is false

     ||       Does NOT mean OR! It means...
              'execute next command if last command return code(rc) was false'

 echo FALSE   The 'echo' command rc is always true
              (i.e. it successfully echoed the word "FALSE")

     &&       Execute next command if last command rc was true

 echo TRUE    Since the 'echo FALSE' rc was true, then echo "TRUE"

Bueno. Si eso es correcto, ¿por qué el penúltimo ejemplo hace eco de algo?

[[ A == A ]]  || echo FALSE  && echo TRUE


[[ A == A ]]  is true

     ||       execute next command if last command rc was false.

 echo FALSE   Since last rc was true, shouldn't it have stopped before this?
                Nope. Instead, it skips the 'echo FALSE', does not even try to
                execute it, and continues looking for a `&&` clause.

     &&       ... which it finds here

 echo TRUE    ... so, since `[[ A == A ]]` is true, then it echos "TRUE"

El riesgo de errores lógicos cuando se usa más de uno &&o ||en una lista de comandos es bastante alto.

Recomendaciones

Un solo &&o ||en una lista de comandos funciona como se esperaba, por lo que es bastante seguro. Si es una situación en la que no necesita una cláusula else, puede ser más claro seguir algo como lo siguiente (las llaves se requieren para agrupar los últimos 2 comandos) ...

[[ $1 == --help ]]  && { echo "$HELP"; exit; }

Los operadores múltiples &&y ||, donde cada comando, excepto el último, es una prueba (es decir, entre corchetes [[ ]]), también suelen ser seguros, ya que todos menos el último operador se comportan como se esperaba. El último operador actúa más como una theno elsecláusula.

DocSalvager
fuente
0

También me confundí con esto, pero así es como pienso en la forma en que Bash lee su declaración (ya que lee los símbolos de izquierda a derecha):

  1. Símbolo que se encuentra true. Esto deberá evaluarse una vez que se llegue al final del comando. En este punto, no sé si tiene algún argumento. Almacene el comando en el búfer de ejecución.
  2. Símbolo que se encuentra ||. El comando anterior ahora está completo, así que evalúelo. Comando (buffer) está ejecutando: true. Resultado de la evaluación: 0 (es decir, éxito). Almacene el resultado 0 en el registro de "última evaluación". Ahora considere el símbolo ||mismo. Esto depende del resultado de que la última evaluación sea distinta de cero. Se verificó el registro de "última evaluación" y se encontró 0. Dado que 0 no es distinto de cero, no es necesario evaluar el siguiente comando.
  3. Símbolo que se encuentra echo. Puede ignorar este símbolo, porque el siguiente comando no necesitaba ser evaluado.
  4. Símbolo que se encuentra aaa. Este es un argumento para el comando echo(3), pero como echo(3) no necesitaba ser evaluado, puede ignorarse.
  5. Símbolo que se encuentra &&. Esto depende de que el resultado de la última evaluación sea cero. Se verificó el registro de "última evaluación" y se encontró 0. Ya que 0 es cero, el siguiente comando necesita ser evaluado.
  6. Símbolo que se encuentra echo. Este comando debe evaluarse una vez que se alcanza el final del comando, porque el siguiente comando sí tuvo que evaluarse. Almacene el comando en el búfer de ejecución.
  7. Símbolo que se encuentra bbb. Este es un argumento para ordenar echo(6). Como echosí necesitaba ser evaluado, agregue bbbal búfer de ejecución.
  8. Alcanzado el final de la línea. El comando anterior ahora está completo y necesitaba ser evaluado. Comando (buffer) está ejecutando: echo bbb. Resultado de la evaluación: 0 (es decir, éxito). Almacene el resultado 0 en el registro de "última evaluación".

Y, por supuesto, el último paso hace bbbque se repita en la consola.

Kidburla
fuente