Diviser la sortie de la commande par colonnes à l'aide de Bash?


87

Je veux faire ça:

  1. exécuter une commande
  2. capturer la sortie
  3. sélectionnez une ligne
  4. sélectionnez une colonne de cette ligne

À titre d'exemple, disons que je veux obtenir le nom de la commande à partir de a $PID(veuillez noter que ce n'est qu'un exemple, je ne suggère pas que ce soit le moyen le plus simple d'obtenir un nom de commande à partir d'un identifiant de processus - mon vrai problème est avec une autre commande dont je ne peux pas contrôler le format de sortie).

Si je cours, psj'obtiens:


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

Maintenant je fais ps | egrep 11383et je reçois

11383 pts/1    00:00:00 bash

Étape suivante: ps | egrep 11383 | cut -d" " -f 4. La sortie est:

<absolutely nothing/>

Le problème est que cutcoupe la sortie par des espaces simples, et comme psajoute des espaces entre les 2ème et 3ème colonnes pour garder une certaine ressemblance avec une table, cutchoisit une chaîne vide. Bien sûr, je pourrais utiliser cutpour sélectionner le 7ème et non le 4ème champ, mais comment puis-je savoir, surtout quand la sortie est variable et inconnue au préalable.


2
Utilisez awk (et 25 autres caractères).
Michael Foukarakis

Réponses:


178

Un moyen simple consiste à ajouter une passe de trpour éliminer tous les séparateurs de champ répétés:

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

1
J'aime celui-ci, il semble trplus léger queawk
flybywire

3
J'aurais tendance à être d'accord, mais c'est peut-être aussi parce que je n'ai pas appris les choses. :)
détendez-vous

Ne fonctionnera pas si vous avez un processus avec PID qui contient le PID qui vous intéresse en tant que sous-chaîne.
David Grayson

1
Et aussi, les nunbers de champ seront désactivés si certains PID: s sont remplis d'espaces à gauche tandis que d'autres ne le sont pas.
tripleee

68

Je pense que le moyen le plus simple est d'utiliser awk . Exemple:

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

4
Pour la compatibilité avec la question d'origine, ps | awk "\$1==$PID{print\$4}"ou (mieux) ps | awk -v"PID=$PID" '$1=PID{print$4}'. Bien sûr, sous Linux, vous pouvez simplement faire xargs -0n1 </proc/$PID/cmdline | head -n1ou readlink /proc/$PID/exe, mais de toute façon ...
éphémère

Est -ce le ;au { print $4; }besoin? Le supprimer ne semble avoir aucun effet pour moi sur Linux, juste curieux de son objectif
igniteflow

@igniteflow n'indiquerait-il pas la fin de la commande si vous vouliez continuer à ajouter au-delà de l'instruction d'impression?
joshmcode

16

Veuillez noter que l' tr -s ' 'option ne supprimera aucun espace de début unique. Si votre colonne est alignée à droite (comme avec pspid) ...

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

Ensuite, la coupe entraînera une ligne vide pour certains de ces champs s'il s'agit de la première colonne:

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

19645
19731

A moins que vous ne le précédiez d'un espace, évidemment

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

Maintenant, pour ce cas particulier de nombres pid (pas de noms), il existe une fonction appelée pgrep:

$ pgrep ssh


Fonctions du shell

Cependant, en général, il est encore possible d'utiliser les fonctions du shell de manière concise, car il y a une chose intéressante à propos de la readcommande:

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

Le premier paramètre à lire, asélectionne la première colonne, et s'il y en a plus, tout le reste sera inséré b. Par conséquent, vous n'avez jamais besoin de plus de variables que le numéro de votre colonne +1 .

Donc,

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

affichera alors la 3e colonne. Comme indiqué dans mon commentaire ...

Une lecture canalisée sera exécutée dans un environnement qui ne transmet pas de variables au script appelant.

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 solution Array

On se retrouve donc avec la réponse de @frayser qui est d'utiliser la variable shell IFS qui par défaut est un espace, pour diviser la chaîne en un tableau. Cela ne fonctionne que dans Bash. Dash et Ash ne le supportent pas. J'ai eu du mal à diviser une chaîne en composants dans un objet Busybox. Il est assez facile d'obtenir un seul composant (par exemple en utilisant awk) et de le répéter pour chaque paramètre dont vous avez besoin. Mais vous finissez par appeler à plusieurs reprises awk sur la même ligne, ou à plusieurs reprises en utilisant un bloc de lecture avec écho sur la même ligne. Ce qui n'est ni efficace ni joli. Alors vous finissez par vous séparer en utilisant ${name%% *}etc. Vous donne envie de certaines compétences Python car en fait, les scripts shell ne sont plus très amusants si la moitié ou plus des fonctionnalités auxquelles vous êtes habitué ont disparu. Mais vous pouvez supposer que même python ne serait pas installé sur un tel système, et ce n'était pas le cas ;-).


Vous devez utiliser des guillemets autour de la variable dans echo "$a"et echo "$c"cependant.
tripleee

Il semble cependant que chaque bloc canalisé est exécuté dans son propre sous-shell ou processus et que vous ne pouvez pas renvoyer de variables au bloc englobant? Bien que vous puissiez en obtenir le résultat après l'avoir fait écho. var=$(....... | { read a b c d; echo $c; }). Cela ne fonctionne que pour une seule (chaîne), mais dans Bash, vous pouvez la diviser en un tableau en utilisantar=($var)
Xennex81

@tripleee Je ne pense pas que ce soit un problème à un tel stade du processus. Vous découvrirez assez tôt si vous en avez besoin ou non, et si cela se rompt à un moment donné, c'est une leçon d'apprentissage. Et puis vous savez pourquoi vous avez dû utiliser ces guillemets doubles ;-). Et puis ce n'est plus quelque chose que vous avez entendu dire des autres. Jouer avec le feu! :RÉ. : p.
Xennex81

réponse élaborée: D
ncomputers

C'était une réponse trop utile pour que je ne le dise pas.
Ivan X

4

essayer

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

1
@flybywire - peut-être exagéré pour cet exemple simple, mais cet idiome est excellent si vous devez effectuer un traitement plus complexe sur les données sélectionnées.
James Anderson

Sachez également que de nos jours, le shell de script par défaut n'est généralement pas bash.
David donné

2

Utilisation de variables de tableau

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

ou

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

2

Semblable à la solution awk de brianegge, voici l'équivalent Perl:

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

-aactive le mode de partage automatique, qui remplit le @Ftableau avec les données de la colonne.
À utiliser -F,si vos données sont délimitées par des virgules, plutôt que par des espaces.

Le champ 3 est imprimé car Perl commence à compter de 0 au lieu de 1


1
Merci pour votre solution perl - je ne connaissais pas l'autosplit, et je pense toujours que perl est l'outil pour mettre fin aux autres outils ..;).
Gerard ONeill

1

Obtenir la ligne correcte (exemple pour la ligne n ° 6) se fait avec head and tail et le mot correct (mot n ° 4) peut être capturé avec awk:

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

Il suffit de noter pour les futurs lecteurs que awk peut également sélectionner par ligne: ce awk NR=6 {print $4}serait un peu plus efficace
David Z

1
et par cela bien sûr, je voulais dire awk NR==6 {print $4}* doh *
David Z

1

Votre commande

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

manque un tr -spour serrer les espaces, comme l'explique se détendre dans sa réponse .

Cependant, vous voudrez peut-être utiliser awk, car il gère toutes ces actions en une seule commande:

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

Ceci imprime la 4ème colonne de ces lignes contenant 11383. Si vous voulez que cela corresponde 11383s'il apparaît au début de la ligne, vous pouvez dire ps | awk '/^11383/ {print $4}'.


0

Au lieu de faire tous ces greps et trucs, je vous conseillerais d'utiliser les capacités ps pour changer le format de sortie.

ps -o cmd= -p 12345

Vous obtenez la ligne cmmand d'un processus avec le pid spécifié et rien d'autre.

Ceci est conforme à POSIX et peut donc être considéré comme portable.


1
flybywire déclare qu'il utilise simplement ps comme exemple, la question est plus générale que cela.
Ogre Psalm33

0

Bash setanalysera toutes les sorties en paramètres de position.

Par exemple, avec la set $(free -h)commande, echo $7affichera "Mem:"


Cette méthode n'est utile que lorsque la commande a une seule ligne de sortie. Pas assez générique.
codeforester

Ce n'est pas vrai, toutes les sorties sont placées dans des paramètres de position indépendamment des lignes. ex set $(sar -r 1 1); echo "${23}"
dman

Mon point était qu'il est difficile de déterminer la position de l'argument lorsque la sortie est volumineuse et comporte de nombreux champs. awkest la meilleure façon de procéder.
codeforester

Ceci est juste une autre solution. L'OP peut ne pas vouloir apprendre le langage awk pour ce cas d'utilisation unique. Les balises indiquent bashet non awk.
dman
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.