web-dev-qa-db-fra.com

Capture de groupes à partir d'un RegEx de Grep

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'ici (évidemment pour vous, gourous de Shell) $name ne contient que 0, 1 ou 2, selon que _ grep trouve que le nom du fichier correspond au contenu fourni. Ce que j'aimerais, c'est capturer le contenu des parenthèses ([a-z]+) et le stocker dans une variable.

Je voudrais tiliser grep seulement, si possible. Sinon, veuillez ne pas utiliser Python ou Perl, etc. sed ou quelque chose du genre - je suis nouveau dans Shell et je voudrais attaquer cela sous l'angle puriste * nix.

En outre, en tant que super-cool bon s, je suis curieux de savoir comment je peux concaténer string dans une coque? Est-ce que le groupe que j'ai capturé était la chaîne "somename" stockée dans $ name, et je voulais ajouter la chaîne ".jpg" à la fin, pourrais-je cat $name '.jpg'?

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

343
Isaac

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 rationnelle dans une variable. Certains modèles ne fonctionneront pas s'ils sont inclus littéralement.

Ceci utilise =~ qui est l'opérateur de correspondance des expressions rationnelles 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 deuxième (le cas échéant) dans l'index 2, etc. L'indice zéro correspond à la correspondance complète.

Sachez que sans ancres, cette expression rationnelle (et celle utilisant grep) correspondra à l'un des exemples suivants, et plus encore, ce qui peut ne pas correspondre à 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 début 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, procédez comme suit:

^[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 (environ 2.5 ou plus tard, je pense, lorsque l'opérateur \K a été ajouté):

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

L'opérateur \K (recherche de longueur variable) fait correspondre le 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 \K si les quantificateurs peuvent correspondre à des chaînes de différentes longueurs (par exemple +, *, {2,4}).

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

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

Il peut être nécessaire d’ajuster l’expression rationnelle 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.

454
Dennis Williamson

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

Toutefois, si votre modèle convient, vous pourrez peut-être utiliser plusieurs fois grep dans un pipeline pour réduire votre ligne à un format connu, puis pour extraire le bit souhaité. (Bien que des outils comme cut et sed soient bien meilleurs à cela).

Supposons, par souci d'argument, que votre modèle était un peu plus simple: [0-9]+_([a-z]+)_ Vous pouvez extraire ceci de la manière suivante:

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

Le premier grep supprime toutes les lignes qui ne correspondent pas à votre motif général, le second grep (qui a --only-matching spécifié) affichera la partie alpha du nom. Cela ne fonctionne que parce que le motif est approprié: "partie alpha" est suffisamment spécifique pour extraire ce que vous voulez.

(À part: j'utiliserais personnellement grep + cut pour obtenir ce que vous recherchez: echo $name | grep {pattern} | cut -d _ -f 2. Ceci permet à cut d'analyser la ligne en champs en divisant le délimiteur. _, et ne renvoie que le champ 2 (les numéros de champ commencent par 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 + sed etc est une manière plus Unixy de faire des choses :-)

132
RobM

Je me rends compte qu’une réponse était déjà acceptée pour cela, mais d’un "angle puriste strictement * nix", il semble que le bon outil pour le travail est pcregrep , qui ne semble pas avoir encore été mentionné. 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.

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

Le paramètre -o fonctionne comme la version grep s'il est nu, mais il accepte également un paramètre numérique dans pcregrep qui indique quelle capture. groupe que vous voulez montrer.

Avec cette solution, un minimum de modifications est nécessaire dans le script. Vous remplacez simplement un utilitaire modulaire par un autre et modifiez 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.

87
John Sherwood

Pas possible dans juste grep je crois

pour sed:

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

Je vais tenter le bonus:

echo "$name.jpg"
25
cobbal

C'est une solution qui utilise gawk. C'est quelque chose que j'ai besoin d'utiliser souvent, alors j'ai créé une fonction pour ça

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

utiliser juste faire

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

Une suggestion pour vous - vous pouvez utiliser le paramètre expand pour supprimer la partie du nom à partir 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 name aura la valeur abc.

Voir Apple documentation de développeur , recherchez "Expansion des paramètres".

4
martin clayton

si vous avez bash, vous pouvez utiliser le globbing étendu

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
2
ghostdog74