Como gerar apenas grupos capturados com sed?

277

Existe alguma maneira de dizer sedpara produzir apenas grupos capturados? Por exemplo, dada a entrada:

This is a sample 123 text and some 987 numbers

e padrão:

/([\d]+)/

Eu poderia obter apenas saída 123 e 987 da maneira formatada por referências anteriores?

Pablo
fonte
Observe que a captura de grupo requer sedativar expressões regulares estendidas com o -Esinalizador.
peterh - Restabelece Monica

Respostas:

333

A chave para fazer isso funcionar é dizer sedpara excluir o que você não deseja exibir, bem como especificar o que você deseja.

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

Isto diz:

  • não padrão para imprimir cada linha ( -n)
  • excluir zero ou mais dígitos não
  • inclua um ou mais dígitos
  • excluir um ou mais não dígitos
  • inclua um ou mais dígitos
  • excluir zero ou mais dígitos não
  • imprimir a substituição ( p)

Em geral, em sedvocê captura grupos usando parênteses e gera o que captura usando uma referência anterior:

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

irá produzir "bar". Se você usar -r( -Epara OS X) para regex estendido, não precisará escapar dos parênteses:

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

Pode haver até 9 grupos de captura e suas referências anteriores. As referências anteriores são numeradas na ordem em que os grupos aparecem, mas podem ser usadas em qualquer ordem e podem ser repetidas:

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

gera "uma barra a".

Se você possui o GNU grep(também pode funcionar no BSD, incluindo o OS X):

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

ou variações como:

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

A -Popção habilita expressões regulares compatíveis com Perl. Veja man 3 pcrepatternou man 3 pcresyntax.

Pausado até novo aviso.
fonte
24
Como uma observação, o OSX Mountain Lion não oferece mais suporte ao PCRE no grep.
precisa saber é
1
Como observação, a opção grep -o não é suportada no Solaris 9. Além disso, o Solaris 9 não suporta a opção sed -r. :(
Daniel Kats
7
Peça ao seu administrador de sistema para instalar o gsed. Você ficaria espantado com o que algumas rosquinhas vai chegar ...
AVGVSTVS
3
Note que você pode precisar prefixar '(' e ')' com '\', não sei por quê.
Lumbric
7
@ Lumbric: Se você está se referindo ao sedexemplo, se usa a -ropção (ou -Epara OS X, IIRC), não precisa escapar dos parênteses. A diferença é que entre expressões regulares básicas e expressões regulares estendidas ( -r).
Pausado até novo aviso.
55

O Sed tem até nove padrões lembrados, mas você precisa usar parênteses escapados para lembrar partes da expressão regular.

Veja aqui exemplos e mais detalhes

Peter McG
fonte
58
sed -e 's/version=\(.+\)/\1/' input.txteste ainda irá saída o todo input.txt
Pablo
@ Pablo, no seu padrão você tem que escrever em \+vez de +. E eu não entendo por que as pessoas usam -eapenas um comando sed.
Fredrick Gauss
1
use sed -e -n 's/version=\(.+\)/\1/p' input.txtsee: mikeplate.com/2012/05/09/…
awattar
1
Eu sugiro sed -Eusar as chamadas expressões regulares "modernas" ou "estendidas" que parecem muito mais próximas aos tipos Perl / Java / JavaScript / Go / qualquer que seja. (Compare com grep -Eou egrep.) A sintaxe padrão possui essas regras de escape estranhas e é considerada "obsoleta". Para mais informações sobre as diferenças entre os dois, execute man 7 re_format.
AndrewF
31

você pode usar grep

grep -Eow "[0-9]+" file
ghostdog74
fonte
4
@ ghostdog74: Concordo absolutamente com você. Como posso fazer com que o greo produza apenas grupos capturados?
Pablo
1
@ Michaelo - é por isso que a opção existe - unixhelp.ed.ac.uk/CGI/man-cgi?grep : -o, --only-matching Mostra apenas a parte de uma linha correspondente que corresponde ao PATTERN
Bert F
14
@ Bert F: Eu entendo a parte correspondente, mas não está capturando o grupo. O que eu quero é ter assim ([0-9] +). + ([Abc] {2,3}), para que haja 2 grupos de captura. Eu quero produzir apenas capturando grupos por referências anteriores ou de alguma outra forma.
Pablo
Olá Michael. Você conseguiu extrair o enésimo grupo capturado pelo grep?
DOC_ID
1
@ Pablo: grep está apenas produzindo o que corresponde. Para atribuir vários grupos, use várias expressões: grep -Eow -e "[0-9]+" -e "[abc]{2,3}"Não sei como você pode exigir que essas duas expressões estejam em uma linha, exceto a canalização de um grep anterior (que ainda não funcionaria se um dos padrões corresponder mais de uma vez em uma linha )
Idbrii # 3/10
13

número (s) de dígitos

Esta resposta funciona com qualquer contagem de grupos de dígitos. Exemplo:

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

Resposta expandida.

Existe alguma maneira de dizer ao sed para produzir apenas grupos capturados?

Sim. substitua todo o texto pelo grupo de captura:

$ 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 com sintaxe estendida (menos aspas anteriores e permite o uso de +):

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

Para evitar imprimir o texto original quando não houver número, use:

$ echo 'Number xxx in text' | sed -En 's/[^0-9]*([0-9]+)[^0-9]*/\1/p'
  • (-n) Não imprima a entrada por padrão.
  • (/ p) imprima somente se uma substituição tiver sido feita.

E para combinar vários números (e também imprimi-los):

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

Isso funciona para qualquer contagem de execuções de dígitos:

$ 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

O que é muito semelhante ao comando grep:

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

Sobre \ d

e padrão: /([\d]+)/

O Sed não reconhece a sintaxe '\ d' (atalho). O equivalente ascii usado acima [0-9]não é exatamente equivalente. A única solução alternativa é usar uma classe de caracteres: '[[: digit:]] `.

A resposta selecionada usa essas "classes de caracteres" para criar uma solução:

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

Essa solução funciona apenas para (exatamente) duas execuções de dígitos.

Obviamente, como a resposta está sendo executada dentro do shell, podemos definir algumas variáveis ​​para tornar essa resposta mais curta:

$ 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"

Mas, como já foi explicado, usar um s/…/…/gpcomando é melhor:

$ 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

Isso abrangerá repetidas execuções de dígitos e gravará um comando curto (er).

Isaac
fonte
Surpreso depois de ler a resposta aceita com mais voto, rolei para baixo para escrever sobre seu escopo estreito e realmente abordar o espírito da pergunta. Eu deveria ter imaginado que alguém já teria feito isso anos atrás. Isso está muito bem explicado e é a verdadeira resposta correta.
Amit Naidu
9

Eu acredito que o padrão dado na pergunta foi apenas a título de exemplo, e o objetivo era corresponder a qualquer padrão.

Se você tiver um sed com a extensão GNU permitindo a inserção de uma nova linha no espaço do padrão, uma sugestão é:

> 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

Estes exemplos estão com o tcsh (sim, eu sei que é o shell errado) com o CYGWIN. (Editar: para o bash, remova o conjunto e os espaços ao redor =.)

Joseph Quinsey
fonte
@ Joseph: obrigado, no entanto, com base na minha tarefa, sinto que o grep é mais natural, como sugerido pelo ghostdog74. Só preciso descobrir como fazer com que o grep produza apenas os grupos de captura, não a correspondência inteira.
Pablo
2
Apenas uma observação, mas o sinal de mais '+' significa 'um ou mais', o que eliminaria a necessidade de se repetir nos padrões. Assim, "[0-9] [0-9] *" se tornaria "[0-9] +"
RandomInsano
4
@RandomInsano: Para usar o +, você precisaria escapar dele ou usar a -ropção ( -Epara OS X). Você também pode usar \{1,\}(ou -rou -Esem o escape).
Pausado até novo aviso.
9

Desista e use Perl

Como sednão o corta, vamos jogar a toalha e usar o Perl, pelo menos é LSB, enquanto as grepextensões GNU não são :-)

  • Imprima a peça correspondente inteira, sem grupos correspondentes ou olhando para trás:

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

    Resultado:

    12
    3456
  • Correspondência única por linha, geralmente campos de dados estruturados:

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

    Resultado:

    1
    34

    Com lookbehind:

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

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

    Resultado:

    1 2
    34 56
  • Várias correspondências por linha, geralmente dados não estruturados:

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

    Resultado:

    1 
    34 78

    Com lookbehind:

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

    Resultado:

    1
    3478
Ciro Santilli adicionou uma nova foto
fonte
1
O que você não conseguiu com o final da pergunta: "com sed"?
Moonchild
@ Googlers Moonchild não se importam.
Ciro Santilli escreveu
1
Eu achei isso útil. nem todos os problemas de regex da linha de comando precisam ser resolvidos com o sed.
precisa saber é o seguinte
5

Experimentar

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"

Compreendi isso no 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
fonte
2

Não é o que o OP solicitou (capturando grupos), mas você pode extrair os números usando:

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

Fornece o seguinte:

123
987
Thomas Bratt
fonte