如何在 bash 函數中將“grep | grep”命令作為字串運行?

如何在 bash 函數中將“grep | grep”命令作為字串運行?

我正在嘗試建立一個命令,將一個 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 原始碼語法中的某些內容。例如,如果您需要檔案名,則必須正確引用該檔案名稱。因此,您不能只是將$patternand$@放在那裡,您需要建立一個字串,該字串在解析時會產生包含模式的單字以及包含參數的單字清單。

總結一下:不要將 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

相關內容