Bashのmanページを読んだ後、これに関して 投稿 。
私はeval
コマンドが正確に何をしているのか、そしてそれがその典型的な用途なのか理解するのにまだ苦労しています。たとえば、次のようにします。
bash$ set -- one two three # sets $1 $2 $3
bash$ echo $1
one
bash$ n=1
bash$ echo ${$n} ## First attempt to echo $1 using brackets fails
bash: ${$n}: bad substitution
bash$ echo $($n) ## Second attempt to echo $1 using parentheses fails
bash: 1: command not found
bash$ eval echo \${$n} ## Third attempt to echo $1 using 'eval' succeeds
one
ここでは何が起きているのでしょうか。ドル記号とバックスラッシュはどのように問題に結び付いていますか?
eval
は文字列を引数として取り、あたかもその文字列をコマンドラインで入力したかのように評価します。 (いくつかの引数を渡すと、最初にそれらの間にスペースが入ります。)
${$n}
はbashの構文エラーです。中括弧の中では、いくつかの可能なプレフィックスとサフィックスを持つ変数名しか持てませんが、任意のbash構文を持つことはできず、特に変数展開を使用することはできません。ただし、「この変数に名前が含まれている変数の値」と言う方法があります。
echo ${!n}
one
$(…)
は、括弧内で指定されたコマンドをサブシェル内(つまり、現在のシェルから変数値などのすべての設定を継承する別のプロセス内)で実行し、その出力を収集します。そのため、echo $($n)
は$n
をシェルコマンドとして実行し、その出力を表示します。 $n
は1
と評価されるので、$($n)
はコマンド1
を実行しようとしますが、これは存在しません。
eval echo \${$n}
はeval
に渡されたパラメータを実行します。展開後のパラメータはecho
と${1}
です。したがって、eval echo \${$n}
はコマンドecho ${1}
を実行します。
ほとんどの場合、変数の置換とコマンドの置換には二重引用符を使用する必要があります(例:$
がある場合):"$foo", "$(foo)"
。 変数とコマンドの置換を必ず二重引用符で囲みます。ただし、それらを省略する必要があることがわかっている場合を除きます。二重引用符がないと、シェルはフィールド分割を実行し(つまり、変数の値またはコマンドからの出力を別々の単語に分割します)、次に各Wordをワイルドカードパターンとして扱います。例えば:
$ ls
file1 file2 otherfile
$ set -- 'f* *'
$ echo "$1"
f* *
$ echo $1
file1 file2 file1 file2 otherfile
$ n=1
$ eval echo \${$n}
file1 file2 file1 file2 otherfile
$eval echo \"\${$n}\"
f* *
$ echo "${!n}"
f* *
eval
はあまり使われません。シェルによっては、最も一般的な使い方は、実行時まで名前がわからない変数の値を取得することです。 bashでは、${!VAR}
構文のおかげでこれは必要ありません。 eval
は、演算子や予約語などを含む長いコマンドを作成する必要があるときにも便利です。
単にevalを「実行前にもう1回式を評価する」と考えてください。
1回目の評価の後、eval echo \${$n}
はecho $1
になります。注意すべき3つの変更点
\$
は$
になりました(バックスラッシュが必要です。それ以外の場合は${$n}
を評価しようとします。これは、{$n}
という名前の変数が許可されていないことを意味します)。$n
は1
に評価されましたeval
が消えた2回目のラウンドでは、直接実行できるのは基本的にecho $1
です。
そのため、eval <some command>
は最初に<some command>
を評価し(ここで評価するとは置換変数を意味し、エスケープした文字を正しいものに置き換えるなど)、次に結果の式をもう一度実行します。
eval
は、動的に変数を作成したいとき、またはこのように読まれるように特別に設計されたプログラムからの出力を読み込むときに使用されます。例については、 http://mywiki.wooledge.org/BashFAQ/048 を参照してください。このリンクには、eval
が使われる典型的な方法とそれに伴うリスクも含まれています。
私の経験では、evalの「典型的な」用途は、環境変数を設定するためのシェルコマンドを生成するコマンドを実行することです。
多分あなたは環境変数のコレクションを使用するシステムを持っていて、あなたは設定されるべきものとそれらの値を決定するスクリプトかプログラムを持っています。あなたがスクリプトやプログラムを実行するときはいつでも、それはフォークされたプロセスで実行されるので、それが環境変数に直接することは終了時に失われます。しかし、そのスクリプトまたはプログラムはエクスポートコマンドをstdoutに送信できます。
Evalがないと、標準出力を一時ファイルにリダイレクトし、一時ファイルを取得してから削除する必要があります。 evalを使えば、次のことができます。
eval "$(script-or-program)"
引用符は重要です。この(人為的な)例を見てください。
# activate.sh
echo 'I got activated!'
# test.py
print("export foo=bar/baz/womp")
print(". activate.sh")
$ eval $(python test.py)
bash: export: `.': not a valid identifier
bash: export: `activate.sh': not a valid identifier
$ eval "$(python test.py)"
I got activated!
Evalステートメントはシェルにevalの引数をコマンドとして受け取り、それらをコマンドラインで実行するように指示します。以下のような状況で役立ちます。
あなたのスクリプトであなたが変数にコマンドを定義していて、後でそのコマンドを使いたいなら、あなたはevalを使うべきです:
/home/user1 > a="ls | more"
/home/user1 > $a
bash: command not found: ls | more
/home/user1 > # Above command didn't work as ls tried to list file with name pipe (|) and more. But these files are not there
/home/user1 > eval $a
file.txt
mailids
remote_cmd.sh
sample.txt
tmp
/home/user1 >
更新:一部の人々は、evalを使用しないでくださいと言います。同意しません。破損した入力がeval
に渡されると、リスクが生じると思います。しかし、それがリスクではない一般的な状況は多くあります。したがって、どのような場合でもevalの使用方法を知っておく価値があります。この stackoverflow answer は、evalのリスクとevalの代替案を説明しています。最終的に、evalの使用が安全で効率的かどうかを判断するのはユーザー次第です。
Bash eval
ステートメントを使用すると、bashスクリプトによって計算または取得されたコード行を実行できます。
おそらく最も簡単な例は、別のbashスクリプトをテキストファイルとして開き、テキストの各行を読み取り、eval
を使用してそれらを順番に実行するbashプログラムです。それは、bash source
ステートメントと本質的に同じ動作です。これは、インポートされたスクリプトのコンテンツに対して何らかの変換(フィルタリングや置換など)を実行する必要がない限り、使用するものです。
eval
を必要とすることはめったにありませんでしたが、namesが他の変数に割り当てられた文字列に含まれている変数の読み取りまたは書き込みを行うと便利です。たとえば、コードのフットプリントを小さく保ち、冗長性を回避しながら、変数のセットに対してアクションを実行します。
eval
は概念的にシンプルです。ただし、bash言語の厳密な構文とbashインタープリターの解析順序は微妙であり、eval
が不可解で、使用や理解が困難になる場合があります。必須事項は次のとおりです。
eval
に渡される引数は、実行時に計算される文字列式です。 eval
は、引数の最終的な解析結果をスクリプト内のactualコード行として実行します。
構文と解析順序は厳しいです。結果がスクリプトのスコープ内のbashコードの実行可能な行ではない場合、プログラムはeval
ステートメントでガベージを実行しようとするためクラッシュします。
テスト時には、eval
ステートメントをecho
に置き換えて、表示される内容を確認できます。現在のコンテキストで正当なコードである場合は、eval
を介して実行できます。
例1:
eval
ステートメントはNOPです$ eval a=b
$ eval echo $a
b
上記の例では、最初のeval
ステートメントには目的がなく、削除できます。 eval
は、コードに動的な側面がないため、つまりbashコードの最終行に既に解析されているため、最初の行では無意味です。したがって、bashスクリプトのコードの通常のステートメントと同じです。 2番目のeval
も無意味です。これは、$a
をそのリテラル文字列に変換する解析ステップがありますが、間接性がないためです(たとえば、actualbash名詞またはbash-heldスクリプト変数)。したがって、eval
プレフィックスのないコード行と同じように動作します。
例2:
$ key="mykey"
$ val="myval"
$ eval $key=$val
$ echo $mykey
myval
echo $key=$val
にした場合、出力は次のようになります。
mykey=myval
Thatは、文字列解析の最終結果であり、evalによって実行されるものです。したがって、最後のechoステートメントの結果です...
例3:
$ keyA="keyB"
$ valA="valB"
$ keyB="that"
$ valB="amazing"
$ eval eval \$$keyA=\$$valA
$ echo $that
amazing
上記は前の例よりも少し複雑で、解析順序とbashの特性に大きく依存しています。 eval
行は、次の順序で内部的に大まかに解析されます(次のステートメントは、実際のコードではなく擬似コードであり、ステートメントが内部ステップに分割されて、最終結果)。
eval eval \$$keyA=\$$valA # substitution of $keyA and $valA by interpreter
eval eval \$keyB=\$valB # convert '$' + name-strings to real vars by eval
eval $keyB=$valB # substitution of $keyB and $valB by interpreter
eval that=amazing # execute string literal 'that=amazing' by eval
想定された解析順序でevalが何をしているのかを十分に説明できない場合、3番目の例では、解析の詳細を説明して、何が起こっているかを明確にすることができます。
例4:
a="User-provided"
b="Another user-provided optional value"
c=""
myvarname_a="a"
myvarname_b="b"
myvarname_c="c"
for varname in "myvarname_a" "myvarname_b" "myvarname_c"; do
eval varval=\$$varname
if [ -z "$varval" ]; then
read -p "$varname? " $varname
fi
done
最初の反復では:
varname="myvarname_a"
Bashはeval
への引数を解析し、eval
は実行時に文字通りこれを参照します。
eval varval=\$$myvarname_a
次のpseudocodeは、howbash上記のrealコードの行を解釈し、eval
によって実行される最終値に到達します。 (正確なbashコードではなく、説明的な次の行):
1. eval varval="\$" + "$varname" # This substitution resolved in eval statement
2. .................. "$myvarname_a" # $myvarname_a previously resolved by for-loop
3. .................. "a" # ... to this value
4. eval "varval=$a" # This requires one more parsing step
5. eval varval="User-provided" # Final result of parsing (eval executes this)
すべての解析が完了すると、結果は実行され、その効果は明らかであり、eval
自体に特に神秘的なものは何もないことを示し、複雑さはparsingにありますその議論。
varval="User-provided"
上記の例の残りのコードは、$ varvalに割り当てられた値がnullであるかどうかをテストし、もしそうであれば、ユーザーに値を提供するように促します。
私はもともと意図的にevalの使い方を学んだことはありません。なぜなら、ほとんどの人はペストのように、evalの使用を避けることをお勧めするからです。しかし、私は最近、ユースケースを発見しました。
対話的に実行してテストするcronジョブがある場合は、catを使用してファイルの内容を表示し、それを実行するためにcronジョブをコピーして貼り付けることができます。残念ながら、これは私の本では罪であるマウスに触れることを含みます。
内容が/etc/cron.d/repeatmeにあるcronジョブがあるとしましょう。
*/10 * * * * root program arg1 arg2
これをすべてのがらくたを前にしてスクリプトとして実行することはできませんが、cutを使用してすべてのがらくたを取り除き、それをサブシェルでラップして、evalを使用してストリングを実行することができます
eval $( cut -d ' ' -f 6- /etc/cron.d/repeatme)
Cutコマンドは、スペースで区切られたファイルの6番目のフィールドのみを印刷します。その後、Evalはそのコマンドを実行します。
ここでは例としてcronジョブを使用しましたが、概念は標準出力からテキストをフォーマットしてからそのテキストを評価することです。
この場合のevalの使用は安全ではありません。これは、事前に評価する内容が正確にわかっているためです。
あなたは典型的な用途について尋ねました。
シェルスクリプティングに関する一般的な不満の1つは、関数から値を取り戻すために参照によって渡すことができないということです。
しかし実際には、 "eval"を介して、あなたは参照渡しすることができます。呼び出し先は、呼び出し元によって評価される変数割り当てのリストを返すことができます。呼び出し元が結果変数の名前を指定できるため、参照渡しとなります。以下の例を参照してください。エラーの結果はerrnoやerrstrのような標準名に戻すことができます。
これはbashで参照渡しする例です。
#!/bin/bash
isint()
{
re='^[-]?[0-9]+$'
[[ $1 =~ $re ]]
}
#args 1: name of result variable, 2: first addend, 3: second addend
iadd()
{
if isint ${2} && isint ${3} ; then
echo "$1=$((${2}+${3}));errno=0"
return 0
else
echo "errstr=\"Error: non-integer argument to iadd $*\" ; errno=329"
return 1
fi
}
var=1
echo "[1] var=$var"
eval $(iadd var A B)
if [[ $errno -ne 0 ]]; then
echo "errstr=$errstr"
echo "errno=$errno"
fi
echo "[2] var=$var (unchanged after error)"
eval $(iadd var $var 1)
if [[ $errno -ne 0 ]]; then
echo "errstr=$errstr"
echo "errno=$errno"
fi
echo "[3] var=$var (successfully changed)"
出力は次のようになります。
[1] var=1
errstr=Error: non-integer argument to iadd var A B
errno=329
[2] var=1 (unchanged after error)
[3] var=2 (successfully changed)
そのテキスト出力には、ほとんど無制限のバンド幅があります。そして、複数の出力行が使用される場合、より多くの可能性があります。例えば、最初の行は変数の割り当てに使用され、2番目は連続的な「思考の流れ」に使用されますが、それはこの記事の範囲を超えます。
私は最近、必要な順番で複数のブレース展開を評価するためにeval
を使わなければなりませんでした。 Bashは左から右へ複数のブレース展開をするので、
xargs -I_ cat _/{11..15}/{8..5}.jpg
に展開
xargs -I_ cat _/11/8.jpg _/11/7.jpg _/11/6.jpg _/11/5.jpg _/12/8.jpg _/12/7.jpg _/12/6.jpg _/12/5.jpg _/13/8.jpg _/13/7.jpg _/13/6.jpg _/13/5.jpg _/14/8.jpg _/14/7.jpg _/14/6.jpg _/14/5.jpg _/15/8.jpg _/15/7.jpg _/15/6.jpg _/15/5.jpg
しかし、最初に2回目のブレース拡張を行う必要がありました。
xargs -I_ cat _/11/8.jpg _/12/8.jpg _/13/8.jpg _/14/8.jpg _/15/8.jpg _/11/7.jpg _/12/7.jpg _/13/7.jpg _/14/7.jpg _/15/7.jpg _/11/6.jpg _/12/6.jpg _/13/6.jpg _/14/6.jpg _/15/6.jpg _/11/5.jpg _/12/5.jpg _/13/5.jpg _/14/5.jpg _/15/5.jpg
私がそれをするために思い付くことができる最高のものは、
xargs -I_ cat $(eval echo _/'{11..15}'/{8..5}.jpg)
これは、eval
コマンドラインの解析中に一重引用符が最初のブレースセットを展開から保護し、eval
によって呼び出されたサブシェルによって展開されるようになるためです。
これが一段階で起こることを可能にするネストされたブレース展開を含むいくつかの狡猾な計画があるかもしれません、しかし私がそれを見るには古すぎるとばかげているならば。
「実行前にもう1回あなたの表現を評価する」という回答が好きで、別の例で明確にしたいと思います。
var="\"par1 par2\""
echo $var # prints nicely "par1 par2"
function cntpars() {
echo " > Count: $#"
echo " > Pars : $*"
echo " > par1 : $1"
echo " > par2 : $2"
if [[ $# = 1 && $1 = "par1 par2" ]]; then
echo " > PASS"
else
echo " > FAIL"
return 1
fi
}
# Option 1: Will Pass
echo "eval \"cntpars \$var\""
eval "cntpars $var"
# Option 2: Will Fail, with curious results
echo "cntpars \$var"
cntpars $var
オプション2の奇妙な結果は、次のように2つのパラメーターを渡したことです。
"value
content"
カウンター直感はどうですか?追加のeval
はそれを修正します。
質問では:
who | grep $(tty | sed s:/dev/::)
ファイルaとttyが存在しないことを示すエラーを出力します。これは、ttyがgrepの実行前に解釈されているのではなく、bashがgrepへのパラメータとしてttyを渡し、ファイル名として解釈していることを意味すると理解しました。
ネストされたリダイレクトの状況もあります。これは子プロセスを指定する一致括弧で処理する必要がありますが、bashは基本的にWordの区切り文字で、プログラムに送信するパラメータを作成するため、括弧が最初に一致しないので解釈されます。見た。
私はgrepに特化して、ファイルをパイプではなくパラメータとして指定しました。また、I/Oパイピングがネストされないように、コマンドからの出力をファイルとして渡す基本コマンドも単純化しました。
grep $(tty | sed s:/dev/::) <(who)
うまくいきます。
who | grep $(echo pts/3)
実際には望ましくありませんが、ネストされたパイプを排除し、またうまく機能します。
結論として、bashはネストピッピングが好きではないようです。 bashは再帰的に書かれたニューウェーブプログラムではないことを理解することは重要です。代わりに、bashは機能が追加された古い1,2,3プログラムです。下位互換性を保証するために、最初の解釈方法は変更されていません。 bashが最初の括弧と一致するように書き直された場合、いくつのbashプログラムにいくつのバグが導入されるでしょうか。多くのプログラマは不可解であることを好みます。