bash - regulares - sed linux reemplazar
Compruebe si existen todas las cadenas múltiples o expresiones regulares en un archivo (20)
git grep
Aquí está la sintaxis usando
git grep
con múltiples patrones:
git grep --all-match --no-index -l -e string1 -e string2 -e string3 file
También puede combinar patrones con expresiones
booleanas
como
--and
,
--or
y
--not
.
Consulte
man git-grep
para obtener ayuda.
--all-match
Al dar múltiples expresiones de patrón, esta bandera se especifica para limitar la coincidencia a los archivos que tienen líneas para que coincidan con todos ellos .
--no-index
Busca archivos en el directorio actual que Git no administra.
-l
/--files-with-matches
/--name-only
Muestra solo los nombres de los archivos.
-e
El siguiente parámetro es el patrón. El valor predeterminado es usar regexp básico.
Otros parámetros a considerar:
--threads
Número de hilos de trabajo grep para usar.
-q
/--quiet
/--silent
No--silent
líneas coincidentes; salir con el estado 0 cuando hay una coincidencia.
Para cambiar el tipo de patrón, también puede usar
-G
/
--basic-regexp
(predeterminado),
-F
/
--fixed-strings
,
-E
/
--extended-regexp
,
-P
/
--perl-regexp
,
-f file
, y otros.
Quiero verificar si todas mis cadenas existen en un archivo de texto. Podrían existir en la misma línea o en diferentes líneas. Y las coincidencias parciales deberían estar bien. Me gusta esto:
...
string1
...
string2
...
string3
...
string1 string2
...
string1 string2 string3
...
string3 string1 string2
...
string2 string3
... and so on
En el ejemplo anterior, podríamos tener expresiones regulares en lugar de cadenas.
Por ejemplo, el siguiente code verifica si alguna de mis cadenas existe en el archivo:
if grep -EFq "string1|string2|string3" file; then
# there is at least one match
fi
¿Cómo verificar si todos existen? Como solo estamos interesados en la presencia de todas las coincidencias, deberíamos dejar de leer el archivo tan pronto como todas las cadenas coincidan.
¿Es posible hacerlo sin tener que invocar
grep
varias veces (que no se escalará cuando el archivo de entrada es grande o si tenemos una gran cantidad de cadenas para que coincida) o usar una herramienta como
awk
o
python
?
Además, ¿hay una solución para las cadenas que se pueden extender fácilmente para expresiones regulares?
El siguiente
python
script debería hacer el truco.
De alguna manera llama al equivalente de
grep
(
re.search
) varias veces para cada línea, es decir, busca cada patrón para cada línea, pero como no está bifurcando un proceso cada vez, debería ser mucho más eficiente.
Además, elimina los patrones que ya se han encontrado y se detiene cuando se han encontrado todos.
#!/usr/bin/env python
import re
# the file to search
filename = ''/path/to/your/file.txt''
# list of patterns -- can be read from a file or command line
# depending on the count
patterns = [r''py.*$'', r''/s+open/s+'', r''^import/s+'']
patterns = map(re.compile, patterns)
with open(filename) as f:
for line in f:
# search for pattern matches
results = map(lambda x: x.search(line), patterns)
# remove the patterns that did match
results = zip(results, patterns)
results = filter(lambda x: x[0] == None, results)
patterns = map(lambda x: x[1], results)
# stop if no more patterns are left
if len(patterns) == 0:
break
# print the patterns which were not found
for p in patterns:
print p.pattern
Puede agregar una verificación por separado para cadenas simples (
string in line
) si se trata de cadenas simples (no regex), será un poco más eficiente.
¿Eso resuelve tu problema?
En python, el uso del módulo fileinput permite que los archivos se especifiquen en la línea de comando o en el texto leído línea por línea desde stdin. Podría codificar las cadenas en una lista de Python.
# Strings to match, must be valid regular expression patterns
# or be escaped when compiled into regex below.
strings = (
r''string1'',
r''string2'',
r''string3'',
)
o leer las cadenas de otro archivo
import re
from fileinput import input, filename, nextfile, isfirstline
for line in input():
if isfirstline():
regexs = map(re.compile, strings) # new file, reload all strings
# keep only strings that have not been seen in this file
regexs = [rx for rx in regexs if not rx.match(line)]
if not regexs: # found all strings
print filename()
nextfile()
Muchas de estas respuestas están bien hasta donde llegan.
Pero si el rendimiento es un problema, ciertamente posible si la entrada es grande y tiene muchos miles de patrones, entonces obtendrá una
gran
aceleración utilizando una herramienta como
lex
o
flex
que genera un verdadero autómata finito determinista como un reconocedor en lugar de llamar un intérprete de expresiones regulares una vez por patrón.
El autómata finito ejecutará algunas instrucciones de máquina por carácter de entrada, independientemente del número de patrones .
Una solución flexible sin lujos:
%{
void match(int);
%}
%option noyywrap
%%
"abc" match(0);
"ABC" match(1);
[0-9]+ match(2);
/* Continue adding regex and exact string patterns... */
[ /t/n] /* Do nothing with whitespace. */
. /* Do nothing with unknown characters. */
%%
// Total number of patterns.
#define N_PATTERNS 3
int n_matches = 0;
int counts[10000];
void match(int n) {
if (counts[n]++ == 0 && ++n_matches == N_PATTERNS) {
printf("All matched!/n");
exit(0);
}
}
int main(void) {
yyin = stdin;
yylex();
printf("Only matched %d patterns./n", n_matches);
return 1;
}
Una desventaja es que tendrías que construir esto para cada conjunto de patrones. Eso no es tan malo:
flex matcher.y
gcc -O lex.yy.c -o matcher
Ahora ejecútelo:
./matcher < input.txt
No vi un contador simple entre las respuestas, así que aquí hay una solución orientada al contador
awk
que se detiene tan pronto como se satisfacen todas las coincidencias:
/string1/ { a = 1 }
/string2/ { b = 1 }
/string3/ { c = 1 }
{
if (c + a + b == 3) {
print "Found!";
exit;
}
}
Un script genérico
para expandir el uso a través de argumentos de shell:
#! /bin/sh
awk -v vars="$*" -v argc=$# ''
BEGIN { split(vars, args); }
{
for (arg in args) {
if (!temp[arg] && $0 ~ args[arg]) {
inc++;
temp[arg] = 1;
}
}
if (inc == argc) {
print "Found!";
exit;
}
}
END { exit 1; }
'' filename
Uso (en el que puede pasar expresiones regulares):
./script "str1?" "(wo)?men" str3
o para aplicar una cadena de patrones:
./script "str1? (wo)?men str3"
Para una velocidad simple, sin limitaciones de herramientas externas y sin expresiones regulares, esta versión (cruda) C hace un trabajo decente.
(Posiblemente solo Linux, aunque debería funcionar en todos los sistemas tipo Unix con
mmap
)
#include <sys/mman.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
/* https://.com/a/8584708/1837991 */
inline char *sstrstr(char *haystack, char *needle, size_t length)
{
size_t needle_length = strlen(needle);
size_t i;
for (i = 0; i < length; i++) {
if (i + needle_length > length) {
return NULL;
}
if (strncmp(&haystack[i], needle, needle_length) == 0) {
return &haystack[i];
}
}
return NULL;
}
int matcher(char * filename, char ** strings, unsigned int str_count)
{
int fd;
struct stat sb;
char *addr;
unsigned int i = 0; /* Used to keep us from running of the end of strings into SIGSEGV */
fd = open(filename, O_RDONLY);
if (fd == -1) {
fprintf(stderr,"Error ''%s'' with open on ''%s''/n",strerror(errno),filename);
return 2;
}
if (fstat(fd, &sb) == -1) { /* To obtain file size */
fprintf(stderr,"Error ''%s'' with fstat on ''%s''/n",strerror(errno),filename);
close(fd);
return 2;
}
if (sb.st_size <= 0) { /* zero byte file */
close(fd);
return 1; /* 0 byte files don''t match anything */
}
/* mmap the file. */
addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) {
fprintf(stderr,"Error ''%s'' with mmap on ''%s''/n",strerror(errno),filename);
close(fd);
return 2;
}
while (i++ < str_count) {
char * found = sstrstr(addr,strings[0],sb.st_size);
if (found == NULL) { /* If we haven''t found this string, we can''t find all of them */
munmap(addr, sb.st_size);
close(fd);
return 1; /* so give the user an error */
}
strings++;
}
munmap(addr, sb.st_size);
close(fd);
return 0; /* if we get here, we found everything */
}
int main(int argc, char *argv[])
{
char *filename;
char **strings;
unsigned int str_count;
if (argc < 3) { /* Lets count parameters at least... */
fprintf(stderr,"%i is not enough parameters!/n",argc);
return 2;
}
filename = argv[1]; /* First parameter is filename */
strings = argv + 2; /* Search strings start from 3rd parameter */
str_count = argc - 2; /* strings are two ($0 and filename) less than argc */
return matcher(filename,strings,str_count);
}
Compilarlo con:
gcc matcher.c -o matcher
Ejecútalo con:
./matcher filename needle1 needle2 needle3
Créditos:
- utiliza this
-
Manejo de archivos mayormente robado de la
mmap
página man
Notas:
- Escaneará las partes del archivo que preceden a las cadenas coincidentes varias veces; sin embargo, solo abrirá el archivo una vez.
- El archivo completo podría terminar cargado en la memoria, especialmente si una cadena no coincide, el sistema operativo debe decidir que
- El soporte de expresiones regulares probablemente se puede agregar mediante el uso de la biblioteca de expresiones regulares POSIX (el rendimiento probablemente sea un poco mejor que grep; se debe basar en la misma biblioteca y obtendría una sobrecarga reducida al abrir el archivo una sola vez para buscar múltiples expresiones regulares)
- Los archivos que contienen valores nulos deberían funcionar, aunque las cadenas de búsqueda con ellos no ...
- Todos los caracteres que no sean nulos deben poder buscarse (/ r, / n, etc.)
Suponiendo que todas sus cadenas para verificar están en un archivo strings.txt, y el archivo que desea registrar es input.txt, lo siguiente hará:
Actualización de la respuesta basada en comentarios:
$ diff <( sort -u strings.txt ) <( grep -o -f strings.txt input.txt | sort -u )
Explicacion:
Use la opción grep''s -o para hacer coincidir solo las cadenas que le interesan. Esto le da todas las cadenas que están presentes en el archivo input.txt. Luego use diff para obtener las cadenas que no se encuentran. Si se encontraran todas las cadenas, el resultado sería nada. O simplemente verifique el código de salida de diff.
Lo que no hace:
- Salga tan pronto como se encuentren todas las coincidencias.
- Extensible a regx.
- Partidos superpuestos.
Lo que hace:
- Encuentra todos los partidos.
- Llamada única a grep.
- No utiliza awk ni python.
Una variante más de Perl: cada vez que todas las cadenas coinciden ... incluso cuando el archivo se lee a la mitad, el procesamiento se completa y solo imprime los resultados
> perl -lne '' //b(string1|string2|string3)/b/ and $m{$1}++; eof if keys %m == 3; END { print keys %m == 3 ? "Match": "No Match"}'' all_match.txt
Match
> perl -lne '' //b(string1|string2|stringx)/b/ and $m{$1}++; eof if keys %m == 3; END { print keys %m == 3 ? "Match": "No Match"}'' all_match.txt
No Match
Awk es la herramienta que los chicos que inventaron grep, shell, etc., inventaron para realizar trabajos generales de manipulación de texto como este, así que no estoy seguro de por qué querrías evitarlo.
En caso de que la brevedad sea lo que está buscando, aquí está el GNU awk one-liner para hacer exactamente lo que pidió:
awk ''NR==FNR{a[$0];next} {for(s in a) if(!index($0,s)) exit 1}'' strings RS=''^$'' file
Y aquí hay un montón de otra información y opciones:
Asumiendo que realmente estás buscando cadenas, sería:
awk -v strings=''string1 string2 string3'' ''
BEGIN {
numStrings = split(strings,tmp)
for (i in tmp) strs[tmp[i]]
}
numStrings == 0 { exit }
{
for (str in strs) {
if ( index($0,str) ) {
delete strs[str]
numStrings--
}
}
}
END { exit (numStrings ? 1 : 0) }
'' file
lo anterior dejará de leer el archivo tan pronto como todas las cadenas coincidan.
Si estaba buscando expresiones regulares en lugar de cadenas, entonces con GNU awk para RS de múltiples caracteres y retención de $ 0 en la sección FINAL, podría hacer:
awk -v RS=''^$'' ''END{exit !(/regexp1/ && /regexp2/ && /regexp3/)}'' file
En realidad, incluso si se tratara de cadenas, podría hacer:
awk -v RS=''^$'' ''END{exit !(index($0,"string1") && index($0,"string2") && index($0,"string3"))}'' file
El problema principal con las 2 soluciones anteriores de GNU awk es que, al igual que la solución GNU grep -P de @anubhava, todo el archivo debe leerse en la memoria al mismo tiempo, mientras que con el primer script awk anterior, funcionará en cualquier awk en cualquier shell en cualquier cuadro de UNIX y solo almacena una línea de entrada a la vez.
Veo que ha agregado un comentario debajo de su pregunta para decir que podría tener varios miles de "patrones". Suponiendo que quiere decir "cadenas", en lugar de pasarlas como argumentos al script, puede leerlas desde un archivo, por ejemplo, con GNU awk para RS de múltiples caracteres y un archivo con una cadena de búsqueda por línea:
awk ''
NR==FNR { strings[$0]; next }
{
for (string in strings)
if ( !index($0,string) )
exit 1
}
'' file_of_strings RS=''^$'' file_to_be_searched
y para regexps sería:
awk ''
NR==FNR { regexps[$0]; next }
{
for (regexp in regexps)
if ( $0 !~ regexp )
exit 1
}
'' file_of_regexps RS=''^$'' file_to_be_searched
Si no tiene GNU awk y su archivo de entrada no contiene caracteres NUL, puede obtener el mismo efecto que el anterior utilizando
RS=''/0''
lugar de
RS=''^$''
o agregando a la variable una línea en un tiempo a medida que se lee y luego procesa esa variable en la sección FIN.
Si su file_to_be_searched es demasiado grande para caber en la memoria, sería esto para cadenas:
awk ''
NR==FNR { strings[$0]; numStrings=NR; next }
numStrings == 0 { exit }
{
for (string in strings) {
if ( index($0,string) ) {
delete strings[string]
numStrings--
}
}
}
END { exit (numStrings ? 1 : 0) }
'' file_of_strings file_to_be_searched
y el equivalente para regexps:
awk ''
NR==FNR { regexps[$0]; numRegexps=NR; next }
numRegexps == 0 { exit }
{
for (regexp in regexps) {
if ( $0 ~ regexp ) {
delete regexps[regexp]
numRegexps--
}
}
}
END { exit (numRegexps ? 1 : 0) }
'' file_of_regexps file_to_be_searched
Es un problema interesante, y no hay nada obvio en la página de manual de grep que sugiera una respuesta fácil. Puede haber una expresión regular demente que lo haga, pero puede ser más clara con una cadena directa de greps, aunque eso termine escaneando el archivo n veces. Al menos la opción -q tiene fianza en el primer partido cada vez, y el && atajará la evaluación si no se encuentra una de las cadenas.
$grep -Fq string1 t && grep -Fq string2 t && grep -Fq string3 t
$echo $?
0
$grep -Fq string1 t && grep -Fq blah t && grep -Fq string3 t
$echo $?
1
Este script
gnu-awk
puede funcionar:
cat fileSearch.awk
re == "" {
exit
}
{
split($0, null, "//<(" re "//>)", b)
for (i=1; i<=length(b); i++)
gsub("//<" b[i] "([|]|$)", "", re)
}
END {
exit (re != "")
}
Luego úsalo como:
if awk -v re=''string1|string2|string3'' -f fileSearch.awk file; then
echo "all strings were found"
else
echo "all strings were not found"
fi
Alternativamente
, puede usar esta solución
gnu grep
con la opción
PCRE
:
grep -qzP ''(?s)(?=.*/bstring1/b)(?=.*/bstring2/b)(?=.*/bstring3/b)'' file
-
Usando
-z
hacemos quegrep
lea el archivo completo en una sola cadena. - Estamos utilizando múltiples aserciones anticipadas para afirmar que todas las cadenas están presentes en el archivo.
-
Regex debe usar
(?s)
oDOTALL
mod para hacer que.*
coincida entre líneas.
Según el
man grep
:
-z, --null-data
Treat input and output data as sequences of lines, each terminated by a
zero byte (the ASCII NUL character) instead of a newline.
Ignorando el "¿Es posible hacerlo sin ... o usar una herramienta como
awk
o
python
?"
requisito, puede hacerlo con un script Perl:
(Use un shebang apropiado para su sistema o algo como
/bin/env perl
)
#!/usr/bin/perl
use Getopt::Std; # option parsing
my %opts;
my $filename;
my @patterns;
getopts(''rf:'',/%opts); # Allowing -f <filename> and -r to enable regex processing
if ($opts{''f''}) { # if -f is given
$filename = $opts{''f''};
@patterns = @ARGV[0 .. $#ARGV]; # Use everything else as patterns
} else { # Otherwise
$filename = $ARGV[0]; # First parameter is filename
@patterns = @ARGV[1 .. $#ARGV]; # Rest is patterns
}
my $use_re= $opts{''r''}; # Flag on whether patterns are regex or not
open(INF,''<'',$filename) or die("Can''t open input file ''$filename''");
while (my $line = <INF>) {
my @removal_list = (); # List of stuff that matched that we don''t want to check again
for (my $i=0;$i <= $#patterns;$i++) {
my $pattern = $patterns[$i];
if (($use_re&& $line =~ /$pattern/) || # regex match
(!$use_re&& index($line,$pattern) >= 0)) { # or string search
push(@removal_list,$i); # Mark to be removed
}
}
# Now remove everything we found this time
# We need to work backwards to keep us from messing
# with the list while we''re busy
for (my $i=$#removal_list;$i >= 0;$i--) {
splice(@patterns,$removal_list[$i],1);
}
if (scalar(@patterns) == 0) { # If we don''t need to match anything anymore
close(INF) or warn("Error closing ''$filename''");
exit(0); # We found everything
}
}
# End of file
close(INF) or die("Error closing ''$filename''");
exit(1); # If we reach this, we haven''t matched everything
Se guarda como
matcher.pl
esto buscará cadenas de texto sin formato:
./matcher filename string1 string2 string3 ''complex string''
Esto buscará expresiones regulares:
./matcher -r filename regex1 ''regex2'' ''regex4''
(El nombre de archivo se puede dar con
-f
lugar):
./matcher -f filename -r string1 string2 string3 ''complex string''
Está limitado a patrones de coincidencia de una sola línea (debido a que se trata el archivo en línea).
El rendimiento, cuando se requieren muchos archivos de un script de shell, es más lento que
awk
(pero los patrones de búsqueda pueden contener espacios, a diferencia de los que se pasan separados por espacios en
-v
a
awk
).
Si se convierte a una función y se llama desde el código Perl (con un archivo que contiene una lista de archivos para buscar), debería ser mucho más rápido que la mayoría de las implementaciones de
awk
.
(Cuando se invoca en varios archivos pequeños, el tiempo de inicio de perl (análisis, etc. del script) domina el tiempo)
Se puede acelerar significativamente codificando si se usan o no expresiones regulares, a costa de la flexibilidad.
(Vea mis
puntos de referencia aquí
para ver qué efecto tiene la eliminación de
Getopt::Std
)
La forma más fácil para mí de verificar si el archivo tiene los tres patrones es obtener solo patrones coincidentes, generar solo partes únicas y líneas de conteo.
Luego podrá verificarlo con una simple
condición de
test 3 -eq $grep_lines
:
test 3 -eq $grep_lines
.
grep_lines=$(grep -Eo ''string1|string2|string3'' file | uniq | wc -l)
Con respecto a su
segunda pregunta
, no creo que sea posible dejar de leer el archivo tan pronto como se encuentre más de un patrón.
He leído la página de manual de grep y no hay opciones que puedan ayudarte con eso.
Solo puede dejar de leer líneas después de una específica con una opción
grep -m [number]
que ocurre sin importar los patrones coincidentes.
Bastante seguro de que se necesita una función personalizada para ese propósito.
Primero, probablemente quieras usar
awk
.
Como eliminó esa opción en el enunciado de la pregunta, sí, es posible hacerlo y esto proporciona una forma de hacerlo.
Es probable que sea MUCHO más lento que usar
awk
, pero si quieres hacerlo de todos modos ...
Esto se basa en los siguientes supuestos: G
- Invocar AWK es inaceptable
-
Invocar
grep
varias veces es inaceptable - El uso de cualquier otra herramienta externa es inaceptable
-
Invocar
grep
menos de una vez es aceptable - Debe devolver el éxito si se encuentra todo, el fracaso cuando no
-
Usar
bash
lugar de herramientas externas es aceptable -
bash
versiónbash
es> = 3 para la versión de expresión regular
Esto podría cumplir con todos sus requisitos: (la versión de regex pierde algunos comentarios, mire la versión de cadena en su lugar)
#!/bin/bash
multimatch() {
filename="$1" # Filename is first parameter
shift # move it out of the way that "$@" is useful
strings=( "$@" ) # search strings into an array
declare -a matches # Array to keep track which strings already match
# Initiate array tracking what we have matches for
for ((i=0;i<${#strings[@]};i++)); do
matches[$i]=0
done
while IFS= read -r line; do # Read file linewise
foundmatch=0 # Flag to indicate whether this line matched anything
for ((i=0;i<${#strings[@]};i++)); do # Loop through strings indexes
if [ "${matches[$i]}" -eq 0 ]; then # If no previous line matched this string yet
string="${strings[$i]}" # fetch the string
if [[ $line = *$string* ]]; then # check if it matches
matches[$i]=1 # mark that we have found this
foundmatch=1 # set the flag, we need to check whether we have something left
fi
fi
done
# If we found something, we need to check whether we
# can stop looking
if [ "$foundmatch" -eq 1 ]; then
somethingleft=0 # Flag to see if we still have unmatched strings
for ((i=0;i<${#matches[@]};i++)); do
if [ "${matches[$i]}" -eq 0 ]; then
somethingleft=1 # Something is still outstanding
break # no need check whether more strings are outstanding
fi
done
# If we didn''t find anything unmatched, we have everything
if [ "$somethingleft" -eq 0 ]; then return 0; fi
fi
done < "$filename"
# If we get here, we didn''t have everything in the file
return 1
}
multimatch_regex() {
filename="$1" # Filename is first parameter
shift # move it out of the way that "$@" is useful
regexes=( "$@" ) # Regexes into an array
declare -a matches # Array to keep track which regexes already match
# Initiate array tracking what we have matches for
for ((i=0;i<${#regexes[@]};i++)); do
matches[$i]=0
done
while IFS= read -r line; do # Read file linewise
foundmatch=0 # Flag to indicate whether this line matched anything
for ((i=0;i<${#strings[@]};i++)); do # Loop through strings indexes
if [ "${matches[$i]}" -eq 0 ]; then # If no previous line matched this string yet
regex="${regexes[$i]}" # Get regex from array
if [[ $line =~ $regex ]]; then # We use the bash regex operator here
matches[$i]=1 # mark that we have found this
foundmatch=1 # set the flag, we need to check whether we have something left
fi
fi
done
# If we found something, we need to check whether we
# can stop looking
if [ "$foundmatch" -eq 1 ]; then
somethingleft=0 # Flag to see if we still have unmatched strings
for ((i=0;i<${#matches[@]};i++)); do
if [ "${matches[$i]}" -eq 0 ]; then
somethingleft=1 # Something is still outstanding
break # no need check whether more strings are outstanding
fi
done
# If we didn''t find anything unmatched, we have everything
if [ "$somethingleft" -eq 0 ]; then return 0; fi
fi
done < "$filename"
# If we get here, we didn''t have everything in the file
return 1
}
if multimatch "filename" string1 string2 string3; then
echo "file has all strings"
else
echo "file miss one or more strings"
fi
if multimatch_regex "filename" "regex1" "regex2" "regex3"; then
echo "file match all regular expressions"
else
echo "file does not match all regular expressions"
fi
Puntos de referencia
Hice algunos benchmarking buscando
.c
,
.h
y
.sh
en arch / arm / desde Linux 4.16.2 para las cadenas "void", "function" y "#define".
(Se agregaron envoltorios de shell / se ajustó el código para que todos puedan llamarse como
testname <filename> <searchstring> [...]
y que se pueda usar un
if
para verificar el resultado)
Resultados: (medido con el
time
, tiempo
real
redondeado al medio segundo más cercano)
-
multimatch
: 49s -
multimatch_regex
: 55s - matchall : 10.5s
- fileMatchesAllNames : 4s
- awk (primera versión): 4s
- agrep: 4.5s
- Perl re (-r): 10.5s
- Perl no re : 9.5s
- Perl no re-optimizado : 5s (Getopt eliminado :: Std y soporte de expresiones regulares para un inicio más rápido)
- Perl re optimized : 7s (Getopt eliminado :: Std y soporte no regex para un inicio más rápido)
- git grep : 3.5s
- Versión C (sin expresiones regulares): 1.5s
(Invocar
grep
varias veces, especialmente con el método recursivo, funcionó mejor de lo que esperaba)
Quizás con gnu sed
cat match_word.sh
sed -z ''
//b''"$2"''/!bA
//b''"$3"''/!bA
//b''"$4"''/!bA
//b''"$5"''/!bA
s/.*/0/n/
q
:A
s/.*/1/n/
'' "$1"
y lo llamas así:
./match_word.sh infile string1 string2 string3
devuelve 0 si se encuentran todas las coincidencias más 1
aquí puedes buscar 4 cuerdas
si quieres más, puedes agregar líneas como
//b''"$x"''/!bA
Solo para "soluciones completas", puede usar una herramienta diferente y evitar múltiples greps y awk / sed o bucles de shell grandes (y probablemente lentos); Tal herramienta es agrep .
agrep
es en realidad una especie de soporte
egrep
también
and
operación entre patrones, usando
;
como un separador de patrones
Al igual que
egrep
y la mayoría de las herramientas conocidas,
agrep
es una herramienta que funciona en registros / líneas y, por lo tanto, todavía necesitamos una forma de tratar todo el archivo como un único registro.
Además agrep proporciona una opción
-d
para establecer su delimitador de registro personalizado.
Algunas pruebas:
$ cat file6
str4
str1
str2
str3
str1 str2
str1 str2 str3
str3 str1 str2
str2 str3
$ agrep -d ''$$/n'' ''str3;str2;str1;str4'' file6;echo $?
str4
str1
str2
str3
str1 str2
str1 str2 str3
str3 str1 str2
str2 str3
0
$ agrep -d ''$$/n'' ''str3;str2;str1;str4;str5'' file6;echo $?
1
$ agrep -p ''str3;str2;str1'' file6 #-p prints lines containing all three patterns in any position
str1 str2 str3
str3 str1 str2
Ninguna herramienta es perfecta y
agrep
también tiene algunas limitaciones;
no puede usar una expresión regular / patrón de más de 32 caracteres y algunas opciones no están disponibles cuando se usa con expresiones regulares, todo esto se explica en la
agrep
Una solución recursiva. Iterar sobre los archivos uno por uno. Para cada archivo, verifique si coincide con el primer patrón y rompa temprano (-m1: en la primera coincidencia), solo si coincide con el primer patrón, busque el segundo patrón y así sucesivamente:
#!/bin/bash
patterns="$@"
fileMatchesAllNames () {
file=$1
if [[ $# -eq 1 ]]
then
echo "$file"
else
shift
pattern=$1
shift
grep -m1 -q "$pattern" "$file" && fileMatchesAllNames "$file" $@
fi
}
for file in *
do
test -f "$file" && fileMatchesAllNames "$file" $patterns
done
Uso:
./allfilter.sh cat filter java
test.sh
Busca en el directorio actual los tokens "cat", "filter" y "java". Los encontré solo en "test.sh".
Entonces grep se invoca a menudo en el peor de los casos (encontrar los primeros patrones N-1 en la última línea de cada archivo, excepto el patrón N-ésimo).
Pero con un pedido informado (coincide poco antes, primero coincide primero) si es posible, la solución debe ser razonablemente rápida, ya que muchos archivos se abandonan temprano porque no coinciden con la primera palabra clave, o se aceptan temprano, ya que coinciden con el cierre de una palabra clave a la cima.
Ejemplo: Usted busca un archivo fuente scala que contiene tailrec (algo raramente usado), mutable (raramente usado, pero si es así, cerca de la parte superior en las declaraciones de importación) main (raramente usado, a menudo no cerca de la parte superior) e println (a menudo posición impredecible), los ordenaría:
./allfilter.sh mutable tailrec main println
Actuación:
ls *.scala | wc
89 89 2030
En 89 archivos scala, tengo la distribución de palabras clave:
for keyword in mutable tailrec main println; do grep -m 1 $keyword *.scala | wc -l ; done
16
34
41
71
Al buscarlos con una versión ligeramente modificada de los scripts, lo que permite usar un patrón de archivos como primer argumento toma alrededor de 0.2s:
time ./allfilter.sh "*.scala" mutable tailrec main println
Filepattern: *.scala Patterns: mutable tailrec main println
aoc21-2017-12-22_00:16:21.scala
aoc25.scala
CondenseString.scala
Partition.scala
StringCondense.scala
real 0m0.216s
user 0m0.024s
sys 0m0.028s
en cerca de 15,000 líneas de código:
cat *.scala | wc
14913 81614 610893
actualizar:
Después de leer en los comentarios a la pregunta, que podríamos estar hablando de miles de patrones, entregarlos como argumentos no parece ser una idea inteligente; mejor léalos de un archivo y pase el nombre del archivo como argumento, tal vez para que la lista de archivos también se filtre:
#!/bin/bash
filelist="$1"
patternfile="$2"
patterns="$(< $patternfile)"
fileMatchesAllNames () {
file=$1
if [[ $# -eq 1 ]]
then
echo "$file"
else
shift
pattern=$1
shift
grep -m1 -q "$pattern" "$file" && fileMatchesAllNames "$file" $@
fi
}
echo -e "Filepattern: $filepattern/tPatterns: $patterns"
for file in $(< $filelist)
do
test -f "$file" && fileMatchesAllNames "$file" $patterns
done
Si el número y la longitud de los patrones / archivos exceden las posibilidades de pasar argumentos, la lista de patrones podría dividirse en muchos archivos de patrones y procesarse en un bucle (por ejemplo, de 20 archivos de patrones):
for i in {1..20}
do
./allfilter2.sh file.$i.lst pattern.$i.lst > file.$((i+1)).lst
done
Usted puede
-
hacer uso de la
-o
|--only-matching
opción de--only-matching
degrep
(que obliga a generar solo las partes coincidentes de una línea coincidente, con cada parte en una línea de salida separada), -
luego elimine las ocurrencias duplicadas de cadenas coincidentes con
sort -u
, -
y finalmente verifique que el recuento de las líneas restantes sea igual al recuento de las cadenas de entrada.
Demostración:
$ cat input
...
string1
...
string2
...
string3
...
string1 string2
...
string1 string2 string3
...
string3 string1 string2
...
string2 string3
... and so on
$ grep -o -F $''string1/nstring2/nstring3'' input|sort -u|wc -l
3
$ grep -o -F $''string1/nstring3'' input|sort -u|wc -l
2
$ grep -o -F $''string1/nstring2/nfoo'' input|sort -u|wc -l
2
Una deficiencia con esta solución (no cumplir con las
coincidencias parciales debería ser un
requisito
correcto
) es que
grep
no detecta coincidencias superpuestas.
Por ejemplo, aunque el texto
abcd
coincide con
abc
y
bcd
,
grep
encuentra solo uno de ellos:
$ grep -o -F $''abc/nbcd'' <<< abcd
abc
$ grep -o -F $''bcd/nabc'' <<< abcd
abc
Tenga en cuenta que este enfoque / solución solo funciona para cadenas fijas.
No se puede extender para expresiones regulares, porque una sola expresión regular puede coincidir con varias cadenas diferentes y no podemos rastrear qué coincidencia corresponde a qué expresión regular.
Lo mejor que puede hacer es almacenar las coincidencias en un archivo temporal y luego ejecutar
grep
varias veces utilizando una expresión regular a la vez.
La solución implementada como un script bash:
matchall :
#!/usr/bin/env bash
if [ $# -lt 2 ]
then
echo "Usage: $(basename "$0") input_file string1 [string2 ...]"
exit 1
fi
function find_all_matches()
(
infile="$1"
shift
IFS=$''/n''
newline_separated_list_of_strings="$*"
grep -o -F "$newline_separated_list_of_strings" "$infile"
)
string_count=$(($# - 1))
matched_string_count=$(find_all_matches "$@"|sort -u|wc -l)
if [ "$matched_string_count" -eq "$string_count" ]
then
echo "ALL strings matched"
exit 0
else
echo "Some strings DID NOT match"
exit 1
fi
Demostración:
$ ./matchall
Usage: matchall input_file string1 [string2 ...]
$ ./matchall input string1 string2 string3
ALL strings matched
$ ./matchall input string1 string2
ALL strings matched
$ ./matchall input string1 string2 foo
Some strings DID NOT match
$ cat allstringsfile | tr ''/n'' '' '' | awk -f awkpattern1
Donde allstringsfile es su archivo de texto, como en la pregunta original. awkpattern1 contiene los patrones de cadena, con && condición:
$ cat awkpattern1
/string1/ && /string2/ && /string3/
perl -lne ''%m = (%m, map {$_ => 1} m!/b(string1|string2|string3)/b!g); END { print scalar keys %m == 3 ? "Match": "No Match"}'' file