2

In bash, is there any way to read in user input but still allow bash variable expansion?

I am trying to request the user enter a path in the middle of a program but since ~ and other variables are not expanded as a part of the read builtin, users have to enter in an absolute path.

Example: When a user enters a path into:

read -ep "input> " dirin
  [[ -d "$dirin" ]] 

returns true when a user enters /home/user/bin but not ~/bin or $HOME/bin.

1

1 Answer 1

5

A naive way would be:

eval "dirin=$dirin"

What that does is evaluate the expansion of dirin=$dirin as shell code.

With dirin containing ~/foo, it's actually evaluating:

dirin=~/foo

It's easy to see the limitations. With a dirin containing foo bar, that becomes:

dirin=foo bar

So it's running bar with dirin=foo in its environment (and you'd have other problems with all the shell special characters).

Here, you'd need to decide what expansions are allowed (tilde, command substitution, parameter expansion, process substitution, arithmetic expansion, filename expansion...) and either do those substitutions by hand, or use eval but escape every character but those that would allow them which would be virtually impossible other than by implementing a full shell syntax parser unless you limit it to for instance ~foo, $VAR, ${VAR}.

Here, I'd use zsh instead of bash that has a dedicated operator for that:

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

vared is the variable editor, similar to bash's read -e.

(e) is a parameter expansion flag that performs expansions (parameter, command, arithmetic but not tilde) in the content of the parameter.

To address tilde expansion, which only takes place at the beginning of the string, we'd do:

vared -cp "input> " dirin
if [[ $dirin =~ '^(~[[:alnum:]_.-]*(/|$))(.*)' ]]; then
  eval "dirin=$match[1]\${(e)match[3]}"
else
  dirin=${(e)dirin}
fi

POSIXly (so bashly as well), to perform tilde and variable (not parameter) expansion, you could write a function like:

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"
}

Example:

$ var='~mail/$USER'
$ expand_var var;
$ printf '%s\n' "$var"
/var/mail/stephane

As an approximation, we could also prepend every character but ~${}-_. and alnums with backslash before passing to eval:

eval "dirin=$(
  printf '%s\n' "$dirin" |
    sed 's/[^[:alnum:]~${}_.-]/\\&/g')"

(here simplified on the ground that $dirin can't contain newline characters as it comes from read)

That would trigger syntax errors if one entered ${foo#bar} for instance but at least that can't do much harm as a simple eval would.

Edit: a working solution for bash and other POSIX shells would be to separate the tilde and other expansions like in zsh and use eval with a here-document for the other expansions part like:

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//
)"

That would allow tilde, parameter, arithmetic and command expansions like in zsh above. }

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .