Suddividi l'output del comando per colonne usando Bash?


87

Voglio farlo:

  1. eseguire un comando
  2. cattura l'output
  3. seleziona una riga
  4. seleziona una colonna di quella riga

Ad esempio, diciamo che voglio ottenere il nome del comando da a $PID(tieni presente che questo è solo un esempio, non sto suggerendo che questo sia il modo più semplice per ottenere un nome di comando da un ID di processo - il mio vero problema è con un altro comando il cui formato di output non posso controllare).

Se corro psottengo:


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

Ora lo faccio ps | egrep 11383e ottengo

11383 pts/1    00:00:00 bash

Prossimo passo: ps | egrep 11383 | cut -d" " -f 4. L'output è:

<absolutely nothing/>

Il problema è che cuttaglia l'output di singoli spazi e, poiché psaggiunge alcuni spazi tra la 2a e la 3a colonna per mantenere una certa somiglianza con una tabella, cutsceglie una stringa vuota. Certo, potrei usare cutper selezionare il settimo e non il quarto campo, ma come posso sapere, specialmente quando l'output è variabile e sconosciuto in anticipo.


2
Usa awk (e altri 25 caratteri).
Michael Foukarakis

Risposte:


178

Un modo semplice è aggiungere un passaggio di trper spremere eventuali separatori di campo ripetuti:

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

1
Mi piace questo, sembra che trsia più leggero diawk
flybywire

3
Tenderei ad essere d'accordo, ma potrebbe anche essere perché non ho imparato awk. :)
rilassati il

Non funzionerà se ti capita di avere un processo con PID che contiene il PID a cui sei interessato come sottostringa.
David Grayson

1
Inoltre, le suore di campo saranno spente se alcuni PID sono riempiti di spazio a sinistra mentre altri no.
tripleee

68

Penso che il modo più semplice sia usare awk . Esempio:

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

4
Per compatibilità con la domanda originale, ps | awk "\$1==$PID{print\$4}"o (migliore) ps | awk -v"PID=$PID" '$1=PID{print$4}'. Certo, su Linux potresti semplicemente fare xargs -0n1 </proc/$PID/cmdline | head -n1o readlink /proc/$PID/exe, ma comunque ...
effimero

È la ;a { print $4; }richiesta? Rimuoverlo sembra non avere alcun effetto per me su Linux, sono solo curioso del suo scopo
igniteflow

@igniteflow non indicherebbe la fine del comando se si volesse continuare ad aggiungere oltre l'istruzione print?
joshmcode

16

Tieni presente che l' tr -s ' 'opzione non rimuoverà alcun singolo spazio iniziale. Se la tua colonna è allineata a destra (come con pspid) ...

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

Quindi il taglio risulterà in una riga vuota per alcuni di questi campi se è la prima colonna:

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

19645
19731

A meno che tu non lo preceda con uno spazio, ovviamente

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

Ora, per questo caso particolare di numeri pid (non nomi), esiste una funzione chiamata pgrep:

$ pgrep ssh


Funzioni di shell

Tuttavia, in generale è ancora possibile utilizzare le funzioni della shell in modo conciso, perché c'è una cosa carina nel readcomando:

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

Il primo parametro da leggere a,, seleziona la prima colonna e, se ce n'è di più, verrà inserito tutto il restob . Di conseguenza, non avrai mai bisogno di più variabili del numero della tua colonna +1 .

Così,

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

produrrà quindi la terza colonna. Come indicato nel mio commento ...

Una lettura in pipe verrà eseguita in un ambiente che non passa variabili allo script chiamante.

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'`


La soluzione array

Quindi finiamo con la risposta di @frayser che consiste nell'usare la variabile di shell IFS che ha come impostazione predefinita uno spazio, per dividere la stringa in un array. Funziona solo in Bash però. Dash e Ash non lo supportano. Ho avuto davvero difficoltà a dividere una stringa in componenti in una cosa Busybox. È abbastanza facile ottenere un singolo componente (ad esempio usando awk) e poi ripeterlo per ogni parametro di cui hai bisogno. Ma poi si finisce per chiamare ripetutamente awk sulla stessa riga o utilizzare ripetutamente un blocco di lettura con echo sulla stessa riga. Che non è efficiente o carino. Quindi finisci per dividere usando ${name%% *}e così via. Ti fa desiderare alcune abilità di Python perché in effetti lo scripting di shell non è più molto divertente se metà o più delle funzionalità a cui sei abituato sono sparite. Ma puoi presumere che anche python non sarebbe stato installato su un sistema del genere, e non lo era ;-).


Dovresti usare le virgolette intorno alla variabile in echo "$a"e echo "$c"sebbene.
tripleee

Sembra però che ogni blocco piped venga eseguito nella propria subshell o processo e non sia possibile restituire alcuna variabile al blocco che lo racchiude? Anche se puoi ottenere l'output di questo dopo averlo ripetuto. var=$(....... | { read a b c d; echo $c; }). Funziona solo per una singola (stringa), anche se in Bash puoi dividerlo in un array usandoar=($var)
Xennex81

@tripleee Non credo che sia un problema in questa fase del processo. Scoprirai abbastanza presto se ne hai bisogno o no, e se a un certo punto si rompe, è una lezione di apprendimento. E poi sai perché hai dovuto usare quelle doppie virgolette ;-). E poi non è più qualcosa che hai sentito dire da altri. Gioca con il fuoco! : D. : p.
Xennex81

risposta elaborata: D
ncomputers

Questa è stata una risposta troppo utile per non dirlo.
Ivan X

4

provare

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

1
@flybywire - forse eccessivo per questo semplice esempio, ma questo idioma è eccellente se devi eseguire elaborazioni più complesse sui dati selezionati.
James Anderson

Inoltre, tieni presente che in questi giorni la shell di scripting predefinita di solito non è bash.
David Given

2

Utilizzo di variabili array

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

o

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

2

Simile alla soluzione awk di brianegge, ecco l'equivalente Perl:

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

-aabilita la modalità autosplit, che popola l' @Farray con i dati della colonna.
Da utilizzare -F,se i dati sono delimitati da virgole, anziché delimitati da spazi.

Il campo 3 viene stampato poiché Perl inizia a contare da 0 invece che da 1


1
Grazie per la tua soluzione perl - non conoscevo l'autosplit e penso ancora che perl sia lo strumento per terminare gli altri strumenti ..;).
Gerard ONeill

1

Ottenere la riga corretta (esempio per la riga n. 6) è fatto con testa e coda e la parola corretta (parola n. 4) può essere catturata con awk:

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

Notando solo per i futuri lettori che awk può selezionare anche per riga: awk NR=6 {print $4}sarebbe un po 'più efficiente
David Z

1
e con questo ovviamente intendevo awk NR==6 {print $4}* doh *
David Z

1

Il tuo comando

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

manca uno tr -sspazio per stringere, come spiega rilassarsi nella sua risposta .

Tuttavia, potresti voler usare awk, poiché gestisce tutte queste azioni in un unico comando:

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

Questo stampa la quarta colonna in quelle righe che contengono 11383. Se vuoi che questo corrisponda 11383se appare all'inizio della riga, puoi dire ps | awk '/^11383/ {print $4}'.


0

Invece di fare tutti questi greps e cose del genere, ti consiglio di usare le capacità di ps per cambiare il formato di output.

ps -o cmd= -p 12345

Ottieni la riga cmmand di un processo con il pid specificato e nient'altro.

Questo è conforme a POSIX e può essere quindi considerato portatile.


1
flybywire afferma che sta usando ps come esempio, la domanda è più generale di così.
Salmo dell'orco33

0

Bash setanalizzerà tutto l'output in parametri di posizione.

Ad esempio, con il set $(free -h)comando, echo $7mostrerà "Mem:"


Questo metodo è utile solo quando il comando ha una sola riga di output. Non abbastanza generico.
codeforester

Questo non è vero, tutto l'output viene inserito nei parametri posizionali indipendentemente dalle righe. ex set $(sar -r 1 1); echo "${23}"
dman

Il punto era che è difficile determinare la posizione dell'argomento quando l'output è voluminoso e ha molti campi. awkè il modo migliore per farlo.
codeforester

Questa è solo un'altra soluzione. L'OP potrebbe non voler imparare il linguaggio awk per questo caso monouso. I tag indicano bashe non awk.
dman
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.