Capture de groupes à partir d'un RegEx Grep


380

J'ai ce petit script dans sh(Mac OSX 10.6) pour parcourir un tableau de fichiers. Google a cessé d'être utile à ce stade:

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

Jusqu'à présent (évidemment, pour vous, gourous du shell), il $namene contient que 0, 1 ou 2, selon grepque le nom de fichier correspond au sujet fourni. Ce que j'aimerais, c'est capturer ce qui est à l'intérieur des parens ([a-z]+)et le stocker dans une variable .

J'aimerais utiliser grepuniquement, si possible . Sinon, s'il vous plaît pas de Python ou Perl, etc. sedou quelque chose comme ça - je suis nouveau dans le shell et je voudrais attaquer cela sous l'angle puriste * nix.

De plus, en tant que bonu super cool , je suis curieux de savoir comment je peux concaténer une chaîne en shell? Est-ce que le groupe que j'ai capturé était la chaîne "un nom" stockée dans $ name, et je voulais ajouter la chaîne ".jpg" à la fin, est-ce possible cat $name '.jpg'?

Veuillez expliquer ce qui se passe, si vous avez le temps.


30
Grep est-il vraiment plus pur unix que sed?
martin clayton

3
Ah, je ne voulais pas suggérer ça. J'espérais juste qu'une solution pourrait être trouvée en utilisant un outil que j'essaie spécifiquement d'apprendre ici. S'il n'est pas possible de résoudre en utilisant grep, alors ce sedserait génial, s'il est possible de résoudre en utilisant sed.
Isaac

3
J'aurais dû mettre un :) sur ce btw ...
martin clayton

Psh, mon cerveau est beaucoup trop frit aujourd'hui haha.
Isaac

2
@martinclayton Ce serait un argument intéressant. Je pense vraiment que sed, (ou ed pour être précis) serait plus ancien (et donc plus pur? Peut-être?) Unix parce que grep tire son nom de l'expression ed g (lobal) / re (expression gular) / p (rint).
ffledgling

Réponses:


500

Si vous utilisez Bash, vous n'avez même pas besoin d'utiliser 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

Il est préférable de mettre l'expression régulière dans une variable. Certains modèles ne fonctionneront pas s'ils sont inclus littéralement.

Ceci utilise =~qui est l'opérateur de correspondance d'expressions régulières de Bash. Les résultats de la correspondance sont enregistrés dans un tableau appelé $BASH_REMATCH. Le premier groupe de capture est stocké dans l'index 1, le second (le cas échéant) dans l'index 2, etc. L'index zéro est la correspondance complète.

Vous devez savoir que sans ancrages, cette expression régulière (et celle qui utilise grep) correspondra à l'un des exemples suivants et plus, ce qui peut ne pas être ce que vous recherchez:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

Pour éliminer les deuxième et quatrième exemples, faites votre regex comme ceci:

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

qui dit que la chaîne doit commencer par un ou plusieurs chiffres. Le carat représente le début de la chaîne. Si vous ajoutez un signe dollar à la fin de l'expression régulière, comme ceci:

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

alors le troisième exemple sera également éliminé car le point ne fait pas partie des caractères de l'expression régulière et le signe dollar représente la fin de la chaîne. Notez que le quatrième exemple échoue également cette correspondance.

Si vous avez GNU grep(environ 2,5 ou plus tard, je pense, lorsque l' \Kopérateur a été ajouté):

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

L' \Kopérateur (recherche de longueur variable) entraîne la correspondance du modèle précédent, mais n'inclut pas la correspondance dans le résultat. L'équivalent de longueur fixe est (?<=)- le motif serait inclus avant la parenthèse fermante. Vous devez utiliser \Ksi quantificateurs peuvent correspondre à des chaînes de longueurs différentes (par exemple +, *, {2,4}).

L' (?=)opérateur correspond à des modèles de longueur fixe ou variable et est appelé "anticipation". Il n'inclut pas non plus la chaîne correspondante dans le résultat.

Afin de rendre la correspondance insensible à la casse, l' (?i)opérateur est utilisé. Il affecte les motifs qui le suivent, sa position est donc significative.

Le regex peut avoir besoin d'être ajusté selon qu'il y a d'autres caractères dans le nom de fichier. Vous remarquerez que dans ce cas, je montre un exemple de concaténation d'une chaîne en même temps que la sous-chaîne est capturée.


48
Dans cette réponse, je veux voter en faveur de la ligne spécifique qui dit "Il est préférable de mettre l'expression régulière dans une variable. Certains modèles ne fonctionneront pas s'ils sont inclus littéralement."
Brandin

5
@FrancescoFrassinelli: Un exemple est un motif qui inclut un espace blanc. Il est difficile de s'échapper et vous ne pouvez pas utiliser de guillemets car cela le force d'une expression régulière à une chaîne ordinaire. La façon correcte de le faire est d'utiliser une variable. Les citations peuvent être utilisées pendant la mission, ce qui simplifie beaucoup les choses.
pause jusqu'à nouvel ordre.

5
/Kopérateur roches.
razz

2
@Brandon: Cela fonctionne. Quelle version de Bash utilisez-vous? Montrez-moi ce que vous faites qui ne fonctionne pas et je peux peut-être vous dire pourquoi.
pause jusqu'à nouvel ordre.

2
@mdelolmo: Ma réponse comprend des informations sur grep. Il a également été accepté par le PO et a beaucoup voté. Merci pour le downvote.
pause jusqu'à nouvel ordre.

145

Ce n'est pas vraiment possible avec pure grep, du moins pas en général.

Mais si votre modèle convient, vous pouvez utiliser grepplusieurs fois dans un pipeline pour réduire d'abord votre ligne à un format connu, puis extraire juste le bit que vous voulez. (Bien que les outils aiment cutet sedsont bien meilleurs dans ce domaine).

Supposons à titre d'argument que votre modèle était un peu plus simple: [0-9]+_([a-z]+)_vous pouvez extraire ceci comme ceci:

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

Le premier grepsupprimerait toutes les lignes qui ne correspondraient pas à votre motif global, le second grep(qui l'a --only-matchingspécifié) afficherait la partie alpha du nom. Cela ne fonctionne que parce que le motif est approprié: la "partie alpha" est suffisamment spécifique pour extraire ce que vous voulez.

(À part: Personnellement, j'utiliserais grep+ cutpour obtenir ce que vous recherchez: echo $name | grep {pattern} | cut -d _ -f 2cela permet cutd'analyser la ligne en champs en la divisant sur le délimiteur _et renvoie uniquement le champ 2 (les numéros de champ commencent à 1)).

La philosophie Unix est d'avoir des outils qui font une chose, et le font bien, et les combinent pour réaliser des tâches non triviales, donc je dirais que grep+ sedetc est une façon plus Unixy de faire les choses :-)


3
for f in $files; do name=écho $ f | grep -oEi '[0-9] + _ ([az] +) _ [0-9a-z] *' | couper -d _ -f 2 ;Aha!
Isaac

2
je ne suis pas d'accord avec cette "philosophie". si vous pouvez utiliser les capacités intégrées du shell sans appeler de commandes externes, alors votre script sera beaucoup plus rapide en termes de performances. il existe certains outils qui se chevauchent dans la fonction. par exemple grep et sed et awk. tous font des manipulations de cordes, mais awk se démarque de tous parce qu'il peut faire beaucoup plus. Pratiquement, tous ces chaînages de commandes, comme les doubles greps ci-dessus ou grep + sed peuvent être raccourcis en les faisant avec un processus awk.
ghostdog74

7
@ ghostdog74: Aucun argument ici selon lequel l'enchaînement de nombreuses opérations minuscules ensemble est généralement moins efficace que de tout faire en un seul endroit, mais je maintiens mon affirmation selon laquelle la philosophie Unix est beaucoup d'outils fonctionnant ensemble. Par exemple, tar archive uniquement les fichiers, il ne les comprime pas, et parce qu'il sort par défaut sur STDOUT, vous pouvez le diriger sur le réseau avec netcat, ou le compresser avec bzip2, etc. Ce qui, à mon avis, renforce la convention et le général éthos que les outils Unix devraient être capables de fonctionner ensemble dans les tuyaux.
RobM

la coupe est géniale - merci pour le conseil! Quant à l'argument outils vs efficacité, j'aime la simplicité des outils de chaînage.
ether_joe du

accessoires pour l'option o de grep, ce qui est très utile
chiliNUT

97

Je me rends compte qu'une réponse a déjà été acceptée pour cela, mais sous un "angle puriste strictement * nix", il semble que le bon outil pour le travail soit pcregrep, ce qui ne semble pas encore avoir été mentionné. Essayez de changer les lignes:

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

aux éléments suivants:

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

pour obtenir uniquement le contenu du groupe de capture 1.

L' pcregrepoutil utilise la même syntaxe que celle que vous avez déjà utilisée grep, mais implémente les fonctionnalités dont vous avez besoin.

Le paramètre -ofonctionne comme la grepversion s'il est nu, mais il accepte également un paramètre numérique dans pcregrep, qui indique le groupe de capture que vous souhaitez afficher.

Avec cette solution, le script requiert un minimum de modifications. Vous remplacez simplement un utilitaire modulaire par un autre et ajustez les paramètres.

Remarque intéressante: vous pouvez utiliser plusieurs arguments -o pour renvoyer plusieurs groupes de capture dans l'ordre dans lequel ils apparaissent sur la ligne.


3
pcregrepn'est pas disponible par défaut dans Mac OS Xlequel l'OP utilise
grebneke

4
Mon pcregrepne semble pas comprendre le chiffre après la -o: "Lettre d'option inconnue '1' dans" -o1 ". Aucune mention de cette fonctionnalité en regardantpcregrep --help
Peter Herdenborg

1
@WAF désolé, je suppose que j'aurais dû inclure cette information dans mon commentaire. Je suis sur CentOS 6.5 et la version pcregrep est apparemment très ancienne: 7.8 2008-09-05.
Peter Herdenborg

2
oui, très aide, par exempleecho 'r123456 foo 2016-03-17' | pcregrep -o1 'r([0-9]+)' 123456
zhuguowei

5
pcregrep8,41 (installé avec apt-get install pcregreple Ubuntu 16.03) ne reconnaît pas le -Eicommutateur. Mais cela fonctionne parfaitement sans. Sur macOS, avec pcregrepinstallé via homebrew(également 8.41) comme le mentionne @anishpatel ci-dessus, au moins sur High Sierra, le -Ecommutateur n'est pas non plus reconnu.
Ville

27

Pas possible en juste grep je crois

pour sed:

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

Je vais essayer le bonus:

echo "$name.jpg"

2
Malheureusement, cette sedsolution ne fonctionne pas. Il imprime simplement tout dans mon répertoire.
Isaac

mis à jour, affichera une ligne vierge s'il n'y a pas de correspondance, alors assurez-vous de vérifier cela
cobbal

Il ne produit désormais que des lignes vides!
Isaac

ce sed a un problème. Le premier groupe de parenthèses de capture englobe tout. Bien sûr, \ 2 n'aura rien.
ghostdog74

cela a fonctionné pour quelques cas de test simples ... \ 2 obtient le groupe interne
cobbal

16

Il s'agit d'une solution qui utilise gawk. C'est quelque chose que je trouve que je dois utiliser souvent, donc j'ai créé une fonction pour cela

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

utiliser juste faire

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

Excellente idée, mais ne semble pas fonctionner avec les espaces dans l'expression régulière - ils doivent être remplacés par \s. Savez-vous comment y remédier?
Adam Ryczkowski

4

Une suggestion pour vous - vous pouvez utiliser l'expansion des paramètres pour supprimer la partie du nom du dernier trait de soulignement, et de la même manière au début:

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

Alors nameaura la valeur abc.

Consultez la documentation des développeurs Apple , recherchez «Extension des paramètres».


cela ne vérifiera pas ([az] +).
ghostdog74

@levislevis - c'est vrai, mais, comme l'a fait remarquer l'OP, il fait ce qui était nécessaire.
martin clayton

2

si vous avez bash, vous pouvez utiliser la globalisation étendue

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

ou

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

Cela semble intrigant. Pourriez-vous peut-être y ajouter une petite explication? Ou, si vous êtes si enclin, un lien vers une ressource particulièrement perspicace qui l'explique? Merci!
Isaac
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.