Captura de grupos desde un Grep RegEx

380

Tengo este pequeño script en sh(Mac OSX 10.6) para mirar a través de una variedad de archivos. Google ha dejado de ser útil en este punto:

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

Hasta ahora (obviamente, para ustedes gurús de shell) $namesimplemente tiene 0, 1 o 2, dependiendo de si se grepencontró que el nombre del archivo coincide con el asunto proporcionado. Lo que me gustaría es capturar lo que hay dentro de los padres ([a-z]+)y almacenarlo en una variable .

Me gustaría usar grepsolo, si es posible . Si no es así, no use Python o Perl, etc. sedo algo así: soy nuevo en shell y me gustaría atacar esto desde el ángulo purista * nix.

Además, como bonificaciones súper geniales , tengo curiosidad por saber cómo puedo concatenar la cuerda en la concha. ¿El grupo que capturé era la cadena "somename" almacenada en $ name, y quería agregar la cadena ".jpg" al final, ¿podría cat $name '.jpg'?

Por favor explique qué está pasando, si tiene tiempo.

Isaac
fuente
30
¿Es grep realmente unix más puro que sed?
martin clayton
3
Ah, no quise sugerir eso. Solo esperaba que se pudiera encontrar una solución usando una herramienta que específicamente estoy tratando de aprender aquí. Si no es posible resolver usando grep, entonces sedsería genial, si es posible resolver usando sed.
Isaac
2
Debería haber puesto un :) en ese por cierto ...
Martin Clayton
Psh, mi cerebro está demasiado frito hoy jaja.
Isaac
2
@martinclayton Ese sería un argumento interesante. Realmente creo que sed, (o ed para ser precisos) sería más antiguo (¿y por lo tanto más puro? ¿Quizás?) Unix porque grep deriva su nombre de la expresión ed g (lobal) / re (expresión gular) / p (rint).
incipiente

Respuestas:

500

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

files="*.jpg"
regex="[0-9]+_([a-z]+)_[0-9a-z]*"
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.

Debe tener en cuenta que sin las anclas, 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á buscando:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

Para eliminar los ejemplos segundo y cuarto, haga su expresión regular de esta manera:

^[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 cadena. 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 será eliminado ya que el punto no está entre los caracteres en la expresión regular y el signo de dólar representa el final de la cadena. Tenga en cuenta que el cuarto ejemplo también falla en esta coincidencia.

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 (retrospectiva 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 usar \Ksi los cuantificadores pueden coincidir con cadenas de diferentes longitudes (p +. Ej . *, {2,4}).

El (?=)operador coincide con patrones de longitud fija o variable y se llama "mirar hacia adelante". 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 concatenación de una cadena al mismo tiempo que se captura la subcadena.

Pausado hasta nuevo aviso.
fuente
48
En esta respuesta, quiero votar a favor la línea específica que dice "Es mejor poner la expresión regular en una variable. Algunos patrones no funcionarán si se incluyen literalmente".
Brandin
55
@FrancescoFrassinelli: Un ejemplo es un patrón que incluye espacios en blanco. Es incómodo escapar y no puedes usar comillas ya que eso lo obliga a pasar de una expresión regular a una cadena ordinaria. La forma correcta de hacerlo es usar una variable. Las citas se pueden usar durante la tarea haciendo las cosas mucho más simples.
Pausado hasta nuevo aviso.
55
/Koperador de rocas.
razz
2
@ Brandon: Funciona. ¿Qué versión de Bash estás usando? Muéstrame lo que estás haciendo que no funciona y quizás pueda decirte por qué.
Pausado hasta nuevo aviso.
2
@mdelolmo: Mi respuesta incluye información sobre grep. También fue aceptado por el OP y votó bastante. Gracias por el voto negativo.
Pausado hasta nuevo aviso.
145

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

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

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

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 ha --only-matchingespecificado) mostrará la parte alfa del nombre. Esto solo 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 busca: echo $name | grep {pattern} | cut -d _ -f 2esto cutpermite analizar la línea en campos al dividir 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 hacen una cosa, y lo hacen bien, y combinarlas para lograr tareas no triviales, por lo que diría que grep+ sedetc es una forma más Unixy de hacer las cosas :-)

RobM
fuente
3
for f in $files; do name=echo $ f | grep -oEi '[0-9] + _ ([az] +) _ [0-9a-z] *' | corte -d _ -f 2 ;¡Ajá!
Isaac
2
No estoy de acuerdo con esa "filosofía". Si puede usar las capacidades integradas del shell sin llamar a comandos externos, su script tendrá un rendimiento mucho más rápido. Hay algunas herramientas que se superponen en la función. por ejemplo grep y sed y awk. todos ellos manipulan cadenas, pero awk se destaca sobre todos porque puede hacer mucho más. Prácticamente, todos esos encadenamientos de comandos, como el doble greps anterior o grep + sed, se pueden acortar haciéndolos con un proceso awk.
ghostdog74
77
@ ghostdog74: Aquí no hay argumento de que encadenar muchas operaciones pequeñas en general es menos eficiente que hacerlo todo en un solo lugar, pero mantengo mi afirmación de que la filosofía de Unix es que muchas herramientas funcionan juntas. Por ejemplo, tar solo archiva archivos, no los comprime, y debido a que sale a STDOUT de forma predeterminada, puede canalizarlo a través de la red con netcat, o comprimirlo con bzip2, etc. Lo que a mi juicio refuerza la convención y la información general. ethos de que las herramientas de Unix deberían poder trabajar juntas en tuberías.
RobM
cortar es increíble, ¡gracias por el consejo! En cuanto al argumento de herramientas vs eficiencia, me gusta la simplicidad de encadenar herramientas.
ether_joe
accesorios para la opción o de grep, eso es muy útil
chiliNUT
96

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, lo que parece no haber sido mencionado todavía. Intenta 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 los contenidos 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 simple , pero también acepta un parámetro numérico pcregrep, que indica qué grupo de captura desea mostrar.

Con esta solución, se requiere un mínimo de cambio en el script. Simplemente reemplace una utilidad modular con otra y modifique los parámetros.

Nota interesante: puede usar múltiples argumentos -o para devolver múltiples grupos de captura en el orden en que aparecen en la línea.

John Sherwood
fuente
3
pcregrepno está disponible por defecto, Mac OS Xque es lo que usa el OP
grebneke
44
Mi pcregrepparece no entender el dígito después de -o: "Letra de opción desconocida '1' en" -o1 ". Tampoco se menciona esa funcionalidad cuando se mirapcregrep --help
Peter Herdenborg
1
@WAF lo siento, supongo que debería haber incluido esa información en mi comentario. Estoy en Centos 6.5 y la versión pcregrep es aparentemente muy antigua: 7.8 2008-09-05.
Peter Herdenborg
2
sí, mucha ayuda, por ejemploecho 'r123456 foo 2016-03-17' | pcregrep -o1 'r([0-9]+)' 123456
zhuguowei
55
pcregrep8.41 (instalado con apt-get install pcregrepencendido Ubuntu 16.03) no reconoce el -Eiinterruptor. Sin embargo, funciona perfectamente sin él. En macOS, con pcregrepinstalado a través de homebrew(también 8.41) como @anishpatel menciona anteriormente, al menos en High Sierra -Etampoco se reconoce el interruptor.
Ville
27

No es posible solo con grep, creo

para sed:

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

Sin embargo, probaré la bonificación:

echo "$name.jpg"
cobbal
fuente
2
Desafortunadamente, esa sedsolución no funciona. Simplemente imprime todo en mi directorio.
Isaac
actualizado, generará una línea en blanco si no hay una coincidencia, así que asegúrese de verificarlo
cobbal
¡Ahora solo genera líneas en blanco!
Isaac
Esto tiene un problema. El primer grupo de paréntesis de captura abarca todo. Por supuesto, \ 2 no tendrá nada.
ghostdog74
funcionó para algunos casos de prueba simples ... \ 2 obtiene el grupo interno
cobbal
16

Esta es una solución que usa gawk. Es algo que creo que necesito usar a menudo, así que creé una función para ello.

function regex1 { gawk 'match($0,/'$1'/, ary) {print ary['${2:-'1'}']}'; }

para usar solo hacer

$ echo 'hello world' | regex1 'hello\s(.*)'
world
opsb
fuente
Gran idea, pero no parece funcionar con espacios en la expresión regular, necesitan ser reemplazados por \s. ¿Sabes cómo solucionarlo?
Adam Ryczkowski
4

Una sugerencia para usted: puede usar la expansión de parámetros para eliminar la parte del nombre del último guión bajo y, de manera similar, al principio:

f=001_abc_0za.jpg
work=${f%_*}
name=${work#*_}

Entonces nametendrá el valor abc.

Consulte los documentos para desarrolladores de Apple , busque 'Expansión de parámetros'.

Martin Clayton
fuente
esto no verificará ([az] +).
ghostdog74
@levislevis: eso es cierto, pero, como comentó el OP, hace lo que se necesitaba.
Martin Clayton
2

si tienes bash, puedes usar globbing extendido

shopt -s extglob
shopt -s nullglob
shopt -s nocaseglob
for file in +([0-9])_+([a-z])_+([a-z0-9]).jpg
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

o

ls +([0-9])_+([a-z])_+([a-z0-9]).jpg | while read file
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done
ghostdog74
fuente
Eso se ve intrigante. ¿Podrías quizás agregarle una pequeña explicación? O, si está tan inclinado, ¿tiene un enlace a un recurso particularmente perspicaz que lo explique? ¡Gracias!
Isaac