Acabo de asignar una variable, pero echo $ variable muestra algo más

104

A continuación, se muestran una serie de casos en los que se echo $varpuede mostrar un valor diferente al que se acaba de asignar. Esto sucede independientemente de si el valor asignado fue "entre comillas dobles", "entre comillas simples" o no.

¿Cómo consigo que el shell establezca mi variable correctamente?

Asteriscos

La salida esperada es /* Foobar is free software */, pero en su lugar obtengo una lista de nombres de archivo:

$ var="/* Foobar is free software */"
$ echo $var 
/bin /boot /dev /etc /home /initrd.img /lib /lib64 /media /mnt /opt /proc ...

Corchetes

El valor esperado es [a-z], ¡pero a veces obtengo una sola letra!

$ var=[a-z]
$ echo $var
c

Saltos de línea (nuevas líneas)

El valor esperado es una lista de líneas separadas, ¡pero en cambio todos los valores están en una línea!

$ cat file
foo
bar
baz

$ var=$(cat file)
$ echo $var
foo bar baz

Múltiples espacios

Esperaba un encabezado de tabla cuidadosamente alineado, pero en su lugar, varios espacios desaparecen o se contraen en uno.

$ var="       title     |    count"
$ echo $var
title | count

Pestañas

Esperaba dos valores separados por tabulaciones, ¡pero en su lugar obtengo dos valores separados por espacios!

$ var=$'key\tvalue'
$ echo $var
key value
ese otro chico
fuente
2
Gracias por hacer esto Me encuentro con la línea que se alimenta a menudo. Entonces var=$(cat file)está bien, pero echo "$var"es necesario.
snd
3
Por cierto, esto también es BashPitfalls # 14: mywiki.wooledge.org/BashPitfalls#echo_.24foo
Charles Duffy
Véase también stackoverflow.com/questions/10067266/…
tripleee
Además, consulte también stackoverflow.com/questions/2414150/…
tripleee

Respuestas:

139

En todos los casos anteriores, la variable está configurada correctamente, ¡pero no se lee correctamente! La forma correcta es usar comillas dobles al hacer referencia :

echo "$var"

Esto da el valor esperado en todos los ejemplos dados. ¡Cite siempre referencias de variables!


¿Por qué?

Cuando una variable no está entre comillas , hará lo siguiente:

  1. Realice la división de campos donde el valor se divide en varias palabras en espacios en blanco (por defecto):

    Antes de: /* Foobar is free software */

    Después: /*, Foobar, is, free, software,*/

  2. Cada una de estas palabras experimentará una expansión de nombre de ruta , donde los patrones se expandirán en archivos coincidentes:

    Antes de: /*

    Después: /bin, /boot, /dev, /etc, /home, ...

  3. Finalmente, todos los argumentos se pasan a echo, que los escribe separados por espacios simples , dando

    /bin /boot /dev /etc /home Foobar is free software Desktop/ Downloads/

    en lugar del valor de la variable.

Cuando se cite la variable :

  1. Sustituirse por su valor.
  2. No hay paso 2.

Es por eso que siempre debe citar todas las referencias de variables , a menos que requiera específicamente la división de palabras y la expansión de la ruta. Herramientas como shellcheck están ahí para ayudar y advertirán sobre comillas faltantes en todos los casos anteriores.

ese otro chico
fuente
no siempre está funcionando. Puedo dar un ejemplo: paste.ubuntu.com/p/8RjR6CS668
recolic
1
Sí, $(..)elimina los avances de línea finales. Puede utilizarlo var=$(cat file; printf x); var="${var%x}"para solucionarlo.
ese otro chico
17

Es posible que desee saber por qué sucede esto. Junto con la gran explicación de ese otro tipo , encuentre una referencia de ¿Por qué mi script de shell se ahoga con espacios en blanco u otros caracteres especiales? escrito por Gilles en Unix y Linux :

¿Por qué necesito escribir "$foo"? ¿Qué pasa sin las comillas?

$foono significa "tomar el valor de la variable foo". Significa algo mucho más complejo:

  • Primero, toma el valor de la variable.
  • División de campos: trate ese valor como una lista de campos separados por espacios en blanco y cree la lista resultante. Por ejemplo, si la variable contiene foo * bar ​entonces el resultado de este paso es la lista 3-elemento foo, *, bar.
  • Generación de nombre de archivo: trate cada campo como un glob, es decir, como un patrón comodín, y reemplácelo por la lista de nombres de archivo que coinciden con este patrón. Si el patrón no coincide con ningún archivo, no se modifica. En nuestro ejemplo, esto da como resultado la lista que contiene foo, seguida de la lista de archivos en el directorio actual, y finalmente bar. Si el directorio actual está vacía, el resultado es foo, *, bar.

Tenga en cuenta que el resultado es una lista de cadenas. Hay dos contextos en la sintaxis de shell: contexto de lista y contexto de cadena. La división de campos y la generación de nombres de archivos solo ocurren en el contexto de la lista, pero eso es la mayor parte del tiempo. Las comillas dobles delimitan el contexto de una cadena: toda la cadena entre comillas dobles es una sola cadena, que no debe dividirse. (Excepción: "$@"expandirse a la lista de parámetros posicionales, por ejemplo, "$@"es equivalente a "$1" "$2" "$3"si hay tres parámetros posicionales. Consulte ¿Cuál es la diferencia entre $ * y $ @? )

Lo mismo ocurre con la sustitución de comandos con $(foo)o con `foo`. En una nota al margen, no lo use `foo`: sus reglas de citas son extrañas y no portátiles, y todos los shells modernos admiten lo $(foo)que es absolutamente equivalente, excepto por tener reglas de citas intuitivas.

La salida de la sustitución aritmética también experimenta las mismas expansiones, pero eso normalmente no es una preocupación ya que solo contiene caracteres no expandibles (asumiendo IFSque no contiene dígitos o -).

Consulte ¿ Cuándo es necesaria la doble cotización? para obtener más detalles sobre los casos en los que puede omitir las citas.

A menos que quiera que ocurra todo este galimatías, recuerde siempre usar comillas dobles en las sustituciones de variables y comandos. Tenga cuidado: omitir las comillas puede provocar no solo errores, sino también agujeros de seguridad .

fedorqui 'Así que deja de hacer daño'
fuente
7

Además de otros problemas causados ​​por no citar, -ny -epueden consumirse echocomo argumentos. (Solo el primero es legal según la especificación POSIX echo, pero varias implementaciones comunes violan la especificación y también consumen -e).

Para evitar esto, utilice en printflugar de echocuando los detalles importan.

Así:

$ vars="-e -n -a"
$ echo $vars      # breaks because -e and -n can be treated as arguments to echo
-a
$ echo "$vars"
-e -n -a

Sin embargo, las citas correctas no siempre lo salvarán al usar echo:

$ vars="-n"
$ echo $vars
$ ## not even an empty line was printed

... mientras que te salvará con printf:

$ vars="-n"
$ printf '%s\n' "$vars"
-n
Charles Duffy
fuente
¡Sí, necesitamos una buena deduplicación para esto! Estoy de acuerdo en que esto se ajusta al título de la pregunta, pero no creo que obtenga la visibilidad que se merece aquí. ¿Qué tal una nueva pregunta a la "¿Por qué mi -e/ -n/ barra invertida no aparece?" Podemos agregar enlaces desde aquí según corresponda.
ese otro chico
¿Quiso decir consumir -ntambién ?
Pesa el
1
@PesaThe, no, quise decir -e. El estándar para echono especifica la salida cuando su primer argumento es -n, lo que hace que cualquier salida posible sea legal en ese caso; no existe tal provisión para -e.
Charles Duffy
Oh ... no puedo leer. Culpemos a mi inglés por eso. Gracias por la explicación.
Pesa el
6

comillas dobles del usuario para obtener el valor exacto. Me gusta esto:

echo "${var}"

y leerá su valor correctamente.

desaparecido
fuente
Funciona ... Gracias
Tshilidzi Mudau
2

echo $varla salida depende en gran medida del valor de la IFSvariable. De forma predeterminada, contiene espacios, tabulaciones y caracteres de nueva línea:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$

Esto significa que cuando shell está dividiendo campos (o palabras), usa todos estos caracteres como separadores de palabras. Esto es lo que sucede cuando se hace referencia a una variable sin comillas dobles para hacerla eco ( $var) y, por lo tanto, se altera la salida esperada.

Una forma de evitar la división de palabras (además de usar comillas dobles) es establecer IFSun valor nulo. Ver http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_05 :

Si el valor de IFS es nulo, no se realizará ninguna división de campo.

Establecer en nulo significa establecer en valor vacío:

IFS=

Prueba:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$
[ks@localhost ~]$ var=$'key\nvalue'
[ks@localhost ~]$ echo $var
key value
[ks@localhost ~]$ IFS=
[ks@localhost ~]$ echo $var
key
value
[ks@localhost ~]$ 
ks1322
fuente
2
También set -ftendrías
que
@thatotherguy, ¿es realmente necesario para su primer ejemplo con expansión de ruta? Con el IFSvalor nulo, echo $varse expandirá echo '/* Foobar is free software */'ay la expansión de ruta no se realiza dentro de cadenas entre comillas simples.
ks1322
1
Si. Si mkdir "/this thing called Foobar is free software etc/"ves que todavía se expande. Obviamente, es más práctico para el [a-z]ejemplo.
ese otro chico
Ya veo, esto tiene sentido, por [a-z]ejemplo.
ks1322
2

La respuesta de ks1322 me ayudó a identificar el problema mientras usaba docker-compose exec:

Si omite la -Tbandera, docker-compose execagregue un carácter especial que rompa la salida, vemos en blugar de 1b:

$ test=$(/usr/local/bin/docker-compose exec db bash -c "echo 1")
$ echo "${test}b"
b
echo "${test}" | cat -vte
1^M$

Con -Tbandera, docker-compose execfunciona como se esperaba:

$ test=$(/usr/local/bin/docker-compose exec -T db bash -c "echo 1")
$ echo "${test}b"
1b
Alabama
fuente
-2

Además de poner la variable entre comillas, también se podría traducir la salida de la variable usando try convirtiendo espacios en nuevas líneas.

$ echo $var | tr " " "\n"
foo
bar
baz

Aunque esto es un poco más complicado, agrega más diversidad con la salida, ya que puede sustituir cualquier carácter como separador entre las variables de matriz.

Alek
fuente
2
Pero esto sustituye todos los espacios por nuevas líneas. Citar conserva los espacios y nuevas líneas existentes.
user000001
Es cierto que sí. Supongo que depende de lo que haya dentro de la variable. De hecho, uso tral revés para crear matrices a partir de archivos de texto.
Alek
3
Crear un problema al no citar la variable correctamente y luego solucionarlo con un proceso adicional complicado no es una buena programación.
tripleee
@Alek, ... eh, ¿qué? No es trnecesario crear correctamente / correctamente una matriz a partir de un archivo de texto; puede especificar el separador que desee configurando IFS. Por ejemplo: IFS=$'\n' read -r -d '' -a arrayname < <(cat file.txt && printf '\0')funciona desde bash 3.2 (la versión más antigua en circulación) y establece correctamente el estado de salida en falso si catfalla. Y si quisiera, digamos, pestañas en lugar de nuevas líneas, simplemente reemplazaría $'\n'por $'\t'.
Charles Duffy
1
@Alek, ... si está haciendo algo como arrayname=( $( cat file | tr '\n' ' ' ) ), entonces eso está roto en múltiples capas: está englobando sus resultados (por lo que a se *convierte en una lista de archivos en el directorio actual), y funcionaría igual de bien sin eltr ( o el cat, para el caso; uno podría simplemente usar arrayname=$( $(<file) )y se rompería de la misma manera, pero de manera menos ineficiente).
Charles Duffy