Como grep para grupos de n dígitos, mas não mais que n?

20

Estou aprendendo Linux e tenho um desafio que pareço não conseguir resolver sozinho. Aqui está:

  

grep uma linha de um arquivo que contém 4 números em uma linha, mas não mais que 4.

Não sei como abordar isso. Eu posso procurar números específicos, mas não a quantidade deles em uma string.

    
por Buddha 18.10.2014 / 23:19

3 respostas

35

Existem duas maneiras de interpretar essa questão; Vou abordar os dois casos. Você pode querer exibir linhas:

  1. que contêm uma sequência de quatro dígitos que não faz parte de uma sequência mais longa de dígitos, ou
  2. que contém uma sequência de quatro dígitos, mas não mais sequência de dígitos (nem mesmo separadamente).

Por exemplo, (1) exibe 1234a56789 , mas (2) não.

Se você deseja exibir todas as linhas que contêm uma seqüência de quatro dígitos que não faz parte de uma sequência mais longa de dígitos, uma delas é:

grep -P '(?<!\d)\d{4}(?!\d)' file

Isso usa expressões regulares Perl , que são do Ubuntu grep ( GNU grep ) suporta via -P . Ele não corresponderá ao texto como 12345 nem corresponderá ao 1234 ou 2345 que fazem parte dele. Mas corresponderá à 1234 em 1234a56789 .

Em expressões regulares Perl:

  • \d significa qualquer dígito (é um caminho curto para dizer [0-9] ou [[:digit:]] ).
  • x{4} corresponde a x 4 vezes. ( { } sintaxe não é específica para expressões regulares Perl; também está em expressões regulares estendidas via grep -E .) Então \d{4} é o mesmo que \d\d\d\d .
  • (?<!\d) é uma asserção negativa de largura zero. Significa "a menos que precedido por \d ".
  • (?!\d) é uma asserção de look-ahead negativa de largura zero. Significa "a menos que seja seguido por \d ".

(?<!\d) e (?!\d) não correspondem a texto fora da sequência de quatro dígitos; em vez disso, eles (quando usados juntos) impedirão que uma sequência de quatro dígitos seja correspondida se for parte de uma sequência mais longa de dígitos.

Usar apenas o look-behind ou apenas o look-ahead é insuficiente porque a subsequência de quatro dígitos mais à direita ou mais à esquerda ainda seria correspondida.

Um benefício do uso de asserções atrasadas e look-ahead é que seu padrão corresponde apenas aos quatro seqüências de dígitos, e não o texto ao redor. Isso é útil ao usar o realce de cor (com a opção --color ).

ek@Io:~$ grep -P '(?&lt!\d)\d{4}(?!\d)' &lt&lt&lt 12345abc789d0123e4
12345abc789d0123e4

Por padrão no Ubuntu, cada usuário tem alias grep='grep --color=auto' em seu ~.bashrc file . Assim, você obtém destaque de cor automaticamente quando você executa um comando simples que começa com grep (isto é, quando aliases são expandidos) e a saída padrão é um terminal (é isso que é --color=auto verifica se há). As correspondências são normalmente destacadas em um tom de vermelho (perto de vermelhão ), mas mostrei em negrito em itálico. Aqui está uma imagem:

E você pode até fazer grep imprimir apenas o texto correspondente, e não a linha inteira, com -o :

ek@Io:~$ grep -oP '(?<!\d)\d{4}(?!\d)' <<< 12345abc789d0123e4
0123

Alternative Way, Sem Look-Behind e Look-Ahead Assertions

No entanto, se você:

  1. precisa de um comando que também seja executado em sistemas em que grep não suporte -P ou, caso contrário, não queira usar uma expressão regular Perl, e
  2. não precisa corresponder aos quatro dígitos especificamente - o que geralmente é o caso se seu objetivo é simplesmente exibir linhas contendo correspondências, e
  3. estão bem com uma solução que é um pouco menos elegante

... então você pode conseguir isso com uma expressão regular estendida :

grep -E '(^|[^0-9])[0-9]{4}($|[^0-9])' file

Isso corresponde a quatro dígitos e o caractere não digitável - ou início ou fim da linha - em torno deles. Especificamente:

  • [0-9] corresponde a qualquer dígito (como [[:digit:]] ou \d em expressões regulares Perl) e {4} significa "quatro vezes". Então, [0-9]{4} corresponde a uma sequência de quatro dígitos.
  • [^0-9] corresponde a caracteres que não estão no intervalo de 0 a 9 . É equivalente a [^[:digit:]] (ou \D , em expressões regulares Perl).
  • ^ , quando não aparece em [ ] colchetes, corresponde ao início de uma linha. Da mesma forma, $ corresponde ao final de uma linha.
  • | significa ou e parênteses são para agrupamento (como na álgebra).Portanto, (^|[^0-9]) corresponde ao início da linha ou a um caractere não digitável, enquanto ($|[^0-9]) corresponde ao final da linha ou a um caractere não dígito.

Portanto, as correspondências ocorrem apenas nas linhas que contêm uma sequência de quatro dígitos ( [0-9]{4} ) que é simultaneamente:

  • no início da linha ou precedido por um não dígito ( (^|[^0-9]) ), e
  • no final da linha ou seguido por um não dígito ( ($|[^0-9]) ).

Se, por outro lado, você quiser exibir todas as linhas que contenham uma sequência de quatro dígitos, mas não contenha nenhuma seqüência com mais de quatro dígitos (mesmo uma separada de outra) seqüência de apenas quatro dígitos), então, conceitualmente, seu objetivo é encontrar linhas que correspondam a um padrão, mas não a outro.

Portanto, mesmo que você saiba como fazer isso com um único padrão, sugiro usar algo como matt's segunda sugestão, grep ing para os dois padrões separadamente.

Você não se beneficia muito de nenhum dos recursos avançados das expressões regulares Perl ao fazer isso, portanto talvez prefira não usá-los. Mas, de acordo com o estilo acima, aqui está um encurtamento da solução da matt usando \d (e chaves) no lugar de [0-9] :

grep -P '\d{4}' file | grep -Pv '\d{5}'

Como ele usa [0-9] , o caminho do matt é mais portátil - ele funcionará em sistemas em que grep não suporta expressões regulares Perl. Se você usa [0-9] (ou [[:digit:]] ) em vez de \d , mas continua a usar { } , você obtém a portabilidade do caminho de Matt de maneira um pouco mais concisa:

grep -E '[0-9]{4}' file | grep -Ev '[0-9]{5}'

Caminho alternativo, com um único padrão

Se você realmente preferir um comando grep ,

  1. usa uma única expressão regular (não dois grep s separados por um tubo , como acima)
  2. para exibir linhas que contenham pelo menos uma sequência de quatro dígitos,
  3. mas sem sequências de cinco (ou mais) dígitos,
  4. e você não se importa em combinar a linha inteira, não apenas os dígitos (provavelmente você não se importa com isso)

... então você pode usar:

grep -Px '(\d{0,4}\D)*\d{4}(\D\d{0,4})*' file

O -x sinalizador faz com que grep exiba apenas as linhas em que a linha inteira corresponde (em vez de qualquer linha contendo uma correspondência).

Eu usei uma expressão regular Perl porque acho que a brevidade de \d e \D aumenta substancialmente a clareza nesse caso. Mas se você precisar de algo portátil para sistemas em que grep não suporte -P , você poderá substituí-los por [0-9] e [^0-9] (ou com [[:digit:]] e [^[:digit]] ):

grep -Ex '([0-9]{0,4}[^0-9])*[0-9]{4}([^0-9][0-9]{0,4})*' file

A maneira como essas expressões regulares funcionam é:

  • No meio, \d{4} ou [0-9]{4} corresponde a uma sequência de quatro dígitos. Podemos ter mais de um deles, mas precisamos ter pelo menos um.

  • À esquerda, (\d{0,4}\D)* ou ([0-9]{0,4}[^0-9])* corresponde a zero ou mais ( * ) instâncias de não mais que quatro dígitos seguidos por um não dígito. Dígitos zero (isto é, nada) é uma possibilidade para "não mais de quatro dígitos". Isso corresponde a (a) a string vazia ou (b) qualquer string terminando em um não-dígito e não contendo nenhuma sequência de mais de quatro dígitos.

    Como o texto imediatamente à esquerda da central \d{4} (ou [0-9]{4} ) deve estar vazio ou terminar com um não dígito, isso impede que a central \d{4} corresponda a quatro dígitos que têm outra ( quinto) dígito apenas à esquerda deles.

  • À direita, (\D\d{0,4})* ou ([^0-9][0-9]{0,4})* corresponde a zero ou mais ( * ) instâncias de um dígito não seguido por não mais de quatro dígitos (que, como antes, poderia ser quatro, três, dois, um, ou mesmo nenhum em todos). Isso corresponde a (a) a string vazia ou (b) qualquer string começando em um não-dígito e não contendo nenhuma sequência de mais de quatro dígitos.

    Como o texto imediatamente à direita da central \d{4} (ou [0-9]{4} ) deve estar vazio ou começar com um não dígito, isso impede que a central \d{4} corresponda a quatro dígitos que tenham outra (quinta ) dígito à direita deles.

Isso garante que uma seqüência de quatro dígitos esteja presente em algum lugar e que nenhuma seqüência de cinco ou mais dígitos esteja presente em lugar algum.

Não é ruim ou errado fazer isso dessa maneira. Mas talvez a razão mais importante para considerar essa alternativa é que ela esclarece o benefício de usar grep -P '\d{4}' file | grep -Pv '\d{5}' (ou similar), como sugerido acima e em resposta de matt .

Dessa forma, fica claro que seu objetivo é selecionar linhas que contenham uma coisa, mas não outra. Além disso, a sintaxe é mais simples (por isso, pode ser mais rapidamente compreendida por muitos leitores / mantenedores).

    
por Eliah Kagan 19.10.2014 / 01:36
6

Isso mostrará 4 números seguidos, mas não mais

grep '[0-9][0-9][0-9][0-9][^0-9]' file

Note que o ^ não significa

Há um problema com isso, embora eu não tenha certeza de como consertar ... se o número for o fim da linha, ele não aparecerá.

Esta versão mais feia, no entanto, funcionaria para esse caso

grep '[0-9][0-9][0-9][0-9]' file | grep -v [0-9][0-9][0-9][0-9][0-9]
    
por matt 18.10.2014 / 23:44
0

Se grep não suportar expressões regulares perl ( -P ), use o seguinte comando shell:

grep -w "$(printf '[0-9]%.0s' {1..4})" file

em que printf '[0-9]%.0s' {1..4} produzirá 4 vezes [0-9] . Esse método é útil quando você tem dígitos longos e não quer repetir o padrão (basta substituir 4 pelo número de dígitos que você deseja procurar).

Usar -w procurará as palavras inteiras. No entanto, se estiver interessado em sequências alfanuméricas, como 1234a , adicione [^0-9] no final do padrão, por exemplo

grep "$(printf '[0-9]%.0s' {1..4})[^0-9]" file

Usar $() é basicamente uma substituição de comando . Marque esta postagem para ver como o printf repete o padrão.

    
por kenorb 03.03.2018 / 18:18