bash: dividere l'output del comando per colonne
Domanda
Voglio fare questo:
- esegui un comando
- cattura l'output
- seleziona una linea
- seleziona una colonna di quella linea
Ad esempio, diciamo che voglio ottenere il nome del comando da un $ PID
(si noti che questo è solo un esempio, non sto suggerendo che sia il modo più semplice per ottenere un nome del comando da un ID processo - il mio vero problema è con un altro comando il cui formato di output non posso controllare).
Se eseguo ps
ottengo:
PID TTY TIME CMD
11383 pts/1 00:00:00 bash
11771 pts/1 00:00:00 ps
Ora faccio ps | egrep 11383
e ottieni
11383 pts/1 00:00:00 bash
Passaggio successivo: ps | egrep 11383 | taglia -d " & Quot; -f 4
. L'output è:
<absolutely nothing/>
Il problema è che cut
taglia l'output di singoli spazi e poiché ps
aggiunge alcuni spazi tra la 2a e la 3a colonna per mantenere la somiglianza di una tabella, < code> cut seleziona una stringa vuota. Ovviamente, potrei usare cut
per selezionare il 7 ° e non il 4 ° campo, ma come posso sapere, specialmente quando l'output è variabile e sconosciuto in anticipo.
Soluzione
Un modo semplice è aggiungere un passaggio di tr
per eliminare eventuali separatori di campo ripetuti:
$ ps | egrep 11383 | tr -s ' ' | cut -d ' ' -f 4
Altri suggerimenti
Penso che il modo più semplice sia usare awk . Esempio:
$ echo "11383 pts/1 00:00:00 bash" | awk '{ print $4; }'
bash
Nota che l'opzione tr -s ''
non rimuoverà nessuno spazio iniziale. Se la colonna è allineata a destra (come con ps
pid) ...
$ ps h -o pid,user -C ssh,sshd | tr -s " "
1543 root
19645 root
19731 root
Quindi il taglio comporterà una riga vuota per alcuni di quei 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 shell
Tuttavia, in generale è ancora possibile utilizzare funzioni di shell in modo conciso, poiché il comando read
contiene una cosa chiara:
$ <command> | while read a b; do echo $a; done
Il primo parametro da leggere, a
, seleziona la prima colonna, e se c'è di più, tutto il resto verrà inserito in b
. Di conseguenza, non hai mai bisogno di più variabili del numero della colonna +1 .
Quindi,
while read a b c d; do echo $c; done
genererà quindi la terza colonna. Come indicato nel mio commento ...
Una lettura con pipe verrà eseguita in un ambiente che non passa le 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 è usare la variabile shell IFS che per 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 delle difficoltà a dividere una stringa in componenti in una cosa di Busybox. È abbastanza facile ottenere un singolo componente (ad es. Usando awk) e quindi ripeterlo per ogni parametro necessario. Ma poi finisci ripetutamente chiamando awk sulla stessa linea o usando ripetutamente un blocco di lettura con eco sulla stessa linea. 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 della shell non è più molto divertente se la metà o più delle funzionalità a cui sei abituato, sono sparite. Ma puoi presumere che anche Python non sarebbe installato su un tale sistema, e non lo era ;-).
try
ps |&
while read -p first second third fourth etc ; do
if [[ $first == '11383' ]]
then
echo got: $fourth
fi
done
Simile alla soluzione awk di Brianegge, ecco l'equivalente Perl:
ps | egrep 11383 | perl -lane 'print $F[3]'
-a
abilita la modalità autosplit, che popola l'array @F
con i dati della colonna.
Usa -F,
se i tuoi dati sono delimitati da virgole, piuttosto che spaziati.
Il campo 3 viene stampato poiché Perl inizia a contare da 0 anziché 1
Ottenere la linea corretta (esempio per la linea n. 6) si fa 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}'
Uso delle variabili dell'array
set $(ps | egrep "^11383 "); echo $4
o
A=( $(ps | egrep "^11383 ") ) ; echo ${A[3]}
Invece di fare tutte queste greps e roba del genere, ti consiglio di usare le funzionalità 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ò quindi essere considerato portatile.
Il tuo comando
ps | egrep 11383 | cut -d" " -f 4
manca un tr -s
per comprimere gli spazi, come spiegato in la sua risposta .
Tuttavia, potresti voler usare awk
, poiché gestisce tutte queste azioni in un unico comando:
ps | awk '/11383/ {print $4}'
Stampa la quarta colonna in quelle righe contenenti 11383
. Se vuoi che corrisponda a 11383
se appare all'inizio della riga, allora puoi dire ps | awk '/ ^ 11383 / {print $ 4}'
.
set
di Bash analizzerà tutti gli output in parametri di posizione.
Ad esempio, con set $ (free -h)
, echo $ 7
mostrerà " Mem: "