Capturer des groupes à partir d'une RegEx Grep

J'ai ce petit script dans sh (Mac OSX 10.6) Pour regarder à travers 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 les gourous shell) $name détient simplement 0, 1 ou 2, selon si grep a trouvé que le nom de fichier correspondait à la matière fournie. ce que je voudrais, c'est capturer ce qui est à l'intérieur des parens ([a-z]+) et le stocker dans une variable.

J'aimerais utiliser grep seulement, si possible. Si non, veuillez ne pas Python ou Perl, etc. {[6] } ou quelque chose comme ça-je suis nouveau à shell et je voudrais attaquer cela sous l'angle puriste *nix.

Aussi, en tant que super-cool bonu s, je suis curieux de savoir comment je peux concaténer une chaîne dans shell? Le groupe, j'ai capturé était la chaîne "abc" stocké dans $nom, et je voulais ajouter la chaîne ".jpg" à la fin de celui-ci, pourrais-je cat $name '.jpg'?

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

281
demandé sur royhowie 2009-12-12 03:55:14

7 réponses

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

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.

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. Index Zéro est la correspondance complète.

Vous devez être conscient que sans ancres, cette expression rationnelle (et celle l'utilisation de grep) correspondra à l'un des exemples suivants et plus encore, ce qui peut ne pas être ce que vous cherchez:

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émarrer, 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 l'expression rationnelle, comme ceci:

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

Alors le troisième exemple sera également éliminé puisque le point n'est pas parmi les les caractères dans l'expression rationnelle et le signe dollar représentent 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, quand 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 (variable-length look-behind) 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 de fermeture. Vous doit utiliser \K si les 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é "look-ahead". 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 modèles qui le suivent de sorte que sa position est significative.

La regex peut avoir besoin d'être ajustée selon que d'autres caractères dans le nom de fichier. Vous noterez 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.

382
répondu Dennis Williamson 2016-06-07 18:45:21

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

Mais si votre modèle est approprié, vous pouvez utiliser grep plusieurs fois dans un pipeline pour réduire d'abord votre ligne à un format connu, puis pour extraire juste le bit que vous voulez. (Bien que des outils comme cut et sed soient bien meilleurs à cela).

Supposons pour des raisons 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 grep serait supprimez toutes les lignes qui ne correspondent pas à votre patern global, le second grep (qui a --only-matching spécifié) afficherait la partie alpha du nom. Cela ne fonctionne que parce que le motif est approprié: "portion alpha" est assez spécifique pour sortir ce que vous voulez.

(mis à part: personnellement, j'utiliserais grep + cut pour réaliser ce que vous recherchez: echo $name | grep {pattern} | cut -d _ -f 2. Cela permet à cut d'analyser la ligne en champs en 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 + sed etc est une façon plus Unixy de faire les choses: -)

122
répondu RobM 2009-12-12 01:26:04

Je me rends compte qu'une réponse a déjà été acceptée pour cela, mais d'un "angle puriste strictement * nix", il semble que le bon outil pour le travail est 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=$?

À 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.

Le pcregrep l'outil utilise la même syntaxe que vous avez déjà utilisé avec grep, mais implémente la fonctionnalité vous avez besoin d'.

Le paramètre -o fonctionne comme la version grep si elle est nue, mais elle accepte également un paramètre numérique dans pcregrep, qui indique le groupe de capture que vous souhaitez afficher.

Avec cette solution, il y a un strict minimum de changement requis dans le script. Vous remplacez simplement un utilitaire modulaire par un autre et modifiez 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.

74
répondu John Sherwood 2013-03-03 17:14:08

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 prendre un coup de couteau au bonus si:

echo "$name.jpg"
22
répondu cobbal 2009-12-12 01:17:33

C'est une solution qui utilise gawk. C'est quelque chose que je trouve que je dois utiliser souvent alors j'ai créé une fonction pour cela

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

Pour utiliser juste faire

$ echo 'hello world' | regex1 'hello\s(.*)'
world
11
répondu opsb 2013-01-09 06:37:31

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

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

Alors {[1] } aura la valeur abc.

Voir Apple developer docs , recherche en avant pour 'parameter Expansion'.

2
répondu martin clayton 2009-12-12 01:16:46

Si vous avez bash, vous pouvez utiliser étendue d'expansion

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
1
répondu ghostdog74 2009-12-12 04:12:25