$ ls -l /tmp/test/my dir/
total 0
上記のコマンドを実行する次の方法が失敗または成功するのはなぜですか?
$ abc='ls -l "/tmp/test/my dir"'
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
$ bash -c $abc
'my dir'
$ bash -c "$abc"
total 0
$ eval $abc
total 0
$ eval "$abc"
total 0
承認された回答:
これはunix.SEに関する多くの質問で議論されています。ここで思いつく可能性のあるすべての問題を収集しようと思います。最後の参照。
失敗する理由
これらの問題に直面する理由は、単語の分割と、変数から展開された引用符が引用符として機能せず、単なる通常の文字であるという事実です。
質問で提示されたケース:
ここでの割り当てでは、単一の文字列ls -l "/tmp/test/my dir"
が割り当てられます。 abc
へ :
$ abc='ls -l "/tmp/test/my dir"'
以下、$abc
空白で分割され、ls
3つの引数を取得します-l
、"/tmp/test/my
およびdir"
(2番目の前に引用符があり、3番目の後ろに別の引用符があります)。このオプションは機能しますが、パスが正しく処理されません:
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
ここでは、展開が引用されているため、1つの単語として保持されます。シェルは、文字通りls -l "/tmp/test/my dir"
と呼ばれるプログラムを見つけようとします。 、スペースと引用符が含まれています。
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
そしてここで、$abc
は分割され、最初の結果の単語のみが-c
の引数として使用されます。 、したがって、Bashはls
を実行するだけです 現在のディレクトリにあります。他の単語はbashの引数であり、$0
を埋めるために使用されます 、$1
、など。
$ bash -c $abc
'my dir'
bash -c "$abc"
を使用 、およびeval "$abc"
、追加のシェル処理ステップがあります。これにより、引用符が機能しますが、また、すべてのシェル拡張が再度処理されます 、したがって、誤って実行するリスクがあります。引用に細心の注意を払っていない限り、ユーザー提供のデータからのコマンド置換。
それを行うためのより良い方法
コマンドを保存するための2つの優れた方法は、a)代わりに関数を使用する、b)配列変数(または位置パラメーター)を使用することです。
関数の使用:
コマンドを内部に使用して関数を宣言し、コマンドであるかのように関数を実行するだけです。関数内のコマンドの展開は、コマンドが定義されたときではなく、実行されたときにのみ処理され、個々のコマンドを引用符で囲む必要はありません。
# define it
myls() {
ls -l "/tmp/test/my dir"
}
# run it
myls
配列の使用:
配列を使用すると、個々の単語に空白が含まれる複数単語の変数を作成できます。ここでは、個々の単語は個別の配列要素として格納され、"${array[@]}"
展開は、各要素を個別のシェルワードとして展開します:
# define the array
mycmd=(ls -l "/tmp/test/my dir")
# run the command
"${mycmd[@]}"
構文は少しひどいですが、配列を使用すると、コマンドラインを1つずつ作成することもできます。例:
mycmd=(ls) # initial command
if [ "$want_detail" = 1 ]; then
mycmd+=(-l) # optional flag
fi
mycmd+=("$targetdir") # the filename
"${mycmd[@]}"
または、コマンドラインの一部を一定に保ち、オプションやファイル名など、配列の一部だけを埋める配列を使用します。
options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir
transmutate "${options[@]}" "${files[@]}" "$target"
配列の欠点は、それらが標準機能ではないため、プレーンなPOSIXシェル(dash
など)であるということです。 、デフォルトの/bin/sh
Debian / Ubuntuで)それらをサポートしていません(ただし、以下を参照してください)。ただし、Bash、ksh、zshは機能するため、システムに配列をサポートするシェルがある可能性があります。
"[email protected]"
を使用する
名前付き配列をサポートしていないシェルでも、位置パラメータを使用できます(疑似配列"[email protected]"
)コマンドの引数を保持します。
以下は、前のセクションのコードビットと同等の機能を備えたポータブルスクリプトビットである必要があります。配列は"[email protected]"
に置き換えられます 、位置パラメータのリスト。 "[email protected]"
を設定する set
で行われます 、および"[email protected]"
の前後の二重引用符 重要です(これらにより、リストの要素が個別に引用されます)。
まず、引数付きのコマンドを"[email protected]"
に保存するだけです。 そしてそれを実行します:
set -- ls -l "/tmp/test/my dir"
"[email protected]"
コマンドのコマンドラインオプションの一部を条件付きで設定する:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "[email protected]" -l
fi
set -- "[email protected]" "$targetdir"
"[email protected]"
"[email protected]"
のみを使用 オプションとオペランドの場合:
set -- -x -v
set -- "[email protected]" file1 "file name with whitespace"
set -- "[email protected]" /somedir
transmutate "[email protected]"
(もちろん、"[email protected]"
通常、スクリプト自体への引数で埋められているため、"[email protected]"
を転用する前に、引数をどこかに保存する必要があります。 。)
eval
の使用 (ここでは注意してください!)
eval
文字列を受け取り、シェルコマンドラインで入力した場合と同じように、コマンドとして実行します。これには、すべての見積もりと拡張処理が含まれ、便利で危険です。
単純なケースでは、それは私たちが望むことをすることを可能にします:
cmd='ls -l "/tmp/test/my dir"'
eval "$cmd"
eval
を使用 、引用符が処理されるため、ls
最終的には2つの引数-l
だけが表示されます および/tmp/test/my dir
、私たちが望むように。 eval
また、取得した引数を連結するのに十分賢いので、eval $cmd
場合によっては機能することもありますが、たとえば空白のすべての実行は単一のスペースに変更されます。変数を引用符で囲むと、eval
に変更されないようになります。 。
ただし、 eval
のコマンド文字列にユーザー入力を含めるのは危険です。 。たとえば、これは機能しているようです:
read -r filename
cmd="ls -ld '$filename'"
eval "$cmd";
ただし、ユーザーが一重引用符を含む入力を行うと、引用符から抜け出して任意のコマンドを実行できます! 例えば。入力'$(whatever)'.txt
、スクリプトはコマンド置換をうまく実行します。 rm -rf
であった可能性があります (またはさらに悪いことに)代わりに。
問題は、$filename
の値が eval
というコマンドラインに埋め込まれていました 実行されます。 eval
の前に拡張されました 、例えば見たコマンドls -l ''$(whatever)'.txt'
。安全のために入力を前処理する必要があります。
逆の場合は、ファイル名を変数に保持し、eval
を使用します。 コマンドで展開すると、再び安全になります:
read -r filename
cmd='ls -ld "$filename"'
eval "$cmd";
外側の引用符が一重引用符になっているため、内側の拡張は発生しないことに注意してください。したがって、eval
コマンドls -l "$filename"
が表示されます ファイル名自体を安全に展開します。
ただし、これは、コマンドを関数または配列に格納することと大差ありません。関数や配列では、単語が常に分離されているため、このような問題はありません。また、filename
の内容について、引用符やその他の処理は行われません。 。
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
eval
を使用する唯一の理由 さまざまな部分に、変数(パイプライン、リダイレクトなど)を介して取り込むことができないシェル構文要素が含まれているものです。それでも、ユーザーからの入力をeval
に埋め込まないようにしてください。 コマンド!
参考資料
- BashGuideでの単語分割
- BashFAQ / 050または「コマンドを変数に入れようとしていますが、複雑なケースは常に失敗します!」
- シェルスクリプトが空白やその他の特殊文字でチョークするのはなぜですか?コマンドの保存など、引用符や空白に関連する多くの問題について説明しています。