変数に保存されたコマンドを実行するにはどうすればよいですか?


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


回答:


54

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


失敗する理由

これらの問題に直面する理由は、単語の分割と、変数から展開された引用符が引用符として機能せず、単なる普通の文字であるという事実です。

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

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

ここでは、$abc分割され、そしてls2つの引数を受け取る"/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[@]}"

参照資料


2
あなたがすることで評価引用の事を回避できますcmd="ls -l $(printf "%q" "$filename")"。きれいではありませんが、ユーザーがを使用して完全に設定されていないeval場合に役立ちます。それはまた、同様のもの、けれどもコマンドを送信するために非常に便利ですssh foohost "ls -l $(printf "%q" "$filename")"、またはこの質問の精神で:ssh foohost "$cmd"
パトリック

直接関係ありませんが、ディレクトリをハードコーディングしましたか?その場合は、エイリアスを見たいかもしれません。何かのように: $ alias abc='ls -l "/tmp/test/my dir"'
ホッピングバニー

4

(自明ではない)コマンドを実行する最も安全な方法は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"

3

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

実行すると:

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

エラーが発生しました。

しかし、私が走るとき

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

エラーはまったくありません

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

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


1
ええ、スペースが埋め込まれたファイル名(またはグロブ文字を含むファイル名)などの必要がない限り機能します。
ilkkachu

0

で動作しない場合は''、次を使用する必要があります``

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

より良い方法を更新する:

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

これにより、コマンドの結果が変数に保存されます。OPは(奇妙なことに)コマンド自体を変数に保存しようとしています。ああ、$( command... )バッククティックの代わりに本当に使い始めるべきです。
ロアイマ

明確化とヒントをありがとうございます!
バロン

0

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 ..
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.