web-dev-qa-db-ja.com

「grep | grep」コマンドをbash関数の文字列として実行するにはどうすればよいですか?

Bash関数で、あるgrepコマンドの結果を別のgrepコマンドにパイプするコマンドを作成しようとしています。最終的に、実行するコマンドは次のようになります。

_grep -I -r FooBar /code/internal/dev/ /code/public/dev/ | grep .c:\|.h:
_

私が書いている関数は、コマンドの最初の部分を文字列に格納し、次に2番目の部分を追加します。

_grep_cmd="grep -I -r $pattern $@"

if (( ${#file_types[@]} > 0 )); then
    file_types="${file_types[@]}"
    file_types=.${file_types// /':\|.'}:

    grep_cmd="$grep_cmd | grep $file_types"
fi

echo "$grep_cmd"
${grep_cmd}
_

これは、最初の部分からの出力後にエラーをスローします。

_grep: |: No such file or directory
grep: grep: No such file or directory
grep: .c:\|.h:: No such file or directory
_

最後の行を_${grep_cmd}_から_"$grep_cmd"_だけに変更すると、最初の部分からの出力は表示されず、別のエラーがスローされます。

_bash: grep -I -r FooBar /code/internal/dev/ /code/public/dev/ | grep .c:\|.h:: No such file or directory
_

this SO answer に従って、最後の行を$(grep_cmd)に変更しようとしました。これにより、別のエラーがスローされます:

_bash: grep_cmd: command not found
_

このSO回答 は_eval $grep_cmd_の使用を提案します。これにより、エラーが抑制されますが、出力も抑制されます。

これ は_eval ${grep_cmd}_の使用を提案します。これは同じ結果になります(エラーと出力を抑制します)。私はbashでデバッグを有効にしてみました(_set -x_を使用)、これは私にこれを与えます:

_+ eval grep -I -r FooBar /code/internal/dev/ /code/public/dev/ '|' grep '.c:\|.h:'
++ grep -I -r FooBar /code/internal/dev/ /code/public/dev/
++ grep '.c:|.h:'
_

パイプがエスケープされているように見えるため、シェルはコマンドを2つのコマンドとして解釈します。パイプ文字を適切にエスケープして1つのコマンドとして解釈するにはどうすればよいですか?

3
Big McLargeHuge

コメントで述べたように、コマンドを変数に格納し、後でそのコマンドを実行しようとしているため、多くの困難が生じます。

コマンドを保存するのではなく、ただちに実行すると、幸運が増します。

たとえば、これはあなたが達成しようとしていることを行うはずです:

if (( ${#file_types[@]} > 0 )); then
    regex="${file_types[*]}"
    regex="\.\(${regex// /\|}\):"
    grep -I -r "$pattern" "$@" | grep "$regex"
else
    grep -I -r "$pattern" "$@"
fi
4
Patrick

チュートリアルでは明確に説明されていないことが多いシェルプログラミングについて覚えておくべきことの1つは、2種類のデータがあることです文字列、および文字列のリスト。文字列のリストは、改行またはスペース区切りのある文字列と同じではなく、独自のものです。

覚えておくべきもう1つのことは、ほとんどの展開はシェルがファイルを解析しているときにのみ適用されるということです。コマンドの実行には、展開は含まれません。

変数の値にいくつかの拡張が行われます:_$foo_は、「変数fooの値を取り、空白を区切り文字として使用して文字列のリストに分割し、リストの各要素を解釈することを意味しますワイルドカードパターンとして展開されます。」この展開は、変数がリストを必要とするコンテキストで使用されている場合にのみ発生します。文字列を必要とするコンテキストでは、_$foo_は「変数fooの値を取る」という意味です。二重引用符は文字列コンテキストを課すため、アドバイス:変数の置換とコマンドの置換を二重引用符で常に使用します:_"$foo"_、"$(somecommand)"²。 (保護されていないコマンド置換でも、変数と同じ拡張が行われます。)

解析と実行の違いの結果として、コマンドを文字列に単純に詰め込んで実行することはできません。 _${grep_cmd}_を記述すると、解析ではなく分割とグロビングのみが行われるため、_|_のような文字には特別な意味はありません。

シェルコマンドを文字列に組み込む必要がある場合は、evalできます。

_eval "$grep_cmd"
_

二重引用符に注意してください。変数の値にはシェルコマンドが含まれているため、正確な文字列値が必要です。ただし、このアプローチは複雑になる傾向があります。シェルのソース構文に何かを実際に含める必要があります。たとえばファイル名が必要な場合は、このファイル名を適切に引用符で囲む必要があります。そのため、_$pattern_と_$@_を単にそこに置くことはできません。解析すると、パターンを含む単一のWordと、引数を含む単語のリストになる文字列を構築する必要があります。

要約すると:シェルコマンドを変数に詰め込まないでください。代わりに、関数を使用です。引数を含む単純なコマンドが必要で、パイプラインなどの複雑なものは必要ない場合は、代わりに配列を使用できます(配列変数は文字列のリストを格納します)。

これが可能なアプローチの1つです。 _run_grep_関数は、ここで示したコードでは実際には必要ありません。これは大きなスクリプトのごく一部であり、中間コードがはるかに多いという前提でここに含めます。これが実際にスクリプト全体である場合、何にパイプするかわかっているところでgrepを実行してください。フィルターを作成するコードも修正しましたが、正しくありませんでした(たとえば、正規表現の_._は「任意の文字」を意味しますが、リテラルドットが必要だと思います)。

_grep_cmd=(grep -I -r "$pattern" "$@")

if (( ${#file_types[@]} > 0 )); then
    regexp='\.\('
    for file_type in "${file_types[@]}"; do
      regexp="$regexp$file_type\\|"
    done
    regexp="${regexp%?}):"
    run_grep () {
      "${grep_cmd[@]}" | grep "$file_types"
    }
else
  run_grep () {
    "${grep_cmd[@]}"
  }
fi

run_grep
_

¹ より一般的には、IFSの値を使用します。
² 専門家のみ:変数とコマンドの置換は常に二重引用符で囲んでください。ただし、それらを省略して正しい効果が得られる理由を理解していない場合に限ります。
³ 専門家のみ:シェルコマンドを変数に詰め込む必要がある場合は、引用符に非常に注意してください。


あなたがやっていることは過度に複雑で信頼できないように見えることに注意してください— _foo.c: 42_を含むファイルがある場合はどうでしょうか? GNU grepには_--include_オプションがあり、再帰トラバーサルで特定のファイルのみを検索します—使用するだけです。

_grep_cmd=(grep -I -r)
for file_type in "${file_types[@]}"; do
  grep_cmd+=(--include "*.$file_type")
done
"${grep_cmd[@]}" "$pattern" "$@"
_
command="grep $regex1 filelist | grep $regex2"
echo $command | bash
0
Joshua