- En sistemas de la familia Unix puede existir un ejecutable llamado
/bin/[, y la sintaxis que parece una expresión condicional del shell en realidad se apoya en la ejecución de comandos y sus códigos de salida testevalúa expresiones y devuelve 0 si son verdaderas y 1 si son falsas; cuando se invoca como[también verifica que el último argumento sea]- Muchos shells también ofrecen
testy[como comandos integrados, por lo que los mensajes de error o el comportamiento pueden diferir entre/bin/testexterno y la implementación integrada del shell - La extensión de Bash
[[no es un comando externo sino una sintaxis integrada, así que aplica reglas distintas a[; por ejemplo, no expandelong*como glob sino que lo compara como cadena literal - En scripts portables conviene usar
[, y si el script es exclusivo de Bash es mejor usar[[de forma consistente, pero hay que elegir sabiendo que ambas opciones difieren en sus reglas de expansión
Qué son /bin/[ y /bin/test
- En sistemas Unix puede existir un ejecutable con un nombre de un solo símbolo:
/bin/[- El comando de ejemplo
ls /bin/?muestra/bin/[
- El comando de ejemplo
/bin/[y/bin/testpueden apuntar al mismo binario- En el ejemplo, ambas rutas aparecen como archivos con el mismo inode y tamaño
- Aun así, no tienen por qué ser enlaces duros en todos los sistemas
testes un programa del shell para evaluar expresiones- comparación de cadenas
- comparación de números
- verificación de condiciones de archivos
- Si el resultado es verdadero devuelve el código de salida 0, y si es falso devuelve 1
Por qué [ funciona como si fuera un comando
test a = bno se ve mucho como una expresión condicional, pero la misma lógica escrita como[ a = b ]resulta más familiar[parece una sintaxis aparte, pero en realidad es una invocación de comandoif [ a = b ]; then ... fiejecuta el comando[y revisa su código de salida- Cuando se invoca como
[, el programa comprueba si el último argumento es el corchete de cierre]
- La sentencia
ifno interpreta directamente la condición, sino que decide en función del código de salida del comando recibidotest a = a; echo $?da0test a = b; echo $?da1[ a = a ]; echo $?da0[ a = b ]; echo $?da1
- En el mismo sentido,
trueyfalsetambién pueden verse como binarios auxiliares que devuelven códigos de salida
Diferencia entre binarios externos y comandos integrados del shell
- Como
testy[se usan con frecuencia en scripts de shell, la mayoría de los shells también los implementan como comandos integrados - Incluso con la misma entrada, la salida puede diferir entre el binario externo y el comando integrado del shell
/bin/test a bproducetest: a: unexpected operatortest a bproducedash: 2: test: a: unexpected operator
- Esta diferencia no solo ocurre con
testy[, sino también con comandos aparentemente simples comoecho - Como cada shell puede tener una implementación integrada distinta, el comportamiento del script puede variar según el shell que lo ejecute
Reglas distintas que aplica la extensión [[ de Bash
[[es una extensión de Bash y puede usarse en lugar de[- La diferencia más grande es que
[[siempre es una sintaxis integrada- A diferencia de
[, que puede ejecutarse como binario externo, con[[Bash puede cambiar las reglas del lenguaje dentro de la expresión
- A diferencia de
- En el ejemplo con globbing,
[y[[se comportan de manera distinta- Después de
touch long-name,[ long* = long-name ] && echo matchimprimematch - A los argumentos del comando
[se les aplican las reglas normales de expansión del shell, así quelong*se expande allong-namedel directorio [[ long* = long-name ]] && echo matchno imprime nada[[tratalong*como una cadena literal y la compara tal cual conlong-name, así que falla
- Después de
- Si el script es exclusivo de Bash, con
[[también se pueden usar funciones como la coincidencia con expresiones regulares=~
Qué elegir en un script
- En scripts de shell portables, lo correcto suele ser usar
[ - También se puede usar
test, pero no es la opción más común - Si el script es exclusivo de Bash, conviene usar
[[de forma consistente - El propio shell también tiene operadores de expresión como
!,&&y||- Estos operadores funcionan en función del estado de salida de los comandos
grep ^hello$ ... && grep ^bye$ ...hace que el código de salida total sea0si ambos comandos tienen éxito- Si el primer
grepfalla, el comando después de&&tampoco llega a hacer que el resultado completo sea exitoso, así que el código final será1
- Por eso es posible combinar expresiones
test/[con los operadores lógicos del shell dentro de una misma condición- Ejemplo:
[ a = b ] || grep -q ^hello$ /usr/share/dict/words
- Ejemplo:
- POSIX no exige que
/bin/[y/bin/testsean enlaces duros- En NetBSD sí eran enlaces duros
- macOS Catalina ofrece copias separadas del mismo binario
- Debian testing ofrece binarios distintos
- La especificación POSIX no exige que ambos archivos sean enlaces
1 comentarios
Opiniones de Hacker News
Soy el autor original. Gracias por compartirlo, y me alegra que haya llegado a la portada. Probablemente corresponde que el título lleve (2020), y “test” se refiere realmente al comando, así que sería mejor no escribirlo con mayúscula inicial.
También tengo un artículo relacionado que escribí en 2021, donde cubro hasta el operador
[[de bash, así que creo que puede ser interesante en este contexto: https://jmmv.dev/2021/08/useless-use-of-gnu.html[[no es un comando incorporado, sino que básicamente se parece más a un elemento de sintaxis. Probablemente por dentro use algún comando incorporado casi inaccesible, pero lo curioso es que]]también es una palabra reservada, aunque no pueda aparecer en una posición donde una palabra reservada tenga sentido.En algunos shells que no son bash, se necesita la palabra clave
functionpara declarar ciertos tipos de funciones.$(shell)demakepuede producir una diferencia de rendimiento medible al compilar muchos targets. Aun así, si no hace nada, es una pérdida, así que normalmente lo correcto para forzar la regeneración es usarinclude. Que GNU ignore POSIX es totalmente razonable: POSIX no es muy útil para resolver la mayoría de los problemas reales..explícito, y es realmente cómodo poder agregar opciones al final del comando que acabas de escribir. Siempre me molesta ver comandos que no soportan eso.En scripts, por lo general tiene sentido ajustarse a POSIX
sh. Como mínimo, hay que saber si se está usando sintaxis exclusiva de Bash.--ignore-caseoset -o pipefailreduce la portabilidad de los scripts. Eso, en sí, es cierto.Pero no explica por qué a los usuarios de Linux debería importarles mucho la portabilidad. OpenBSD y FreeBSD siguen vivos, pero tienen tan pocos usuarios que no parecen ser algo de lo que haya que preocuparse especialmente. Por justicia, se podría decir que también hay que considerar esos sistemas operativos, pero ¿dónde se detiene ese criterio? ¿También hay que considerar algo oscuro como
vxWorks? Lo de BusyBox y Alpine es más interesante, pero los cambios son tan grandes que, de todos modos, casi siempre se necesita un port separado. ¿Hay alguna otra razón convincente para preocuparse por ecosistemas que no sean GNU?if [ a = b ] || grep -q ^hello$ /usr/share/dict/words; then echo "test failed and grep succeeded"; fino es simplemente un uso de shell común y corriente de todos los días?make $(shell …) expansion, cuando debería sermake $(shell ...) expansion.Como en el cuerpo está escrito correctamente con tres puntos y no con un solo signo de puntos suspensivos,
mldren sí tampoco está bien. Probablemente dos bugs no relacionados hayan influido al mismo tiempo.Si llevamos el último punto un paso más allá, también se puede eliminar el bloque
ifen sí.if [ a = b ]; then echo "Oops!"; else echo "Expected; phew!"; fise convierte en[ a = b ] && echo "Oops!" || echo "Expected; phew!".No sé con qué frecuencia conviene hacerlo, pero a veces es útil para imprimir salida de depuración condicionalmente en el error estándar, como en
[ "$debug" ] && echo "what's going on" >&2. El hecho de que un bloqueifevalúe comandos normales también permite cosas comoif grep -q 'debug' /var/log/nginx/access.log; then echo "Debug request found!"; fi. Lo que todavía no investigué es si debería escribirse[ $(expr 1 + 1) -eq 2 ] && [ $(expr 2 + 2) -eq 3 ], o usar la conjunción lógica incorporada detest, es decir[ $(expr 1 + 1) -eq 2 -a $(expr 2 + 2) -eq 4 ]. Si el rendimiento no es un problema, ambas opciones parecen razonables por motivos similares.[ a = b ] && echo "Oops!" || echo "Expected; phew!"como una regla general. Probablemente bash interprete esa línea como([ a = b ] && echo "Oops!") || echo "Expected; phew!".Por eso, si la secuencia de comandos después de
&&falla, el código después de||se ejecuta de todos modos. Por ejemplo, si>/dev/full echo "strings match"falla por un error de escritura, se imprime"strings don't match"aunque las cadenas sí coincidían. Eso no tiene el mismo significado que un bloqueif.set -eque deberías usar,if [ a = b ]; then echo "Oops!"; fifunciona como se espera, pero[ a = b ] && echo "Oops!"termina con error cuando la expresiónano es igual ab.-a,-oy los operadores(,)están marcados como obsoletos. Para más detalles, consulta “Application Usage” en https://pubs.opengroup.org/onlinepubs/9699919799/utilities/t....-ao dos tests con&&, si puedes usar la evaluación aritmética de bash no hace falta salir a ejecutarexpr:[ $((1+1)) -eq 2 ]Hace algunos años dejé de usar
[.testrefuerza la idea de que esto no es sintaxis, sino simplemente un comando como cualquier otro. Yman testes mucho más cómodo que hurgar enman bash.man test, sino tambiénman [.Bash tiene
help testcomo chuleta rápida. El comando[es muy antiguo y ya estaba incluido en Version 7 Unix en 1979.De acuerdo.
[y[[, que es exclusivo de bash, generan mucha confusión sobre lo que realmente ocurre, así que era difícil estar seguro.Aun así, el hecho de que
[[tenga garantizado ser un comando integrado claramente tenía un propósito en la época en que el rendimiento de los scripts de shell importaba, y eso no fue hace tanto.test. Como no escribo scripts en bash con frecuencia, siempre me tropiezo con las sentenciasif, sobre todo con las reglas de espacios. Al ver la razón, se volvió demasiado obvio, y usartestdeja más claro que solo se están pasando argumentos.La trampa más grande con
[ytestes el comportamiento con un solo argumento. Por ejemplo, para comprobar que una variable no esté vacía, podrías escribir[ -n $FOO ].Pero si
FOOno está definida, no se expande a una cadena vacía, sino a nada, y queda igual que[ -n ]. POSIX exige que, en la forma de[con un solo argumento, si ese argumento —en este caso"-n"— no está vacío, debe tener éxito. Así que informa erróneamente que$FOOno está vacía. Las variables siempre deben ir entre comillas.testen sí, sino en el propio shell.El comportamiento mencionado tiene sentido.
[ "$FOO" ]es una forma que comprueba si el contenido no está vacío, sin importar cuál sea, incluso si es"-n".$FOOcontiene espacios, se expande a varios argumentos. Simplemente siempre pon las variables entre comillas.[ x"$FOO" != x"" ][ -n "${FOO?}" ]para que el script se detenga de inmediato si$FOOes nula o no está definida.chubot escribió documentos interesantes que profundizan en las partes más sutiles de
test/[/[[. Otros artículos de ese blog también explican de forma bastante interesante las rarezas del shell.¹ https://www.oilshell.org/blog/2017/08/31.html
² https://www.oilshell.org/blog/2016/11/18.html
No tenía idea de que
[fuera un programa, y me da algo de risa que verifique si el último argumento es el corchete de cierre.Pero eso sí explica por qué se necesitan espacios a ambos lados de los corchetes.
[[es exclusivo de bash. Si sabes que solo vas a usar bash, úsalo. El artículo cubre bien los detalles.https://zsh.sourceforge.io/Doc/Release/Conditional-Expressio...
[[.También está en zsh y ksh, y de hecho estoy casi seguro de que empezó en ksh en 1988 o antes.
testy[están especificados por POSIX y normalmente existen como binarios reales. Aunque un comando integrado del shell puede ocultarlos.En cambio,
[[no está especificado por POSIX y normalmente solo existe como integrado del shell.Apuntar solo al mínimo común denominador de shells se siente como una preocupación antiquísima.
No entendí bien por qué la última sentencia
ifresulta confusa. Si es porque, al aprender shell scripting por primera vez, uno suele asumir que[es parte del lenguaje de scripts de bash y no simplemente otro programa, entonces ahora lo entiendo. Si no, me gustaría que explicaran por qué sorprende.[es un binario, no veo bien por qué sería confuso. Parece bash de lo más normal.Tengo opiniones fuertes sobre el shell, pero no encajan muy bien con la mayoría del mundo.
Creo que nunca debería usarse
[y que solo debería usarsetest.[te hace creer que su mecanismo forma parte de la sintaxis del lenguaje, pero en realidad es solo otro "programa". Aquí "programa" incluye comandos integrados y funciones.if/||/&&miran el estado de salida, y un programa no puede ver el estado de salida de otra cosa salvo mirando la variable mágica$?, que después de la expansión es simplemente una cadena.casemira cadenas, pero no funciona en función del estado de salida ni establece un estado de salida como parte del funcionamiento decase ... esac. Un "programa" establece el estado de salida. Además,[/testsolo deberían usarse para evaluar estructuras del sistema de archivos, como entest -f /dev/null. Creo que para evaluar cadenas debería usarsecase. Naturalmente, la mayoría de los scripts me dan comezón, y los scripts que escribo les parecen raros a los demás.Cuando uso shell, prefiero poner el programa después de
ifen una línea separada y luego pasar athen. Es para enfatizar que lo que sigue aif"mira el estado de salida del último comando antes de then". Por ejemplo, en una estructura comoif; ls /tmp/goober; test -d /tmp/goober; echo "I'll always execute the then clause because echo will always return a 0 return code"; then ... fi,ifal final solo ve el estado de salida deecho, así que la cláusula then siempre se ejecuta.testson prueba de que estoy de acuerdo. Es un camino solitario... Culpo a la guía de estilo de shell de Google.Desarrollé el hábito de preferir
testal escribir scripts que deben funcionar tanto enshcomo enbash, pero lo sigo usando porque semánticamente me resulta más razonable que tratar el carácter[como un comando. También es raro que]no sea un binario aparte sino un argumento de[. Entiendo la razón técnica, pero se siente como un hack.test. ¡Decenas!Aprendí a usar
[[solo cuando quiero hacer matching con expresiones regulares. Ej.:if [[ "${foo}" =~ ^bar$ ]]; then echo Yes; fiFuera de eso, simplemente uso
"test"o"[". Ya voy por 85,000 líneas de bash. No digo que bash sea excelente, pero todavía cubre mis necesidades para muchas cosas.exprpuede hacer matching con expresiones regulares básicas y también puede devolver grupos de captura.1: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/e...
[ "$foo" = bar ] && echo Yes.Para hacer matching de subcadenas, por lo general alcanza con
[y el glob*. Algo como[ "$bar" = extra* ] && echo '$bar began with extra'. El dialecto de expresiones regulares de Bash es primitivo, así que casi no vale la pena esforzarse en usarlo. Para cosas complejas, lo correcto es usar otras herramientas comogrep,awkoperl. Si te obsesionas con intentar hacer todo en bash, incluso tareas complejas que necesitan mayor reutilización, modularidad y tipos integrados, el rendimiento cae en picada.