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: $foo
significa “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, $foo
significa “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 $pattern
e $@
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_grep
funçã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 --include
opçã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