我正在嘗試建立一個命令,將一個 grep 命令的結果透過管道傳輸到 bash 函數中的另一個 grep 命令。最終,我希望執行的命令如下所示:
grep -I -r FooBar /code/internal/dev/ /code/public/dev/ | grep .c:\|.h:
我正在編寫的函數將命令的第一部分儲存在字串中,然後附加第二部分:
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
每這個答案,我嘗試將最後一行更改為$(grep_cmd)
。這會引發另一個錯誤:
bash: grep_cmd: command not found
這個答案建議使用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:'
看起來管道正在被轉義,因此 shell 將該命令解釋為兩個命令。如何正確轉義管道字符,以便將其解釋為一個命令?
答案1
正如評論中提到的,您的許多困難是因為您嘗試將命令儲存在變數中,然後稍後執行該命令。
如果您立即運行該命令而不是嘗試保存它,您的運氣會好得多。
例如,這應該可以完成您想要完成的任務:
if (( ${#file_types[@]} > 0 )); then
regex="${file_types[*]}"
regex="\.\(${regex// /\|}\):"
grep -I -r "$pattern" "$@" | grep "$regex"
else
grep -I -r "$pattern" "$@"
fi
答案2
關於 shell 程式設計需要記住的一件事是,有兩種類型的數據,教程中通常沒有清楚地解釋這一點:字串和字串列表。字串清單與帶有換行符或空格分隔符的字串不同,它有自己的東西。
另一件要記住的事情是,大多數擴充功能僅在 shell 解析檔案時套用。執行命令不涉及任何擴充。
變數的值確實會發生一些擴展:$foo
意味著「獲取變數的值foo
,使用空格作為分隔符號將其拆分為字串列表,並將列表中的每個元素解釋為通配符模式,然後進行擴展」。僅當變數在呼叫列表的上下文中使用時才會發生這種擴展。在需要字串的上下文中,$foo
意味著「取得變數的值foo
」。雙引號強加了字串上下文,因此建議:始終在雙引號中使用變數替換和命令替換:"$foo"
,"$(somecommand)"
². (與變數一樣,未受保護的命令替換也會發生相同的擴展。)
解析和執行之間的區別的一個結果是,您不能簡單地將命令填充到字串中並執行它。當您編寫 時${grep_cmd}
,只會發生拆分和通配符,而不發生解析,因此像這樣的字元|
沒有特殊意義。
如果你絕對需要將 shell 指令填入字串中,你可以eval
這麼做:
eval "$grep_cmd"
請注意雙引號 - 變數的值包含 shell 命令,因此我們需要它的確切字串值。然而,這種方法往往很複雜:您需要真正擁有 shell 原始碼語法中的某些內容。例如,如果您需要檔案名,則必須正確引用該檔案名稱。因此,您不能只是將$pattern
and$@
放在那裡,您需要建立一個字串,該字串在解析時會產生包含模式的單字以及包含參數的單字清單。
總結一下:不要將 shell 命令填入變數中。反而,使用功能。如果您需要帶有參數的簡單命令,而不是更複雜的命令(例如管道),則可以使用陣列(陣列變數儲存字串清單)。
這是一種可能的方法。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
1更一般地,使用 的值IFS
。
²僅適用於專家:始終在變數和命令替換周圍使用雙引號,除非您了解為什麼不使用它們會產生正確的效果。
僅限專家:如果您需要將 shell 命令填入變數中,請務必小心引用。
請注意,您正在做的事情似乎過於複雜且不可靠 - 如果您有一個包含的文件怎麼辦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