web-dev-qa-db-ja.com

変数に格納されているコマンドを実行するにはどうすればよいですか?

$ 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
53
Tim

これはunix.SEに関する多くの質問で議論されています。私はここで思いつくことができるすべての問題を集めようとします。最後に参照。


失敗する理由

これらの問題に直面する理由は Word splitting であり、変数から展開された引用符は引用符として機能せず、単なる通常の文字です。

質問で提示されたケース:

_$ abc='ls -l "/tmp/test/my dir"'
_

ここでは、_$abc_が分割され、lsは2つの引数_"/tmp/test/my_と_dir"_を取得します(最初の引用符の前と2番目の引用符の後ろに引用符があります):

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

そして、ここでは、最初のWordまたは_$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、Debian/Ubuntuのデフォルトの_/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[@]}"
_

参考文献

72
ilkkachu

(重要でない)コマンドを実行する最も安全な方法は、evalです。次に、コマンドラインで行うようにコマンドを記述できます。コマンドは、入力したときとまったく同じように実行されます。しかし、すべてを引用する必要があります。

単純なケース:

abc='ls -l "/tmp/test/my dir"'
eval "$abc"

それほど単純なケースではありません:

# command: awk '! a[$0]++ { print "foo: " $0; }' inputfile
abc='awk '\''! a[$0]++ { print "foo: " $0; }'\'' inputfile'
eval "$abc"
8
Hauke Laging

2番目の引用符はコマンドを分割します。

私が走ると:

abc="ls -l '/home/wattana/Desktop'"
$abc

エラーが発生しました。

でも走ると

abc="ls -l /home/wattana/Desktop"
$abc

エラーは一切ありません

現時点では(私にとって)これを修正する方法はありませんが、ディレクトリ名にスペースがないことでエラーを回避できます。

この答え evalコマンドはこれを修正するために使用できますが、私にとっては機能しません:(

3
Wattana Gaming

abc変数に格納されている(簡単な/簡単でない)コマンドを実行するもう1つの簡単なトリックは、次のとおりです。

$ history -s $abc

UpArrowまたはCtrl-pを押して、コマンドラインに表示します。他のメソッドとは異なり、この方法では、必要に応じて実行前に編集できます。

このコマンドは、変数の内容を新しいエントリとしてBash履歴に追加し、UpArrowで呼び出すことができます。

0
bloody

これが''で機能しない場合は、``を使用する必要があります。

abc=`ls -l /tmp/test/my\ dir/`

より良い方法で更新:

abc=$(ls -l /tmp/test/my\ dir/)
0
balon

Python3ワンライナーはどうですか?

bash-4.4# pwd
/home
bash-4.4# CUSTOM="ls -altr"
bash-4.4# python3 -c "import subprocess; subprocess.call(\"$CUSTOM\", Shell=True)"
total 8
drwxr-xr-x    2 root     root          4096 Mar  4  2019 .
drwxr-xr-x    1 root     root          4096 Nov 27 16:42 ..
0
Marius Mitrofan