¿Cómo dividir una salida en dos archivos con grep?

14

Tengo un script mycommand.shque no puedo ejecutar dos veces. Quiero dividir la salida en dos archivos diferentes: un archivo que contiene las líneas que coinciden con una expresión regular y un archivo que contiene las líneas que no coinciden con una expresión regular. Lo que deseo tener es básicamente algo como esto:

./mycommand.sh | grep -E 'some|very*|cool[regex].here;)' --match file1.txt --not-match file2.txt

Sé que puedo redirigir la salida a un archivo y luego a dos greps diferentes con y sin la opción -v y redirigir su salida a dos archivos diferentes. Pero me preguntaba si sería posible hacerlo con un grep.

Entonces, ¿es posible lograr lo que quiero en una sola línea?

yukashima huksay
fuente

Respuestas:

20

Hay muchas formas de lograr esto.

Usando awk

Lo siguiente envía cualquier línea que coincida coolregexcon el archivo1. Todas las demás líneas van al archivo2:

./mycommand.sh | awk '/[coolregex]/{print>"file1";next} 1' >file2

Cómo funciona:

  1. /[coolregex]/{print>"file1";next}

    Cualquier línea que coincida con la expresión regular coolregexse imprime en file1. Luego, omitimos todos los comandos restantes y saltamos para comenzar de nuevo en la nextlínea.

  2. 1

    Todas las demás líneas se envían a stdout. 1es la abreviatura críptica de awk para imprimir la línea.

La división en múltiples flujos también es posible:

./mycommand.sh | awk '/regex1/{print>"file1"} /regex2/{print>"file2"} /regex3/{print>"file3"}'

Usando la sustitución de procesos

Esto no es tan elegante como la solución awk pero, para completar, también podemos usar múltiples greps combinados con la sustitución del proceso:

./mycommand.sh | tee >(grep 'coolregex' >File1) | grep -v 'coolregex' >File2

También podemos dividirnos en múltiples flujos:

./mycommand.sh | tee >(grep 'coolregex' >File1) >(grep 'otherregex' >File3) >(grep 'anotherregex' >File4) | grep -v 'coolregex' >File2
John1024
fuente
Oh guay! ¿También es posible dividirlo en varios archivos sin simplemente hacer otro awk en lugar de file2? Quiero decir de una manera que las expresiones regulares pueden superponerse, por ejemplo.
yukashima huksay
1
@aran Sí, awk es muy flexible. Precisamente cómo hacerlo dependería de cómo se superponen las expresiones regulares.
John1024
Me encantaría ver una solución incluso si no admite expresiones regulares superpuestas. por superposición quiero decir que la intersección del subconjunto no está nerviosamente vacía.
yukashima huksay
1
@aran He agregado a los ejemplos de respuestas con múltiples transmisiones para ambos métodos.
John1024
8
sed -n -e '/pattern_1/w file_1' -e '/pattern_2/w file_2' input.txt

w filename - escribe el espacio del patrón actual en el nombre del archivo.

Si desea que todas las líneas coincidentes vayan file_1y todas las líneas no coincidentes file_2, puede hacer lo siguiente:

sed -n -e '/pattern/w file_1' -e '/pattern/!w file_2' input.txt

o

sed -n '/pattern/!{p;d}; w file_1' input.txt > file_2

Explicación

  1. /pattern/!{p;d};
    • /pattern/!- negación - si una línea no contiene pattern.
    • p - imprime el espacio del patrón actual.
    • d- Eliminar el espacio del patrón. Comience el próximo ciclo.
    • por lo tanto, si una línea no contiene un patrón, imprime esta línea en la salida estándar y selecciona la siguiente línea. La salida estándar se redirige a la file_2en nuestro caso. La siguiente parte del sedscript ( w file_1) no se alcanza mientras la línea no coincide con el patrón.
  2. w file_1- si una línea contiene un patrón, la /pattern/!{p;d};parte se omite (porque se ejecuta solo cuando el patrón no coincide) y, por lo tanto, esta línea va al file_1.
MiniMax
fuente
¿Puedes agregar alguna explicación más a la última solución?
yukashima huksay
@aran Explicación agregada. Además, el comando se corrigió file_1y file_2se cambió al orden correcto.
MiniMax
0

Me gustó la sedsolución ya que no se basa en bashismos y trata los archivos de salida en el mismo pie. AFAIK, no existe una herramienta Unix independiente que haga lo que desea, por lo que necesitaría programarla usted mismo. Si abandonáramos el enfoque de la navaja suiza, podríamos usar cualquiera de los lenguajes de script (Perl, Python, NodeJS).

Así es como se haría en NodeJS

  #!/usr/bin/env node

  const fs = require('fs');
  const {stderr, stdout, argv} = process;

  const pattern = new RegExp(argv[2] || '');
  const yes = argv[3] ? fs.createWriteStream(argv[3]) : stdout;
  const no = argv[4] ? fs.createWriteStream(argv[4]) : stderr;

  const out = [no, yes];

  const partition = predicate => e => {
    const didMatch = Number(!!predicate(e));
    out[didMatch].write(e + '\n');
  };

  fs.readFileSync(process.stdin.fd)
    .toString()
    .split('\n')
    .forEach(partition(line => line.match(pattern)));

Ejemplo de uso

# Using designated files
./mycommand.sh | partition.js pattern file1.txt file2.txt

# Using standard output streams
./partition.js pattern > file1.txt 2> file2.txt
Elias
fuente
0

Si no le importa el uso de Python y una sintaxis de expresión regular diferente:

#!/usr/bin/env python3
import sys, re

regex, os1, os2 = sys.argv[1:]
regex = re.compile(regex)
with open(os1, 'w') as os1, open(os2, 'w') as os2:
    os = (os1, os2)
    for line in sys.stdin:
        end = len(line) - line.endswith('\n')
        os[regex.search(line, 0, end) is not None].write(line)

Uso

./match-split.py PATTERN FILE-MATCH FILE-NOMATCH

Ejemplo

printf '%s\n' foo bar baz | python3 match-split.py '^b' b.txt not-b.txt
David Foerster
fuente