505 votes

Capturer les groupes à partir d'un RegEx Grep

J'ai ce petit script dans sh (Mac OSX 10.6) pour consulter un ensemble 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 des coquillages) $name tient simplement 0, 1 ou 2, selon si grep a constaté que le nom du fichier correspondait à la matière fournie. Ce que j'aimerais, c'est capturer ce qui se trouve à l'intérieur des parenthèses. ([a-z]+) et le stocker dans une variable .

J'aimerais à utiliser grep seulement, si possible . Dans le cas contraire, veuillez ne pas utiliser Python ou Perl, etc. sed ou quelque chose comme ça - je suis nouveau dans le shell et j'aimerais attaquer cela sous l'angle du puriste *nix.

En outre, en tant que super-cool bonu je suis curieux de savoir comment concaténer des chaînes de caractères dans le shell ? Si le groupe que j'ai capturé était la chaîne "somename" stockée dans $name, et que je voulais ajouter la chaîne ".jpg" à la fin de celle-ci, pourrais-je cat $name '.jpg' ?

S'il vous plaît, expliquez ce qui se passe, si vous avez le temps.

31 votes

Est-ce que grep vraiment unix plus pur que sed ?

7 votes

Ah, je ne voulais pas suggérer ça. J'espérais simplement 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 le problème en utilisant grep entonces sed serait génial, s'il est possible de le résoudre en utilisant sed .

6 votes

J'aurais dû mettre un :) sur cette phrase...

676voto

Dennis Williamson Points 105818

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 la regex dans une variable. Certains motifs ne fonctionneront pas s'ils sont inclus littéralement.

Cela utilise =~ qui est l'opérateur de correspondance regex 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.

Il faut savoir que sans ancres, cette regex (et celle qui utilise la fonction grep ) correspondra à tous les exemples suivants et à d'autres, ce qui n'est peut-être pas 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 avec un ou plusieurs chiffres. Le carat représente le début de la chaîne. Si vous ajoutez un signe dollar à la fin de la regex, comme ceci :

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

alors le troisième exemple sera également éliminé puisque le point ne fait pas partie des caractères de la regex et que 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 (vers 2.5 ou plus tard, je pense, lorsque la \K a été ajouté) :

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

El \K (look-behind de longueur variable) fait correspondre le motif précédent, mais n'inclut pas la correspondance dans le résultat. L'équivalent en longueur fixe est (?<=) - le motif serait inclus avant la parenthèse fermante. Vous devez utiliser \K si les quantificateurs peuvent correspondre à des chaînes de caractères de différentes longueurs (par ex. + , * , {2,4} ).

El (?=) correspond à des motifs de longueur fixe ou variable et est appelé "look-ahead". Il n'inclut pas non plus la chaîne trouvée dans le résultat.

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

La regex peut devoir être ajustée en fonction de la présence d'autres caractères dans le nom du 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.

78 votes

Dans cette réponse, je veux upvote la ligne spécifique qui dit "Il est préférable de mettre le regex dans une variable. Certains motifs ne fonctionneront pas s'ils sont inclus littéralement."

2 votes

"Il est préférable de mettre la regex dans une variable. Certains motifs ne fonctionneront pas s'ils sont inclus littéralement." - Pourquoi cela se produit-il ? Y a-t-il un moyen de les corriger ?

7 votes

@FrancescoFrassinelli : Un exemple est un modèle qui inclut des espaces blancs. Il est difficile de l'échapper et vous ne pouvez pas utiliser de guillemets car cela le fait passer d'une regex à une chaîne ordinaire. La manière correcte de le faire est d'utiliser une variable. Les guillemets peuvent être utilisés pendant l'affectation, ce qui simplifie considérablement les choses.

182voto

RobM Points 2681

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

Mais si votre modèle est adapté, vous pourrez peut-être utiliser grep plusieurs fois dans un pipeline pour d'abord réduire votre ligne à un format connu, et ensuite extraire seulement le bit que vous voulez. (Bien que des outils comme cut y sed sont bien meilleurs dans ce domaine).

Supposons, à titre d'exemple, que votre modèle soit un peu plus simple : [0-9]+_([a-z]+)_ Vous pourriez l'extraire comme suit :

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

Le premier grep enlèverait toutes les lignes qui ne correspondent pas à votre paternité globale, la seconde grep (qui a --only-matching spécifié) afficherait la partie alpha du nom. Cela ne fonctionne que parce que le modèle est approprié : "portion alpha" est suffisamment spécifique pour faire ressortir ce que vous voulez.

(A propos : Personnellement, j'utiliserais grep + cut pour atteindre ce que vous recherchez : echo $name | grep {pattern} | cut -d _ -f 2 . Cela permet cut pour analyser la ligne en champs en la divisant par le délimiteur. _ et renvoie uniquement le champ 2 (les numéros de champ commencent à 1)).

La philosophie d'Unix est de disposer d'outils qui font une chose, et qui la font bien, et de les combiner pour réaliser des tâches non triviales. grep + sed etc est une façon plus Unixy de faire les choses :-)

4 votes

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

4 votes

Je ne suis pas d'accord avec cette "philosophie". Si vous pouvez utiliser les capacités intégrées de l'interpréteur de commandes sans appeler de commandes externes, alors votre script sera beaucoup plus rapide en termes de performance. il y a quelques outils qui se chevauchent dans leur fonction. par exemple grep et sed et awk. tous font des manipulations de chaînes de caractères, mais awk se distingue au-dessus de tous parce qu'il peut faire beaucoup plus. En pratique, tous ces enchaînements de commandes, comme les doubles greps ci-dessus ou grep+sed peuvent être raccourcis en les faisant avec un seul processus awk.

10 votes

@ghostdog74 : Je ne discute pas le fait que l'enchaînement d'un grand nombre de petites opérations est généralement moins efficace que de tout faire en un seul endroit, mais je maintiens mon affirmation que la philosophie d'Unix est que beaucoup d'outils travaillent ensemble. Par exemple, tar se contente d'archiver les fichiers, il ne les compresse pas, et comme il sort sur STDOUT par défaut, vous pouvez le faire passer sur le réseau avec netcat, ou le compresser avec bzip2, etc. Ce qui, à mon avis, renforce la convention et l'éthique générale selon lesquelles les outils Unix devraient pouvoir travailler ensemble dans des pipes.

123voto

John Sherwood Points 191

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

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

à ce qui suit :

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

pour obtenir uniquement le contenu du groupe de capture 1.

El pcregrep utilise la même syntaxe que celle que vous avez déjà utilisée avec l'outil grep mais met en œuvre la fonctionnalité dont vous avez besoin.

Le paramètre -o fonctionne comme le grep s'il est nu, mais il accepte aussi un paramètre numérique en pcregrep qui indique le groupe de capture que vous souhaitez afficher.

Avec cette solution, il n'y a qu'un minimum de changement à faire dans le script. Il suffit de remplacer un utilitaire modulaire par un autre et de modifier les paramètres.

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

6 votes

pcregrep n'est pas disponible par défaut dans Mac OS X qui est ce que l'OP utilise

5 votes

Mon pcregrep n'a pas l'air de comprendre le chiffre après le -o : "Lettre d'option inconnue '1' dans "-o1". De plus, cette fonction n'est pas mentionnée dans le document pcregrep --help

0 votes

Je ne peux pas le reproduire. probablement l'impl de ceci pcregrep est différent. Pourriez-vous nous fournir plus d'informations ? grep -P ? même pas dans la page de manuel : linux.die.net/man/1/pcregrep

41voto

cobbal Points 37900

Pas possible avec juste grep je crois

pour sed :

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

Je vais quand même tenter le coup avec le bonus :

echo "$name.jpg"

4 votes

Malheureusement, cela sed ne fonctionne pas. Elle imprime simplement tout ce qui se trouve dans mon répertoire.

0 votes

Mis à jour, produira une ligne blanche s'il n'y a pas de correspondance, alors assurez-vous de le vérifier.

0 votes

Il ne produit maintenant que des lignes vides !

19voto

opsb Points 6860

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

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

pour l'utiliser, il suffit de faire

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

0 votes

Bonne idée, mais cela ne semble pas fonctionner avec les espaces dans la regexp - ils doivent être remplacés par \s . Savez-vous comment le réparer ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X