web-dev-qa-db-ja.com

Bashのevalコマンドとその典型的な使用法

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

ここでは何が起きているのでしょうか。ドル記号とバックスラッシュはどのように問題に結び付いていますか?

143
kstratis

evalは文字列を引数として取り、あたかもその文字列をコマンドラインで入力したかのように評価します。 (いくつかの引数を渡すと、最初にそれらの間にスペースが入ります。)

${$n}はbashの構文エラーです。中括弧の中では、いくつかの可能なプレフィックスとサフィックスを持つ変数名しか持てませんが、任意のbash構文を持つことはできず、特に変数展開を使用することはできません。ただし、「この変数に名前が含まれている変数の値」と言う方法があります。

echo ${!n}
one

$(…)は、括弧内で指定されたコマンドをサブシェル内(つまり、現在のシェルから変数値などのすべての設定を継承する別のプロセス内)で実行し、その出力を収集します。そのため、echo $($n)$nをシェルコマンドとして実行し、その出力を表示します。 $n1と評価されるので、$($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は、演算子や予約語などを含む長いコマンドを作成する必要があるときにも便利です。

178
Gilles

単にevalを「実行前にもう1回式を評価する」と考えてください。

1回目の評価の後、eval echo \${$n}echo $1になります。注意すべき3つの変更点

  • \$$になりました(バックスラッシュが必要です。それ以外の場合は${$n}を評価しようとします。これは、{$n}という名前の変数が許可されていないことを意味します)。
  • $n1に評価されました
  • evalが消えた

2回目のラウンドでは、直接実行できるのは基本的にecho $1です。

そのため、eval <some command>は最初に<some command>を評価し(ここで評価するとは置換変数を意味し、エスケープした文字を正しいものに置き換えるなど)、次に結果の式をもう一度実行します。

evalは、動的に変数を作成したいとき、またはこのように読まれるように特別に設計されたプログラムからの出力を読み込むときに使用されます。例については、 http://mywiki.wooledge.org/BashFAQ/048 を参照してください。このリンクには、evalが使われる典型的な方法とそれに伴うリスクも含まれています。

34
Hari Menon

私の経験では、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!
22
sootsnoot

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 >
7
Nikhil Gupta

更新:一部の人々は、evalを使用しないでくださいと言います。同意しません。破損した入力がevalに渡されると、リスクが生じると思います。しかし、それがリスクではない一般的な状況は多くあります。したがって、どのような場合でもevalの使用方法を知っておく価値があります。この stackoverflow answer は、evalのリスクとevalの代替案を説明しています。最終的に、evalの使用が安全で効率的かどうかを判断するのはユーザー次第です。


Bash evalステートメントを使用すると、bashスクリプトによって計算または取得されたコード行を実行できます。

おそらく最も簡単な例は、別のbashスクリプトをテキストファイルとして開き、テキストの各行を読み取り、evalを使用してそれらを順番に実行するbashプログラムです。それは、bash sourceステートメントと本質的に同じ動作です。これは、インポートされたスクリプトのコンテンツに対して何らかの変換(フィルタリングや置換など)を実行する必要がない限り、使用するものです。

evalを必要とすることはめったにありませんでしたが、namesが他の変数に割り当てられた文字列に含まれている変数の読み取りまたは書き込みを行うと便利です。たとえば、コードのフットプリントを小さく保ち、冗長性を回避しながら、変数のセットに対してアクションを実行します。

evalは概念的にシンプルです。ただし、bash言語の厳密な構文とbashインタープリターの解析順序は微妙であり、evalが不可解で、使用や理解が困難になる場合があります。必須事項は次のとおりです。

  1. evalに渡される引数は、実行時に計算される文字列式です。 evalは、引数の最終的な解析結果をスクリプト内のactualコード行として実行します。

  2. 構文と解析順序は厳しいです。結果がスクリプトのスコープ内のbashコードの実行可能な行ではない場合、プログラムはevalステートメントでガベージを実行しようとするためクラッシュします。

  3. テスト時には、evalステートメントをechoに置き換えて、表示される内容を確認できます。現在のコンテキストで正当なコードである場合は、evalを介して実行できます。


次の例は、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:

例2に間接性を追加する

$ 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:

namesが文字列に含まれているvars自体に文字列値が含まれているかどうかを検出します。

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であるかどうかをテストし、もしそうであれば、ユーザーに値を提供するように促します。

3
clearlight

私はもともと意図的に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
Luke Pafford

あなたは典型的な用途について尋ねました。

シェルスクリプティングに関する一般的な不満の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番目は連続的な「思考の流れ」に使用されますが、それはこの記事の範囲を超えます。

0
Craig Hicks

私は最近、必要な順番で複数のブレース展開を評価するために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によって呼び出されたサブシェルによって展開されるようになるためです。

これが一段階で起こることを可能にするネストされたブレース展開を含むいくつかの狡猾な計画があるかもしれません、しかし私がそれを見るには古すぎるとばかげているならば。

0
flabdablet

「実行前にもう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
  • 2番目のパラメータ:content"

カウンター直感はどうですか?追加のevalはそれを修正します。

https://stackoverflow.com/a/40646371/744133から適応

0
YoYo

質問では:

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プログラムにいくつのバグが導入されるでしょうか。多くのプログラマは不可解であることを好みます。

0
Carmin