¿Cómo puedo ejecutar un comando "grep | grep" como una cadena en una función bash?

¿Cómo puedo ejecutar un comando "grep | grep" como una cadena en una función bash?

Estoy intentando crear un comando que canalice los resultados de un comando grep a otro comando grep en una función bash. En última instancia, quiero que el comando ejecutado se vea así:

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

La función que estoy escribiendo almacena la primera parte del comando en una cadena y luego agrega la 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}

Esto arroja un error después del resultado de la primera parte:

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

Cambiar la última línea de ${grep_cmd}a simplemente "$grep_cmd"no muestra ningún resultado de la primera parte y arroja un error diferente:

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

Poresta respuesta SO, Intenté cambiar la última línea a $(grep_cmd). Esto arroja otro error:

bash: grep_cmd: command not found

Esta respuesta SOsugiere usar eval $grep_cmd. Esto suprime los errores pero también suprime la salida.

Éstesugiere usar eval ${grep_cmd}. Esto tiene los mismos resultados (suprime los errores y la salida). Intenté habilitar la depuración en bash (con set -x), lo que me da esto:

+ 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 se está escapando de la tubería, por lo que el shell interpreta el comando como dos comandos. ¿Cómo puedo escapar correctamente del carácter de barra vertical para que lo interprete como un comando?

Respuesta1

Como se menciona en el comentario, gran parte de su dificultad se debe a que intenta almacenar un comando en una variable y luego ejecutar ese comando más tarde.

Tendrá mucha más suerte si ejecuta el comando inmediatamente en lugar de intentar guardarlo.

Por ejemplo, esto debería hacer lo que estás intentando lograr:

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

Respuesta2

Una cosa a tener en cuenta acerca de la programación de shell, que a menudo no se explica claramente en los tutoriales, es que existen dos tipos de datos:cadenas y listas de cadenas. Una lista de cadenas no es lo mismo que cadenas con una nueva línea o un separador de espacio, es algo independiente.

Otra cosa a tener en cuenta es que la mayoría de las expansiones sólo se aplican cuando el shell analiza un archivo. La ejecución de un comando no implica ninguna expansión.

Se produce cierta expansión del valor de una variable: $foosignifica "tomar el valor de la variable foo, dividirlo en una lista de cadenas utilizando espacios en blanco¹ como separadores e interpretar cada elemento de la lista como un patrón comodín que luego se expande". Esta expansión solo ocurre si la variable se usa en un contexto que requiere una lista. En un contexto que pide una cadena, $foosignifica “tomar el valor de la variable foo”. Las comillas dobles imponen un contexto de cadena, de ahí el consejo:Utilice siempre sustituciones de variables y sustituciones de comandos entre comillas dobles: "$foo","$(somecommand)"². (Las mismas expansiones ocurren con las sustituciones de comandos desprotegidos que con las variables).

Una consecuencia de la distinción entre análisis y ejecución es que no se puede simplemente introducir un comando en una cadena y ejecutarlo. Cuando escribes ${grep_cmd}, solo se divide y se agrupa, no se analiza, por lo que un carácter como |no tiene un significado especial.

Si es absolutamente necesario incluir un comando de shell en una cadena, puede hacerlo eval:

eval "$grep_cmd"

Tenga en cuenta las comillas dobles: el valor de la variable contiene un comando de shell, por lo que necesitamos su valor de cadena exacto. Sin embargo, este enfoque tiende a ser complicado: realmente necesita tener algo en la sintaxis fuente del shell. Si necesita, digamos, un nombre de archivo, entonces este nombre de archivo debe estar entrecomillado correctamente. Por lo tanto, no puede simplemente poner $patterny $@allí, necesita crear una cadena que, cuando se analiza, dé como resultado una sola palabra que contenga el patrón y una lista de palabras que contengan los argumentos.

Para resumir:no introduzca comandos de shell en variables. En cambio,utilizar funciones. Si necesita un comando simple con argumentos, y no algo más complejo como una canalización, puede usar una matriz en su lugar (una variable de matriz almacena una lista de cadenas).

He aquí un posible enfoque. La run_grepfunción no es realmente necesaria con el código que has mostrado; Lo incluyo aquí asumiendo que es una pequeña parte de un script más grande y que hay mucho más código intermedio. Si este es realmente el script completo, simplemente ejecute grep en el punto donde sepa en qué canalizarlo. También arreglé el código que crea el filtro, que no parecía correcto (por ejemplo, .en una expresión regular significa "cualquier carácter", pero creo que quieres un punto 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 más general, utilizando el valor de IFS.
² Solo para expertos: utilice siempre comillas dobles alrededor de las sustituciones de variables y comandos, a menos que comprenda por qué omitirlas produce el efecto correcto.
³ Sólo para expertos: si necesita incluir un comando de shell en una variable, tenga mucho cuidado con las comillas.


Tenga en cuenta que lo que está haciendo parece demasiado complejo y poco confiable. ¿Qué pasa si tiene un archivo que contiene foo.c: 42? GNU grep tiene una --includeopción para buscar solo ciertos archivos en un recorrido recursivo; simplemente úsela.

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

Respuesta3

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

información relacionada