1. ホーム
  2. linux

[解決済み] Bashでevalを避けるべき理由と、代わりに何を使うべきか?

2022-08-06 08:23:35

質問

Stack Overflow で何度も Bash の回答を見かけます。 eval を使用した Bash の回答を見かけますが、そのような邪悪な構造を使用した回答は、意図的にシャレで、バッシングされます。なぜ eval が邪悪なのか?

もし eval が安全に使用できない場合、代わりに何を使用すればよいでしょうか?

どのように解決するのですか?

この問題には、見た目以上のものがあります。 まずは明白なことから始めましょう。 eval は、quot;dirty" データを実行する可能性を持っています。 ダーティ データとは、状況に応じて安全に使用できる 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回解析することです。最初のパースでは、一重の引用符が取り除かれます。引用符が削除されると、いくつかの変数の内容が実行されます。

この問題は、変数の展開を eval . 二重引用符はそのままにして、すべてをシングルクオートで囲めばいいのです。 ひとつだけ例外があります。 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.

これでうまくいくはずです。 また $1println は決して汚くはありません。

では、ちょっとだけお待ちください。私が使っているのは、同じ 引用されていない という構文を使っています。 sudo で使っていたものです。 なぜそこではうまくいって、ここではうまくいかないのでしょうか? なぜ、すべてをシングルクォートしなければならなかったのでしょうか? sudo はもう少し現代的です:それは単純化しすぎですが、受け取る各引数を引用符で囲むことを知っています。 eval は単にすべてを連結します。

残念ながら eval のような引数を扱えるような代用品はありません。 sudo がそうであるように eval はシェルの組み込みです。関数のように新しいスタックとスコープを作るのではなく、実行時に周囲のコードの環境とスコープを引き受けるので、これは重要です。

evalの代用品

特定のユースケースには、しばしば eval . ここに便利なリストがあります。 command は、通常送信するものを表します。 eval に送るものを表します。

ノーオープン

bashでは、単純なコロンはno-opです。

:

サブシェルを作成する

( 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!

一回きりの呼び出しなら、シェル全体をリダイレクトする必要はないでしょう。

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 .

連想配列

連想配列はbash4で本質的に実装されています。 一つ注意点があります。 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"`"
# ...