
En bash, ¿hay alguna forma de leer la entrada del usuario pero aún así permitir bash
la expansión variable?
Estoy tratando de solicitarle al usuario que ingrese una ruta en medio de un programa, pero como ~
otras variables no se expanden como parte de la read
configuración incorporada, los usuarios deben ingresar una ruta absoluta.
Ejemplo: cuando un usuario ingresa una ruta en:
read -ep "input> " dirin
[[ -d "$dirin" ]]
devuelve verdadero cuando un usuario ingresa /home/user/bin
pero no ~/bin
o $HOME/bin
.
Respuesta1
Una forma ingenua sería:
eval "dirin=$dirin"
Lo que eso hace es evaluar la expansión de dirin=$dirin
como código shell.
Al dirin
contener ~/foo
, en realidad está evaluando:
dirin=~/foo
Es fácil ver las limitaciones. Con un dirin
contenedor foo bar
, eso se convierte en:
dirin=foo bar
Entonces se está ejecutando bar
en dirin=foo
su entorno (y tendría otros problemas con todos los caracteres especiales del shell).
Aquí, necesitarás decidir qué expansiones están permitidas (tilde, sustitución de comandos, expansión de parámetros, sustitución de procesos, expansión aritmética, expansión de nombres de archivos...) y hacer esas sustituciones a mano o usar eval
peroescapartodos los caracteres excepto aquellos que los permitirían, lo cual sería prácticamente imposible excepto implementando un analizador de sintaxis de shell completo a menos que lo limite a, por ejemplo ~foo
, $VAR
, ${VAR}
.
Aquí, usaría zsh
en lugar de bash
eso un operador dedicado para eso:
vared -cp "input> " dirin
printf "%s\n" "${(e)dirin}"
vared
es eleditor de variables, similar al bash
de read -e
.
(e)
es un indicador de expansión de parámetros que realiza expansiones (parámetro, comando, aritmética pero no tilde) en el contenido del parámetro.
Para solucionar la expansión de tilde, que sólo tiene lugar al principio de la cadena, haríamos:
vared -cp "input> " dirin
if [[ $dirin =~ '^(~[[:alnum:]_.-]*(/|$))(.*)' ]]; then
eval "dirin=$match[1]\${(e)match[3]}"
else
dirin=${(e)dirin}
fi
POSIXly ( bash
también ly), para realizar la expansión de tilde y variable (no de parámetro), podría escribir una función como:
expand_var() {
eval "_ev_var=\${$1}"
_ev_outvar=
_ev_v=${_ev_var%%/*}
case $_ev_v in
(?*[![:alnum:]._-]*) ;;
("~"*)
eval "_ev_outvar=$_ev_v"; _ev_var=${_ev_var#"$_ev_v"}
esac
while :; do
case $_ev_var in
(*'$'*)
_ev_outvar=$_ev_outvar${_ev_var%%"$"*}
_ev_var=${_ev_var#*"$"}
case $_ev_var in
('{'*'}'*)
_ev_v=${_ev_var%%\}*}
_ev_v=${_ev_v#"{"}
case $_ev_v in
"" | [![:alpha:]_]* | *[![:alnum:]_]*) _ev_outvar=$_ev_outvar\$ ;;
(*) eval "_ev_outvar=\$_ev_outvar\${$_ev_v}"; _ev_var=${_ev_var#*\}};;
esac;;
([[:alpha:]_]*)
_ev_v=${_ev_var%%[![:alnum:]_]*}
eval "_ev_outvar=\$_ev_outvar\$$_ev_v"
_ev_var=${_ev_var#"$_ev_v"};;
(*)
_ev_outvar=$_ev_outvar\$
esac;;
(*)
_ev_outvar=$_ev_outvar$_ev_var
break
esac
done
eval "$1=\$_ev_outvar"
}
Ejemplo:
$ var='~mail/$USER'
$ expand_var var;
$ printf '%s\n' "$var"
/var/mail/stephane
Como aproximación, también podríamos anteponer cada carácter pero ~${}-_.
y alnums con una barra invertida antes de pasar a eval
:
eval "dirin=$(
printf '%s\n' "$dirin" |
sed 's/[^[:alnum:]~${}_.-]/\\&/g')"
(aquí simplificado porque $dirin
no puede contener caracteres de nueva línea tal como proviene de read
)
Eso provocaría errores de sintaxis si se ingresara, ${foo#bar}
por ejemplo, pero al menos eso no puede causar tanto daño como lo eval
haría un simple.
Editar: una solución funcional para bash
y otros shells POSIX sería separar la tilde y otras expansiones como en zsh
y usar eval
con un documento aquí paraotras expansionesparte como:
expand_var() {
eval "_ev_var=\${$1}"
_ev_outvar=
_ev_v=${_ev_var%%/*}
case $_ev_v in
(?*[![:alnum:]._-]*) ;;
("~"*)
eval "_ev_outvar=$_ev_v"; _ev_var=${_ev_var#"$_ev_v"}
esac
eval "$1=\$_ev_outvar\$(cat << //unlikely//
$_ev_var
//unlikely//
)"
Eso permitiría expansiones de tilde, parámetros, aritmética y comandos como se muestra zsh
arriba. }