
Есть ли в bash способ считывать пользовательский ввод, но при этом разрешать bash
расширение переменных?
Я пытаюсь попросить пользователя ввести путь в середине программы, но поскольку ~
и другие переменные не раскрываются как часть read
встроенной функции, пользователям приходится вводить абсолютный путь.
Пример: Когда пользователь вводит путь в:
read -ep "input> " dirin
[[ -d "$dirin" ]]
возвращает true, когда пользователь вводит , /home/user/bin
но не ~/bin
или $HOME/bin
.
решение1
Наивным способом было бы:
eval "dirin=$dirin"
Это позволяет оценить расширение dirin=$dirin
как шелл-код.
При dirin
наличии ~/foo
, он фактически оценивает:
dirin=~/foo
Легко увидеть ограничения. С dirin
содержащим foo bar
это становится:
dirin=foo bar
Итак, он работает bar
в dirin=foo
своей среде (и у вас возникнут другие проблемы со всеми специальными символами оболочки).
Здесь вам нужно решить, какие расширения разрешены (тильда, подстановка команд, подстановка параметров, подстановка процессов, арифметическое расширение, расширение имен файлов...) и либо выполнить эти подстановки вручную, либо использовать eval
butпобегкаждый символ, кроме тех, которые их допускают, что было бы практически невозможно без реализации полного синтаксического анализатора оболочки, если только вы не ограничите его, например ~foo
, $VAR
, ${VAR}
, .
Здесь я бы использовал zsh
вместо bash
этого специальный оператор:
vared -cp "input> " dirin
printf "%s\n" "${(e)dirin}"
vared
эторедактор переменных, аналогично bash
's read -e
.
(e)
— это флаг расширения параметра, который выполняет расширения (параметр, команда, арифметика, но не тильда) в содержимом параметра.
Чтобы решить проблему с расширением тильды, которое происходит только в начале строки, мы бы сделали следующее:
vared -cp "input> " dirin
if [[ $dirin =~ '^(~[[:alnum:]_.-]*(/|$))(.*)' ]]; then
eval "dirin=$match[1]\${(e)match[3]}"
else
dirin=${(e)dirin}
fi
В POSIX (а bash
также в POSIX) для выполнения тильды и расширения переменной (не параметра) можно написать функцию вроде:
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"
}
Пример:
$ var='~mail/$USER'
$ expand_var var;
$ printf '%s\n' "$var"
/var/mail/stephane
В качестве приближения мы могли бы также добавлять к каждому символу, кроме ~${}-_.
цифр и чисел, обратную косую черту перед переходом к eval
:
eval "dirin=$(
printf '%s\n' "$dirin" |
sed 's/[^[:alnum:]~${}_.-]/\\&/g')"
(здесь упрощено на том основании, что $dirin
не может содержать символы новой строки, как это происходит read
)
${foo#bar}
Это , например, может вызвать синтаксические ошибки, если ввести их, но, по крайней мере, это не может нанести большого вреда, как простой eval
код.
Редактировать: рабочим решением для bash
и других оболочек POSIX было бы отделить тильду и другие расширения, такие как в zsh
и использовать eval
с here-документом длядругие расширениячасть вроде:
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//
)"
Это позволило бы использовать тильду, параметры, арифметические и командные расширения, как указано zsh
выше. }