Como posso executar um comando “grep | grep” como uma string em uma função bash?

Como posso executar um comando “grep | grep” como uma string em uma função bash?

Estou tentando criar um comando que canalize os resultados de um comando grep para outro comando grep em uma função bash. Por fim, quero que o comando executado fique assim:

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

A função que estou escrevendo armazena a primeira parte do comando em uma string e anexa a segunda parte:

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}

Isso gera um erro após a saída da primeira parte:

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

Alterar a última linha de ${grep_cmd}para simplesmente "$grep_cmd"não exibe nenhuma saída da primeira parte e gera um erro diferente:

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

Poresta resposta SO, tentei alterar a última linha para $(grep_cmd). Isso gera outro erro:

bash: grep_cmd: command not found

Esta resposta SOsugere usar eval $grep_cmd. Isso suprime os erros, mas também suprime a saída.

Estesugere usar eval ${grep_cmd}. Isso tem os mesmos resultados (suprime os erros e a saída). Tentei ativar a depuração no bash (com set -x), o que me deu o seguinte:

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

Parece que o pipe está sendo escapado, então o shell interpreta o comando como dois comandos. Como faço para escapar corretamente do caractere de barra vertical para que ele o interprete como um comando?

Responder1

Conforme mencionado no comentário, grande parte da sua dificuldade ocorre porque você está tentando armazenar um comando em uma variável e executá-lo posteriormente.

Você terá muito mais sorte se executar o comando imediatamente em vez de tentar salvá-lo.

Por exemplo, isso deve fazer o que você está tentando realizar:

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

Responder2

Uma coisa a ter em mente sobre a programação shell, que muitas vezes não é explicada claramente nos tutoriais, é que existem dois tipos de dados:strings e listas de strings. Uma lista de strings não é a mesma coisa que strings com uma nova linha ou separador de espaço, é algo próprio.

Outra coisa a ter em mente é que a maioria das expansões só são aplicadas quando o shell está analisando um arquivo. A execução de um comando não envolve nenhuma expansão.

Alguma expansão acontece com o valor de uma variável: $foosignifica “pegar o valor da variável foo, dividi-lo em uma lista de strings usando espaços em branco¹ como separadores e interpretar cada elemento da lista como um padrão curinga que é então expandido”. Essa expansão só acontece se a variável for usada em um contexto que exige uma lista. Em um contexto que pede uma string, $foosignifica “pegar o valor da variável foo”. As aspas duplas impõem um contexto de string, daí o conselho:sempre use substituições de variáveis ​​e substituições de comandos entre aspas duplas: "$foo","$(somecommand)"². (As mesmas expansões acontecem tanto para substituições de comandos desprotegidas quanto para variáveis.)

Uma consequência da distinção entre análise e execução é que você não pode simplesmente inserir um comando em uma string e executá-lo. Quando você escreve ${grep_cmd}, apenas a divisão e o globbing acontecem, não a análise, portanto, um caractere como |não tem significado especial.

Se você realmente precisa inserir um comando shell em uma string, você pode eval:

eval "$grep_cmd"

Observe as aspas duplas — o valor da variável contém um comando shell, portanto, precisamos do valor exato da string. No entanto, essa abordagem tende a ser complicada: você realmente precisa ter algo na sintaxe do código-fonte do shell. Se você precisar, digamos, de um nome de arquivo, esse nome de arquivo deverá ser citado corretamente. Portanto, você não pode simplesmente colocar $patterne $@ali, você precisa construir uma string que, quando analisada, resulta em uma única palavra contendo o padrão e em uma lista de palavras contendo os argumentos.

Para resumir:não coloque comandos shell em variáveis. Em vez de,usar funções. Se você precisar de um comando simples com argumentos, e não de algo mais complexo, como um pipeline, poderá usar um array (uma variável de array armazena uma lista de strings).

Aqui está uma abordagem possível. A run_grepfunção não é realmente necessária com o código que você mostrou; Estou incluindo aqui supondo que esta seja uma pequena parte de um script maior e que haja muito mais código intermediário. Se este for realmente o script completo, basta executar grep no ponto em que você sabe onde canalizá-lo. Também corrigi o código que constrói o filtro, o que não parecia certo (por exemplo, .em uma regexp significa “qualquer caractere”, mas acho que você quer um ponto literal).

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

¹ De forma mais geral, usando o valor de IFS.
² Apenas para especialistas: sempre use aspas duplas em substituições de variáveis ​​e comandos, a menos que você entenda por que deixá-las de fora produz o efeito correto.
³ Apenas para especialistas: se você precisar inserir um comando shell em uma variável, tenha muito cuidado com as aspas.


Observe que o que você está fazendo parece excessivamente complexo e não confiável. E se você tiver um arquivo que contenha foo.c: 42? GNU grep tem a --includeopção de procurar apenas determinados arquivos em uma travessia recursiva - basta usá-la.

grep_cmd=(grep -I -r)
for file_type in "${file_types[@]}"; do
  grep_cmd+=(--include "*.$file_type")
done
"${grep_cmd[@]}" "$pattern" "$@"

Responder3

command="grep $regex1 filelist | grep $regex2"
echo $command | bash

informação relacionada