Cómo expandir manualmente una variable especial (ej: ~ tilde) en bash

135

Tengo una variable en mi script bash cuyo valor es algo como esto:

~/a/b/c

Tenga en cuenta que es tilde sin expandir. Cuando hago ls -lt en esta variable (lo llamo $ VAR), no obtengo tal directorio. Quiero dejar que bash interprete / expanda esta variable sin ejecutarla. En otras palabras, quiero que bash ejecute eval pero no ejecute el comando evaluado. ¿Es esto posible en bash?

¿Cómo logré pasar esto a mi script sin expansión? Pasé el argumento rodeándolo con comillas dobles.

Prueba este comando para ver a qué me refiero:

ls -lt "~"

Esta es exactamente la situación en la que estoy. Quiero que se expanda la tilde. En otras palabras, ¿con qué debería reemplazar la magia para hacer que estos dos comandos sean idénticos:

ls -lt ~/abc/def/ghi

y

ls -lt $(magic "~/abc/def/ghi")

Tenga en cuenta que ~ / abc / def / ghi puede o no existir.

madiyaan damha
fuente
44
También puede encontrar útil la expansión de Tilde entre comillas . Principalmente, pero no del todo, evita el uso eval.
Jonathan Leffler
2
¿Cómo se asignó su variable con una tilde sin expandir? Quizás todo lo que se requiere es asignar esa variable con las tildes fuera de las comillas. foo=~/"$filepath"ofoo="$HOME/$filepath"
Chad Skeeters
dir="$(readlink -f "$dir")"
Jack Wasey

Respuestas:

98

Debido a la naturaleza de StackOverflow, no puedo simplemente hacer que esta respuesta sea inaceptable, pero en los 5 años transcurridos desde que publiqué esto, ha habido respuestas mucho mejores que mi respuesta ciertamente rudimentaria y bastante mala (era joven, no mate yo).

Las otras soluciones en este hilo son soluciones más seguras y mejores. Preferiblemente, iría con cualquiera de estos dos:


Respuesta original para fines históricos (pero no use esto)

Si no me equivoco, "~"un script bash no lo expandirá de esa manera porque se trata como una cadena literal "~". Puede forzar la expansión de evalesta manera.

#!/bin/bash

homedir=~
eval homedir=$homedir
echo $homedir # prints home path

Alternativamente, solo use ${HOME}si desea el directorio de inicio del usuario.

wkl
fuente
3
¿Tiene una solución para cuando la variable tiene un espacio en ella?
Hugo
34
Lo encontré ${HOME}más atractivo. ¿Hay alguna razón para no hacer de esta su recomendación principal? En cualquier caso, gracias!
sabio
1
+1: necesitaba expandir ~ $ some_other_user y eval funciona bien cuando $ HOME no funcionará porque no necesito el usuario actual de inicio.
Olivecoder
11
Usar evales una sugerencia horrible, es realmente malo que reciba tantos votos positivos. Te encontrarás con todo tipo de problemas cuando el valor de la variable contenga meta caracteres de shell.
user2719058
1
No pude terminar mi comentario en ese momento y, luego, no me permitieron editarlo más tarde. Así que estoy agradecido (gracias de nuevo @birryree) por esta solución, ya que ayudó en mi contexto específico en ese momento. Gracias Charles por hacerme consciente.
Olivecoder
114

Si la variable vares de entrada por el usuario, evaldebe no ser utilizado para expandir el tilde usando

eval var=$var  # Do not use this!

La razón es: el usuario podría escribir por accidente (o por propósito), por ejemplo, var="$(rm -rf $HOME/)"con posibles consecuencias desastrosas.

Una forma mejor (y más segura) es usar la expansión de parámetros Bash:

var="${var/#\~/$HOME}"
Håkon Hægland
fuente
8
¿Cómo podría cambiar ~ userName / en lugar de solo ~ /?
aspergillusOryzae
3
¿Cuál es el propósito de #en "${var/#\~/$HOME}"?
Jahid
3
@Jahid Se explica en el manual . Obliga a la tilde a coincidir solo al comienzo de $var.
Håkon Hægland
1
Gracias. (1) ¿Por qué necesitamos, \~es decir, escapar ~? (2) Su respuesta asume que ese ~es el primer personaje en $var. ¿Cómo podemos ignorar los espacios en blanco principales $var?
Tim
1
@Tim Gracias por el comentario. Sí, tiene razón, no necesitamos escapar de una tilde a menos que sea el primer carácter de una cadena sin comillas o siga una :cadena sin comillas. Más información en los documentos . Para eliminar el espacio en blanco inicial, consulte ¿Cómo recortar espacios en blanco de una variable Bash?
Håkon Hægland
24

Plagarme de una respuesta anterior , para hacer esto de manera robusta sin los riesgos de seguridad asociados con eval:

expandPath() {
  local path
  local -a pathElements resultPathElements
  IFS=':' read -r -a pathElements <<<"$1"
  : "${pathElements[@]}"
  for path in "${pathElements[@]}"; do
    : "$path"
    case $path in
      "~+"/*)
        path=$PWD/${path#"~+/"}
        ;;
      "~-"/*)
        path=$OLDPWD/${path#"~-/"}
        ;;
      "~"/*)
        path=$HOME/${path#"~/"}
        ;;
      "~"*)
        username=${path%%/*}
        username=${username#"~"}
        IFS=: read -r _ _ _ _ _ homedir _ < <(getent passwd "$username")
        if [[ $path = */* ]]; then
          path=${homedir}/${path#*/}
        else
          path=$homedir
        fi
        ;;
    esac
    resultPathElements+=( "$path" )
  done
  local result
  printf -v result '%s:' "${resultPathElements[@]}"
  printf '%s\n' "${result%:}"
}

...Usado como...

path=$(expandPath '~/hello')

Alternativamente, un enfoque más simple que utiliza con evalcuidado:

expandPath() {
  case $1 in
    ~[+-]*)
      local content content_q
      printf -v content_q '%q' "${1:2}"
      eval "content=${1:0:2}${content_q}"
      printf '%s\n' "$content"
      ;;
    ~*)
      local content content_q
      printf -v content_q '%q' "${1:1}"
      eval "content=~${content_q}"
      printf '%s\n' "$content"
      ;;
    *)
      printf '%s\n' "$1"
      ;;
  esac
}
Charles Duffy
fuente
44
Al mirar su código, parece que está usando un cañón para matar un mosquito. Hay tiene que haber una manera mucho más simple ..
Gino
2
@Gino, seguramente hay una manera más simple; la pregunta es si hay una forma más sencilla que también sea segura.
Charles Duffy
2
@Gino, ... Yo no supongo que uno puede usar printf %qpara escapar de todo, pero la tilde, y luego usar evalsin riesgo.
Charles Duffy
1
@Gino, ... y así implementado.
Charles Duffy
3
Seguro, sí, pero muy incompleto. Mi código no es complejo por el gusto de hacerlo, es complejo porque las operaciones reales realizadas por la expansión tilde son complejas.
Charles Duffy
10

Una forma segura de usar eval es "$(printf "~/%q" "$dangerous_path")". Tenga en cuenta que es bash específico.

#!/bin/bash

relativepath=a/b/c
eval homedir="$(printf "~/%q" "$relativepath")"
echo $homedir # prints home path

Ver esta pregunta para más detalles

Además, tenga en cuenta que bajo zsh esto sería tan simple como echo ${~dangerous_path}

eddygeek
fuente
echo ${~root}no me dé salida en zsh (mac os x)
Orwellophile
export test="~root/a b"; echo ${~test}
Gyscos
9

Qué tal esto:

path=`realpath "$1"`

O:

path=`readlink -f "$1"`
Arrendajo
fuente
se ve bien, pero realpath no existe en mi mac. Y tendrías que escribir path = $ (realpath "$ 1")
Hugo
Hola @ Hugo. Puede compilar su propio realpathcomando de ejemplo C. Para, puede generar un archivo ejecutable realpath.exeutilizando fiesta y gcc de esta línea de comandos: gcc -o realpath.exe -x c - <<< $'#include <stdlib.h> \n int main(int c,char**v){char p[9999]; realpath(v[1],p); puts(p);}'. Saludos
olibre
@Quuxplusone no es cierto, al menos en Linux: realpath ~->/home/myhome
blueFast
Lo usé con brew en Mac
nhed
1
@dangonfast Esto no funcionará si configura la tilde entre comillas, el resultado es <workingdir>/~.
Murphy
7

Ampliación (sin juego de palabras) en las respuestas de birryree y halloleo: el enfoque general es usar eval, pero viene con algunas advertencias importantes, a saber, espacios y redirección de salida ( >) en la variable. Lo siguiente parece funcionar para mí:

mypath="$1"

if [ -e "`eval echo ${mypath//>}`" ]; then
    echo "FOUND $mypath"
else
    echo "$mypath NOT FOUND"
fi

Pruébelo con cada uno de los siguientes argumentos:

'~'
'~/existing_file'
'~/existing file with spaces'
'~/nonexistant_file'
'~/nonexistant file with spaces'
'~/string containing > redirection'
'~/string containing > redirection > again and >> again'

Explicación

  • La ${mypath//>}excluye los >caracteres que podrían Clobber un archivo durante el eval.
  • Esto eval echo ...es lo que hace la expansión tilde real
  • Las comillas dobles alrededor del -eargumento son para soportar nombres de archivos con espacios.

Quizás haya una solución más elegante, pero esto es lo que pude encontrar.

Noach Magedman
fuente
3
Puede considerar mirar el comportamiento con nombres que contienen $(rm -rf .).
Charles Duffy
1
Sin >embargo, ¿no se rompe esto en los caminos que realmente contienen caracteres?
Radon Rosborough
2

Creo que esto es lo que estás buscando.

magic() { # returns unexpanded tilde express on invalid user
    local _safe_path; printf -v _safe_path "%q" "$1"
    eval "ln -sf ${_safe_path#\\} /tmp/realpath.$$"
    readlink /tmp/realpath.$$
    rm -f /tmp/realpath.$$
}

Ejemplo de uso:

$ magic ~nobody/would/look/here
/var/empty/would/look/here

$ magic ~invalid/this/will/not/expand
~invalid/this/will/not/expand
Orwellophile
fuente
Me sorprende que printf %q no escape a tildes principales: es casi tentador presentar esto como un error, ya que es una situación en la que falla en su propósito declarado. Sin embargo, mientras tanto, ¡una buena decisión!
Charles Duffy
1
En realidad, este error se corrigió en algún punto entre 3.2.57 y 4.3.18, por lo que este código ya no funciona.
Charles Duffy
1
Buen punto, me ajusté al código para eliminar el inicio \ si existe, por lo que todo solucionó y funcionó :) Estaba probando sin citar los argumentos, por lo que se expandió antes de llamar a la función.
Orwellophile
1

Aquí está mi solución:

#!/bin/bash


expandTilde()
{
    local tilde_re='^(~[A-Za-z0-9_.-]*)(.*)'
    local path="$*"
    local pathSuffix=

    if [[ $path =~ $tilde_re ]]
    then
        # only use eval on the ~username portion !
        path=$(eval echo ${BASH_REMATCH[1]})
        pathSuffix=${BASH_REMATCH[2]}
    fi

    echo "${path}${pathSuffix}"
}



result=$(expandTilde "$1")

echo "Result = $result"
Gino
fuente
Además, confiar en echoeso significa que expandTilde -nno se comportará como se esperaba, y POSIX no define el comportamiento con nombres de archivo que contienen barras invertidas. Ver pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html
Charles Duffy
Buena atrapada. Normalmente uso una máquina de un solo usuario, así que no pensé en manejar ese caso. Pero creo que la función podría mejorarse fácilmente para manejar este otro caso al buscar a través del archivo / etc / passwd para el otro usuario. Lo dejaré como un ejercicio para otra persona :).
Gino
Ya hice ese ejercicio (y manejé el caso OLDPWD y otros) en una respuesta que consideró demasiado compleja. :)
Charles Duffy
en realidad, acabo de encontrar una solución de una línea bastante simple que debería manejar el caso del otro usuario: ruta = $ (eval echo $ orgPath)
Gino
1
FYI: Acabo de actualizar mi solución para que ahora pueda manejar ~ nombre de usuario correctamente. Y, también debería ser bastante seguro. Incluso si pone un '/ tmp / $ (rm -rf / *)' como argumento, debe manejarlo con gracia.
Gino
1

Solo use evalcorrectamente: con validación.

case $1${1%%/*} in
([!~]*|"$1"?*[!-+_.[:alnum:]]*|"") ! :;;
(*/*)  set "${1%%/*}" "${1#*/}"       ;;
(*)    set "$1" 
esac&& eval "printf '%s\n' $1${2+/\"\$2\"}"
mikeserv
fuente
Probablemente esto sea seguro: no he encontrado un caso por el que falle. Dicho esto, si vamos a hablar sobre el uso de eval "correctamente", argumentaría que la respuesta de Orwellophile sigue la mejor práctica: confío en que el caparazón printf %qescapará de las cosas de manera más segura de lo que confío en que el código de validación escrito a mano no tenga errores .
Charles Duffy
@Charles Duffy, eso es una tontería. el shell puede no tener un% q - y printfes un $PATHcomando 'd.
mikeserv
1
¿No está etiquetada esta pregunta bash? Si es así, printfestá integrado y %qse garantiza que estará presente.
Charles Duffy
@Charles Duffy: ¿qué versión?
mikeserv
1
@Charles Duffy, eso es ... bastante temprano. pero sigo pensando que es extraño que confíes en un% q arg más de lo que codificarías ante tus ojos, he usado lo bashsuficiente antes para saber que no confías en él. probar:x=$(printf \\1); [ -n "$x" ] || echo but its not null!
mikeserv
1

Aquí está la función POSIX equivalente a la respuesta Bash de Håkon Hægland

expand_tilde() {
    tilde_less="${1#\~/}"
    [ "$1" != "$tilde_less" ] && tilde_less="$HOME/$tilde_less"
    printf '%s' "$tilde_less"
}

2017-12-10 edit: agregar '%s'por @CharlesDuffy en los comentarios.

go2null
fuente
1
printf '%s\n' "$tilde_less", ¿quizás? De lo contrario, se comportará mal si el nombre de archivo que se expande contiene barras invertidas %su otra sintaxis significativa para printf. Sin embargo, aparte de eso, esta es una gran respuesta: correcta (cuando las extensiones bash / ksh no necesitan ser cubiertas), obviamente segura (sin muck eval) y conciso.
Charles Duffy
1

¿por qué no profundizar directamente en obtener el directorio de inicio del usuario con getent?

$ getent passwd mike | cut -d: -f6
/users/mike
Paul M
fuente
0

Solo para extender la respuesta de birryree para rutas con espacios: No puede usar el evalcomando tal como está porque separa la evaluación por espacios. Una solución es reemplazar espacios temporalmente para el comando eval:

mypath="~/a/b/c/Something With Spaces"
expandedpath=${mypath// /_spc_}    # replace spaces 
eval expandedpath=${expandedpath}  # put spaces back
expandedpath=${expandedpath//_spc_/ }
echo "$expandedpath"    # prints e.g. /Users/fred/a/b/c/Something With Spaces"
ls -lt "$expandedpath"  # outputs dir content

Este ejemplo se basa, por supuesto, en el supuesto de que mypathnunca contiene la secuencia de caracteres "_spc_".

halloleo
fuente
1
No funciona con pestañas, líneas nuevas o cualquier otra cosa en IFS ... y no proporciona seguridad en torno a metacaracteres como las rutas que contienen$(rm -rf .)
Charles Duffy
0

Puede encontrar esto más fácil de hacer en Python.

(1) Desde la línea de comando de Unix:

python -c 'import os; import sys; print os.path.expanduser(sys.argv[1])' ~/fred

Resultados en:

/Users/someone/fred

(2) Dentro de un script bash como único: guarde esto como test.sh:

#!/usr/bin/env bash

thepath=$(python -c 'import os; import sys; print os.path.expanduser(sys.argv[1])' $1)

echo $thepath

bash ./test.shResultados en ejecución en:

/Users/someone/fred

(3) Como una utilidad: guarde esto como en expanduseralgún lugar de su ruta, con permisos de ejecución:

#!/usr/bin/env python

import sys
import os

print os.path.expanduser(sys.argv[1])

Esto podría usarse en la línea de comando:

expanduser ~/fred

O en un guión:

#!/usr/bin/env bash

thepath=$(expanduser $1)

echo $thepath
Chris Johnson
fuente
¿O qué tal pasar solo '~' a Python, devolviendo "/ home / fred"?
Tom Russell
1
Necesita citas de moar. echo $thepathtiene errores debe ser echo "$thepath"para arreglar los casos menos comunes (nombres con tabulaciones o series de espacios que se convierten en espacios individuales; nombres con globos que los tienen expandidos), o también printf '%s\n' "$thepath"para corregir los casos poco comunes (es decir, un archivo llamado -no un archivo con literales de barra invertida en un sistema compatible con XSI). Del mismo modo,thepath=$(expanduser "$1")
Charles Duffy
... para entender lo que quise decir sobre los literales de barra invertida, vea pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html : POSIX permite echocomportarse de una manera completamente definida por la implementación si algún argumento contiene barras invertidas; Las extensiones XSI opcionales a POSIX exigen comportamientos de expansión predeterminados (no necesarios -eo -Enecesarios) para dichos nombres.
Charles Duffy
0

Más simple : reemplace 'magia' con 'eval echo'.

$ eval echo "~"
/whatever/the/f/the/home/directory/is

Problema: Te encontrarás con problemas con otras variables porque eval es malo. Por ejemplo:

$ # home is /Users/Hacker$(s)
$ s="echo SCARY COMMAND"
$ eval echo $(eval echo "~")
/Users/HackerSCARY COMMAND

Tenga en cuenta que el problema de la inyección no ocurre en la primera expansión. Entonces, si simplemente reemplazaras magiccon eval echo, deberías estar bien. Pero si lo hace echo $(eval echo ~), eso sería susceptible a la inyección.

Del mismo modo, si lo hace en eval echo ~lugar de eval echo "~", eso contaría como dos veces expandido y, por lo tanto, la inyección sería posible de inmediato.

Karim Alibhai
fuente
1
Al contrario de lo que dijo, este código no es seguro . Por ejemplo, prueba s='echo; EVIL_COMMAND'. (Se fallará porque EVIL_COMMANDno existe en su computadora, pero si ese comando había sido. rm -r ~Por ejemplo, se habría eliminado el directorio de inicio.)
Konrad Rudolph
0

He hecho esto con la sustitución de parámetros variables después de leer en la ruta usando read -e (entre otros). Por lo tanto, el usuario puede completar la ruta con pestañas, y si el usuario ingresa una ruta ~ se ordena.

read -rep "Enter a path:  " -i "${testpath}" testpath 
testpath="${testpath/#~/${HOME}}" 
ls -al "${testpath}" 

El beneficio adicional es que si no hay tilde no pasa nada a la variable, y si hay una tilde pero no en la primera posición, también se ignora.

(Incluyo el -i para leer, ya que lo uso en un bucle para que el usuario pueda corregir la ruta si hay un problema).

JamesIsIn
fuente