Comment puis-je stocker les résultats de la commande «find» sous forme de tableau dans Bash

92

J'essaie d'enregistrer le résultat findsous forme de tableaux. Voici mon code:

#!/bin/bash

echo "input : "
read input

echo "searching file with this pattern '${input}' under present directory"
array=`find . -name ${input}`

len=${#array[*]}
echo "found : ${len}"

i=0

while [ $i -lt $len ]
do
echo ${array[$i]}
let i++
done

J'obtiens 2 fichiers .txt dans le répertoire courant. J'attends donc «2» à la suite de ${len}. Cependant, il imprime 1. La raison est qu'il prend tous les résultats de findcomme un seul élément. Comment puis-je réparer cela?

PS
J'ai trouvé plusieurs solutions sur StackOverFlow à propos d'un problème similaire. Cependant, ils sont un peu différents, je ne peux donc pas postuler dans mon cas. J'ai besoin de stocker les résultats dans une variable avant la boucle. Merci encore.

Juneyoung Oh
la source

Réponses:

133

Mise à jour 2020 pour les utilisateurs Linux:

Si vous avez une version de bash (4,4-alpha ou mieux) la mise à jour, comme vous le faites probablement si vous êtes sous Linux, alors vous devriez utiliser la réponse de Benjamin W. .

Si vous êtes sur Mac OS, qui - j'ai vérifié la dernière fois - utilisait encore bash 3.2, ou si vous utilisez un autre bash, passez à la section suivante.

Réponse pour bash 4.3 ou version antérieure

Voici une solution pour obtenir la sortie de finddans un bashtableau:

array=()
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done < <(find . -name "${input}" -print0)

Ceci est délicat car, en général, les noms de fichiers peuvent avoir des espaces, de nouvelles lignes et d'autres caractères hostiles au script. La seule façon d'utiliser findet de séparer en toute sécurité les noms de fichiers les uns des autres consiste à utiliser -print0qui imprime les noms de fichiers séparés par un caractère nul. Ce ne serait pas très gênant si les fonctions readarray/ mapfilefonctions de bash supportaient les chaînes séparées par des valeurs nulles, mais ce n'est pas le cas. Bash le readfait et cela nous conduit à la boucle ci-dessus.

[Cette réponse a été rédigée à l'origine en 2014. Si vous disposez d'une version récente de bash, veuillez consulter la mise à jour ci-dessous.]

Comment ça fonctionne

  1. La première ligne crée un tableau vide: array=()

  2. Chaque fois que l' readinstruction est exécutée, un nom de fichier séparé par des valeurs nulles est lu à partir de l'entrée standard. L' -roption indique readde laisser les caractères antislash seuls. Le -d $'\0'indique readque l'entrée sera séparée par des valeurs nulles. Puisque nous omettons le nom read, le shell met l'entrée dans le nom par défaut: REPLY.

  3. L' array+=("$REPLY")instruction ajoute le nouveau nom de fichier au tableau array.

  4. La dernière ligne combine la redirection et la substitution de commande pour fournir la sortie de findà l'entrée standard de la whileboucle.

Pourquoi utiliser la substitution de processus?

Si nous n'utilisons pas de substitution de processus, la boucle pourrait être écrite comme suit:

array=()
find . -name "${input}" -print0 >tmpfile
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done <tmpfile
rm -f tmpfile

Dans ce qui précède, la sortie de findest stockée dans un fichier temporaire et ce fichier est utilisé comme entrée standard dans la boucle while. L'idée de la substitution de processus est de rendre ces fichiers temporaires inutiles. Donc, au lieu de whiledemander à la boucle de récupérer son stdin tmpfile, nous pouvons lui demander de récupérer son stdin <(find . -name ${input} -print0).

La substitution de processus est très utile. Dans de nombreux endroits où une commande souhaite lire à partir d'un fichier, vous pouvez spécifier la substitution de processus <(...), au lieu d'un nom de fichier. Il existe une forme analogue,, >(...)qui peut être utilisée à la place d'un nom de fichier où la commande veut écrire dans le fichier.

Comme les tableaux, la substitution de processus est une fonctionnalité de bash et d'autres shells avancés. Il ne fait pas partie du standard POSIX.

Alternative: lastpipe

Si vous le souhaitez, lastpipepeut être utilisé à la place de la substitution de processus (chapeau: César ):

set +m
shopt -s lastpipe
array=()
find . -name "${input}" -print0 | while IFS=  read -r -d $'\0'; do array+=("$REPLY"); done; declare -p array

shopt -s lastpipedit à bash d'exécuter la dernière commande du pipeline dans le shell actuel (pas en arrière-plan). De cette façon, le arrayreste en existence une fois le pipeline terminé. Parce lastpipeque ne prend effet que si le contrôle des tâches est désactivé, nous exécutons set +m. (Dans un script, contrairement à la ligne de commande, le contrôle des travaux est désactivé par défaut.)

Notes complémentaires

La commande suivante crée une variable shell, pas un tableau shell:

array=`find . -name "${input}"`

Si vous souhaitez créer un tableau, vous devez placer des parens autour de la sortie de find. Donc, naïvement, on pourrait:

array=(`find . -name "${input}"`)  # don't do this

Le problème est que le shell effectue la division des mots sur les résultats de findafin que les éléments du tableau ne soient pas garantis comme vous le souhaitez.

Mise à jour 2019

À partir de la version 4.4-alpha, bash prend désormais en charge une -doption pour que la boucle ci-dessus ne soit plus nécessaire. Au lieu de cela, on peut utiliser:

mapfile -d $'\0' array < <(find . -name "${input}" -print0)

Pour plus d' informations à ce sujet , s'il vous plaît voir (et upvote) la réponse de Benjamin W. .

John1024
la source
1
@JuneyoungOh Heureux que cela ait aidé. J'ai ajouté une section de substitution de processus.
John1024
3
@Rockallite C'est une bonne observation mais incomplète. S'il est vrai que nous ne divisons pas en plusieurs mots, nous devons IFS=quand même éviter de supprimer les espaces au début ou à la fin des lignes d'entrée. Vous pouvez tester cela facilement en comparant la sortie de read var <<<' abc '; echo ">$var<"avec la sortie de IFS= read var <<<' abc '; echo ">$var<". Dans le premier cas, les espaces avant et après abcsont supprimés. Dans ce dernier, ils ne le sont pas. Les noms de fichiers qui commencent ou se terminent par des espaces peuvent être inhabituels, mais s'ils existent, nous voulons qu'ils soient traités correctement.
John1024
1
Bonjour, après avoir exécuté votre code, j'obtiens une erreur de syntaxe de message près d'un jeton inattendu <' terminé <<(find aaa / -not -newermt "$ last_build_timestamp_v" -type f -print0) '
Przemysław Sienkiewicz
1
A signaler: le plus simple ''peut être utilisé à la place de $'\0':n=0; while IFS= read -r -d '' line || [ "$line" ]; do echo "$((++n)):$line"; done < <(printf 'first\nstill first\0second\0third')
glenn jackman
1
@theeagle Je suppose que vous aviez l'intention d'écrire BLAH=$(find . -name '*.php'). Comme discuté dans la réponse, cette approche fonctionnera dans des cas limités, mais elle ne fonctionnera pas en général avec tous les noms de fichiers et ne produira pas, comme l'OP s'y attendait, un tableau .
John1024
35

Bash 4.4 a introduit une -doption pour readarray/ mapfile, donc cela peut maintenant être résolu avec

readarray -d '' array < <(find . -name "$input" -print0)

pour une méthode qui fonctionne avec des noms de fichiers arbitraires, y compris des espaces, des sauts de ligne et des caractères globuleux. Cela nécessite que vos findsupports-print0 , comme par exemple GNU find le fait.

À partir du manuel (en omettant les autres options):

mapfile [-d delim] [array]

-d
Le premier caractère de delimest utilisé pour terminer chaque ligne d'entrée, plutôt qu'une nouvelle ligne. Si delimest la chaîne vide, mapfileterminera une ligne lorsqu'il lit un caractère NUL.

Et readarrayc'est juste un synonyme de mapfile.

Benjamin W.
la source
18

Si vous utilisez bash4 ou une version ultérieure, vous pouvez remplacer votre utilisation de findpar

shopt -s globstar nullglob
array=( **/*"$input"* )

Le **modèle activé par globstarcorrespond à 0 ou plusieurs répertoires, permettant au modèle de correspondre à une profondeur arbitraire dans le répertoire courant. Sans l' nullgloboption, le modèle (après le développement des paramètres) est traité littéralement, donc sans correspondance, vous auriez un tableau avec une seule chaîne plutôt qu'un tableau vide.

Ajoutez également l' dotgloboption à la première ligne si vous souhaitez parcourir les répertoires cachés (comme .ssh) et faire correspondre les fichiers cachés (comme .bashrc).

chepner
la source
4
Peut-être nullglobaussi…
kojiro
1
Ouais, j'oublie toujours ça.
chepner
5
Notez que cela n'inclura pas les fichiers et répertoires cachés, à moins que ce ne dotglobsoit défini (cela peut être souhaité ou non, mais cela vaut également la peine d'être mentionné).
gniourf_gniourf
10

tu peux essayer quelque chose comme

array=(`find . -type f | sort -r | head -2`)
, et pour imprimer les valeurs du tableau, vous pouvez essayer quelque chose comme echo "${array[*]}"

Ahmed Al-Haffar
la source
8
Interrompt s'il y a des noms de fichiers avec des espaces ou des caractères globaux
gniourf_gniourf
1

Ce qui suit semble fonctionner à la fois pour Bash et Z Shell sur macOS.

#! /bin/sh

IFS=$'\n'
paths=($(find . -name "foo"))
unset IFS

printf "%s\n" "${paths[@]}"
Sunknudsen
la source
Cela fonctionne avec les fichiers ayant des espaces et d'autres caractères spéciaux, échoue avec le cas (certes rare) des fichiers ayant un saut de ligne dans leur nom. Vous pouvez en créer un pour un test avecprintf "%b" "file name with spaces, a star * ...\012and a second line\0" | xargs -0 touch
Stéphane Gourichon
-1

Dans bash, $(<any_shell_cmd>)permet d'exécuter une commande et de capturer la sortie. Passer ceci à IFSavec \ncomme délimiteur permet de le convertir en tableau.

IFS='\n' read -r -a txt_files <<< $(find /path/to/dir -name "*.txt")
rashok
la source
4
Cela n'obtiendra que le premier fichier des résultats de finddans le tableau.
Benjamin W.
-2

Vous pourriez faire comme ceci:

#!/bin/bash
echo "input : "
read input

echo "searching file with this pattern '${input}' under present directory"
array=(`find . -name '*'${input}'*'`)

for i in "${array[@]}"
do :
    echo $i
done
user1357768
la source
1
Merci. beaucoup. Mais comme @anishsane l'a souligné, les espaces vides dans le nom du fichier doivent être pris en compte dans mon programme. En tout cas merci!
Juneyoung Oh
-3

Pour moi, cela a bien fonctionné sur cygwin:

declare -a names=$(echo "("; find <path> <other options> -printf '"%p" '; echo ")")
for nm in "${names[@]}"
do
    echo "$nm"
done

Cela fonctionne avec des espaces, mais pas avec des guillemets doubles (") dans les noms de répertoire (qui ne sont pas autorisés dans un environnement Windows de toute façon).

Faites attention à l'espace dans l'option -printf.

Risack
la source
3
Cassé et dangereux : ne gère pas les guillemets et est soumis à une injection de code arbitraire. NE PAS UTILISER.
gniourf_gniourf
2
Il semble que quelqu'un ait signalé ce message pour suppression. "C'est faux" n'est pas une raison de suppression sur SO. L'utilisateur a tenté de répondre, c'est sur le sujet et répond aux critères de réponse. Le bouton de vote défavorable est utilisé pour mesurer l'utilité et l'exactitude, pas le bouton de suppression.
Frambot
3
Comme l'a souligné gniourf, ce n'est pas pour les environnements où d'autres saisissent les options de votre système, par exemple les pages Web. Mais tout le monde ne programme pas pour cet environnement. Je l'ai utilisé pour renommer des fichiers dans des répertoires.
R Risack