Capturar grupos desde un RegEx Grep

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:

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

Hasta ahora (obviamente, para los gurús de shell), $name solo tiene 0, 1 o 2, dependiendo de si grep encontró que el nombre del archivo coincidía con el asunto provisto. Lo que me gustaría es capturar lo que está dentro de los parens ([az]+) y almacenar eso en una variable .

Me gustaría utilizar grep solo, 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 un super-cool bonu , tengo curiosidad sobre 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.

Si usas Bash, ni siquiera tienes que usar grep :

 files="*.jpg" regex="[0-9]+_([az]+)_[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 concordancia 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 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 el segundo y el cuarto ejemplo, haz tu expresión regular así:

 ^[0-9]+_([az]+)_[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]+_([az]+)_[0-9a-z]*$ 

luego, el tercer ejemplo también será eliminado ya que el punto no se encuentra 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 también falla esta coincidencia.

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

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

El operador \K (look-behind de longitud variable) hace coincidir el patrón anterior, 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 \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.

Para hacer que el emparejamiento no distinga entre mayúsculas y minúsculas, se usa el operador (?i) . 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.

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

Pero si su patrón es adecuado, es posible que pueda usar grep varias veces dentro de una tubería para primero reducir 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 por razones de argumento que su patrón es un poco más simple: [0-9]+_([az]+)_ Puede extraer esto de la siguiente manera:

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

La primera grep eliminaría cualquier línea que no coincidiera con su patern general, la segunda 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 echo $name | grep {pattern} | cut -d _ -f 2 : echo $name | grep {pattern} | cut -d _ -f 2 Esto se 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 🙂

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

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

a lo siguiente:

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

para obtener solo los contenidos del grupo de captura 1.

La herramienta pcregrep utiliza la misma syntax que ya ha utilizado con grep , pero implementa la funcionalidad que necesita.

El parámetro -o funciona igual que la versión grep si está vacío, 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 cambio en el script. Simplemente reemplaza una utilidad modular por otra y modifica 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.

No es posible solo en grep, creo

para sed:

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

Voy a echar un vistazo a la bonificación, aunque:

 echo "$name.jpg" 

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

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

para usar solo hazlo

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

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

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

Entonces el name tendrá el valor abc .

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

si tienes bash, puedes usar globbing extendido

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

o

 ls +([0-9])_+([az])_+([a-z0-9]).jpg | while read file do IFS="_" set -- $file echo "This is your captured output : $2" done