comment utiliser sed, awk ou gawk pour n'imprimer que ce qui correspond?

100

Je vois beaucoup d'exemples et de pages de manuel sur la façon de faire des choses comme rechercher et remplacer en utilisant sed, awk ou gawk.

Mais dans mon cas, j'ai une expression régulière que je souhaite exécuter sur un fichier texte pour extraire une valeur spécifique. Je ne veux pas faire de recherche et de remplacement. Ceci est appelé de bash. Prenons un exemple:

Exemple d'expression régulière:

.*abc([0-9]+)xyz.*

Exemple de fichier d'entrée:

a
b
c
abc12345xyz
a
b
c

Aussi simple que cela puisse paraître, je ne peux pas comprendre comment appeler correctement sed / awk / gawk. Ce que j'espérais faire, c'est de l'intérieur de mon script bash avoir:

myvalue=$( sed <...something...> input.txt )

Les choses que j'ai essayées incluent:

sed -e 's/.*([0-9]).*/\\1/g' example.txt # extracts the entire input file
sed -n 's/.*([0-9]).*/\\1/g' example.txt # extracts nothing
Stéphane
la source
10
Wow ... les gens ont voté contre cette question -1? Est-ce vraiment inapproprié de poser une question?
Stéphane
Cela semble parfaitement approprié, utiliser Regex et de puissants utilitaires de ligne de commande comme sed / awk ou n'importe quel éditeur comme vi, emacs ou teco peut être plus comme de la programmation que simplement utiliser une ancienne application. IMO cela appartient à SO plus que SU.
Sorti
Peut-être a-t-il été rejeté parce que dans sa forme initiale, il ne définissait pas clairement certaines de ses exigences. Ce n'est toujours pas le cas, à moins que vous ne lisiez les commentaires du PO sur les réponses (y compris celui que j'ai supprimé lorsque les choses ont pris la forme d'une poire).
pavium

Réponses:

42

Mon sed(Mac OS X) ne fonctionnait pas avec +. J'ai essayé à la *place et j'ai ajouté une pétiquette pour l'impression du match:

sed -n 's/^.*abc\([0-9]*\)xyz.*$/\1/p' example.txt

Pour faire correspondre au moins un caractère numérique sans +, j'utiliserais:

sed -n 's/^.*abc\([0-9][0-9]*\)xyz.*$/\1/p' example.txt
mouviciel
la source
Merci, cela a fonctionné pour moi aussi une fois que j'ai utilisé * au lieu de +.
Stéphane
2
... et l'option "p" pour imprimer le match, que je ne connaissais pas non plus. Merci encore.
Stéphane
2
J'ai dû m'échapper +et puis cela a fonctionné pour moi:sed -n 's/^.*abc\([0-9]\+\)xyz.*$/\1/p'
Pause jusqu'à nouvel ordre.
3
C'est parce que vous n'utilisez pas le format RE moderne, donc + est un caractère standard et vous êtes censé l'exprimer avec la syntaxe {,}. Vous pouvez ajouter l'option use -E sed pour déclencher le format RE moderne. Vérifiez re_format (7), en particulier le dernier paragraphe de DESCRIPTION developer.apple.com/library/mac/#documentation/Darwin/Reference/…
anddam
33

Vous pouvez utiliser sed pour ce faire

 sed -rn 's/.*abc([0-9]+)xyz.*/\1/gp'
  • -n ne pas imprimer la ligne résultante
  • -rcela fait en sorte que vous n'ayez pas l'échappement des parens du groupe de capture ().
  • \1 le match de groupe de capture
  • /g match global
  • /p imprimer le résultat

J'ai écrit un outil pour moi-même qui facilite cela

rip 'abc(\d+)xyz' '$1'
Ilia Choly
la source
3
C'est de loin la réponse la meilleure et la mieux expliquée à ce jour!
Nik Reiman
Avec quelques explications, il est préférable de comprendre ce qui ne va pas avec notre problème. Je vous remercie !
r4phG
17

J'utilise perlpour me faciliter la tâche. par exemple

perl -ne 'print $1 if /.*abc([0-9]+)xyz.*/'

Cela exécute Perl, l' -noption demande à Perl de lire une ligne à la fois depuis STDIN et d'exécuter le code. L' -eoption spécifie l'instruction à exécuter.

L'instruction exécute une expression rationnelle sur la ligne lue et, si elle correspond, affiche le contenu du premier ensemble de crochets ( $1).

Vous pouvez le faire avec plusieurs noms de fichiers à la fin également. par exemple

perl -ne 'print $1 if /.*abc([0-9]+)xyz.*/' example1.txt example2.txt

PP.
la source
Merci, mais nous n'avons pas accès à perl, c'est pourquoi je posais une question sur sed / awk / gawk.
Stéphane
5

Si votre version de le grepprend en charge, vous pouvez utiliser l' -ooption pour n'imprimer que la partie de toute ligne qui correspond à votre expression régulière.

Sinon, voici le meilleur que sedje pourrais trouver:

sed -e '/[0-9]/!d' -e 's/^[^0-9]*//' -e 's/[^0-9]*$//'

... qui supprime / saute sans chiffres et, pour les lignes restantes, supprime tous les caractères non numériques de début et de fin. (Je suppose seulement que votre intention est d'extraire le numéro de chaque ligne qui en contient un).

Le problème avec quelque chose comme:

sed -e 's/.*\([0-9]*\).*/&/' 

.... ou

sed -e 's/.*\([0-9]*\).*/\1/'

... est que sedne prend en charge que la correspondance "gourmande" ... donc le premier. * correspondra au reste de la ligne. À moins que nous ne puissions utiliser une classe de caractères annulée pour obtenir une correspondance non gourmande ... ou une version de sedavec Perl compatible ou d'autres extensions de ses expressions rationnelles, nous ne pouvons pas extraire une correspondance de motif précise à partir de l'espace de motif (une ligne ).

Jim Dennis
la source
Vous pouvez simplement combiner deux de vos sedcommandes de cette manière:sed -n 's/[^0-9]*\([0-9]\+\).*/\1/p'
pause jusqu'à nouvel ordre.
Auparavant, je ne connaissais pas l'option -o sur grep. Bon à savoir. Mais il imprime la correspondance entière, pas le "(...)". Donc, si vous correspondez sur "abc ([[: digit:]] +) xyz" alors vous obtenez le "abc" et "xyz" ainsi que les chiffres.
Stéphane
Merci de me le rappeler grep -o! J'essayais de le faire sedet j'ai eu du mal avec mon besoin de trouver plusieurs correspondances sur certaines lignes. Ma solution est stackoverflow.com/a/58308239/117471
Bruno Bronosky
3

Vous pouvez utiliser awkavec match()pour accéder au groupe capturé:

$ awk 'match($0, /abc([0-9]+)xyz/, matches) {print matches[1]}' file
12345

Cela tente de correspondre au modèle abc[0-9]+xyz. Si tel est le cas, il stocke ses tranches dans le tableau matches, dont le premier élément est le bloc [0-9]+. Puisque match() renvoie la position du caractère, ou l'index, d'où commence cette sous-chaîne (1, si elle commence au début de la chaîne) , il déclenche l' printaction.


Avec, grepvous pouvez utiliser un regard en arrière et un regard en avant:

$ grep -oP '(?<=abc)[0-9]+(?=xyz)' file
12345

$ grep -oP 'abc\K[0-9]+(?=xyz)' file
12345

Cela vérifie le modèle [0-9]+lorsqu'il se produit dans abcet xyzet imprime simplement les chiffres.

fedorqui 'Alors arrêtez de nuire'
la source
2

perl est la syntaxe la plus propre, mais si vous n'avez pas perl (pas toujours là, je comprends), alors la seule façon d'utiliser gawk et les composants d'une regex est d'utiliser la fonction gensub.

gawk '/abc[0-9]+xyz/ { print gensub(/.*([0-9]+).*/,"\\1","g"); }' < file

la sortie de l'échantillon de fichier d'entrée sera

12345

Remarque: gensub remplace l'ensemble de l'expression régulière (entre //), donc vous devez mettre le. * Avant et après le ([0-9] +) pour supprimer le texte avant et après le nombre dans la substitution.

Mark Lakata
la source
2
Une solution intelligente et réalisable si vous devez (ou souhaitez) utiliser gawk. Vous l'avez noté, mais pour être clair: awk non-GNU n'a pas gensub (), et ne le supporte donc pas.
cincodenada
Agréable! Cependant, il peut être préférable d'utiliser match()pour accéder aux groupes capturés. Voir ma réponse à ce sujet.
fedorqui 'SO arrêtez de nuire'
1

Si vous voulez sélectionner des lignes, supprimez les bits que vous ne voulez pas:

egrep 'abc[0-9]+xyz' inputFile | sed -e 's/^.*abc//' -e 's/xyz.*$//'

Il sélectionne essentiellement les lignes que vous voulez avec egrep, puis les utilise sedpour supprimer les bits avant et après le nombre.

Vous pouvez voir cela en action ici:

pax> echo 'a
b
c
abc12345xyz
a
b
c' | egrep 'abc[0-9]+xyz' | sed -e 's/^.*abc//' -e 's/xyz.*$//'
12345
pax> 

Mise à jour: évidemment si votre situation actuelle est plus complexe, les RE devront être modifiés. Par exemple, si vous aviez toujours un seul nombre enterré entre zéro ou plusieurs non-numériques au début et à la fin:

egrep '[^0-9]*[0-9]+[^0-9]*$' inputFile | sed -e 's/^[^0-9]*//' -e 's/[^0-9]*$//'
paxdiablo
la source
Intéressant ... Il n'y a donc pas de moyen simple d'appliquer une expression régulière complexe et de récupérer ce qu'il y a dans la section (...)? Parce que si je vois ce que vous avez fait ici d'abord avec grep puis avec sed, notre situation réelle est beaucoup plus complexe que de laisser tomber "abc" et "xyz". L'expression régulière est utilisée car de nombreux textes différents peuvent apparaître de chaque côté du texte que je souhaite extraire.
Stéphane
Je suis sûr qu'il existe un meilleur moyen si les RE sont vraiment complexes. Peut-être que si vous fournissez quelques exemples supplémentaires ou une description plus détaillée, nous pourrions ajuster nos réponses en conséquence.
paxdiablo
0

Le cas de l'OP ne spécifie pas qu'il peut y avoir plusieurs correspondances sur une seule ligne, mais pour le trafic Google, j'ajouterai également un exemple pour cela.

Puisque le besoin du PO est d'extraire un groupe d'un motif, l'utilisation grep -onécessitera 2 passes. Mais je trouve toujours que c'est le moyen le plus intuitif de faire le travail.

$ cat > example.txt <<TXT
a
b
c
abc12345xyz
a
abc23451xyz asdf abc34512xyz
c
TXT

$ cat example.txt | grep -oE 'abc([0-9]+)xyz'
abc12345xyz
abc23451xyz
abc34512xyz

$ cat example.txt | grep -oE 'abc([0-9]+)xyz' | grep -oE '[0-9]+'
12345
23451
34512

Étant donné que le temps processeur est fondamentalement gratuit mais que la lisibilité humaine n'a pas de prix, j'ai tendance à refactoriser mon code en me basant sur la question "dans un an, qu'est-ce que je vais penser que cela fait?" En fait, pour le code que j'ai l'intention de partager publiquement ou avec mon équipe, je vais même ouvrir man greppour comprendre quelles sont les options longues et les remplacer. Ainsi:grep --only-matching --extended-regexp

Bruno Bronosky
la source
-1

vous pouvez le faire avec la coque

while read -r line
do
    case "$line" in
        *abc*[0-9]*xyz* ) 
            t="${line##abc}"
            echo "num is ${t%%xyz}";;
    esac
done <"file"
ghostdog74
la source
-3

Pour awk. J'utiliserais le script suivant:

/.*abc([0-9]+)xyz.*/ {
            print $0;
            next;
            }
            {
            /* default, do nothing */
            }
Pierre
la source
Cela ne produit pas la valeur numérique ([0-9+]), cela génère la ligne entière.
Mark Lakata
-3
gawk '/.*abc([0-9]+)xyz.*/' file
ghostdog74
la source
2
Cela ne semble pas fonctionner. Il imprime la ligne entière au lieu de la correspondance.
Stéphane
dans votre exemple de fichier d'entrée, ce modèle est la ligne entière. droite??? si vous savez que le motif va être dans un champ spécifique: utilisez $ 1, $ 2 etc. par exemple gawk '$ 1 ~ /.*abc([0-9ITED+)xyz.*/' file
ghostdog74