Bash – How to run a “grep | grep” command as a string in a bash function

bashquotingshellshell-script

I'm trying to build a command that pipes the results of one grep command to another grep command in a bash function. Ultimately, I want the command executed to look like this:

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

The function I'm writing stores the first part of the command in a string, then appends the second part:

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}

This throws an error after the output from the first part:

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

Changing the last line from ${grep_cmd} to just "$grep_cmd" displays no output from the first part and throws a different error:

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

Per this SO answer, I tried changing the last line to $(grep_cmd). This throws another error:

bash: grep_cmd: command not found

This SO answer suggests using eval $grep_cmd. This suppresses the errors but also suppresses the output.

This one suggests using eval ${grep_cmd}. This has the same results (suppresses the errors and output). I tried enabling debugging in bash (with set -x), which gives me this:

+ 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:'

It looks like the pipe is being escaped, so the shell interprets the command as two commands. How do I properly escape the pipe character so it interprets it as one command?

Best Answer

As mentioned in the comment, a lot of your difficulty is because you're trying to store a command in a variable, and then run that command later.

You'll have a lot better luck if you just run the command immediately instead of trying to save it.

For example, this should do what you're trying to accomplish:

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