como usar sed, awk ou gawk para imprimir apenas o que é correspondido?

100

Vejo muitos exemplos e páginas de manual sobre como fazer coisas como pesquisar e substituir usando sed, awk ou gawk.

Mas, no meu caso, tenho uma expressão regular que desejo executar em um arquivo de texto para extrair um valor específico. Não quero pesquisar e substituir. Isso está sendo chamado de bash. Vamos usar um exemplo:

Expressão regular de exemplo:

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

Arquivo de entrada de exemplo:

a
b
c
abc12345xyz
a
b
c

Por mais simples que pareça, não consigo descobrir como chamar o sed / awk / gawk corretamente. O que eu esperava fazer é de dentro do meu script bash:

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

As coisas que experimentei incluem:

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
fonte
10
Uau ... as pessoas votaram nesta questão -1? É realmente uma pergunta tão inadequada?
Stéphane de
Parece perfeitamente apropriado, usar Regex e utilitários de linha de comando poderosos como sed / awk ou qualquer editor como vi, emacs ou teco pode ser mais parecido com programação do que apenas usar algum aplicativo antigo. IMO, isso pertence mais ao SO do que ao SU.
Lançado em
Talvez tenha sido rejeitado porque em sua forma inicial não definiu claramente alguns de seus requisitos. Ainda não funciona, a menos que você leia os comentários do OP às respostas (incluindo aquele que excluí quando as coisas ficaram em forma de pera).
pavium

Respostas:

42

Meu sed(Mac OS X) não funcionou com +. Em *vez disso, tentei e adicionei ptag para correspondência de impressão:

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

Para combinar pelo menos um caractere numérico sem +, eu usaria:

sed -n 's/^.*abc\([0-9][0-9]*\)xyz.*$/\1/p' example.txt
mouviciel
fonte
Obrigado, isso funcionou para mim também, uma vez que usei * em vez de +.
Stéphane
2
... e a opção "p" para imprimir o match, que eu também não sabia. Obrigado novamente.
Stéphane de
2
Eu tive que escapar do +e então funcionou para mim:sed -n 's/^.*abc\([0-9]\+\)xyz.*$/\1/p'
Pausado até novo aviso.
3
Isso porque você não está usando o formato RE moderno, portanto, + é um caractere padrão e você deve expressar isso com a sintaxe {,}. Você pode adicionar a opção use -E sed para acionar o formato RE moderno. Verifique re_format (7), especificamente o último parágrafo da DESCRIÇÃO developer.apple.com/library/mac/#documentation/Darwin/Reference/…
anddam
33

Você pode usar o sed para fazer isso

 sed -rn 's/.*abc([0-9]+)xyz.*/\1/gp'
  • -n não imprima a linha resultante
  • -risso faz com que você não tenha como escapar dos parênteses do grupo de captura ().
  • \1 a partida do grupo de captura
  • /g jogo global
  • /p imprima o resultado

Eu escrevi uma ferramenta para mim que torna isso mais fácil

rip 'abc(\d+)xyz' '$1'
Ilia Choly
fonte
3
Esta é de longe a melhor e mais bem explicada resposta até agora!
Nik Reiman
Com alguma explicação, é melhor entender o que há de errado com nosso problema. Obrigado !
r4phG
17

Eu uso perlpara tornar isso mais fácil para mim. por exemplo

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

Isso executa o Perl, a -nopção instrui o Perl a ler uma linha de cada vez de STDIN e executar o código. A -eopção especifica a instrução a ser executada.

A instrução executa um regexp na linha lida e, se corresponder, imprime o conteúdo do primeiro conjunto de bracks ( $1).

Você pode fazer isso com vários nomes de arquivo no final também. por exemplo

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

PP.
fonte
Obrigado, mas não temos acesso ao perl, é por isso que eu estava perguntando sobre sed / awk / gawk.
Stéphane
5

Se a sua versão do grepsuportar, você pode usar a -oopção de imprimir apenas a parte de qualquer linha que corresponda ao seu regexp.

Se não, aqui está o melhor que sedeu poderia fazer:

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

... que exclui / pula sem dígitos e, para as linhas restantes, remove todos os caracteres não-dígitos iniciais e finais. (Estou apenas supondo que sua intenção é extrair o número de cada linha que contém um).

O problema com algo como:

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

.... ou

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

... é que sedsuporta apenas correspondência "gananciosa" ... então o primeiro. * corresponderá ao resto da linha. A menos que possamos usar uma classe de caractere negada para obter uma correspondência não gananciosa ... ou uma versão sedcom compatível com Perl ou outras extensões para seus regexes, não podemos extrair uma correspondência de padrão precisa com o espaço de padrão (uma linha )

Jim Dennis
fonte
Você pode simplesmente combinar dois de seus sedcomandos desta forma:sed -n 's/[^0-9]*\([0-9]\+\).*/\1/p'
Pausado até novo aviso.
Anteriormente não sabia sobre a opção -o no grep. Bom saber. Mas ele imprime a correspondência inteira, não o "(...)". Portanto, se você estiver combinando em "abc ([[: dígito:]] +) xyz", obterá o "abc" e "xyz", bem como os dígitos.
Stéphane
Obrigado por me lembrar disso grep -o! Eu estava tentando fazer isso sede lutava com minha necessidade de encontrar várias correspondências em algumas linhas. Minha solução é stackoverflow.com/a/58308239/117471
Bruno Bronosky
3

Você pode usar awkcom match()para acessar o grupo capturado:

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

Isso tenta corresponder ao padrão abc[0-9]+xyz. Se fizer isso, ele armazena suas fatias no array matches, cujo primeiro item é o bloco [0-9]+. Como match() retorna a posição do caractere, ou índice, de onde essa substring começa (1, se começar no início da string) , ele aciona a printação.


Com grepvocê pode usar um olhar para trás e para o futuro:

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

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

Isso verifica o padrão [0-9]+quando ocorre dentro de abce xyze apenas imprime os dígitos.

fedorqui 'então pare de prejudicar'
fonte
2

perl é a sintaxe mais limpa, mas se você não tiver perl (nem sempre lá, eu entendo), então a única maneira de usar gawk e componentes de um regex é usar o recurso gensub.

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

a saída do arquivo de entrada de amostra será

12345

Nota: gensub substitui todo o regex (entre //), então você precisa colocar o. * Antes e depois do ([0-9] +) para se livrar do texto antes e depois do número na substituição.

Mark Lakata
fonte
2
Uma solução inteligente e viável se você precisar (ou quiser) usar o gawk. Você notou isso, mas para ser claro: o awk não GNU não tem gensub () e, portanto, não suporta isso.
cincodenada
Agradável! No entanto, pode ser melhor usar match()para acessar os grupos capturados. Veja minha resposta para isso.
fedorqui 'SO pare de prejudicar'
1

Se você deseja selecionar linhas, retire os bits que você não deseja:

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

Basicamente, ele seleciona as linhas que você deseja egrepe usa sedpara remover os bits antes e depois do número.

Você pode ver isso em ação aqui:

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

Atualização: obviamente, se sua situação real for mais complexa, os REs precisarão ser modificados. Por exemplo, se você sempre teve um único número dentro de zero ou mais números não numéricos no início e no final:

egrep '[^0-9]*[0-9]+[^0-9]*$' inputFile | sed -e 's/^[^0-9]*//' -e 's/[^0-9]*$//'
paxdiablo
fonte
Interessante ... Então, não há uma maneira simples de aplicar uma expressão regular complexa e obter de volta apenas o que está na seção (...)? Porque embora eu veja o que você fez aqui primeiro com grep e depois com sed, nossa situação real é muito mais complexa do que descartar "abc" e "xyz". A expressão regular é usada porque muitos textos diferentes podem aparecer em qualquer lado do texto que eu gostaria de extrair.
Stéphane
Eu tenho certeza que é uma maneira melhor se os REs são realmente complexa. Talvez se você forneceu mais alguns exemplos ou uma descrição mais detalhada, poderíamos ajustar nossas respostas para se adequar.
paxdiablo
0

O caso do OP não especifica que pode haver várias correspondências em uma única linha, mas para o tráfego do Google, adicionarei um exemplo para isso também.

Como a necessidade do OP é extrair um grupo de um padrão, o uso grep -oexigirá 2 passagens. Mas, ainda acho esta a maneira mais intuitiva de fazer o trabalho.

$ 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

Como o tempo do processador é basicamente gratuito, mas a legibilidade humana não tem preço, tendo a refatorar meu código com base na pergunta: "daqui a um ano, o que vou pensar que isso faz?" Na verdade, para código que pretendo compartilhar publicamente ou com minha equipe, vou até abrir man greppara descobrir quais são as opções longas e substituí-las. Igual a:grep --only-matching --extended-regexp

Bruno Bronosky
fonte
-1

você pode fazer isso com a concha

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

Por awk. Eu usaria o seguinte script:

/.*abc([0-9]+)xyz.*/ {
            print $0;
            next;
            }
            {
            /* default, do nothing */
            }
Pierre
fonte
Isso não produz o valor numérico ([0-9+]), mas sim a linha inteira.
Mark Lakata
-3
gawk '/.*abc([0-9]+)xyz.*/' file
ghostdog74
fonte
2
Isso não parece funcionar. Ele imprime a linha inteira em vez da correspondência.
Stéphane de
em seu arquivo de entrada de amostra, esse padrão é a linha inteira. certo??? se você souber que o padrão estará em um campo específico: use $ 1, $ 2 etc. Por exemplo, gawk '$ 1 ~ /.*abc([0-9]+)xyz.*/' file
ghostdog74