Capturar grupos desde una expresión regular Grep

Resuelto Isaac asked hace 15 años • 10 respuestas

Tengo este script en sh(macOS 10.6) para revisar una serie de archivos:

files="*.jpg"
for f in $files
    do
        echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
        name=$?
        echo $name
    done

Hasta ahora $namesimplemente contiene 0, 1 o 2, dependiendo de si se grepencuentra que el nombre del archivo coincide con el asunto proporcionado. Lo que me gustaría es capturar lo que hay dentro de los pares ([a-z]+)y almacenarlo en una variable.

Me gustaría utilizar grepsólo, si es posible. Si no, por favor nada de Python o Perl, etc. sedo algo parecido. Me gustaría atacar esto desde el ángulo purista de *nix.

Isaac avatar Dec 12 '09 07:12 Isaac
Aceptado

Si estás usando Bash, ni siquiera tienes que usar grep:

files="*.jpg"
regex="[0-9]+_([a-z]+)_[0-9a-z]*" # put the regex in a variable because some patterns won't work if included literally
for f in $files    # unquoted in order to allow the glob to expand
do
    if [[ $f =~ $regex ]]
    then
        name="${BASH_REMATCH[1]}"
        echo "${name}.jpg"    # concatenate strings
        name="${name}.jpg"    # same thing stored in a variable
    else
        echo "$f doesn't match" >&2 # this could get noisy if there are a lot of non-matching files
    fi
done

Es mejor poner la expresión regular en una variable. Algunos patrones no funcionarán si se incluyen literalmente.

Esto utiliza =~cuál es el operador de coincidencia de expresiones regulares de Bash. Los resultados del partido se guardan en una matriz llamada $BASH_REMATCH. El primer grupo de captura se almacena en el índice 1, el segundo (si lo hay) en el índice 2, etc. El índice cero es la coincidencia completa.




nota al margen n.° 1 con respecto a los anclajes de expresiones regulares:

Debes tener en cuenta que sin anclajes, esta expresión regular (y la que usa grep) coincidirá con cualquiera de los siguientes ejemplos y más, que pueden no ser lo que estás buscando:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

Para eliminar el segundo y cuarto ejemplo, haga su expresión regular así:

^[0-9]+_([a-z]+)_[0-9a-z]*

que dice que la cadena debe comenzar con uno o más dígitos. El quilate representa el comienzo de la cuerda. Si agrega un signo de dólar al final de la expresión regular, así:

^[0-9]+_([a-z]+)_[0-9a-z]*$

entonces el tercer ejemplo también se eliminará ya que el punto no está entre los caracteres de la expresión regular y el signo de dólar representa el final de la cadena. Tenga en cuenta que el cuarto ejemplo tampoco cumple con esta coincidencia.

nota al margen n.° 2 sobre grepy el \Koperador:

Si tiene GNU grep(alrededor de 2.5 o posterior, creo, cuando \Kse agregó el operador):

name=$(echo "$f" | grep -Po '(?i)[0-9]+_\K[a-z]+(?=_[0-9a-z]*)').jpg

El \Koperador (mirada hacia atrás de longitud variable) hace que el patrón anterior coincida, pero no incluye la coincidencia en el resultado. El equivalente de longitud fija es (?<=): el patrón se incluiría antes del paréntesis de cierre. Debe utilizar \Ksi los cuantificadores pueden coincidir con cadenas de diferentes longitudes (por ejemplo +, *, {2,4}).

El (?=)operador compara patrones de longitud fija o variable y se denomina "previsión". Tampoco incluye la cadena coincidente en el resultado.

Para que la coincidencia no distinga entre mayúsculas y minúsculas, (?i)se utiliza el operador. Afecta los patrones que lo siguen por lo que su posición es significativa.

Es posible que sea necesario ajustar la expresión regular dependiendo de si hay otros caracteres en el nombre del archivo. Notarás que en este caso, muestro un ejemplo de cómo concatenar una cadena al mismo tiempo que se captura la subcadena.

Dennis Williamson avatar Dec 12 '2009 02:12 Dennis Williamson

Esto no es realmente posible con pure grep, al menos no en general.

Pero si su patrón es adecuado, es posible que pueda usarlo grepvarias veces dentro de una canalización para primero reducir su línea a un formato conocido y luego extraer solo el bit que desea. (Aunque a las herramientas les gusta cuty sedson mucho mejores en esto).

Supongamos, por el bien del argumento, que su patrón fuera un poco más simple: [0-9]+_([a-z]+)_podría extraer esto así:

echo $name | grep -Ei '[0-9]+_[a-z]+_' | grep -oEi '[a-z]+'

El primero grepeliminaría cualquier línea que no coincidiera con su patrón general, el segundo grep(que se ha --only-matchingespecificado) mostraría la parte alfa del nombre. Esto sólo funciona porque el patrón es adecuado: la "porción alfa" es lo suficientemente específica como para extraer lo que desea.

(Aparte: personalmente usaría grep+ cutpara lograr lo que buscas: echo $name | grep {pattern} | cut -d _ -f 2. Esto cutanaliza la línea en campos dividiéndola en el delimitador _y devuelve solo el campo 2 (los números de campo comienzan en 1)).

La filosofía de Unix es tener herramientas que hagan una cosa, la hagan bien y las combinen para lograr tareas no triviales, por lo que yo diría que grep+ sedetc es una forma más Unixy de hacer las cosas :-)

RobM avatar Dec 12 '2009 01:12 RobM

Me doy cuenta de que ya se aceptó una respuesta para esto, pero desde un "ángulo estrictamente *nix purista" parece que la herramienta adecuada para el trabajo es pcregrep, que no parece haberse mencionado todavía. Intente cambiar las líneas:

    echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
    name=$?

a lo siguiente:

    name=$(echo $f | pcregrep -o1 -Ei '[0-9]+_([a-z]+)_[0-9a-z]*')

para obtener solo el contenido del grupo de captura 1.

La pcregrepherramienta utiliza la misma sintaxis que ya usó grep, pero implementa la funcionalidad que necesita.

El parámetro -ofunciona igual que la grepversión si es básica, pero también acepta un parámetro numérico en pcregrep, que indica qué grupo de captura desea mostrar.

Con esta solución, se requiere un mínimo de cambios en el script. Simplemente reemplaza una utilidad modular por otra y modifica los parámetros.

Nota interesante: puede utilizar varios argumentos -o para devolver varios grupos de captura en el orden en que aparecen en la línea.

John Sherwood avatar Mar 03 '2013 17:03 John Sherwood

No es posible solo en grep, creo.

para sed:

name=`echo $f | sed -E 's/([0-9]+_([a-z]+)_[0-9a-z]*)|.*/\2/'`

Sin embargo, intentaré aprovechar el bono:

echo "$name.jpg"
cobbal avatar Dec 12 '2009 01:12 cobbal
str="1w 2d 1h"
regex="([0-9])w ([0-9])d ([0-9])h"
if [[ $str =~ $regex ]]
then
    week="${BASH_REMATCH[1]}"
    day="${BASH_REMATCH[2]}"
    hour="${BASH_REMATCH[3]}"
    echo $week --- $day ---- $hour
fi

salida: 1 --- 2 ---- 1

chirag nayak avatar Feb 03 '2021 11:02 chirag nayak