¿Cómo puedo expandir un Tilde ~ como parte de una variable?

12

Cuando abro un indicador de bash y escribo:

$ set -o xtrace
$ x='~/someDirectory'
+ x='~/someDirectory'
$ echo $x
+ echo '~/someDirectory'
~/someDirectory

Esperaba que la quinta línea de arriba se hubiera ido + echo /home/myUsername/someDirectory. ¿Hay alguna forma de hacer esto? En mi script Bash original, la variable x en realidad se está rellenando a partir de datos de un archivo de entrada, a través de un bucle como este:

while IFS= read line
do
    params=($line)
    echo ${params[0]}
done <"./someInputFile.txt"

Aún así, estoy obteniendo un resultado similar, con el en echo '~/someDirectory'lugar de echo /home/myUsername/someDirectory.

Andrés
fuente
En ZSH esto es x='~'; print -l ${x} ${~x}. Me di por vencido después de revisar el bashmanual por un tiempo.
thrig
@thrig: Esto no es un bashism, este comportamiento es POSIX.
WhiteWinterWolf
Extremadamente relacionado (si no es un engañado
Kusalananda
@Kusalananda: No estoy seguro de que esto sea un engaño ya que la razón aquí es algo diferente: el OP no incluyó $ x entre comillas cuando se hizo eco.
WhiteWinterWolf
No pones tildes en el archivo de entrada. Problema resuelto.
chepner

Respuestas:

11

El estándar POSIX impone que la expansión de palabras se realice en el siguiente orden (el énfasis es mío):

  1. La expansión Tilde (ver Expansión Tilde), la expansión de parámetros (ver Expansión de parámetros), la sustitución de comandos (ver Sustitución de comandos) y la expansión aritmética (ver Expansión aritmética) se realizarán, de principio a fin. Ver ítem 5 en Reconocimiento de tokens.

  2. La división de campo (ver División de campo) se realizará en las porciones de los campos generados por el paso 1, a menos que IFS sea nulo.

  3. La expansión de nombre de ruta (ver Expansión de nombre de ruta) se realizará, a menos que el conjunto -f esté vigente.

  4. La eliminación de la cotización (ver Eliminación de la cotización) siempre se realizará en último lugar.

El único punto que nos interesa aquí es el primero: como puede ver, la expansión de tilde se procesa antes de la expansión de parámetros:

  1. El shell intenta una expansión de tilde echo $x, no se encuentra tilde, por lo que continúa.
  2. El shell intenta expandir un parámetro echo $x, $xse encuentra y se expande y la línea de comando se convierte echo ~/someDirectory.
  3. El procesamiento continúa, ya que la expansión de tilde ya se ha procesado, el ~personaje permanece tal cual.

Al usar las comillas mientras asignaba $x, solicitaba explícitamente no expandir la tilde y tratarla como un carácter normal. Una cosa que a menudo se pasa por alto es que en los comandos de shell no tiene que citar la cadena completa, por lo que puede hacer que la expansión se realice correctamente durante la asignación de variables:

user@host:~$ set -o xtrace
user@host:~$ x=~/'someDirectory'
+ x=/home/user/someDirectory
user@host:~$ echo $x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$

Y también puede hacer que la expansión ocurra en la echolínea de comandos siempre que pueda ocurrir antes de la expansión de parámetros:

user@host:~$ x='someDirectory'
+ x=someDirectory
user@host:~$ echo ~/$x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$

Si por alguna razón realmente necesita afectar la tilde a la $xvariable sin expansión, y poder expandirla con el echocomando, debe proceder dos veces para forzar $xque ocurran dos expansiones de la variable:

user@host:~$ x='~/someDirectory'
+ x='~/someDirectory'
user@host:~$ echo "$( eval echo $x )"
++ eval echo '~/someDirectory'
+++ echo /home/user/someDirectory
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$ 

Sin embargo, tenga en cuenta que, según el contexto en el que utilice dicha estructura, puede tener efectos secundarios no deseados. Como regla general, prefiera evitar usar cualquier cosa que requiera evalcuando tenga otra forma.

Si desea abordar específicamente el problema de tilde en lugar de cualquier otro tipo de expansión, dicha estructura sería más segura y portátil:

user@host:~$ x='~/someDirectory'
+ x='~/someDirectory'
user@host:~$ case "$x" in "~/"*)
>     x="${HOME}/${x#"~/"}"
> esac
+ case "$x" in
+ x=/home/user/someDirectory
user@host:~$ echo $x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$ 

Esta estructura verifica explícitamente la presencia de un líder ~y lo reemplaza con el directorio de inicio del usuario si se encuentra.

Después de su comentario, x="${HOME}/${x#"~/"}"puede ser sorprendente para alguien que no se utiliza en la programación de shell, pero de hecho está vinculado a la misma regla POSIX que cité anteriormente.

Según lo impuesto por el estándar POSIX, la eliminación de comillas ocurre en último lugar y la expansión de parámetros ocurre muy temprano. Por lo tanto, ${#"~"}se evalúa y se expande mucho antes de la evaluación de las comillas externas. Por turnos, como se define en las reglas de expansión de parámetros :

En cada caso que se necesita un valor de palabra (basado en el estado del parámetro, como se describe a continuación), la palabra se someterá a una expansión de tilde, expansión de parámetros, sustitución de comandos y expansión aritmética.

Por lo tanto, el lado derecho del #operador debe ser citado o escapado correctamente para evitar la expansión de la tilde.

Entonces, para decirlo de manera diferente, cuando el intérprete de shell mira x="${HOME}/${x#"~/"}", ve:

  1. ${HOME}y ${x#"~/"}debe ser expandido.
  2. ${HOME}se expande al contenido de la $HOMEvariable.
  3. ${x#"~/"}desencadena una expansión anidada: "~/"se analiza pero, al citarse, se trata como un literal 1 . Podría haber usado comillas simples aquí con el mismo resultado.
  4. ${x#"~/"}la expresión en sí ahora se expande, lo que hace que el prefijo ~/se elimine del valor de $x.
  5. El resultado de lo anterior ahora se concatena: la expansión de ${HOME}, el literal /, la expansión ${x#"~/"}.
  6. El resultado final está encerrado entre comillas dobles, evitando funcionalmente la división de palabras. Digo funcionalmente aquí porque estas comillas dobles no son técnicamente necesarias (ver aquí y allá, por ejemplo), pero como un estilo personal tan pronto como una tarea supera algo a=$b, generalmente encuentro más claro agregar comillas dobles.

Por cierto, si observa más de cerca la casesintaxis, verá la "~/"*construcción que se basa en el mismo concepto x=~/'someDirectory'que expliqué anteriormente (aquí nuevamente, las comillas dobles y simples podrían usarse indistintamente).

No se preocupe si estas cosas pueden parecer oscuras a primera vista (¡tal vez incluso a la segunda o más tarde!). En mi opinión, la expansión de parámetros es, con subcapas, uno de los conceptos más complejos a la hora de programar en lenguaje shell.

Sé que algunas personas pueden estar en total desacuerdo, pero si desea aprender más a fondo sobre la programación de shell, le animo a leer la Guía avanzada de secuencias de comandos Bash : enseña la secuencia de comandos Bash, así que con muchas extensiones y campanas. silbidos en comparación con las secuencias de comandos de shell POSIX, pero lo encontré bien escrito con muchos ejemplos prácticos. Una vez que maneje esto, es fácil restringirse a las funciones POSIX cuando lo necesite, personalmente creo que ingresar directamente en el reino POSIX es una curva de aprendizaje empinada innecesaria para principiantes (compare mi reemplazo de tilde POSIX con Bash tipo regex de @ m0dular equivalente a tener una idea de lo que quiero decir;)!).


1 : Lo que me lleva a encontrar un error en Dash que no implementa la expansión de tilde aquí correctamente (uso comprobable x='~/foo'; echo "${x#~/}"). ¡La expansión de parámetros es un campo complejo tanto para el usuario como para los propios desarrolladores de shell!

WhiteWinterWolf
fuente
¿Cómo está el shell bash analizando la línea x="${HOME}/${x#"~/"}"? Se ve como una concatenación de 3 cuerdas: "${HOME}/${x#", ~/, y "}". ¿El shell permite comillas dobles anidadas cuando el par interno de comillas dobles está dentro de un ${ }bloque?
Andrew
@Andrew: He completado mi respuesta con información adicional con suerte abordando su comentario.
WhiteWinterWolf
Gracias, esta es una gran respuesta. He aprendido muchísimo al leerlo. Ojalá pudiera votarlo más de una vez :)
Andrew
@WhiteWinterWolf: aún así, shell no ve las comillas anidadas, sea cual sea el resultado.
avp
6

Una posible respuesta:

eval echo "$x"

Como estás leyendo la entrada de un archivo, no haría esto.

Puede buscar y reemplazar el ~ con el valor de $ HOME, así:

x='~/.config'
x="${x//\~/$HOME}"
echo "$x"

Me da

/home/adrian/.config
m0dular
fuente
Tenga en cuenta que la ${parameter/pattern/string}expansión es una extensión Bash y puede no estar disponible en otros shells.
WhiteWinterWolf
Cierto. El OP mencionó que estaba usando Bash, así que creo que es una respuesta adecuada.
m0dular
Estoy de acuerdo, siempre y cuando uno se apegue a Bash, ¿por qué no aprovecharlo al máximo (no todos necesitan portabilidad en todas partes), pero vale la pena tenerlo en cuenta para los usuarios que no son Bash (varias distribuciones ahora se envían con Dash en lugar de Bash, por ejemplo ) para que los usuarios afectados no se sorprendan.
WhiteWinterWolf
Me tomé la libertad de mencionar su publicación en mi digresión sobre las diferencias entre las extensiones de Bash y las secuencias de comandos de shell POSIX, ya que creo que su declaración tipo regex de una sola línea de Bash en comparación con mi caseestructura POSIX ilustra bien cómo las secuencias de comandos Bash son más fáciles de usar especialmente para principiantes.
WhiteWinterWolf