私は、bash 関数で 1 つの 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
パーこのSOの答え、最後の行を に変更してみました$(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 つのコマンドとして解釈するにはどうすればよいでしょうか。
答え1
コメントで述べたように、多くの困難は、コマンドを変数に保存し、後でそのコマンドを実行しようとしていることに起因しています。
コマンドを保存しようとするのではなく、すぐに実行したほうがうまくいくでしょう。
たとえば、次のようにすれば目的を達成できるはずです。
if (( ${#file_types[@]} > 0 )); then
regex="${file_types[*]}"
regex="\.\(${regex// /\|}\):"
grep -I -r "$pattern" "$@" | grep "$regex"
else
grep -I -r "$pattern" "$@"
fi
答え2
シェル プログラミングについて覚えておくべきことの 1 つは、チュートリアルでは明確に説明されていないことが多いのですが、次の 2 種類のデータがあるということです。文字列、および文字列のリスト文字列のリストは、改行やスペース区切りの文字列と同じものではなく、独自のものです。
覚えておくべきもう 1 つの点は、ほとんどの展開はシェルがファイルを解析しているときにのみ適用されるということです。コマンドの実行には展開は含まれません。
変数の値には、何らかの展開が行われます。$foo
は、「変数 の値を取得しfoo
、それを空白¹ を区切り文字として文字列のリストに分割し、リストの各要素をワイルドカード パターンとして解釈して展開する」ことを意味します。この展開は、変数がリストを必要とするコンテキストで使用される場合にのみ発生します。文字列を必要とするコンテキストでは、は$foo
「変数 の値を取得するfoo
」ことを意味します。二重引用符は文字列コンテキストを強制するため、次のアドバイスが適用されます。変数置換とコマンド置換は常に二重引用符で囲んで使用してください: "$foo"
,"$(somecommand)"
². (変数の場合と同じ展開が、保護されていないコマンド置換にも発生します。)
解析と実行を区別した結果、単にコマンドを文字列に詰め込んで実行することはできません。 と記述すると${grep_cmd}
、分割とグロブのみが行われ、解析は行われないため、 のような文字には|
特別な意味はありません。
シェル コマンドを文字列に詰め込む必要がある場合は、eval
次のようにします。
eval "$grep_cmd"
二重引用符に注意してください。変数の値にはシェル コマンドが含まれているため、その正確な文字列値が必要です。ただし、この方法は複雑になりがちです。シェル ソース構文で実際に何かを指定する必要があります。たとえば、ファイル名が必要な場合は、このファイル名を適切に引用符で囲む必要があります。したがって、そこに$pattern
and を単に入力することはできず$@
、解析するとパターンを含む単一の単語と引数を含む単語のリストになる文字列を作成する必要があります。
要約する:シェルコマンドを変数に詰め込まない。 その代わり、関数を使用するパイプラインなどの複雑なものではなく、引数付きの単純なコマンドが必要な場合は、代わりに配列を使用できます (配列変数には文字列のリストが格納されます)。
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" "$@"
答え3
command="grep $regex1 filelist | grep $regex2"
echo $command | bash