Divida a saída do comando por colunas usando o Bash?


87

Eu quero fazer isso:

  1. execute um comando
  2. capturar a saída
  3. selecione uma linha
  4. selecione uma coluna dessa linha

Apenas como exemplo, digamos que eu queira obter o nome do comando de um $PID(observe que este é apenas um exemplo, não estou sugerindo que esta seja a maneira mais fácil de obter um nome de comando a partir de um id de processo - meu verdadeiro problema é com outro comando cujo formato de saída não posso controlar).

Se eu correr ps, recebo:


  PID TTY          TIME CMD
11383 pts/1    00:00:00 bash
11771 pts/1    00:00:00 ps

Agora eu faço ps | egrep 11383e consigo

11383 pts/1    00:00:00 bash

Passo seguinte: ps | egrep 11383 | cut -d" " -f 4. O resultado é:

<absolutely nothing/>

O problema é que cutcorta a saída em espaços simples, e conforme psadiciona alguns espaços entre a 2ª e a 3ª colunas para manter alguma semelhança com uma tabela, cutpega uma string vazia. Claro, eu poderia usar cutpara selecionar o 7º e não o 4º campo, mas como posso saber, especialmente quando a saída é variável e desconhecida de antemão.


2
Use awk (e mais 25 caracteres).
Michael Foukarakis

Respostas:


178

Uma maneira fácil é adicionar uma passagem de trpara espremer todos os separadores de campo repetidos:

$ ps | egrep 11383 | tr -s ' ' | cut -d ' ' -f 4

1
Eu gosto deste, parece que tré mais leve do queawk
flybywire

3
Eu tenderia a concordar, mas também pode ser porque não aprendi awk. :)
desenrole

Não funcionará se você tiver um processo com PID que contenha o PID no qual está interessado como subtring.
David Grayson

1
E também, os nunbers de campo estarão desligados se alguns PID: s forem preenchidos com espaço à esquerda e outros não.
tripleee

68

Acho que a maneira mais simples é usar o awk . Exemplo:

$ echo "11383 pts/1    00:00:00 bash" | awk '{ print $4; }'
bash

4
Para compatibilidade com a pergunta original, ps | awk "\$1==$PID{print\$4}"ou (melhor) ps | awk -v"PID=$PID" '$1=PID{print$4}'. Claro, no Linux você poderia simplesmente fazer xargs -0n1 </proc/$PID/cmdline | head -n1ou readlink /proc/$PID/exe, mas de qualquer maneira ...
efêmero

O ;em é { print $4; }obrigatório? Removê-lo parece não ter nenhum efeito para mim no Linux, apenas curioso quanto ao seu propósito
igniteflow

@igniteflow não indicaria o fim do comando se você quisesse continuar adicionando após a instrução de impressão?
joshmcode

16

Observe que a tr -s ' 'opção não removerá nenhum espaço inicial. Se sua coluna estiver alinhada à direita (como com pspid) ...

$ ps h -o pid,user -C ssh,sshd | tr -s " "
 1543 root
19645 root
19731 root

Em seguida, o corte resultará em uma linha em branco para alguns desses campos se for a primeira coluna:

$ <previous command> | cut -d ' ' -f1

19645
19731

A menos que você o preceda com um espaço, obviamente

$ <command> | sed -e "s/.*/ &/" | tr -s " "

Agora, para este caso particular de números pid (não nomes), existe uma função chamada pgrep:

$ pgrep ssh


Funções Shell

No entanto, em geral, ainda é possível usar funções do shell de maneira concisa, porque há uma coisa interessante sobre o readcomando:

$ <command> | while read a b; do echo $a; done

O primeiro parâmetro a ser lido a,, seleciona a primeira coluna e, se houver mais, todo o resto será colocado b. Como resultado, você nunca precisa de mais variáveis ​​do que o número de sua coluna +1 .

Então,

while read a b c d; do echo $c; done

irá então imprimir a 3ª coluna. Conforme indicado em meu comentário ...

Uma leitura canalizada será executada em um ambiente que não passa variáveis ​​para o script de chamada.

out=$(ps whatever | { read a b c d; echo $c; })

arr=($(ps whatever | { read a b c d; echo $c $b; }))
echo ${arr[1]}     # will output 'b'`


A Solução Array

Então, terminamos com a resposta de @frayser, que é usar a variável shell IFS, que tem como padrão um espaço, para dividir a string em um array. Porém, ele só funciona no Bash. Dash e Ash não apóiam isso. Tive muita dificuldade em dividir uma string em componentes em uma coisa do Busybox. É fácil obter um único componente (por exemplo, usando awk) e, em seguida, repeti-lo para cada parâmetro de que você precisa. Mas então você acaba chamando awk repetidamente na mesma linha, ou repetidamente usando um bloco de leitura com eco na mesma linha. O que não é eficiente nem bonito. Então você acaba dividindo usando ${name%% *}e assim por diante. Faz você ansiar por algumas habilidades em Python porque, na verdade, o script de shell não é mais tão divertido se metade ou mais dos recursos com os quais você está acostumado se foram. Mas você pode assumir que mesmo o python não seria instalado em tal sistema, e não foi ;-).


Você deve usar aspas em torno da variável em echo "$a"e echo "$c"embora.
tripleee

Parece que cada bloco canalizado é executado em seu próprio subshell ou processo e você não pode retornar nenhuma variável para o bloco envolvente. Embora você possa obter a saída disso depois de ecoá-la. var=$(....... | { read a b c d; echo $c; }). Isso só funciona para uma única (string), embora no Bash você possa dividi-la em uma matriz usandoar=($var)
Xennex81

@tripleee Não creio que isso seja um problema nesta fase do processo. Você logo descobrirá se precisa disso ou não, e se isso quebrar em algum ponto, será uma lição de aprendizado. E então você sabe por que teve que usar essas aspas duplas ;-). E então não é mais algo que você ouviu dizer dos outros. Brincar com fogo! : D. : p.
Xennex81

resposta elaborada: D
ncomputers

Essa foi uma resposta muito útil para eu não dizer isso.
Ivan X

4

tentar

ps |&
while read -p first second third fourth etc ; do
   if [[ $first == '11383' ]]
   then
       echo got: $fourth
   fi       
done

1
@flybywire - possivelmente um exagero para este exemplo simples, mas este idioma é excelente se você precisar fazer um processamento mais complexo nos dados selecionados.
James Anderson

Além disso, esteja ciente de que atualmente o shell de script padrão geralmente não é o bash.
David Dado

2

Usando variáveis ​​de matriz

set $(ps | egrep "^11383 "); echo $4

ou

A=( $(ps | egrep "^11383 ") ) ; echo ${A[3]}

2

Semelhante à solução awk de brianegge, aqui está o equivalente em Perl:

ps | egrep 11383 | perl -lane 'print $F[3]'

-aativa o modo de divisão automática, que preenche a @Fmatriz com os dados da coluna.
Use -F,se seus dados forem delimitados por vírgulas, em vez de delimitados por espaço.

O campo 3 é impresso porque o Perl começa a contar a partir de 0 ao invés de 1


1
Obrigado por sua solução perl - não sabia sobre autosplit, e ainda acho que perl é a ferramenta para acabar com as outras ferramentas ..;).
Gerard ONeill

1

Obter a linha correta (exemplo para a linha nº 6) é feito com cabeça e cauda e a palavra correta (palavra nº 4) pode ser capturada com awk:

command|head -n 6|tail -n 1|awk '{print $4}'

Apenas observando para futuros leitores que awk também pode selecionar por linha: awk NR=6 {print $4}seria um pouco mais eficiente
David Z

1
e com isso, é claro, eu quis dizer awk NR==6 {print $4}* doh *
David Z

1

Seu comando

ps | egrep 11383 | cut -d" " -f 4

perde um tr -spara espremer espaços, como a descontração explica em sua resposta .

No entanto, talvez você queira usar awk, já que ele lida com todas essas ações em um único comando:

ps | awk '/11383/ {print $4}'

Isso imprime a 4ª coluna nas linhas que contêm 11383. Se você quer que isso combine11383 que ele se ele aparecer no início da linha, você pode dizer ps | awk '/^11383/ {print $4}'.


0

Em vez de fazer todos esses greps e outras coisas, eu aconselho você a usar os recursos do ps para alterar o formato de saída.

ps -o cmd= -p 12345

Você obtém a linha de comando de um processo com o pid especificado e nada mais.

Isso é compatível com POSIX e, portanto, pode ser considerado portátil.


1
flybywire afirma que está usando apenas o ps como exemplo, a questão é mais geral do que isso.
Ogre Salmo 33 de

0

O Bash setanalisará todas as saídas em parâmetros de posição.

Por exemplo, com o set $(free -h)comando, echo $7mostrará "Mem:"


Este método é útil apenas quando o comando tem uma única linha de saída. Não é genérico o suficiente.
codeforester

Isso não é verdade, todas as saídas são colocadas em parâmetros posicionais, independentemente das linhas. ex set $(sar -r 1 1); echo "${23}"
dman

Meu ponto é que é difícil determinar a posição do argumento quando a saída é volumosa e tem muitos campos. awké a melhor maneira de fazer isso.
codeforester

Esta é apenas outra solução. O OP pode não querer aprender a linguagem awk para este único caso de uso. As tags indicam bashe não awk.
dman
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.