この問題には、目に見える以上のものがあります。明らかなことから始めましょう:eval
「ダーティ」データを実行する可能性があります。ダーティ データとは、状況に応じて安全に使用できる XYZ として書き換えられていないデータです。この場合、安全に評価できるようにフォーマットされていない任意の文字列です。
データのサニタイズは一見簡単に見えます。オプションのリストを投げかけていると仮定すると、bash は個々の要素をサニタイズする優れた方法と、配列全体を単一の文字列としてサニタイズする別の方法をすでに提供しています。
function println
{
# Send each element as a separate argument, starting with the second element.
# Arguments to printf:
# 1 -> "$1\n"
# 2 -> "$2"
# 3 -> "$3"
# 4 -> "$4"
# etc.
printf "$1\n" "${@:2}"
}
function error
{
# Send the first element as one argument, and the rest of the elements as a combined argument.
# Arguments to println:
# 1 -> '\e[31mError (%d): %s\e[m'
# 2 -> "$1"
# 3 -> "${*:2}"
println '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
exit "$1"
}
# This...
error 1234 Something went wrong.
# And this...
error 1234 'Something went wrong.'
# Result in the same output (as long as $IFS has not been modified).
ここで、出力を引数として println にリダイレクトするオプションを追加したいとします。もちろん、呼び出しのたびに println の出力をリダイレクトすることもできますが、例を示すために、それは行いません。 eval
を使用する必要があります 、変数を使用して出力をリダイレクトできないためです。
function println
{
eval printf "$2\n" "${@:3}" $1
}
function error
{
println '>&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
exit $1
}
error 1234 Something went wrong.
良さそうですよね?問題は、eval が (どのシェルでも) コマンドラインを 2 回解析することです。解析の最初のパスで、引用の 1 つの層が削除されます。引用符を削除すると、一部の可変コンテンツが実行されます。
eval
内で変数展開を行うことで、これを修正できます。 .すべてを一重引用符で囲み、二重引用符はそのままにしておきます。 1 つの例外:eval
より前にリダイレクトを拡張する必要があります。 であるため、引用符の外に置く必要があります:
function println
{
eval 'printf "$2\n" "${@:3}"' $1
}
function error
{
println '&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
exit $1
}
error 1234 Something went wrong.
これはうまくいくはずです。 $1
までは安全です println
で
ちょっと待ってください。同じ unquoted を使用しています sudo
で元々使用していた構文 いつも!ここではなく、そこで機能するのはなぜですか?すべてを単一引用符で囲む必要があるのはなぜですか? sudo
はもう少し現代的です。単純化しすぎていますが、受け取った各引数を引用符で囲むことを知っています。 eval
すべてを連結するだけです。
残念ながら、eval
の簡単な代替品はありません。 sudo
のような引数を扱う eval
のように シェル組み込みです。関数のように新しいスタックとスコープを作成するのではなく、実行時に周囲のコードの環境とスコープを取るため、これは重要です。
代替案の評価
特定のユース ケースでは、多くの場合、eval
の実行可能な代替手段があります。 .ここに便利なリストがあります。 command
通常 eval
に送信するものを表します;好きなものに置き換えてください。
ノーオペレーション
単純なコロンは bash ではノーオペレーションです:
:
サブシェルを作成する
( command ) # Standard notation
コマンドの出力を実行
外部コマンドに頼らないでください。常に戻り値を制御する必要があります。これらを独自の行に入れます:
$(command) # Preferred
`command` # Old: should be avoided, and often considered deprecated
# Nesting:
$(command1 "$(command2)")
`command "\`command\`"` # Careful: \ only escapes $ and \ with old style, and
# special case \` results in nesting.
変数に基づくリダイレクト
呼び出しコードで、&3
をマップします。 (または &2
より大きいもの) ) をターゲットに:
exec 3<&0 # Redirect from stdin
exec 3>&1 # Redirect to stdout
exec 3>&2 # Redirect to stderr
exec 3> /dev/null # Don't save output anywhere
exec 3> file.txt # Redirect to file
exec 3> "$var" # Redirect to file stored in $var--only works for files!
exec 3<&0 4>&1 # Input and output!
1 回限りの呼び出しであれば、シェル全体をリダイレクトする必要はありません:
func arg1 arg2 3>&2
呼び出される関数内で、&3
にリダイレクトします。 :
command <&3 # Redirect stdin
command >&3 # Redirect stdout
command 2>&3 # Redirect stderr
command &>&3 # Redirect stdout and stderr
command 2>&1 >&3 # idem, but for older bash versions
command >&3 2>&1 # Redirect stdout to &3, and stderr to stdout: order matters
command <&3 >&4 # Input and output!
可変間接
シナリオ:
VAR='1 2 3'
REF=VAR
悪い:
eval "echo \"\$$REF\""
なんで? REF に二重引用符が含まれていると、コードが壊れてエクスプロイトに対して開かれます。 REF をサニタイズすることは可能ですが、これがあると時間の無駄です:
echo "${!REF}"
そうです、bash にはバージョン 2 の時点で変数の間接化が組み込まれています。これは eval
より少しトリッキーになります もっと複雑なことをしたい場合:
# Add to scenario:
VAR_2='4 5 6'
# We could use:
local ref="${REF}_2"
echo "${!ref}"
# Versus the bash < 2 method, which might be simpler to those accustomed to eval:
eval "echo \"\$${REF}_2\""
とにかく、新しい方法はより直感的ですが、eval
に慣れている経験豊富なプログラムされた人にはそう見えないかもしれません。 .
連想配列
連想配列は、bash 4 で本質的に実装されています。1 つの注意点:declare
を使用して作成する必要があります。 .
declare -A VAR # Local
declare -gA VAR # Global
# Use spaces between parentheses and contents; I've heard reports of subtle bugs
# on some versions when they are omitted having to do with spaces in keys.
declare -A VAR=( ['']='a' [0]='1' ['duck']='quack' )
VAR+=( ['alpha']='beta' [2]=3 ) # Combine arrays
VAR['cow']='moo' # Set a single element
unset VAR['cow'] # Unset a single element
unset VAR # Unset an entire array
unset VAR[@] # Unset an entire array
unset VAR[*] # Unset each element with a key corresponding to a file in the
# current directory; if * doesn't expand, unset the entire array
local KEYS=( "${!VAR[@]}" ) # Get all of the keys in VAR
古いバージョンの bash では、変数の間接化を使用できます:
VAR=( ) # This will store our keys.
# Store a value with a simple key.
# You will need to declare it in a global scope to make it global prior to bash 4.
# In bash 4, use the -g option.
declare "VAR_$key"="$value"
VAR+="$key"
# Or, if your version is lacking +=
VAR=( "$VAR[@]" "$key" )
# Recover a simple value.
local var_key="VAR_$key" # The name of the variable that holds the value
local var_value="${!var_key}" # The actual value--requires bash 2
# For < bash 2, eval is required for this method. Safe as long as $key is not dirty.
local var_value="`eval echo -n \"\$$var_value\""
# If you don't need to enumerate the indices quickly, and you're on bash 2+, this
# can be cut down to one line per operation:
declare "VAR_$key"="$value" # Store
echo "`var_key="VAR_$key" echo -n "${!var_key}"`" # Retrieve
# If you're using more complex values, you'll need to hash your keys:
function mkkey
{
local key="`mkpasswd -5R0 "$1" 00000000`"
echo -n "${key##*$}"
}
local var_key="VAR_`mkkey "$key"`"
# ...
eval
の作り方 安全
eval
できる 安全に使用できますが、すべての引数を最初に引用する必要があります。方法は次のとおりです:
あなたのためにそれを行うこの関数:
function token_quote {
local quoted=()
for token; do
quoted+=( "$(printf '%q' "$token")" )
done
printf '%s\n' "${quoted[*]}"
}
使用例:
信頼できないユーザー入力がある場合:
% input="Trying to hack you; date"
評価するコマンドを作成します:
% cmd=(echo "User gave:" "$input")
一見で評価する 正しい引用:
% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018
ハッキングされたことに注意してください。 date
文字通り印刷されるのではなく、実行されました。
代わりに token_quote()
:
% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%
eval
悪ではありません - 誤解されているだけです :)
この回答を2つの部分に分割します 、これは eval
に誘惑されがちなケースの大部分をカバーしていると思います :
奇妙に構築されたコマンドの実行
単純なインデックス付き配列を何度も何度も 配列を定義する際に展開を保護するために二重引用符に関する良い習慣を身につけていれば、これで十分です。
# One nasty argument which must remain a single argument and not be split:
f='foo bar'
# The command in an indexed array (use `declare -a` if you really want to be explicit):
cmd=(
touch
"$f"
# Yet another nasty argument, this time hardcoded:
'plop yo'
)
# Let Bash expand the array and run it as a command:
"${cmd[@]}"
これにより foo bar
が作成されます と plop yo
(4 ファイルではなく 2 ファイル)
引数 (または多数のオプション) だけを配列に入れると、より読みやすいスクリプトが生成される場合があることに注意してください (少なくとも、何を実行しているのか一目でわかります)。
touch "${args[@]}"
touch "${opts[@]}" file1 file2
おまけとして、配列を使用すると次のことが簡単にできます。
<オール>cmd=(
# Important because blah blah:
-v
)
- 配列定義内に空白行を残して、読みやすくするために引数をグループ化します。
- デバッグ用に特定の引数をコメントアウトします。
- コマンドに引数を追加します。場合によっては特定の条件に応じて、またはループ内で動的に追加します:
cmd=(myprog)
for f in foo bar
do
cmd+=(-i "$f")
done
if [[ $1 = yo ]]
then
cmd+=(plop)
fi
to_be_added=(one two 't h r e e')
cmd+=("${to_be_added[@]}")
- 構成定義の空白を含む引数を許可しながら、構成ファイルでコマンドを定義します:
readonly ENCODER=(ffmpeg -blah --blah 'yo plop')
# Deprecated:
#readonly ENCODER=(avconv -bloh --bloh 'ya plap')
# […]
"${ENCODER[@]}" foo bar
- printf の
%q
を使用して、実行されているものを完全に表す、堅牢に実行可能なコマンドをログに記録します :
function please_log_that {
printf 'Running:'
# From `help printf`:
# “The format is re-used as necessary to consume all of the arguments.”
# From `man printf` for %q:
# “printed in a format that can be reused as shell input,
# escaping non-printable characters with the proposed POSIX $'' syntax.”
printf ' %q' "[email protected]"
echo
}
arg='foo bar'
cmd=(prog "$arg" 'plop yo' $'arg\nnewline\tand tab')
please_log_that "${cmd[@]}"
# ⇒ “Running: prog foo\ bar plop\ yo $'arg\nnewline\tand tab'”
# You can literally copy and paste that ↑ to a terminal and get the same execution.
eval
よりも優れた構文強調表示をお楽しみください 引用符をネストしたり$
を使用したりする必要がないため、文字列 -「すぐには評価されませんが、いずれ評価されるでしょう」
私にとって、このアプローチの主な利点 (そして逆に eval
の欠点) ) は、引用、展開などに関して通常と同じロジックに従うことができるということです。 どのコマンドがどの時点でどのペアの引用符を解釈するかを理解しようとしているときに、「事前に」引用符を引用符で囲んで頭を悩ませる必要はありません。そしてもちろん、上記の多くのことを eval
で達成するのは難しいか、まったく不可能です。 .
これで eval
に頼る必要がなくなりました 過去 6 年ほどの間に、可読性と堅牢性 (特に空白を含む引数に関して) は間違いなく向上しました。 IFS
かどうかを知る必要さえありません 和らげられました!もちろん、まだ eval
のエッジ ケースがあります。 実際には必要になるかもしれません (たとえば、ユーザーがインタラクティブなプロンプトなどを介して本格的なスクリプトを提供できるようにする必要がある場合など) が、日常的に遭遇するものではないことを願っています.
動的に名前が付けられた変数をいじる
declare -n
(またはその関数内 local -n
対応する)、および ${!foo}
、ほとんどの場合、トリックを実行します。
$ help declare | grep -- -n
-n make NAME a reference to the variable named by its value
まあ、例がなければ非常に明確ではありません:
declare -A global_associative_array=(
[foo]=bar
[plop]=yo
)
# $1 Name of global array to fiddle with.
fiddle_with_array() {
# Check this if you want to make sure you’ll avoid
# circular references, but it’s only if you really
# want this to be robust.
# You can also give an ugly name like “__ref” to your
# local variable as a cheaper way to make collisions less likely.
if [[ $1 != ref ]]
then
local -n ref=$1
fi
printf 'foo → %s\nplop → %s\n' "${ref[foo]}" "${ref[plop]}"
}
# Call the function with the array NAME as argument,
# not trying to get its content right away here or anything.
fiddle_with_array global_associative_array
# This will print:
# foo → bar
# plop → yo
(オブジェクト指向言語のように、関数にオブジェクトを渡しているような気分になれるので、このトリック ↑ が大好きです。その可能性は信じられないほどです。)
${!…}
について (別の変数によって名前が付けられた変数の値を取得します):
foo=bar
plop=yo
for var_name in foo plop
do
printf '%s = %q\n' "$var_name" "${!var_name}"
done
# This will print:
# foo = bar
# plop = yo