これは、unix.SEの多くの質問で議論されています。ここで思いつくすべての問題を収集しようと思います。最後に参照。
失敗する理由
これらの問題に直面する理由は、単語の分割と、変数から展開された引用符が引用符として機能せず、単なる普通の文字であるという事実です。
質問で提示されたケース:
$ abc='ls -l "/tmp/test/my dir"'
ここでは、$abc
分割され、そしてls
2つの引数を受け取る"/tmp/test/my
と、dir"
(第1および第2の背面の前で引用符で):
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
ここでは、展開が引用されているため、単一の単語として保持されます。シェルはls -l "/tmp/test/my dir"
、スペースおよび引用符が含まれるというプログラムを見つけようとします。
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
そして、ここでは、最初の単語orのみが$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シェル(Debian / Ubuntuのdash
デフォルトなど/bin/sh
)はそれらをサポートしません(ただし、以下を参照)。ただし、bash、ksh、zshには対応しているため、システムに配列をサポートするシェルがある可能性があります。
を使用して "$@"
名前付き配列をサポートしていないシェルでは、位置パラメーター(疑似配列"$@"
)を使用してコマンドの引数を保持できます。
以下は、前のセクションのコードビットと同等の機能を果たす移植可能なスクリプトビットです。配列は"$@"
、位置パラメーターのリストであるに置き換えられます。設定"$@"
はで行われset
、前後の二重引用符"$@"
は重要です(これにより、リストの要素が個別に引用されます)。
まず、引数を指定してコマンドを保存し"$@"
て実行するだけです:
set -- ls -l "/tmp/test/my dir"
"$@"
コマンドのコマンドラインオプションの一部を条件付きで設定する:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "$@" -l
fi
set -- "$@" "$targetdir"
"$@"
"$@"
オプションとオペランドにのみ使用:
set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir
transmutate "$@"
(もちろん、"$@"
通常はスクリプト自体の引数で満たされているため、目的を変更する前にどこかに保存する必要があります"$@"
。)
に注意してくださいeval
!
eval
引用符と拡大処理の追加レベルを導入し、ユーザー入力には注意する必要があります。たとえば、ユーザーが単一引用符を入力しない限り、これは機能します。
read -r filename
cmd="ls -l '$filename'"
eval "$cmd";
しかし、それらが入力を与える場合'$(uname)'.txt
、スクリプトはコマンド置換を喜んで実行します。
配列を使用するバージョンは、単語が常に別々に保持されるため、のコンテンツに対して引用符またはその他の処理がないため、免疫の影響を受けませんfilename
。
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
参照資料