Comment sortir uniquement les groupes capturés avec sed?

278

Existe-t-il un moyen de dire sedde sortir uniquement les groupes capturés? Par exemple, étant donné l'entrée:

This is a sample 123 text and some 987 numbers

et motif:

/([\d]+)/

Puis-je obtenir uniquement une sortie 123 et 987 de la manière formatée par des références arrières?

Pablo
la source
Remarque: la capture de groupe nécessite sedd'activer les expressions régulières étendues avec l' -Eindicateur.
peterh

Réponses:

333

La clé pour que cela fonctionne est de dire sedd'exclure ce que vous ne voulez pas sortir et de spécifier ce que vous voulez.

string='This is a sample 123 text and some 987 numbers'
echo "$string" | sed -rn 's/[^[:digit:]]*([[:digit:]]+)[^[:digit:]]+([[:digit:]]+)[^[:digit:]]*/\1 \2/p'

Cela dit:

  • ne pas imprimer par défaut chaque ligne ( -n)
  • exclure zéro ou plusieurs chiffres
  • inclure un ou plusieurs chiffres
  • exclure un ou plusieurs chiffres
  • inclure un ou plusieurs chiffres
  • exclure zéro ou plusieurs chiffres
  • imprimer la substitution ( p)

En général, dans sedvous capturez des groupes à l'aide de parenthèses et sortez ce que vous capturez à l'aide d'une référence arrière:

echo "foobarbaz" | sed 's/^foo\(.*\)baz$/\1/'

affichera "bar". Si vous utilisez -r( -Epour OS X) pour l'expression régulière étendue, vous n'avez pas besoin d'échapper aux parenthèses:

echo "foobarbaz" | sed -r 's/^foo(.*)baz$/\1/'

Il peut y avoir jusqu'à 9 groupes de capture et leurs références arrières. Les références arrières sont numérotées dans l'ordre d'apparition des groupes, mais elles peuvent être utilisées dans n'importe quel ordre et peuvent être répétées:

echo "foobarbaz" | sed -r 's/^foo(.*)b(.)z$/\2 \1 \2/'

affiche "une barre a".

Si vous avez GNU grep(il peut également fonctionner dans BSD, y compris OS X):

echo "$string" | grep -Po '\d+'

ou des variations telles que:

echo "$string" | grep -Po '(?<=\D )(\d+)'

L' -Poption active les expressions régulières compatibles Perl. Voir man 3 pcrepatternou man 3 pcresyntax.

En pause jusqu'à nouvel ordre.
la source
24
Remarque: OSX Mountain Lion ne prend plus en charge PCRE dans grep.
yincrash
1
En remarque, l'option grep -o n'est pas prise en charge sur Solaris 9. En outre, Solaris 9 ne prend pas en charge l'option sed -r. :(
Daniel Kats
7
Demandez à votre administrateur système d'installer gsed. Vous seriez étonné de ce que quelques beignets vous procureront ...
avgvstvs
3
Notez que vous pourriez avoir besoin de préfixer les '(' et ')' avec '\', je ne sais pas pourquoi.
lumbric
7
@lumbric: Si vous faites référence à l' sedexemple, si vous utilisez l' -roption (ou -Epour OS X, IIRC), vous n'avez pas besoin d'échapper aux parenthèses. La différence est celle entre les expressions régulières de base et les expressions régulières étendues ( -r).
pause jusqu'à nouvel ordre.
55

Sed a jusqu'à neuf modèles mémorisés, mais vous devez utiliser des parenthèses d'échappement pour mémoriser des parties de l'expression régulière.

Voir ici pour des exemples et plus de détails

Peter McG
la source
58
sed -e 's/version=\(.+\)/\1/' input.txtcela affichera toujours le fichier input.txt entier
Pablo
@Pablo, Dans votre modèle, vous devez écrire \+au lieu de +. Et je ne comprends pas pourquoi les gens utilisent -epour une seule commande sed.
Fredrick Gauss
1
utiliser sed -e -n 's/version=\(.+\)/\1/p' input.txtvoir: mikeplate.com/2012/05/09/…
awattar
1
Je suggère sed -Ed'utiliser les expressions régulières dites "modernes" ou "étendues" qui ressemblent beaucoup plus à Perl / Java / JavaScript / Go / quelles que soient les saveurs. (Comparez avec grep -Eou egrep.) La syntaxe par défaut a ces étranges règles d'échappement et est considérée comme "obsolète". Pour plus d'informations sur les différences entre les deux, exécutez man 7 re_format.
AndrewF
31

vous pouvez utiliser grep

grep -Eow "[0-9]+" file
ghostdog74
la source
4
@ ghostdog74: Tout à fait d'accord avec vous. Comment puis-je faire en sorte que greo affiche uniquement les groupes capturés?
Pablo
1
@Michael - c'est pourquoi l' ooption est là - unixhelp.ed.ac.uk/CGI/man-cgi?grep : -o, --only-matching N'afficher que la partie d'une ligne correspondante qui correspond au MOTIF
Bert F
14
@Bert F: Je comprends la partie correspondante, mais ce n'est pas un groupe de capture. Ce que je veux, c'est avoir comme ça ([0-9] +). + ([Abc] {2,3}) donc il y a 2 groupes de capture. Je veux produire UNIQUEMENT des groupes de capture par des références inverses ou d'une autre manière.
Pablo
Bonjour Michael. Avez-vous réussi à extraire le nième groupe capturé par grep?
doc_id
1
@Pablo: grep ne produit que ce qui correspond. Pour lui donner plusieurs groupes, utilisez plusieurs expressions: grep -Eow -e "[0-9]+" -e "[abc]{2,3}"je ne sais pas comment vous pourriez exiger que ces deux expressions soient sur une seule ligne en dehors de la tuyauterie d'un grep précédent (qui pourrait toujours ne pas fonctionner si l'un des motifs correspond plus d'une fois sur une ligne ).
idbrii
13

série (s) de chiffres

Cette réponse fonctionne avec n'importe quel nombre de groupes de chiffres. Exemple:

$ echo 'Num123that456are7899900contained0018166intext' |
> sed -En 's/[^0-9]*([0-9]{1,})[^0-9]*/\1 /gp'
123 456 7899900 0018166

Réponse élargie.

Existe-t-il un moyen de dire à sed de sortir uniquement les groupes capturés?

Oui. remplacer tout le texte par le groupe de capture:

$ echo 'Number 123 inside text' | sed 's/[^0-9]*\([0-9]\{1,\}\)[^0-9]*/\1/'
123

s/[^0-9]*                           # several non-digits
         \([0-9]\{1,\}\)            # followed by one or more digits
                        [^0-9]*     # and followed by more non-digits.
                               /\1/ # gets replaced only by the digits.

Ou avec une syntaxe étendue (moins de guillemets et autorise l'utilisation de +):

$ echo 'Number 123 in text' | sed -E 's/[^0-9]*([0-9]+)[^0-9]*/\1/'
123

Pour éviter d'imprimer le texte d'origine lorsqu'il n'y a pas de numéro, utilisez:

$ echo 'Number xxx in text' | sed -En 's/[^0-9]*([0-9]+)[^0-9]*/\1/p'
  • (-n) N'imprime pas l'entrée par défaut.
  • (/ p) imprimer uniquement si un remplacement a été effectué.

Et pour faire correspondre plusieurs nombres (et aussi les imprimer):

$ echo 'N 123 in 456 text' | sed -En 's/[^0-9]*([0-9]+)[^0-9]*/\1 /gp'
123 456

Cela fonctionne pour n'importe quel nombre d'exécutions de chiffres:

$ str='Test Num(s) 123 456 7899900 contained as0018166df in text'
$ echo "$str" | sed -En 's/[^0-9]*([0-9]{1,})[^0-9]*/\1 /gp'
123 456 7899900 0018166

Ce qui est très similaire à la commande grep:

$ str='Test Num(s) 123 456 7899900 contained as0018166df in text'
$ echo "$str" | grep -Po '\d+'
123
456
7899900
0018166

À propos \ d

et motif: /([\d]+)/

Sed ne reconnaît pas la syntaxe '\ d' (raccourci). L'équivalent ascii utilisé ci [0-9]- dessus n'est pas exactement équivalent. La seule solution alternative consiste à utiliser une classe de caractères: '[[: digit:]] `.

La réponse sélectionnée utilise ces "classes de caractères" pour construire une solution:

$ str='This is a sample 123 text and some 987 numbers'
$ echo "$str" | sed -rn 's/[^[:digit:]]*([[:digit:]]+)[^[:digit:]]+([[:digit:]]+)[^[:digit:]]*/\1 \2/p'

Cette solution ne fonctionne que pour (exactement) deux séries de chiffres.

Bien sûr, comme la réponse est exécutée à l'intérieur du shell, nous pouvons définir quelques variables pour raccourcir cette réponse:

$ str='This is a sample 123 text and some 987 numbers'
$ d=[[:digit:]]     D=[^[:digit:]]
$ echo "$str" | sed -rn "s/$D*($d+)$D+($d+)$D*/\1 \2/p"

Mais, comme cela a déjà été expliqué, l'utilisation d'une s/…/…/gpcommande est préférable:

$ str='This is 75577 a sam33ple 123 text and some 987 numbers'
$ d=[[:digit:]]     D=[^[:digit:]]
$ echo "$str" | sed -rn "s/$D*($d+)$D*/\1 /gp"
75577 33 123 987

Cela couvrira les deux séries répétées de chiffres et l'écriture d'une commande courte (er).

Isaac
la source
Surpris après avoir lu la réponse acceptée au vote élevé, j'ai fait défiler vers le bas pour écrire sur sa portée étroite et pour répondre à l'esprit de la question. J'aurais dû deviner que quelqu'un l'aurait déjà fait il y a des années. Ceci est très bien expliqué et c'est la vraie bonne réponse.
Amit Naidu
9

Je crois que le modèle donné dans la question était à titre d'exemple uniquement, et le but était de correspondre à n'importe quel modèle.

Si vous avez un sed avec l'extension GNU permettant l'insertion d'une nouvelle ligne dans l'espace de motif, une suggestion est:

> set string = "This is a sample 123 text and some 987 numbers"
>
> set pattern = "[0-9][0-9]*"
> echo $string | sed "s/$pattern/\n&\n/g" | sed -n "/$pattern/p"
123
987
> set pattern = "[a-z][a-z]*"
> echo $string | sed "s/$pattern/\n&\n/g" | sed -n "/$pattern/p"
his
is
a
sample
text
and
some
numbers

Ces exemples sont avec tcsh (oui, je sais que c'est le mauvais shell) avec CYGWIN. (Modifier: pour bash, supprimez l'ensemble et les espaces autour de =.)

Joseph Quinsey
la source
@Joseph: merci, cependant, sur la base de ma tâche, j'ai l'impression que grep est plus naturel, comme l'a suggéré ghostdog74. Juste besoin de comprendre comment faire sortir grep uniquement les groupes de capture, pas la correspondance entière.
Pablo
2
Juste une note, mais le signe plus «+» signifie «un ou plusieurs», ce qui éliminerait le besoin de vous répéter dans les motifs. Donc, "[0-9] [0-9] *" deviendrait "[0-9] +"
RandomInsano
4
@RandomInsano: Pour utiliser le +, vous devez l'échapper ou utiliser l' -roption ( -Epour OS X). Vous pouvez également utiliser \{1,\}(ou -rou -Esans l'échappement).
pause jusqu'à nouvel ordre.
9

Abandonnez et utilisez Perl

Puisque sedne le coupe pas, jetons simplement la serviette et utilisons Perl, au moins c'est LSB alors que les grepextensions GNU ne le sont pas :-)

  • Imprimez l'intégralité de la partie correspondante, pas de groupes correspondants ou de recherche nécessaire:

    cat <<EOS | perl -lane 'print m/\d+/g'
    a1 b2
    a34 b56
    EOS

    Production:

    12
    3456
  • Correspondance unique par ligne, champs de données souvent structurés:

    cat <<EOS | perl -lape 's/.*?a(\d+).*/$1/g'
    a1 b2
    a34 b56
    EOS

    Production:

    1
    34

    Avec lookbehind:

    cat <<EOS | perl -lane 'print m/(?<=a)(\d+)/'
    a1 b2
    a34 b56
    EOS
  • Champs multiples:

    cat <<EOS | perl -lape 's/.*?a(\d+).*?b(\d+).*/$1 $2/g'
    a1 c0 b2 c0
    a34 c0 b56 c0
    EOS

    Production:

    1 2
    34 56
  • Correspondances multiples par ligne, données souvent non structurées:

    cat <<EOS | perl -lape 's/.*?a(\d+)|.*/$1 /g'
    a1 b2
    a34 b56 a78 b90
    EOS

    Production:

    1 
    34 78

    Avec lookbehind:

    cat EOS<< | perl -lane 'print m/(?<=a)(\d+)/g'
    a1 b2
    a34 b56 a78 b90
    EOS

    Production:

    1
    3478
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
la source
1
Que n'avez-vous pas obtenu à la fin de la question: "avec sed"?
Moonchild
@Moonchild Googlers s'en fiche.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1
j'ai trouvé cela utile. tous les problèmes de regex en ligne de commande ne doivent pas être résolus avec sed.
PPPaul
5

Essayer

sed -n -e "/[0-9]/s/^[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\).*$/\1 \2 \3 \4 \5 \6 \7 \8 \9/p"

J'ai obtenu ceci sous cygwin:

$ (echo "asdf"; \
   echo "1234"; \
   echo "asdf1234adsf1234asdf"; \
   echo "1m2m3m4m5m6m7m8m9m0m1m2m3m4m5m6m7m8m9") | \
  sed -n -e "/[0-9]/s/^[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\).*$/\1 \2 \3 \4 \5 \6 \7 \8 \9/p"

1234
1234 1234
1 2 3 4 5 6 7 8 9
$
Bert F
la source
2

Ce n'est pas ce que l'OP a demandé (capture de groupes) mais vous pouvez extraire les chiffres en utilisant:

S='This is a sample 123 text and some 987 numbers'
echo "$S" | sed 's/ /\n/g' | sed -r '/([0-9]+)/ !d'

Donne ce qui suit:

123
987
Thomas Bratt
la source