¿Se puede realizar la expansión de variables de Bash directamente según la entrada del usuario?

¿Se puede realizar la expansión de variables de Bash directamente según la entrada del usuario?

En bash, ¿hay alguna forma de leer la entrada del usuario pero aún así permitir bashla 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 readconfiguració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/binpero no ~/bino $HOME/bin.

Respuesta1

Una forma ingenua sería:

eval "dirin=$dirin"

Lo que eso hace es evaluar la expansión de dirin=$dirincomo código shell.

Al dirincontener ~/foo, en realidad está evaluando:

dirin=~/foo

Es fácil ver las limitaciones. Con un dirincontenedor foo bar, eso se convierte en:

dirin=foo bar

Entonces se está ejecutando baren dirin=foosu 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 evalperoescapartodos 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 zshen lugar de basheso un operador dedicado para eso:

vared -cp "input> " dirin
printf "%s\n" "${(e)dirin}"

varedes eleditor de variables, similar al bashde 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 ( bashtambié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 $dirinno 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 evalharía un simple.

Editar: una solución funcional para bashy otros shells POSIX sería separar la tilde y otras expansiones como en zshy usar evalcon 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 zsharriba. }

información relacionada