¿Por qué necesito citar variables para if, pero no para echo?

26

He leído que necesita comillas dobles para expandir variables, por ejemplo

if [ -n "$test" ]; then echo '$test ok'; else echo '$test null'; fi

funcionará como se esperaba, mientras

if [ -n $test ]; then echo '$test ok'; else echo '$test null'; fi

siempre dirá $test okincluso si $testes nulo.

pero entonces, ¿por qué no necesitamos citas echo $test?

CharlesB
fuente
2
Si no cita una variable para usar como echoargumento, se eliminarán los espacios adicionales y las nuevas líneas.
jordanm

Respuestas:

36

Siempre necesita comillas alrededor de las variables en todos los contextos de la lista , es decir, en todas partes, la variable puede expandirse a múltiples valores a menos que desee los 3 efectos secundarios de dejar una variable sin comillas.

Los contextos de lista incluyen argumentos a comandos simples como [o echo, las for i in <here>asignaciones a matrices ... Hay otros contextos donde las variables también necesitan ser citadas. Lo mejor es siempre citar variables a menos que tenga una muy buena razón para no hacerlo.

Piense en la ausencia de comillas (en contextos de lista) como el operador split + glob .

Como si echo $testfuera echo glob(split("$test")).

El comportamiento del shell es confuso para la mayoría de las personas porque en la mayoría de los otros idiomas, se colocan comillas alrededor de cadenas fijas, como puts("foo"), y no alrededor de variables (like puts(var)), mientras que en shell es al revés: todo es una cadena en shell, por lo que poner comillas alrededor de todo sería engorroso, usted echo test, no necesita hacerlo "echo" "test". En shell, las comillas se usan para otra cosa: evitar algún significado especial de algunos caracteres y / o afectar el comportamiento de algunas expansiones.

En [ -n $test ]o echo $test, el shell se dividirá $test(en espacios en blanco por defecto), y luego realizará la generación de nombre de archivo (expandirá todos los *patrones '?' ... a la lista de archivos coincidentes), y luego pasará esa lista de argumentos a los comandos [o echo.

Nuevamente, piense en ello como "[" "-n" glob(split("$test")) "]". Si $testestá vacío o contiene solo espacios en blanco (spc, tab, nl), entonces el operador split + glob devolverá una lista vacía, por lo [ -n $test ]que será "[" "-n" "]", que es una prueba para verificar si "-n" es la cadena vacía o no. Pero imagina lo que hubiera pasado si $testfuera "*" o "= foo" ...

En [ -n "$test" ], [se pasa los cuatro argumentos "[", "-n", ""y "]"(sin las comillas), que es lo que queremos.

Ya sea que haga echoo [no la diferencia, es solo que echogenera lo mismo, ya sea que haya pasado un argumento vacío o ningún argumento.

Consulte también esta respuesta a una pregunta similar para obtener más detalles sobre el [comando y la [[...]]construcción.

Stéphane Chazelas
fuente
7

La respuesta de @ h3rrmiller es buena para explicar por qué necesita las comillas para if(o mejor dicho, [/ test), pero en realidad diría que su pregunta es incorrecta.

Pruebe los siguientes comandos y verá lo que quiero decir.

export testvar="123    456"
echo $testvar
echo "$testvar"

Sin las comillas, la sustitución de variables hace que el segundo comando se expanda a:

echo 123    456

y los múltiples espacios se colapsan en uno solo:

echo 123 456

Con las comillas, se conservan los espacios.

Esto sucede porque cuando se cita un parámetro (si ese parámetro se pasa a echo, testo algún otro comando), el valor de ese parámetro se envía como un valor a la orden. Si no lo cita, el shell hace su magia normal de buscar espacios en blanco para determinar dónde comienza y dónde termina cada parámetro.

Esto también se puede ilustrar con el siguiente programa C (muy, muy simple). Pruebe lo siguiente en la línea de comando (es posible que desee hacerlo en un directorio vacío para no arriesgarse a sobrescribir algo).

cat <<EOF >paramtest.c
#include <stdio.h>
int main(int argc, char **argv) {
  int nparams = argc-1; /* because 1 parameter means only the executable's name */
  printf("%d parameters received\n", nparams);
  return nparams;
}
EOF
cc -o paramtest paramtest.c

y entonces...

./paramtest 123 456
./paramtest "123 456"
./paramtest 123   456
./paramtest "123   456"

Después de ejecutar paramtest, $?mantendrá el número de parámetros que se pasó (y ese número se imprimirá).

un CVn
fuente
2

Esto se trata de cómo el shell interpreta la línea antes de ejecutar un programa.

Si la línea se lee echo I am $USER, el shell la expande echo I am blrfly echono tiene idea de si el origen del texto es una expansión literal o variable. Del mismo modo, si se lee una línea echo I am $UNDEFINED, el shell se expandirá $UNDEFINEDen nada y los argumentos de echo lo serán I am, y ese es el final. Dado que echofunciona bien sin argumentos, echo $UNDEFINEDes completamente válido.

Su problema con ifno es realmente con if, porque ifsolo ejecuta el programa y los argumentos que le siguen y ejecuta la thenparte si el programa sale 0(o la elseparte si hay una y el programa sale de otro modo 0):

if /bin/true ; then echo True dat. ; fi
if fgrep -q blrfl /etc/passwd ; then echo Blrfl has an account. ; fi

Cuando if [ ... ]haces una comparación, no estás usando primitivas integradas en el shell. En realidad, le está ordenando al shell que ejecute un programa llamado [que es un superconjunto muy leve test(1)que requiere que su último argumento sea ]. Ambos programas salen 0si la condición de prueba se cumplió y 1no fue así.

La razón por la que algunas pruebas se rompen cuando una variable no está definida es porque testno ve que está usando una variable. Ergo, se [ $UNDEFINED -eq 2 ]rompe porque cuando el shell termina con él, todos los testargumentos para ver son -eq 2 ], lo cual no es una prueba válida. Si lo hiciera con algo definido, como [ $DEFINED -ne 0 ], eso funcionaría porque el shell lo expandiría en una prueba válida (por ejemplo, 0 -ne 0).

Hay una diferencia semántica entre foo $UNDEFINED bar, que se expande a dos argumentos ( fooy bar) porque $UNDEFINEDestá a la altura de su nombre. Compare esto con foo "$UNDEFINED" bar, que se expande a tres argumentos ( foo, una cadena vacía y `bar). Las citas obligan al shell a interpretarlas como un argumento, ya sea que haya algo entre ellas o no.

Blrfl
fuente
0

Sin comillas $testpodría expandirse para ser más de una palabra, por lo que debe ser citado para no romper la sintaxis ya que cada interruptor dentro del [comando espera un argumento que es lo que hacen las comillas (lo que se $testexpande en un argumento)

La razón por la que no necesita comillas para expandir una variable echoes porque no espera un argumento. Simplemente imprimirá lo que le digas. Entonces, incluso si se $testexpande a 100 palabras, echo aún lo imprimirá.

Echa un vistazo a las trampas de Bash

h3rrmiller
fuente
sí, pero ¿por qué no lo necesitamos echo?
CharlesB
@CharlesB Necesitas las comillas para echo. ¿Qué te hace pensar lo contrario?
Gilles 'SO- deja de ser malvado'
No los necesito, puedo echo $testy funciona (genera el valor de $ test)
CharlesB
1
@CharlesB Solo genera el valor de $ test si no contiene múltiples espacios en ninguna parte. Pruebe el programa en mi respuesta para una ilustración de la razón.
un CVn
0

Los parámetros vacíos se eliminan si no se citan:

start cmd:> strace -e trace=execve echo foo $bar baz
execve("/usr/bin/echo", ["echo", "foo", "baz"], [/* 100 vars */]) = 0

start cmd:> strace -e trace=execve echo foo "$bar" baz
execve("/usr/bin/echo", ["echo", "foo", "", "baz"], [/* 100 vars */]) = 0

El comando llamado no ve que haya un parámetro vacío en la línea de comando del shell. Parece que [se define para devolver 0 para -n sin nada siguiente. Por qué siempre.

Las citas también marcan la diferencia para echo, en varios casos:

var='*'
echo $var
echo "$var"

var="foo        bar"
echo $var
echo "$var"
Hauke ​​Laging
fuente
2
No es echo, es la cáscara. Verías el mismo comportamiento con ls. Pruebe touch '*'algo de tiempo si se siente aventurero. :)
un CVn
Eso es solo una redacción, ya que no hay diferencia en el caso "si". [no es un comando de shell especial. Eso es diferente de [[(en bash) donde no es necesario citar.
Hauke ​​Laging