変数に保存されたコマンドをどのように実行できますか?

変数に保存されたコマンドをどのように実行できますか?
$ 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

答え1

これはunix.SEに関する多くの質問で議論されており、ここで質問できるすべての質問を収集しようとします。以下は

  • さまざまな試みが失敗した理由と方法の説明、
  • 機能(固定コマンドの場合)を使用してこれを正しく実行する方法、または
  • シェル配列(Bash / ksh / zsh)または$@疑似配列(POSIX sh)を使用してください。どちらもいくつかのオプションを変更する必要がある場合は、コマンドラインフラグメントを作成することもできます。
  • evalこのタスクの使用に関する注意事項。

最後にいくつかの参考資料があります。

ここでは、コマンドパラメータまたはコマンド名を変数に保存することは重要ではありません。コマンドが実行されるまで同様に処理されます。ここで、シェルは最初の単語だけが実行されるコマンドの名前として使用されます。


なぜ失敗したのか

これらの問題に直面した理由は次のとおりです。噴射非常に単純で複雑なケースには適していません。変数で拡張された引用符は、引用符として機能せず、通常の文字としてのみ機能します。

(引用符に関する部分は他のすべてのプログラミング言語と似ています。たとえば、Cの関数はchar *s = "foo()"; printf("%s\n", s)呼び出されませんが、文字列は印刷されます。m4、Cプリプロセッサ、またはMake(To)などのマクロプロセッサでは異なります。マクロプロセッサではなくプログラミング言語である程度)。foo()foo()

Unixシリーズシステムでは、シェルはコマンドラインで引用符と変数拡張を処理し、それを単一の文字列から基本システム呼び出しによって開始コマンドに渡された文字列のリストに変換します。プログラム自体は、シェルが処理する引用符を見ることはできません。たとえば、コマンドが与えられると、ls -l "foo bar"シェルはそれを3つの文字列ls-lおよびfoo bar(引用符を削除)に変換してに渡しますls(すべてのプログラムで使用されるわけではありませんが、コマンド名も渡されます。)

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

ここで、割り当ては単一の文字列をls -l "/tmp/test/my dir"次に割り当てますabc

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

以下では、$abcスペースで除算してls3つのパラメータsumを-l取得します。ここの引用符は単なるデータなので、2番目のパラメータの前に1つ、3番目のパラメータの後に1つあります。このオプションは機能しますが、引用符はファイル名の一部として処理されるため、パスは誤って処理されます。"/tmp/test/mydir"ls

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

ここでは$abc分割され、最初の結果の単語だけが引数として使用されるため、-cBashは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")

# expand the array, run the command
"${mycmd[@]}"

コマンドは、コマンドの実行時に作成されたのと同じように括弧内に作成されます。シェルで実行される処理は、どちらの場合も同じです。ただし、1つのケースでは、プログラムの実行に使用せず、結果の文字列リストを保存するだけです。

しかし、後で配列を拡張するための構文は少し悪く、その周りの引用符が重要です。

配列を使用すると、コマンドラインを1つずつ作成できます。たとえば、

mycmd=(ls)               # initial command
if [ "$want_detail" = 1 ]; then
    mycmd+=(-l)          # optional flag, append to array
fi
mycmd+=("$targetdir")    # the filename

"${mycmd[@]}"

または、コマンドラインの一部を変更せずにそのままにして、配列を使用してオプションやファイル名などの一部を埋めます。

options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir

somecommand "${options[@]}" "${files[@]}" "$target"

somecommandこれは実際のコマンドではなく、一般的なプレースホルダ名です。)

配列の欠点は標準機能ではないため、一般的な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

somecommand "$@"

もちろん、"$@"通常はスクリプト自体のパラメータで埋められているので、再利用する前にどこかに保存する必要があります"$@"

${var:+word}単一のパラメータを条件付きで渡すには、代替値の拡張といくつかの注意深い引用を使用することもできます。ここでは、-f ファイル名が空でない場合にのみ、合計ファイル名が含まれます。

file="foo bar"
somecommand ${file:+-f "$file"}

使用eval(ここに注意してください!)

eval文字列を受け入れ、シェルのコマンドラインに入力されているかのようにコマンドとして実行します。これには、有用で危険なすべての参照および拡張処理が含まれます。

簡単な場合は、私たちが望むことをすることができます:

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

を使用すると、eval引用符が処理され、必要に応じて2つの引数とのみls表示されます。また、取得したすべての引数を連結できるほどスマートなので、場合によっては機能することがありますが、たとえば、すべてのスペースは単一のスペースに変わります。変数がに変更されないように、ここで変数を引用するのが最善です。-l/tmp/test/my direvaleval $cmdeval

しかし、コマンド文字列にユーザー入力を含めることは危険です。eval。たとえば、次のように動作するようです。

read -r filename
cmd="ls -ld '$filename'"
eval "$cmd";

ただし、ユーザーが一重引用符を含む入力を提供したら、引用符を分離して任意のコマンドを実行できます。たとえば、inputを使用すると、'$(whatever)'.txtスクリプトはコマンド置換をうまく実行します。真実はそうかもしれrm -rfないし悪いかもしれません。

$filename問題は、実行されるコマンドラインに値が含まれていることですevalevalコマンドのように以前に拡張されました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コマンドに含めます。


引用する

答え2

(重要)コマンドを実行する最も安全な方法はです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コマンドを使用してこの問題を解決できますが、私には機能しません。

答え4

@ilkkachuが言及したが、バッシュの噴射、IFSシェル変数の重要性を明示的に指摘してほしいと思いました。たとえば、Bashでは次のようになります。

OLD_IFS="$IFS"
IFS=$'\x1a'
my_command=$'ls\x1a-l\x1a-a\x1a/tmp/test/my dir'
$my_command
IFS="$OLD_IFS"

my_command に保存されたコマンドは期待どおりに実行されます。 \ x1aはCTRL-Z、aはASCIIです。良いセパレータの選択。これは、実行されるコマンドにCTRL + Z文字が含まれていない限り機能します(空白よりも可能性が高い)。 $'...'を引用するANSI-CスタイルはまだPOSIXではないので、bashについても言及しました。

この手法は、コマンドをハードコーディングしたり、コマンドを設定したりするときに利用されます。IFS を以前の値にリセットすることを忘れないでください。

関連情報