2009-12-12 38 views
257

Tengo este pequeño script en sh (Mac OSX 10.6) para examinar una matriz de archivos. Google ha dejado de ser útil en este punto:Capturar grupos desde un RegEx Grep

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

Hasta el momento (obviamente, a desembolsar gurús) $name simplemente sostiene 0, 1 o 2, dependiendo de si grep encontró que el nombre del archivo coincide con la materia proporcionada. Lo que me gustaría es capturar lo que está dentro de los parens ([a-z]+) y almacenar eso en una variable.

Me gustaría para usar grep solamente, si es posible. Si no, por favor, no Python o Perl, etc. sed o algo así - Soy nuevo en shell y me gustaría atacar esto desde el ángulo purista * nix.

Además, como super-cool bonu s, tengo curiosidad por saber cómo puedo concatenar cadena en shell? ¿El grupo que capturé fue la cadena "somename" almacenada en $ name, y quería agregar la cadena ".jpg" al final, podría cat $name '.jpg'?

Por favor, explique lo que está pasando, si tiene tiempo.

+20

¿Es grep * really * purer unix than sed? –

+1

Ah, no quise sugerir eso. Solo esperaba encontrar una solución usando una herramienta que estoy tratando de aprender aquí. Si no es posible resolver usando 'grep', entonces' sed' sería genial, si es posible resolverlo con 'sed'. – Isaac

+2

Debería haber puesto un :) en ese por cierto ... –

Respuesta

344

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

files="*.jpg" 
regex="[0-9]+_([a-z]+)_[0-9a-z]*" 
for f in $files 
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 usa =~ que es el operador de coincidencia de expresiones regulares de Bash. Los resultados de la coincidencia se guardan en una matriz llamada $BASH_REMATCH. El primer grupo de captura se almacena en el índice 1, el segundo (si hay alguno) en el índice 2, etc. El índice cero es la coincidencia completa.

Debe tener en cuenta que sin anclas, esta expresión regular (y el uso de grep) coincidirá con cualquiera de los siguientes ejemplos y más, lo que puede 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, que su expresión regular como esto:

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

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

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

entonces el tercer ejemplo también serán eliminados desde el punto no se encuentra entre los personajes de la expresión regular y de dólar representa el final de la cuerda. Tenga en cuenta que el cuarto ejemplo también falla esta coincidencia.

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

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

El \K operador (de longitud variable mirada-detrás) hace que el patrón precedente para que coincida , pero no incluye el partido en el resultado. El equivalente de longitud fija es (?<=) - el patrón se incluiría antes del paréntesis de cierre. Debe usar \K si los cuantificadores pueden coincidir con cadenas de diferentes longitudes (por ejemplo, +, *, {2,4}).

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

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

Es posible que haya que 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.

+23

En esta respuesta quiero votar 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

+0

"Es mejor poner la expresión regular en una variable. Algunos patrones no funcionarán si se incluyen literalmente". - ¿Por qué sucede? ¿Hay alguna manera de arreglarlos? –

+2

@FrancescoFrassinelli: Un ejemplo es un patrón que incluye espacios en blanco. Es incómodo escapar y no se pueden usar comillas, ya que eso lo fuerza de una expresión regular a una cadena común. La forma correcta de hacerlo es usar una variable. Las citas se pueden usar durante la tarea haciendo las cosas mucho más simples. –

18

No es posible en tan sólo grep Creo

de sed:

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

voy a tomar una puñalada en el bono sin embargo:

echo "$name.jpg" 
+0

Ah, por supuesto, gracias por eso jaja. – Isaac

+2

Desafortunadamente, esa solución 'sed' no funciona. Simplemente imprime todo en mi directorio. – Isaac

+0

actualizado, dará como resultado una línea en blanco si no hay coincidencia, así que asegúrese de verificar que – cobbal

1

Una sugerencia para usted - usted puede utilice la expansión de parámetros para eliminar la parte del nombre del último subrayado en adelante, y de manera similar al inicio:

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

Entonces name tendrá el valor abc.

Consulte Apple developer docs, busque 'Expansión de parámetros'.

+1

Ah, ahora esto funciona. ¿Pero es * unix-y * suficiente? Hmm ... – Isaac

+0

esto no se verificará para ([a-z] +). – ghostdog74

+0

@levislevis - eso es cierto, pero, como lo comentó el OP, hace lo que se necesita. –

112

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

Pero si su patrón es adecuado, puede usar grep varias 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 como cut y sed son mucho mejores en esto).

Supongamos que por el bien del argumento de que su patrón era un poco más simple: [0-9]+_([a-z]+)_ Se podría extraer este modo:

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

grep La primera sería eliminar las líneas que no responden a su patern en general, la el segundo grep (que tiene --only-matching especificado) mostraría 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 + cut para lograr lo que busca: echo $name | grep {pattern} | cut -d _ -f 2.Esto obtiene cut para analizar la línea en campos dividiendo 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 hacerlo bien, y combinarlas para lograr tareas no triviales, por lo que yo diría que grep + sed etc. es una forma más Unixy de hacer las cosas: -)

+2

'for f in $ files; do name = 'echo $ f | grep -oEi '[0-9] + _ ([a-z] +) _ [0-9a-z] *' | cut -d _ -f 2'; '¡Ajá! – Isaac

+1

utilizando shell, sin necesidad de grep + cut. desperdicio de gastos generales si OP tiene muchos archivos. – ghostdog74

+2

no estoy de acuerdo con esa "filosofía". si puede usar las funciones integradas de la shell sin llamar a comandos externos, su script será mucho más rápido en rendimiento. hay algunas herramientas que se superponen en la función. por ejemplo grep y sed y awk. todos ellos hacen manipulaciones de cadenas, pero awk se destaca sobre todos porque puede hacer mucho más. Prácticamente, todos esos encadenamientos de comandos, como los grep dobles o grep + sed anteriores, se pueden acortar haciéndolos con un proceso awk. – ghostdog74

1

si tiene fiesta, puede utilizar englobamiento 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 
+0

Eso parece intrigante. ¿Podrías agregarle una pequeña explicación? O, si le apetece, enlace a un recurso particularmente perspicaz que lo explique. ¡Gracias! – Isaac

+0

manual de referencia de bash - 3.5.8.1 Coincidencia de patrones – ghostdog74

+1

olvidó el enlace: aquí está http://www.gnu.org/software/bash/manual/bashref.html – ghostdog74

9

Esta es una solución que utiliza gawk. Es algo que me parece que necesito utilizar a menudo así que creé una función para la que

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

utilizar simplemente hacer

$ echo 'hello world' | regex1 'hello\s(.*)' 
world 
67

Soy consciente de que una respuesta ya fue aceptado por esto, pero a partir de una "estricta * nix purist angle "parece que la herramienta correcta para el trabajo es pcregrep, que no parece que se haya mencionado aún. 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 sólo el contenido del grupo de captura 1.

La herramienta pcregrep utiliza todos de la misma sintaxis que se ya lo usé con grep, pero implementa la funcionalidad que necesita.

El parámetro -o funciona igual que la versión grep si está desnudo, pero también acepta un parámetro numérico en pcregrep, lo que indica, que la captura de grupo que desee mostrar.

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

Nota de interés: Puede usar varios argumentos -o para devolver múltiples grupos de captura en el orden en que aparecen en la línea.

+3

'pcregrep' no está disponible por defecto en' Mac OS X', que es lo que OP usa – grebneke

+1

+1 para el liner –

+4

Mi 'pcregrep' no parece entender el dígito después de' -o': " Letra de opción desconocida '1' en "-o1". Tampoco se menciona esa función al mirar 'pcregrep --help' –

Cuestiones relacionadas