Lecture de la sortie d'une commande dans un tableau dans Bash

111

J'ai besoin de lire la sortie d'une commande dans mon script dans un tableau. La commande est, par exemple:

ps aux | grep | grep | x 

et il donne la sortie ligne par ligne comme ceci:

10
20
30

J'ai besoin de lire les valeurs de la sortie de la commande dans un tableau, puis je ferai du travail si la taille du tableau est inférieure à trois.

barp
la source
5
Hé @barp, RÉPONDEZ À VOS QUESTIONS, de peur que votre type ne pèse sur toute la communauté.
James
9
@James le problème n'est pas avec le fait qu'il ne répond pas à sa question ... c'est un site de questions / réponses. Il ne les a tout simplement pas marqués comme ayant répondu. Il devrait les marquer. Allusion. @ barp
DDPWNAGE
4
Veuillez @barp, marquez la question comme ayant répondu.
smonff
En relation: Faire une boucle sur le contenu d'un fichier dans Bash depuis la lecture de la sortie d'une commande via la substitution de processus est similaire à la lecture d'un fichier.
codeforester

Réponses:

161

Les autres réponses éclateront si la sortie de la commande contient des espaces ( ce qui est assez fréquent) ou des personnages comme glob *, ?, [...].

Pour obtenir la sortie d'une commande dans un tableau, avec une ligne par élément, il existe essentiellement 3 façons:

  1. Avec Bash≥4 mapfile, c'est le plus efficace:

    mapfile -t my_array < <( my_command )
  2. Sinon, une boucle lisant la sortie (plus lente, mais sûre):

    my_array=()
    while IFS= read -r line; do
        my_array+=( "$line" )
    done < <( my_command )
  3. Comme suggéré par Charles Duffy dans les commentaires (merci!), Ce qui suit pourrait fonctionner mieux que la méthode en boucle du numéro 2:

    IFS=$'\n' read -r -d '' -a my_array < <( my_command && printf '\0' )

    Veuillez vous assurer que vous utilisez exactement ce formulaire, c'est-à-dire que vous disposez des éléments suivants:

    • IFS=$'\n' sur la même ligne que l' readinstruction: cela ne définira la variable d'environnement que IFS pour l' readinstruction uniquement. Cela n'affectera donc pas du tout le reste de votre script. Le but de cette variable est de dire readde couper le flux au caractère EOL \n.
    • -r: c'est important. Il dit read de ne pas interpréter les contre-obliques comme des séquences d'échappement.
    • -d '': veuillez noter l'espace entre l' -doption et son argument ''. Si vous ne laissez pas d'espace ici, le ''ne sera jamais vu, car il disparaîtra lors de l' étape de suppression du devis lorsque Bash analysera l'instruction. Cela indique readd'arrêter la lecture à l'octet nul. Certaines personnes l'écrivent comme -d $'\0', mais ce n'est pas vraiment nécessaire. -d ''est mieux.
    • -a my_arraydit readde remplir le tableau my_arraylors de la lecture du flux.
    • Vous devez utiliser l' printf '\0'instruction après my_command , de sorte que readretourne 0; ce n'est en fait pas un gros problème si vous ne le faites pas (vous obtiendrez simplement un code de retour 1, ce qui est bien si vous ne l'utilisez pas set -e- ce que vous ne devriez pas de toute façon), mais gardez cela à l'esprit. C'est plus propre et plus sémantiquement correct. Notez que c'est différent de printf '', qui ne produit rien. printf '\0'affiche un octet nul, nécessaire readpour arrêter joyeusement la lecture (vous vous souvenez de l' -d ''option?).

Si vous le pouvez, c'est-à-dire si vous êtes sûr que votre code fonctionnera sur Bash≥4, utilisez la première méthode. Et vous pouvez voir que c'est plus court aussi.

Si vous souhaitez l'utiliser read, la boucle (méthode 2) peut avoir un avantage sur la méthode 3 si vous souhaitez effectuer un traitement au fur et à mesure que les lignes sont lues: vous y avez un accès direct (via la $linevariable dans l'exemple que j'ai donné), et vous avez également accès aux lignes déjà lues (via le tableau ${my_array[@]}dans l'exemple que j'ai donné).

Notez que cela mapfilefournit un moyen d'avoir un rappel évalué sur chaque ligne lue, et en fait vous pouvez même lui dire de n'appeler ce rappel que toutes les N lignes lues; jetez un oeil à help mapfileet les options -Cet -clà - dedans. (Mon opinion à ce sujet est que c'est un peu maladroit, mais peut être utilisé parfois si vous n'avez que des choses simples à faire - je ne comprends pas vraiment pourquoi cela a même été implémenté en premier lieu!).


Maintenant, je vais vous dire pourquoi la méthode suivante:

my_array=( $( my_command) )

est cassé lorsqu'il y a des espaces:

$ # I'm using this command to test:
$ echo "one two"; echo "three four"
one two
three four
$ # Now I'm going to use the broken method:
$ my_array=( $( echo "one two"; echo "three four" ) )
$ declare -p my_array
declare -a my_array='([0]="one" [1]="two" [2]="three" [3]="four")'
$ # As you can see, the fields are not the lines
$
$ # Now look at the correct method:
$ mapfile -t my_array < <(echo "one two"; echo "three four")
$ declare -p my_array
declare -a my_array='([0]="one two" [1]="three four")'
$ # Good!

Ensuite, certaines personnes recommanderont d'utiliser IFS=$'\n'pour le réparer:

$ IFS=$'\n'
$ my_array=( $(echo "one two"; echo "three four") )
$ declare -p my_array
declare -a my_array='([0]="one two" [1]="three four")'
$ # It works!

Mais maintenant, utilisons une autre commande, avec des globs :

$ echo "* one two"; echo "[three four]"
* one two
[three four]
$ IFS=$'\n'
$ my_array=( $(echo "* one two"; echo "[three four]") )
$ declare -p my_array
declare -a my_array='([0]="* one two" [1]="t")'
$ # What?

C'est parce que j'ai un fichier appelé tdans le répertoire courant ... et ce nom de fichier correspond au glob [three four] ... à ce stade, certaines personnes recommanderaient d'utiliser set -fpour désactiver le globbing: mais regardez-le: vous devez changer IFSet utiliser set -fpour pouvoir réparer un technique cassée (et vous ne la réparez même pas vraiment)! en faisant cela, nous luttons vraiment contre le shell, pas avec le shell .

$ mapfile -t my_array < <( echo "* one two"; echo "[three four]")
$ declare -p my_array
declare -a my_array='([0]="* one two" [1]="[three four]")'

ici nous travaillons avec le shell!

gniourf_gniourf
la source
4
C'est génial, je n'en ai jamais entendu parler mapfileauparavant, c'est exactement ce qui me manque depuis des années. Je suppose que les versions récentes de bashont tellement de nouvelles fonctionnalités intéressantes, je devrais juste passer quelques jours à lire la documentation et à écrire une belle feuille de triche.
Gene Pavlovsky
6
Btw, pour utiliser cette syntaxe < <(command)dans les scripts shell, la ligne shebang doit être #!/bin/bash- si elle est exécutée en tant que #!/bin/sh, bash se terminera avec une erreur de syntaxe.
Gene Pavlovsky
1
S'étendant sur la note utile de @ GenePavlovsky, le script doit également être exécuté avec la commande bash bash my_script.shet non la commande shsh my_script.sh
Vito
2
@Vito: en effet, cette réponse est uniquement pour Bash, mais cela ne devrait pas être un problème, car les shells POSIX strictement conformes n'implémentent même pas les tableaux ( shet dashne connaissent pas du tout les tableaux, sauf, bien sûr, pour le $@tableau des paramètres de position ).
gniourf_gniourf
3
Comme autre alternative qui ne nécessite pas bash 4.0, considérez que IFS=$'\n' read -r -d '' -a my_array < <(my_command && printf '\0')cela fonctionne correctement dans bash 3.x et passe également par un état de sortie d'échec de my_commandà read.
Charles Duffy
86

Vous pouvez utiliser

my_array=( $(<command>) )

pour stocker la sortie de la commande <command>dans le tableau my_array.

Vous pouvez accéder à la longueur de ce tableau en utilisant

my_array_length=${#my_array[@]}

Maintenant, la longueur est stockée dans my_array_length.

Michael Schlottke-Lakemper
la source
19
Que faire si la sortie de $ (commande) comporte des espaces et plusieurs lignes avec des espaces? J'ai ajouté "$ (command)" et il place toute la sortie de toutes les lignes dans le premier élément [0] du tableau.
ikwyl6
3
@ ikwyl6 une solution de contournement consiste à affecter la sortie de la commande à une variable, puis à créer un tableau avec elle ou à l'ajouter à un tableau. VAR="$(<command>)"et puis my_array=("$VAR")oumy_array+=("$VAR")
Vito
10

Imaginez que vous allez mettre les fichiers et les noms de répertoire (sous le dossier actuel) dans un tableau et compter ses éléments. Le script serait comme;

my_array=( `ls` )
my_array_length=${#my_array[@]}
echo $my_array_length

Ou, vous pouvez parcourir ce tableau en ajoutant le script suivant:

for element in "${my_array[@]}"
do
   echo "${element}"
done

Veuillez noter que c'est le concept de base et que l'entrée est considérée comme nettoyée avant, c'est-à-dire supprimer des caractères supplémentaires, gérer des chaînes vides, etc. (ce qui est hors du sujet de ce fil).

Youness
la source
3
Terrible idée pour les raisons mentionnées dans la réponse ci
Hubert Grzeskowiak