Como obter a parte de um arquivo após a primeira linha que corresponde a uma expressão regular?

169

Eu tenho um arquivo com cerca de 1000 linhas. Quero a parte do meu arquivo após a linha que corresponde à minha declaração grep.

Isso é:

$ cat file | grep 'TERMINATE'     # It is found on line 534

Então, quero o arquivo da linha 535 à linha 1000 para processamento adicional.

Como eu posso fazer isso?

Yugal Jindle
fonte
34
UUOC (Use inútil de gato):grep 'TERMINATE' file
Jacob
30
Eu sei disso, é como se eu fosse assim. Vamos voltar à pergunta.
Yugal Jindle
3
Essa é uma questão de programação perfeitamente adequada e adequada para o fluxo de pilha.
precisa saber é o seguinte
13
@ Jacob Não é um uso inútil de gato. Seu uso é imprimir um arquivo na saída padrão, o que significa que podemos usar grepa interface de entrada padrão da s para ler dados, em vez de ter que aprender em que opção aplicar grep, e sed, e awk, e pandoc, e, e ffmpegetc. quando queremos ler de um arquivo. Isso economiza tempo, porque não precisamos aprender uma nova opção toda vez que queremos fazer a mesma coisa: ler de um arquivo.
Runeks 13/08/16
@runeks Concordo com seu sentimento - mas você pode conseguir isso sem cat: grep 'TERMINATE' < file. Talvez ele faz tornar a leitura um pouco mais difícil - mas isto é shell script, de modo que é sempre vai ser um problema :)
LOAS

Respostas:

307

A seguir, imprimirá a linha correspondente TERMINATEaté o final do arquivo:

sed -n -e '/TERMINATE/,$p'

Explicado: -n desativa o comportamento padrão sedde imprimir cada linha depois de executar seu script, -eindicar um script para sed, /TERMINATE/,$é uma seleção de intervalo de endereço (linha), o que significa que a primeira linha corresponde à TERMINATEexpressão regular (como grep) no final do arquivo ( $) , e pé o comando de impressão que imprime a linha atual.

Isso será impresso a partir da linha que segue a linha correspondente TERMINATEaté o final do arquivo:
(de APÓS a linha correspondente a EOF, NÃO incluindo a linha correspondente)

sed -e '1,/TERMINATE/d'

Explicado: 1,/TERMINATE/ é uma seleção de faixa de endereço (linha) que significa a primeira linha da entrada da 1ª linha correspondente à TERMINATEexpressão regular e dé o comando delete que exclui a linha atual e pula para a próxima linha. Como sedo comportamento padrão é imprimir as linhas, ele imprimirá as linhas após TERMINATE o final da entrada.

Editar:

Se você deseja as linhas antes TERMINATE:

sed -e '/TERMINATE/,$d'

E se você deseja as duas linhas antes e depois TERMINATEem 2 arquivos diferentes em uma única passagem:

sed -e '1,/TERMINATE/w before
/TERMINATE/,$w after' file

Os arquivos antes e depois conterão a linha com terminação, para processar cada um que você precisa usar:

head -n -1 before
tail -n +2 after

Edit2:

Se você não desejar codificar os nomes de arquivos no script sed, poderá:

before=before.txt
after=after.txt
sed -e "1,/TERMINATE/w $before
/TERMINATE/,\$w $after" file

Mas então você precisa escapar do $significado da última linha para que o shell não tente expandir a $wvariável (observe que agora usamos aspas duplas ao redor do script em vez de aspas simples).

Esqueci de dizer que a nova linha é importante após os nomes dos arquivos no script, para que o sed saiba que os nomes dos arquivos terminam.


Edição: 2016-0530

Sébastien Clément perguntou: "Como você substituiria o código fixo TERMINATEpor uma variável?"

Você faria uma variável para o texto correspondente e, em seguida, faria da mesma maneira que no exemplo anterior:

matchtext=TERMINATE
before=before.txt
after=after.txt
sed -e "1,/$matchtext/w $before
/$matchtext/,\$w $after" file

para usar uma variável para o texto correspondente com os exemplos anteriores:

## Print the line containing the matching text, till the end of the file:
## (from the matching line to EOF, including the matching line)
matchtext=TERMINATE
sed -n -e "/$matchtext/,\$p"
## Print from the line that follows the line containing the 
## matching text, till the end of the file:
## (from AFTER the matching line to EOF, NOT including the matching line)
matchtext=TERMINATE
sed -e "1,/$matchtext/d"
## Print all the lines before the line containing the matching text:
## (from line-1 to BEFORE the matching line, NOT including the matching line)
matchtext=TERMINATE
sed -e "/$matchtext/,\$d"

Os pontos importantes sobre a substituição de texto por variáveis ​​nesses casos são:

  1. Variáveis ​​( $variablename) incluídas em single quotes[ '] não serão "expandidas", mas variáveis ​​dentro de double quotes[ "] serão. Portanto, você deve alterar todos os single quotespara double quotesse eles contiverem texto que você deseja substituir por uma variável.
  2. As sedfaixas também conter um $e são imediatamente seguidos por uma letra como: $p, $d, $w. Eles também irá olhar como variáveis a serem expandidas, então você tem que escapar esses $caracteres com uma barra invertida [ \] como: \$p, \$d, \$w.
jfg956
fonte
Como podemos obter as linhas antes de TERMINATE e excluir tudo o que se segue?
Yugal Jindle
Como você substituiria o TERMINAL codificado por uma variável?
Sébastien Clément
2
Um caso de uso que está faltando aqui é como imprimir linhas após o último marcador (se houver várias delas no arquivo. Pense nos arquivos de log etc.).
Mato
O exemplo sed -e "1,/$matchtext/d"não funciona quando $matchtextocorre na primeira linha. Eu tive que mudar isso para sed -e "0,/$matchtext/d".
Karalga 27/01
61

Como uma aproximação simples, você pode usar

grep -A100000 TERMINATE file

que cumprimenta TERMINATE e gera até 100000 linhas após essa linha.

Da página do manual

-A NUM, --after-context=NUM

Imprima NUM linhas do contexto à direita após as linhas correspondentes. Coloca uma linha que contém um separador de grupo (-) entre grupos contíguos de correspondências. Com a opção -o ou --only-matching, isso não tem efeito e um aviso é dado.

aioobe
fonte
Isso pode funcionar para isso, mas preciso codificá-lo no meu script para processar muitos arquivos. Então, mostre alguma solução genérica.
Yugal Jindle
3
Eu acho que essa é uma solução prática!
Michelgotta
2
similarmente -B NUM, --before-context = NUM ​​Imprime NUM linhas do contexto inicial antes de corresponder as linhas. Coloca uma linha contendo um separador de grupo (-) entre grupos contíguos de correspondências. Com a opção -o ou --only-matching, isso não tem efeito e um aviso é dado.
precisa saber é o seguinte
esta solução funcionou para mim porque eu posso facilmente usar variáveis ​​como minha string para verificar.
Jose Martinez
3
Boa ideia! Se você não tiver certeza sobre o tamanho do contexto, pode contar as linhas file:grep -A$(cat file | wc -l) TERMINATE file
Lemming
26

Uma ferramenta para usar aqui é awk:

cat file | awk 'BEGIN{ found=0} /TERMINATE/{found=1}  {if (found) print }'

Como é que isso funciona:

  1. Definimos a variável 'encontrada' como zero, avaliando false
  2. se uma correspondência para 'TERMINATE' for encontrada com a expressão regular, a definiremos como uma.
  3. Se a nossa variável 'found' for avaliada como True, imprima :)

As outras soluções podem consumir muita memória se você as usar em arquivos muito grandes.

Jos De Graeve
fonte
Simples, elegante e muito genérico. No meu caso, estava imprimindo tudo até segunda ocorrência de '###':cat file | awk 'BEGIN{ found=0} /###/{found=found+1} {if (found<2) print }'
Aleksander Stelmaczonek
3
Uma ferramenta para não usar aqui é cat. awké perfeitamente capaz de usar um ou mais nomes de arquivos como argumentos. Veja também stackoverflow.com/questions/11710552/useless-use-of-cat
tripleee
9

Se entendi sua pergunta corretamente, você deseja as linhas depois TERMINATE , sem incluir a TERMINATElinha. awkpode fazer isso de uma maneira simples:

awk '{if(found) print} /TERMINATE/{found=1}' your_file

Explicação:

  1. Embora não seja uma prática recomendada, você pode confiar no fato de que todos os vars assumem o padrão 0 ou a sequência vazia, se não definida. Então a primeira expressão (if(found) print ) não imprimirá nada para começar.
  2. Após a impressão, verificamos se esta é a linha de partida (que não deve ser incluída).

Isso imprimirá todas as linhas após a TERMINATElinha.


Generalização:

  • Você tem um arquivo com start - e finais -lines e desejar que as linhas entre as linhas excluindo o início - e finais -lines.
  • As linhas inicial e final podem ser definidas por uma expressão regular correspondente à linha.

Exemplo:

$ cat ex_file.txt 
not this line
second line
START
A good line to include
And this line
Yep
END
Nope more
...
never ever
$ awk '/END/{found=0} {if(found) print} /START/{found=1}' ex_file.txt 
A good line to include
And this line
Yep
$

Explicação:

  1. Se a linha final for encontrada, nenhuma impressão deverá ser feita. Observe que essa verificação é feita antes da impressão real para excluir o final linha do resultado.
  2. Imprima a linha atual se foundestiver definida.
  3. Se a linha inicial for encontrada, defina found=1para que as seguintes linhas sejam impressas. Observe que essa verificação é feita após a impressão real para excluir a linha de partida do resultado.

Notas:

  • O código baseia-se no fato de que todos os awk-vars assumem o padrão 0 ou a sequência vazia, se não definida. Isso é válido, mas pode não ser uma prática recomendada, portanto, você pode adicionar umBEGIN{found=0} ao início da expressão awk.
  • Se forem encontrados vários blocos de início e fim , todos serão impressos.
UlfR
fonte
1
Exemplo impressionante. Passei apenas duas horas olhando csplit, sed e todo o tipo de comandos complicados do awk. Isso não apenas fez o que eu queria, mas mostrou-se simples o suficiente para inferir como modificá-lo para fazer algumas outras coisas relacionadas que eu precisava. Me faz lembrar que awk é ótimo e não apenas em uma bagunça indecifrável de porcaria. Obrigado.
user1169420
{if(found) print}é um pouco antipadrão no awk, é mais idiomático substituir o bloco por apenas foundou found;se você precisar de outro filtro posteriormente.
user000001
@ user000001 por favor explique. Eu não entendo o que substituir e como. De qualquer forma, acho que o modo como está escrito deixa muito claro o que está acontecendo.
precisa saber é o seguinte
1
Você substituiria awk '{if(found) print} /TERMINATE/{found=1}' your_filepor awk 'found; /TERMINATE/{found=1}' your_file, ambos devem fazer a mesma coisa.
user000001
7

Use a expansão de parâmetros do bash como a seguir:

content=$(cat file)
echo "${content#*TERMINATE}"
Mu Qiao
fonte
Você pode explicar o que está fazendo?
Yugal Jindle
Copiei o conteúdo de "arquivo" para a variável $ content. Em seguida, removi todos os caracteres até que "TERMINATE" fosse visto. Não utilizou correspondência gananciosa, mas você pode usar a correspondência gananciosa em $ {content ## * TERMINATE}.
Mu Qiao
aqui está o link do manual bash: gnu.org/software/bash/manual/...
Mu Qiao
6
o que acontecerá se o arquivo tiver 100 GB de tamanho?
Znik
1
Downvote: Isso é horrível (ler o arquivo em uma variável) e errado (usar a variável sem citá-la; e você deve usar corretamente printfou garantir que sabe exatamente para o que está passando echo.).
tripleee
6

grep -Um arquivo 10000000 'TERMINATE'

  • é muito, muito mais rápido que o sed, especialmente trabalhando em arquivos realmente grandes. Ele trabalha com até 10 milhões de linhas (ou o que você colocar), para não prejudicar o tamanho suficiente para lidar com qualquer coisa que você atingir.
user8910163
fonte
4

Existem várias maneiras de fazer isso com sedou awk:

sed -n '/TERMINATE/,$p' file

Isso procura TERMINATEno seu arquivo e é impresso a partir dessa linha até o final do arquivo.

awk '/TERMINATE/,0' file

Esse é exatamente o mesmo comportamento que sed .

Caso você saiba o número da linha a partir da qual deseja iniciar a impressão, é possível especificá-la juntamente com NR(número do registro, que eventualmente indica o número da linha):

awk 'NR>=535' file

Exemplo

$ seq 10 > a        #generate a file with one number per line, from 1 to 10
$ sed -n '/7/,$p' a
7
8
9
10
$ awk '/7/,0' a
7
8
9
10
$ awk 'NR>=7' a
7
8
9
10
fedorqui 'Então pare de prejudicar'
fonte
Para o número que você também pode usarmore +7 file
123
Isso inclui a linha correspondente, que não é o que se deseja nesta pergunta.
mivk
@mivk bem, esse também é o caso da resposta aceita e da segunda mais votada, então o problema pode estar com um título enganoso.
fedorqui 'Então, pare de prejudicar'
3

Se, por qualquer motivo, você desejar evitar o uso do sed, o seguinte imprimirá a linha correspondente TERMINATEaté o final do arquivo:

tail -n "+$(grep -n 'TERMINATE' file | head -n 1 | cut -d ":" -f 1)" file

e o seguinte será impresso a partir da seguinte linha correspondente TERMINATEaté o final do arquivo:

tail -n "+$(($(grep -n 'TERMINATE' file | head -n 1 | cut -d ":" -f 1)+1))" file

São necessários dois processos para fazer o que o sed pode fazer em um processo e, se o arquivo mudar entre a execução do grep e do tail, o resultado pode ser incoerente, por isso recomendo o uso do sed. Além disso, se o arquivo não contiver TERMINATE, o 1º comando falhará.

jfg956
fonte
arquivo é verificado duas vezes. e se tiver tamanho de 100 GB?
Znik 22/12/14
1
Voto negativo porque esta é uma solução ruim, mas depois votado porque 90% da resposta são advertências.
Mad Físico
0

Esta poderia ser uma maneira de fazê-lo. Se você souber qual linha do arquivo possui sua palavra grep e quantas linhas possui no seu arquivo:

grep -A466 'TERMINATE' arquivo

Mariah
fonte
1
Se o número da linha for conhecido, grepnem será necessário; você pode simplesmente usar tail -n $NUM, então isso não é realmente uma resposta.
Samveen
-1

sed é uma ferramenta muito melhor para o trabalho: sed -n '/ re /, $ p' file

onde re é regexp.

Outra opção é o sinalizador - após o contexto do grep. Você precisa passar um número para finalizar, usando wc no arquivo deve fornecer o valor certo para parar em. Combine isso com -n e sua expressão de correspondência.

ckwang
fonte
--after-context é bom, mas não em todos os casos.
Yugal Jindle
Você pode sugerir outra coisa .. ??
Yugal Jindle
-2

Eles imprimirão todas as linhas da última linha encontrada "TERMINATE" até o final do arquivo:

LINE_NUMBER=`grep -o -n TERMINATE $OSCAM_LOG|tail -n 1|sed "s/:/ \\'/g"|awk -F" " '{print $1}'`
tail -n +$LINE_NUMBER $YOUR_FILE_NAME
easyyu
fonte
A extração de um número de linha greppara que você possa alimentá-lo tailé um antipadrão de desperdício. A localização da correspondência e a impressão até o final do arquivo (ou, inversamente, a impressão e a parada na primeira correspondência) são eminentemente realizadas com as próprias ferramentas regulares e essenciais de regex. O massivo grep | tail | sed | awktambém é, por si só, um uso inútil egrep massivo de amigos .
Tripleee
Acho que ele estava tentando nos fornecer algo que encontrasse a / last instance / de 'TERMINATE' e fornecesse as linhas a partir dessa instância. Outras implementações fornecem a primeira instância em diante. O LINE_NUMBER provavelmente deve ter a seguinte aparência: LINE_NUMBER = $ (grep -o -n 'TERMINATE' $ OSCAM_LOG | tail -n 1 | awk -F: '{print $ 1}') Talvez não seja a maneira mais elegante, mas parece fazer o trabalho. ^. ^
fbicknel
... ou tudo em uma linha, mas feio: tail -n + $ (grep -o -n 'TERMINATE' $ YOUR_FILE_NAME | tail -n 1 | awk -F: '{print $ 1}') $ YOUR_FILE_NAME
fbicknel
.... e eu ia voltar e editar $ OSCAM_LOG no lugar de $ YOUR_FILE_NAME ... mas não posso por algum motivo. Não faço ideia de onde $ OSCAM_LOG veio; Eu simplesmente imitei o papagaio. oO
fbicknel 01/07
Fazer isso sozinho no Awk é uma tarefa comum no Awk 101. Se você já estiver usando uma ferramenta mais capaz apenas para obter o número da linha, solte taile execute a tarefa na ferramenta mais capaz. De qualquer forma, o título diz claramente "primeira partida".
Tripleee