Bash 变量扩展可以直接在用户输入上执行吗?

Squ*_*ven 2 shell-script wildcards variable-substitution read

在 bash 中,有没有办法读入用户输入但仍然允许bash变量扩展?

我试图要求用户在程序中间输入路径,但由于~其他变量没有作为read内置的一部分进行扩展,因此用户必须输入绝对路径。

示例:当用户输入路径时:

read -ep "input> " dirin
  [[ -d "$dirin" ]] 
Run Code Online (Sandbox Code Playgroud)

当用户输入/home/user/bin但不输入~/bin或输入时返回 true $HOME/bin

Sté*_*las 5

一种天真的方法是:

eval "dirin=$dirin"
Run Code Online (Sandbox Code Playgroud)

它所做的是评估dirin=$dirin作为 shell 代码的扩展。

使用dirincontains ~/foo,它实际上是在评估:

dirin=~/foo
Run Code Online (Sandbox Code Playgroud)

很容易看出局限性。使用dirincontains foo bar,则变为:

dirin=foo bar
Run Code Online (Sandbox Code Playgroud)

所以它的运行bardirin=foo在其环境中(而且你还有其他问题,所有的shell特殊字符)。

在这里,您需要决定允许哪些扩展(波浪号、命令替换、参数扩展、进程替换、算术扩展、文件名扩展...)并手动执行这些替换,或者使用eval转义除那些字符之外的每个字符将允许它们除了实现完整的 shell 语法解析器之外几乎不可能实现,除非您将其限制为~foo, $VAR, ${VAR}.

在这里,我会使用一个专门的运算符来zsh代替bash它:

vared -cp "input> " dirin
printf "%s\n" "${(e)dirin}"
Run Code Online (Sandbox Code Playgroud)

vared变量编辑器,类似于bashread -e

(e) 是一个参数扩展标志,它在参数的内容中执行扩展(参数、命令、算术但不是波浪号)。

为了解决只发生在字符串开头的波浪号扩展,我们会这样做:

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

POSIXly(也是如此bash),要执行波浪号和变量(非参数)扩展,您可以编写如下函数:

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"
}
Run Code Online (Sandbox Code Playgroud)

例子:

$ var='~mail/$USER'
$ expand_var var;
$ printf '%s\n' "$var"
/var/mail/stephane
Run Code Online (Sandbox Code Playgroud)

作为近似值,我们也可以~${}-_.在传递给 之前在每个字符 but和 alnums前面加上反斜杠eval

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

(这里简化了,$dirin不能包含换行符,因为它来自read

${foo#bar}例如,如果有人输入,这将触发语法错误,但至少不会像简单的eval那样造成太大伤害。

编辑bash和其他 POSIX shell的工作解决方案是将波浪号和其他扩展分开,例如 inzsheval与此处文档一起使用其他扩展部分,例如:

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//
)"
Run Code Online (Sandbox Code Playgroud)

这将允许像zsh上面那样的波浪号、参数、算术和命令扩展。}