Como grep uma linha específica _e_ a primeira linha de um arquivo?

76

Supondo um grep simples como:

$ psa aux | grep someApp
1000     11634 51.2  0.1  32824  9112 pts/1    SN+  13:24   7:49 someApp

Isso fornece muita informação, mas como a primeira linha do comando ps está ausente, não há contexto para a informação. Eu preferiria que a primeira linha do ps seja mostrada também:

$ psa aux | someMagic someApp
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
1000     11634 51.2  0.1  32824  9112 pts/1    SN+  13:24   7:49 someApp

Obviamente, eu poderia adicionar um regex ao grep especificamente para ps:

$ ps aux | grep -E "COMMAND|someApp"

No entanto, eu preferiria uma solução mais geral, pois há outros casos em que gostaria de ter a primeira linha também.

Parece que este seria um bom caso de uso para um descritor de arquivo "stdmeta" .

dotancohen
fonte
9
A complexidade exigida por essas respostas mostra como a filosofia do Unix de "faça uma coisa e faça bem" às vezes falha quando avaliada pelo critério da usabilidade: conhecer todos esses comandos suficientemente bem para aplicá-los a esse problema comum (filtrar informações do processo e ainda vendo os rótulos das colunas) mostra a desvantagem da abordagem: às vezes as coisas não se encaixam muito bem. É por isso que ferramentas como acksão tão úteis, e por que perlpassado disparou sed, awketc. em popularidade: é importante para as partes para resumir em um todo coerente.
Iconoclast
3
é claro, para este exemplo em particular, você pode usar o -Cargumento pse não precisará canalizá-lo para o grep. por exemplo, ps u -C someAppou mesmops u -C app1 -C app2 -C app3
cas
1
@iconoclast: é claro que a solução Unixy seria uma ferramenta que pode multiplexar várias linhas para serem filtradas através de diferentes conjuntos de filtros. Meio que uma versão generalizada da ps aux | { head -1; grep foo; }mencionada por @Nahuel FOUILLEUL abaixo (o seu é provavelmente a única solução que eu seria capaz de recordar no local, se necessário)
Lie Ryan
@iconoclast: falta de experiência e conhecimento das ferramentas, o que as ferramentas realmente fazem bem sempre parecerão totalmente inúteis. Conhecer bem um comando não é o lugar certo na usabilidade, é na facilidade de ler o manual e a prática. Essas ferramentas existem há décadas. Eles funcionam e se encaixam muito bem (e de forma limpa).
Ярослав Рахматуллин
@ ЯрославРахматуллин: Eu acho que você pode ter entendido completamente o que eu disse. (Talvez porque o inglês não seja seu primeiro idioma?) "Usabilidade" está relacionada ao UX ("experiência do usuário"), não ao utilitário (ou "utilidade"). Salientando que quando uma operação simples é complexa, isso prejudica a usabilidade NÃO é o mesmo que dizer que as ferramentas são inúteis. Obviamente, eles não são inúteis. Ninguém em sã consciência diria que são inúteis.
Iconoclast

Respostas:

67

Bom caminho

Normalmente você não pode fazer isso com o grep, mas pode usar outras ferramentas. O AWK já foi mencionado, mas você também pode usar o sedseguinte:

sed -e '1p' -e '/youpattern/!d'

Como funciona:

  1. O utilitário Sed trabalha em cada linha individualmente, executando comandos especificados em cada uma delas. Você pode ter vários comandos, especificando várias -eopções. Podemos acrescentar cada comando com um parâmetro range que especifica se esse comando deve ser aplicado a uma linha específica ou não.

  2. "1p" é o primeiro comando. Ele usa o pcomando que normalmente imprime todas as linhas. Mas o anexamos com um valor numérico que especifica o intervalo ao qual ele deve ser aplicado. Aqui, usamos o 1que significa primeira linha. Se você quiser imprimir mais linhas, poderá usar x,yponde xestá a primeira linha a ser impressa, a yúltima linha a ser impressa. Por exemplo, para imprimir as 3 primeiras linhas, você usaria1,3p

  3. O próximo comando é o dque normalmente exclui todas as linhas do buffer. Antes deste comando, colocamos yourpatternentre dois /caracteres. Essa é a outra maneira (primeiro foi especificar em quais linhas, como fizemos com o pcomando), de endereçar as linhas nas quais o comando deve estar em execução. Isso significa que o comando funcionará apenas para as linhas correspondentes yourpattern. Exceto, usamos !caractere antes do dcomando que inverte sua lógica. Portanto, agora ele removerá todas as linhas que não correspondem ao padrão especificado.

  4. No final, o sed imprimirá todas as linhas restantes no buffer. Mas removemos as linhas que não correspondem ao buffer, para que apenas as linhas correspondentes sejam impressas.

Resumindo: imprimimos a primeira linha e excluímos todas as linhas que não correspondem ao nosso padrão da entrada. Resto das linhas são impressos (para que apenas as linhas que fazem corresponder ao padrão).

Problema de primeira linha

Como mencionado nos comentários, há um problema com essa abordagem. Se o padrão especificado corresponder também à primeira linha, ele será impresso duas vezes (uma vez por pcomando e uma vez por causa de uma correspondência). Podemos evitar isso de duas maneiras:

  1. Adicionando 1dcomando depois 1p. Como já mencionei, o dcomando exclui as linhas do buffer e especificamos seu intervalo pelo número 1, o que significa que ele excluirá apenas a primeira linha. Então o comando seriased -e '1p' -e '1d' -e '/youpattern/!d'

  2. Usando o 1bcomando, em vez de 1p. É um truque. bO comando nos permite pular para outro comando especificado por um rótulo (dessa maneira, alguns comandos podem ser omitidos). Mas se esse rótulo não for especificado (como no nosso exemplo), ele simplesmente pula para o final dos comandos, ignorando o restante dos comandos da nossa linha. Portanto, no nosso caso, o último dcomando não removerá essa linha do buffer.

Exemplo completo:

ps aux | sed -e '1b' -e '/syslog/!d'

Usando ponto e vírgula

Algumas sedimplementações podem economizar digitação usando ponto-e-vírgula para separar comandos, em vez de usar várias -eopções. Portanto, se você não se importa em ser portátil, o comando seria ps aux | sed '1b;/syslog/!d'. Funciona pelo menos em GNU sede busyboximplementações.

Maneira louca

Aqui está, no entanto, uma maneira bastante louca de fazer isso com o grep. Definitivamente não é o ideal, estou publicando isso apenas para fins de aprendizado, mas você pode usá-lo, por exemplo, se não tiver nenhuma outra ferramenta no seu sistema:

ps aux | grep -n '.*' | grep -e '\(^1:\)\|syslog'

Como funciona

  1. Primeiro, usamos a -nopção para adicionar números de linha antes de cada linha. Queremos numerar todas as linhas com as quais estamos combinando .*- qualquer coisa, até a linha vazia. Como sugerido nos comentários, também podemos combinar '^', o resultado é o mesmo.

  2. Então, estamos usando expressões regulares estendidas para que possamos usar \|caracteres especiais que funcionam como OR. Então, combinamos se a linha começa com 1:(primeira linha) ou contém nosso padrão (nesse caso, é syslog).

Problema de números de linha

Agora, o problema é que estamos obtendo esses números de linha feios em nossa saída. Se este for um problema, podemos removê-los com cut, desta forma:

ps aux | grep -n '.*' | grep -e '\(^1:\)\|syslog' | cut -d ':' -f2-

-dopção especifica delimitador, -fespecifica campos (ou colunas) que queremos imprimir. Portanto, queremos cortar cada linha de cada :caractere e imprimir apenas a segunda e todas as colunas subseqüentes. Isso efetivamente remove a primeira coluna com seu delimitador e é exatamente isso que precisamos.

Krzysztof Adamski
fonte
4
A numeração de linha também pode ser feita cat -ne pareceria mais clara, com um grep abusado por isso.
Alfe
1
nlnão conta linhas vazias (mas as imprime sem número de linha), cat -nformata a numeração com espaços anteriores, grep -n .retira as linhas vazias e adiciona dois pontos. Todos têm os seus ... er ... características ;-)
Alfe
2
Resposta bem escrita muito educativa. Tentei substituir "Pretend" (Perto do começo) por "Prepend" para você, mas ele queria mais alterações e não estava com vontade de mudar porcaria aleatória na sua postagem, então você pode querer corrigir isso.
Bill K
2
ps aux | sed '1p;/pattern/!d'imprimirá a primeira linha duas vezes se corresponder ao padrão . Melhor é utilizado o bcomando: ps aux | sed -e 1b -e '/pattern/!d'. cat -nnão é POSIX. grep -n '^'numeraria todas as linhas (não é um problema para a saída ps que não possui linhas vazias). nl -ba -d $'\n'numera cada linha.
Stéphane Chazelas
2
Observe que 1b;...não é portátil nem POSIX, não pode haver nenhum outro comando após "b"; portanto, você precisa de uma nova linha ou outra expressão -e.
Stéphane Chazelas
58

Como você se sente ao usar em awkvez de grep?

chopper:~> ps aux | awk 'NR == 1 || /syslogd/'
USER              PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
root               19   0.0  0.0  2518684   1160   ??  Ss   26Aug12   1:00.22 /usr/sbin/syslogd
mrb               574   0.0  0.0  2432852    696 s006  R+    8:04am   0:00.00 awk NR == 1 || /syslogd/
  • NR == 1: Número de registro == 1; ie a primeira linha
  • ||: ou:
  • /syslogd/: Padrão para procurar

Também pode valer a pena olhar pgrep, embora isso seja mais para scripts, em vez de saída voltada para o usuário. grepPorém, evita que o próprio comando apareça na saída.

chopper:~> pgrep -l syslogd
19 syslogd
mrb
fonte
Muito bom, obrigado. Isso também é muito programável para expansão futura.
dotancohen 12/09/12
Eu preciso me aprender um pouco estranho. muito agradável.
user606723
30
ps aux | { read line;echo "$line";grep someApp;}

EDIT: após comentários

ps aux | { head -1;grep someApp;}

Embora eu head -1lesse toda a entrada, mas depois de testá-la, também funciona.

{ head -1;grep ok;} <<END
this is a test
this line should be ok
not this one
END

saída é

this is a test
this line should be ok
Nahuel Fouilleul
fonte
2
Essa é a ideia explicitada diretamente no bash. Eu gostaria de dar mais de um polegar para isso. Eu usaria apenas { IFS='' read line; ... }no caso de o cabeçalho começar com espaços.
Alfe
Isso ataca exatamente o problema diretamente. Agradável!
dotancohen 12/09/12
3
Eu apenas usaria em head -1vez da combinação de leitura / eco.
chepner 12/09/12
1
Bem, funciona head -n1no meu bash. Provavelmente, isso pode ser específico da implementação. Minha cabeça não está lendo toda a entrada neste caso, apenas a primeira linha, deixando o restante delas no buffer de entrada.
Krzysztof Adamski
2
head -n1é mais curto, mas parece que até a especificação POSIX é silenciosa quanto a sua entrada é permitida a leitura, portanto, talvez read line; echo $lineseja mais portátil, afinal.
chepner 13/09/12
14

Ps suporte filtro interno,

Suponha que você esteja procurando pelo processo bash:

ps -C bash -f

Irá listar todos os processos que nomearam bash.

margarida
fonte
Obrigado, é bom saber. No entanto, não encontrará scripts iniciados em python, entre outros.
dotancohen 12/09/12
6

Costumo enviar o cabeçalho para stderr :

ps | (IFS= read -r HEADER; echo "$HEADER" >&2; cat) | grep ps

Isso geralmente é suficiente para fins de leitura humana. por exemplo:

  PID TTY          TIME CMD
 4738 pts/0    00:00:00 ps

A parte entre colchetes pode entrar em seu próprio script para uso geral.

Há uma conveniência adicional, pois a saída pode ser canalizada ainda mais (para sortetc.) e o cabeçalho permanecerá no topo.

Antak
fonte
5

Você também pode usar teee head:

ps aux | tee >(head -n1) | grep syslog

Observe, no entanto, que, desde que teenão seja possível ignorar SIGPIPEsinais (consulte, por exemplo, a discussão aqui ), essa abordagem precisa de uma solução alternativa para ser confiável. A solução alternativa é ignorar os sinais SIGPIPE; isso pode, por exemplo, ser feito assim no bash como shells:

trap '' PIPE    # ignore SIGPIPE
ps aux | tee >(head -n1) 2> /dev/null | grep syslog
trap - PIPE     # restore SIGPIPE handling

Observe também que a ordem de saída não é garantida .

Thor
fonte
Eu não confiaria nisso para funcionar, na primeira vez em que o executei (zsh), ele produziu cabeçalhos de coluna abaixo dos resultados do grep. Segunda vez foi bom.
RQomey # 12/12
1
Eu não vi isso ainda, mas uma maneira de aumentar a confiabilidade é inserir um pequeno atraso no pipeline antes do grep: | { sleep .5; cat }.
Thor
2
Adicionar adormecidos para evitar problemas de simultaneidade é sempre um hack. Embora isso possa funcionar, é um passo em direção ao lado sombrio. -1 para isso.
Alfe
1
Eu tive algumas outras questões estranhas ao tentar essa resposta, eu configurar uma questão para verificar
Rqomey
Esse é um uso interessante do tee, mas acho que não é confiável e geralmente imprime apenas a linha de saída, mas não a linha de cabeçalho.
dotancohen 12/09/12
4

Talvez dois pscomandos sejam mais fáceis.

$ ps aux | head -1 && ps aux | grep someApp
USER             PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
100         3304   0.0  0.2  2466308   6476   ??  Ss    2Sep12   0:01.75 /usr/bin/someApp
emcconville
fonte
2
Não gosto dessa solução, principalmente porque a situação pode mudar entre a primeira e a segunda ps auxchamada ... E se você quer apenas a primeira linha estática, por que não repetir manualmente?
Shadur 12/09/12
1
As alterações entre as duas chamadas não devem ser incomodadas nessa situação. O primeiro fornecerá apenas o título que sempre se ajustará à saída do segundo.
Alfe
2
Não vejo por que isso foi prejudicado, certamente é uma opção viável. Voto a favor.
dotancohen 12/09/12
4

Você pode usar o pidstat com:

pidstat -C someApp
or
pidstat -p <PID>

Exemplo:

# pidstat -C java
Linux 3.0.26-0.7-default (hostname)    09/12/12        _x86_64_

13:41:21          PID    %usr %system  %guest    %CPU   CPU  Command
13:41:21         3671    0.07    0.02    0.00    0.09     1  java

Informações adicionais: http://linux.die.net/man/1/pidstat

harpa
fonte
Obrigado, é bom saber. No entanto, não encontrará scripts iniciados em python, entre outros.
dotancohen 12/09/12
4

Coloque o seguinte no seu arquivo .bashrc ou copie / cole no shell primeiro, para teste.

function psls { 
ps aux|head -1 && ps aux|grep "$1"|grep -v grep;
}

Uso: psls [padrão grep]

$ psls someApp
USER             PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
root              21   0.0  0.0  2467312   1116   ??  Ss   Tue07PM   0:00.17 /sbin/someApp

Certifique-se de fornecer seu arquivo .bashrc (ou .bash_profile, se você o colocar lá):

source ~/.bashrc

A função será concluída automaticamente na linha de comando do shell. Como você afirmou em outra resposta, você pode canalizar a primeira linha para um arquivo para salvar uma chamada em ps.

taco
fonte
1
Bom, eu tenho usado esse tipo de função há anos. Eu chamo minha versãopsl , que apenas chama pse grepuma vez cada (e não precisa head).
Adam Katz
3

classifique, mas mantenha a linha do cabeçalho no topo

# print the header (the first line of input)
# and then run the specified command on the body (the rest of the input)
# use it in a pipeline, e.g. ps | body grep somepattern
body() {
    IFS= read -r header
    printf '%s\n' "$header"
    "$@"
}

E use assim

$ ps aux | body grep someApp
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
1000     11634 51.2  0.1  32824  9112 pts/1    SN+  13:24   7:49 someApp
Mikel
fonte
Obrigado, algumas dessas respostas discutem o caso geral dessa pergunta. Perfeito!
dotancohen 13/09/12
3

Graças principalmente a Janis Papanagnou no comp.unix.shell, eu uso a seguinte função:

function grep1 {
    IFS= read -r header && printf "%s\n" "$header"; grep "$@"
}

Isso tem várias vantagens:

  • Funciona com bash, zsh e provavelmente ksh
  • É um substituto para o grep, para que você possa continuar usando os sinalizadores que desejar: -ipara correspondência sem distinção entre maiúsculas e minúsculas, -Epara expressões regulares ampliadas etc.
  • Sempre gera o mesmo código de saída que o grep, caso você queira determinar programaticamente se alguma linha realmente corresponde
  • Não imprime nada se a entrada estiver vazia

Exemplo de uso:

$ ps -rcA | grep1 databases
  PID TTY           TIME CMD

$ ps -rcA | grep1 -i databases
  PID TTY           TIME CMD
62891 ??         0:00.33 com.apple.WebKit.Databases
Bdesham
fonte
2

Outra maneira com gnu ed:

ed -s '!ps aux' <<< $'2,$v/PATTERN/d\n,p\nq\n'

ou, se o shell suportar a substituição do processo:

printf '%s\n' '2,$v/PATTERN/d' ,p q | ed -s <(ps aux)

isso é:

2,$v/PATTERN/d  - remove all lines not matching pattern (ignore the header)
,p              - print the remaining lines
q               - quit

Mais portátil, sem gnu '!' substituição de shell ou - usando apenas o edbuilt-in rpara rdirecionar a saída ps auxpara o buffer e, em seguida, excluir linhas não correspondentes no 2,$intervalo e imprimir o resultado:

printf '%s\n' 'r !ps aux' '2,$v/PATTERN/d' ,p q | ed -s

E como os sedcomandos na resposta aceita também geram a linha correspondente, com um sedque suporta -f-e um shell que suporta a substituição de processos, eu executaria:

printf '%s\n' '2,${' '/PATTERN/!d' '}' | sed -f - <(ps aux)

que praticamente faz a mesma coisa que os edcomandos anteriores .

don_crissti
fonte
1

O caminho Perl:

ps aux | perl -ne 'print if /pattern/ || $.==1'

Muito mais fácil de ler do que sed, mais rápido, sem risco de escolher linhas indesejadas.

emazep
fonte
Perl?!?
dotancohen 23/09/16
0

Se isso for apenas para processos grepping com cabeçalhos completos, eu expandiria a sugestão do @ mrb:

$ ps -f -p $(pgrep bash)
UID        PID  PPID  C STIME TTY      STAT   TIME CMD
nasha     2810  2771  0  2014 pts/6    Ss+    0:00 bash
...

pgrep bash | xargs ps -fpobterá o mesmo resultado, mas sem um subshell. Se for necessária outra formatação:

$ pgrep bash | xargs ps fo uid,pid,stime,cmd -p
  UID   PID STIME CMD
    0  3599  2014 -bash
 1000  3286  2014 /bin/bash
 ...

fonte
-2

Se você conhece os números exatos das linhas, é fácil com o perl! Se você deseja obter as linhas 1 e 5 de um arquivo, diga / etc / passwd:

perl -e 'while(<>){if(++$l~~[1,5]){print}}' < /etc/passwd

Se você quiser obter outras linhas também, basta adicionar seus números na matriz.

Dagelf
fonte
1
Obrigado. De acordo com o OP, conheço parte do texto da linha, mas não o número da linha.
dotancohen
Isso aparece como uma resposta no Google ao procurar esse caso de uso intimamente relacionado ao OP, então vale a pena notar aqui.
Dagelf
1
Se for esse o caso, sugiro que você inicie uma nova pergunta e responda com essa resposta. É perfeitamente bom responder suas próprias perguntas sobre SE, especialmente na situação mencionada. Vá em frente e faça o link para sua nova pergunta em um comentário sobre o OP.
23416 dotarkhen
Existem essas perguntas, mas elas não aparecem no Google no momento.
Dagelf
Dagelf, a linha inferior é - sua resposta não responde à pergunta aqui. @dotancohen está certo - se isso aparecer como uma resposta no Google ao procurar esse caso de uso intimamente relacionado ao OP , faça uma pergunta separada - detalhando esse caso de uso intimamente relacionado - e responda.
22416 Don_crissti