Grep: el asterisco (*) no siempre funciona

11

Si grep un documento que contiene lo siguiente:

ThisExampleString

... para la expresión This*Stringo *String, no se devuelve nada. Sin embargo, This*devuelve la línea anterior como se esperaba.

Si la expresión está entre comillas no hace diferencia.

¿Pensé que el asterisco indicaba cualquier número de caracteres desconocidos? ¿Por qué solo funciona si está al comienzo de la expresión? Si este es el comportamiento previsto, ¿qué uso en lugar de las expresiones This*Stringy *String?

Trae
fuente
porque no es así como funciona la expresión regular ... (en particular: * != any number of unknown characterslea el documento)
njzk2

Respuestas:

18

Un asterisco en expresiones regulares significa "hacer coincidir el elemento anterior 0 o más veces".

En su caso particular con grep 'This*String' file.txt, está tratando de decir, "hey, grep, concédeme la palabra Thi, seguida de scero en minúscula o más veces, seguida de la palabra String". La minúscula sno se encuentra en ninguna parte Example, por lo tanto, grep ignora ThisExampleString.

En el caso de grep '*String' file.txt, estás diciendo "grep, empareja la cadena vacía, literalmente nada, que precede a la palabra String". Por supuesto, no ThisExampleStringes así como se supone que debe leerse. (Hay otros significados posibles, puede intentar esto con y sin la -Ebandera, pero ninguno de los significados es lo que realmente quiere aquí).

Sabiendo que .significa "cualquier carácter individual", podríamos hacer esto: grep 'This.*String' file.txt. Ahora el comando grep lo leerá correctamente: Thisseguido de cualquier carácter (piénselo como una selección de caracteres ASCII) repetido cualquier número de veces, seguido de String.

Sergiy Kolodyazhnyy
fuente
66
En Bash (y la mayoría de los shells de Unix) *hay un personaje especial y debe citarse o escaparse, por ejemplo, de esta manera: grep 'This*String' file.txto esto: grep This\*String file.txtpara no sorprenderse con resultados inesperados.
pabouk
2
@pabouk en conchas, *es un comodín. En grep, *es un operador de expresión regular. Ver unix.stackexchange.com/q/57957/70524
muru
11
pabouk tiene razón, la expansión del nombre de archivo se lleva a cabo antes de ejecutar el comando; comparar strace grep .* file.txt |& head -n 1 y strace grep '.*' file.txt |& head -n 1. También en realidad grepfunciona también con cualquier carácter Unicode (por ejemplo echo -ne ⇏ | grep ⇏salidas )
Kos
1
@Serg: tienes una gran reputación aquí, así que pensé que inmediatamente notaste lo que quiero decir. El OP ha etiquetado la pregunta bash, así que supongo que los comandos discutidos son interpretados por bash. Esto significa que primero bashinterpreta sus caracteres especiales y solo después de todas las expansiones realizadas, pasa los parámetros al proceso generado. ----- Por ejemplo este comando en Bash: grep This.\*String file.txtse generan /bin/grepcon estos parámetros: 0 grep, 1: This.*String2: file.txt. Tenga en cuenta que Bash eliminó la barra diagonal inversa y el original escapado *se pasó literalmente.
pabouk
77
Lo divertido (y para solucionar problemas bastante desagradables :) es que sus comandos grep This.*String file.txtnormalmente funcionarán porque lo más probable es que no haya un archivo que coincida con la expresión comodín del shell This.*String. En tal caso, Bash pasará el argumento literalmente incluido *.
pabouk
8

El *metacarácter en BRE 1 s, ERE 1 sy PCRE 1 s coincide con 0 o más ocurrencias del patrón agrupado previamente (si un patrón agrupado precede al *metacarácter), 0 o más ocurrencias de la clase de caracteres anterior (si una clase de caracteres es anterior al *metacarácter) o 0 o más ocurrencias del carácter anterior (si ni un patrón agrupado ni una clase de caracteres precede al *metacarácter);

Esto significa que en el This*Stringpatrón, siendo el *metacarácter no precedido por un patrón agrupado o una clase de caracteres, el *metacarácter coincide con 0 o más ocurrencias del carácter anterior (en este caso, el scarácter):

% cat infile               
ThisExampleString
ThisString
ThissString
% grep 'This*String' infile
ThisString
ThissString

Para hacer coincidir 0 o más ocurrencias de cualquier carácter, desea hacer coincidir 0 o más ocurrencias del .metacarácter, que coincide con cualquier carácter:

% cat infile               
ThisExampleString
% grep 'This.*String' infile
ThisExampleString

El *metacarácter en BRE y ERE siempre es "codicioso", es decir, coincidirá con la coincidencia más larga:

% cat infile
ThisExampleStringIsAString
% grep -o 'This.*String' infile
ThisExampleStringIsAString

Este puede no ser el comportamiento deseado; en caso de que no sea así, puede encender el grepmotor PCRE (usando la -Popción) y agregar el ?metacarácter, que cuando se coloca después de los metacaracteres *y +tiene el efecto de cambiar su codicia:

% cat infile
ThisExampleStringIsAString
% grep -Po 'This.*?String' infile
ThisExampleString

1: Expresiones regulares básicas, Expresiones regulares extendidas y Expresiones regulares compatibles con Perl

kos
fuente
Gracias por la respuesta muy informativa. Sin embargo, elegí una respuesta diferente porque era más corta y fácil de entender. +1 por proporcionar tantos detalles.
Trae
@Trae De nada. Está bien, estoy de acuerdo en que tal vez esto era demasiado complejo e hizo muchas suposiciones para alguien que no estaba demasiado familiarizado con el tema.
kos
4

Una de las explicaciones encontradas aquí enlace :

El asterisco " *" no significa lo mismo en las expresiones regulares que en los comodines; Es un modificador que se aplica al carácter único precedente, o expresión como [0-9]. Un asterisco coincide con cero o más de lo que le precede. Por lo tanto, [A-Z]*coincide con cualquier número de letras mayúsculas, incluida ninguna, mientras que [A-Z][A-Z]*coincide con una o más letras mayúsculas.

Óvulo
fuente
1

*tiene un significado especial tanto como un personaje de shell globbing ("comodín") como un metacarácter de expresión regular . Debe tener en cuenta ambos, aunque si cita su expresión regular, puede evitar que el shell la trate especialmente y asegurarse de que la pase sin cambios grep. Aunque es algo similar conceptualmente, lo que *significa para el shell es bastante diferente de lo que significa grep.

Primero, el shell se trata *como un comodín.

Tu dijiste:

Si la expresión está entre comillas no hace diferencia.

Eso depende de los archivos que existan en el directorio en el que se encuentre cuando ejecute el comando. Para los patrones que contienen el separador de directorio /, puede depender de qué archivos existen en todo el sistema. Siempre debe citar expresiones regulares para, grepy las comillas simples generalmente son las mejores, a menos que esté seguro de que está de acuerdo con los nueve tipos de transformaciones potencialmente sorprendentes que el shell realiza antes de ejecutar el grepcomando.

Cuando el shell encuentra un *carácter que no está entre comillas , lo hace significar "cero o más de cualquier carácter" y reemplaza la palabra que lo contiene con una lista de nombres de archivo que coinciden con el patrón. (Los nombres de archivo que comienzan con .están excluidos, a menos que su propio patrón comience . o haya configurado su shell para incluirlos de todos modos). Esto se conoce como globbing, y también por los nombres de expansión de nombre de archivo y expansión de nombre de ruta .

El efecto con greppor lo general será que el primer nombre de archivo coincidente se toma como la expresión regular - incluso si sería bastante obvio para un lector humano que está no pretende ser una expresión regular - mientras que todos los otros nombres de archivo enumerados automáticamente de su glob se toman como los archivos dentro de los cuales buscar coincidencias. (No ve la lista, se pasa opacamente a grep). Prácticamente nunca quiere que esto suceda.

La razón por la que esto a veces no es un problema, y ​​en su caso particular, al menos hasta ahora , no lo fue, es que *se dejará solo si se cumple todo lo siguiente :

  1. No había archivos cuyos nombres coincidieran. ... O ha deshabilitado el globbing en su caparazón, generalmente con set -fo el equivalente set -o noglob. Pero esto es poco común y probablemente sabrás que lo hiciste.

  2. Está utilizando un shell cuyo comportamiento predeterminado es dejar *solo cuando no hay nombres de archivo coincidentes. Este es el caso de Bash, que probablemente esté utilizando, pero no en todos los shells de estilo Bourne. (El comportamiento predeterminado en el popular shell Zsh, por ejemplo, es que los globos (a) se expandan o (b) produzcan un error.) ... O ha cambiado este comportamiento de su shell: la forma en que se hace varía a través de conchas.

  3. De lo contrario, no le ha dicho a su shell que permita que los globos sean reemplazados por nada cuando no hay archivos coincidentes, ni que falle con un mensaje de error en esta situación. En Bash, eso se habría hecho habilitando la opciónnullglob o failglob shell , respectivamente.

A veces puede confiar en el n. ° 2 y n. ° 3, pero rara vez puede confiar en el n. ° 1. Un grepcomando con un patrón sin comillas que funciona ahora puede dejar de funcionar cuando tiene archivos diferentes o cuando lo ejecuta desde un lugar diferente. Cita tu expresión regular y el problema desaparece.

Entonces el grepcomando trata *como un cuantificador.

Las otras respuestas, como las de Sergiy Kolodyazhnyy y kos, también abordan este aspecto de esta pregunta, de maneras algo diferentes. Así que animo a aquellos que aún no los han leído, que lo hagan antes o después de leer el resto de esta respuesta.

Suponiendo que *sí llega a grep, lo que debería garantizar la cita, grepsignifica que el elemento que lo precede puede ocurrir varias veces , en lugar de tener que ocurrir exactamente una vez . Todavía podría ocurrir una vez. O puede que no esté presente en absoluto. O podría repetirse. Se combinará el texto que se ajuste a cualquiera de esas posibilidades.

¿Qué quiero decir con "artículo"?

  • Un solo personaje . Desde bpartidos un literal b, b*coincide con cero o más bs, por lo tanto ab*clos partidos ac, abc, abbc, abbbc, etc.

    Del mismo modo, ya que .coincide con cualquier carácter , .*coincide con cero o más caracteres 1 , por lo tanto, a.*clos partidos ac, akc, ahjglhdfjkdlgjdfkshlgc, incluso acccccchjckhcc, etc. Or

  • Una clase de personaje . Desde [xy]partidos xo y, [xy]*concuerda con cero o más caracteres, donde cada uno es o bien xo y, por lo tanto p[xy]*qlos partidos pq, pxq, pyq, pxxq, pxyq, pyxq, pyyq, pxxxq, pxxyq, etc.

    Esto también se aplica a taquigrafía formas de clases de personajes como \w, \W, \s, y \S. Como \wcoincide con cualquier carácter de palabra, \w*coincide con cero o más caracteres de palabra. O

  • Un grupo . Desde \(bar\)partidos bar, \(bar\)*partidos cero o más bars, por lo tanto foo\(bar\)*bazlos partidos foobaz, foobarbaz, foobarbarbaz, foobarbarbarbaz, etc.

    Con las opciones -Eo -P, greptrata su expresión regular como un ERE o PCRE respectivamente, en lugar de como un BRE , y luego los grupos están rodeados por en ( )lugar de \( \), por lo que usaría en (bar)lugar de \(bar\)y en foo(bar)bazlugar de foo\(bar\)baz.

man grepproporciona una explicación razonablemente accesible de la sintaxis BRE y ERE al final, así como una lista de todas las opciones de línea de comandos grepacepta al principio. Recomiendo esa página del manual como recurso, y también la documentación de GNU Grep y este tutorial / sitio de referencia (que he vinculado a varias páginas, arriba).

Para probar y aprender grep, recomiendo llamarlo con un patrón pero sin nombre de archivo. Luego toma entrada de su terminal. Introduce líneas; las líneas que se repiten son las que contenían el texto que coincidió con su patrón. Para salir, presione Ctrl+ Dal comienzo de una línea, que señala el final de la entrada. (O puede presionar Ctrl+ Ccomo con la mayoría de los programas de línea de comandos). Por ejemplo:

grep 'This.*String'

Si usa la --colorbandera, grepresaltará las partes específicas de sus líneas que coinciden con su expresión regular, lo cual es muy útil tanto para descubrir qué hace una expresión regular como para encontrar lo que está buscando una vez que lo hace. Por defecto, los usuarios de Ubuntu tienen un alias Bash que hace grep --color=autoque se ejecute, lo cual es suficiente para este propósito, cuando se ejecuta grepdesde la línea de comandos, por lo que es probable que ni siquiera necesite pasar --colormanualmente.

1 Por lo tanto, .*en una expresión regular significa lo que *significa en un globo de shell. Sin embargo, la diferencia es que grepimprime automáticamente líneas que contienen su coincidencia en cualquier parte de ellas, por lo que generalmente no es necesario tenerlas .*al principio o al final de una expresión regular.

Eliah Kagan
fuente