Lister tous les fichiers d'un dossier

a marqué ce sujet comme résolu.

Bonjour,

J’essaye de lister tous les fichiers contenus dans un dossier et ses sous-dossiers. Je ne peux pas utiliser la commande find pour cet exercice. J’ai codé une solution ci dessous. J’aimerais tout placer dans une seule boucle si possible, ça simplifierait le code.

Comment auriez-vous procédé ?

#!/bin/bash
MYPATH='/home/desktop/example'


function print_files()
{
    cd $1
    local current_dir=$1
    local tab=()            # Liste des dossiers contenus dans $current_dir
    local n='0'             # Nombre d'éléments dans tab
    
    for file in *; do
        if [ -d $file ]; then
            tab[$n]=$file
            let "n += 1"
        elif [ -f $file ]; then
            echo "$current_dir/$file"
        fi
    done
    
    for i in ${tab[@]}; do
         print_files "$current_dir/$i"
    done
}

print_files $MYPATH
+0 -0

Quand tu dis « tous les fichiers », tu inclus aussi les cachés (préfixés d’un point) ?

Euh non l’exercice ne précise pas, donc on va faire sans.

Tu ne peux pas utiliser find, mais quelles autres commandes sont autorisées ?

entwanne

Toutes les commandes standards ne dépendant pas d’un package tiers.
Exemples : ls cd cat chmod grep

Personnellement, j’aurais tendance à utiliser une fonction récursive pour le parcours de l’arbre: pour un dossier, il suffit d’afficher chaque fichier contenu (précédé du dossier), et, si le fichier est un dossier, rappeler la fonction sur ce fichier.

Personnellement, j’aurais tendance à utiliser une fonction récursive pour le parcours de l’arbre: pour un dossier, il suffit d’afficher chaque fichier contenu (précédé du dossier), et, si le fichier est un dossier, rappeler la fonction sur ce fichier.

Jacen

J’ai implémenté cette solution dans le code plus haut, mais j’utilise deux boucles pour y arriver.

Salut,

Si c’est pour toi et pas un exercice de programmation, tu peux utiliser tree -if. Tu peux rajouter -a pour les fichiers cachés, et si tu veux retirer les dossiers il doit y avoir moyen de ruser (par exemple un pattern d’exclusion des fichiers qui finissent par /).

Sinon, je ne comprends pas pourquoi tu veux tout stocker dans un tableau avant de tout afficher.

C’est tout aussi simple de faire un parcours en profondeur bateau, et d’afficher à la fin de chaque profondeur, non ? Je connais très mal Bash, mais ça donnerait ça (je n’ai pas testé).

#!/bin/bash
MYPATH='/home/desktop/example'


function print_files()
{
    cd $1
    local current_dir=$1
    
    for file in *; do
        if [ -d $file ]; then
            # echo "$current_dir/$i"  # Si tu veux préfixer par les noms de dossier
            print_files "$current_dir/$i"

        elif [ -f $file ]; then
            echo "$current_dir/$file"
        fi
    done
    

}

print_files $MYPATH
+0 -0

Salut,

C’est tout aussi simple de faire un parcours en profondeur bateau, et d’afficher à la fin de chaque profondeur, non ? Je connais très mal Bash, mais ça donnerait ça (je n’ai pas testé).

melepe

Il y a une petite typo (tu utilises une variables $i au lieu de $file). Sinon, la récursion n’est pas bonne à cause de cd qui modifie le répertoire courant globalement (ce qui du coup casse les conditions qui font appelle à test). Il est nécessaire de faire appel à cd en fin de fonction pour retourner au répertoire précédent.

#!/bin/bash

MYPATH='/home/desktop/example'


function print_files()
{
    local new_dir=$1
    local cur_dir=$(pwd)

    cd $new_dir
    
    for file in *; do
        if [ -d $file ]; then
            print_files "$new_dir/$file"
        elif [ -f $file ]; then
            echo "$new_dir/$file"
        fi
    done
    
    cd $cur_dir
}

print_files $MYPATH

Cela étant, se baser sur l’expension du symbole * par bash n’est pas une méthode infaillible dans le cas par exemple d’espaces au sein du nom de fichier. Il est à mon sens préférable de recourir à ls, qui est d’ailleurs capable de faire la récursion avec l’option -R.

#! /bin/sh

cur_dir=$(pwd)


ls -R -1 | while read line
do
    if echo "${line}" | grep -q ':$'
    then
        cur_dir="${line%:*}"
    elif [ -f "${cur_dir}/${line}" ]
    then
        echo "${cur_dir}/${line}"
    fi
done

exit 0
+1 -0

Pour la méthode récursive avec le *, il y a plus efficace (de ~0.016 sec en listant 56 fichiers donc c’est pas énorme non plus):

#!/bin/bash

MYPATH='/home/desktop/example'


function print_files()
{
    cd $1
    
    for file in *; do
        if [ -d $file ]; then
            print_files "$1/$file"
        elif [ -f $file ]; then
            echo "$1/$file"
        fi
    done
    
    [[ "$1" != "$MYPATH" ]] && cd ..
}

print_files $MYPATH

Par contre remplacer le * par un ls (que ce soit via un for ou un while) rend tout de suite 2x plus intéressante la solution non récursive (je ne savait pas que c’était plus lourd….).

Il y a pas moyen de garder le jocker, tout en ne posant pas de problèmes au niveau des fichiers à espace?

+0 -0

Il y a pas moyen de garder le jocker, tout en ne posant pas de problèmes au niveau des fichiers à espace?

charlie02

À ma connaissance, non. Le soucis, c’est que le symbole * est remplacé par tous les éléments non cachés du répertoire courant et que c’est cette liste produite qui est utilisée par la boucle for. Sauf que pour ses itérations, la boucle for cherche des éléments séparés par des espaces ou des tabulations (valeur par défaut de la variable IFS). Du coup, si les noms de fichiers comportent des espaces, la boucle for va également itérer sur les noms eux-mêmes.

Le soucis pourrait être contourné en modifiant la variable IFS, mais cela impliquerait aussi de modifier l’expension du symbole * pour par exemple séparer les noms d’éléments par des tabulations (ce qui n’est à ma connaissance pas possible) et cela ne ferait que reporter le problème (quel que soit le séparateur choisis, s’il peut être utilisé dans un nom de fichier, c’est le même combat).

+1 -0
#!/bin/bash

shopt -s globstar
IFS=$'\0'
for i in **
do
    [[ -f "$i" ]] && echo "$i"
done
IFS=$' \t\n'

Je sais pas si ça correspond bien à la demande, mais je trouve que le résultat est pas trop mal.

** avec l’option globstar liste tous les dossiers, sous-dossiers et leurs contenus. Du coup un petit for dessus et le [[ -f vérifie que c’est bien un fichier et si oui (&&) l’affiche (echo).

S’il faut tout lister sauf les dossier, un [[ ! -d fait l’affaire à la place. Ça n’inclue pas les fichiers et dossiers cachés par contre (s’il faut ça doit se faire).

L’IFS est modifié pour ne prendre que les retours à la ligne (pour éviter les soucis d’espaces), puis remis à sa valeur d’origine. Si un petit malin a mis un retour à la ligne dans un nom de fichier (oui, on peut) faudra bricoler un peu plus par contre.

EDIT : après vérification, utiliser le null byte permet d’éviter le soucis des retours à la ligne dans les noms de fichier et fonctionne quand même (si on mets un echo --- entre les lignes 7 et 8, on voit bien les séparations, y compris avec un fichier contenant \n). Et il est interdit de mettre un null byte dans un nom de fichier. Du coup IFS est modifié pour prendre le null byte comme séparateur et remis à sa valeur d’origine.

S’il faut que le nom de fichier et pas le chemin complet, echo $(basename "$i") à la place du echo permettra de le faire.

Le script liste à partir du dossier courant, s’il faut prendre une variable ou un argument ça s’adapte.

Si vous avez des questions ou que vous voulez d’autres précisions (ou que j’ai rien compris à l’énoncé), hésitez pas ^^

+1 -0

Mmm… Dans les faits il semble que définir IFS comme suit IFS=$'\0' est équivalent à IFS= (c’est-à-dire définir IFS comme étant nul) car bash va considérer $'\0' comme une chaîne vide (comme le ferait n’importe quel programme C en fait, considérant le multiplet nul comme la fin de la chaîne). Autrement dit, en C cela reviendrait à écrire char IFS[] = "\0";, soit un tableau de deux char nuls.

Ce qui me perturbe par contre, c’est que quand IFS est nul, bash lui attribue sa valeur par défaut pour ce qui est de la séparation des mots (word splitting). Cependant, l’itération avec la boucle for que tu présentes fonctionne effectivement… En fait, c’est même pire que cela, cela fonctionne de manière assez étrange.

# IFS=$' \t\n'

$ ls -1
un fichier avec des espaces
un-fichier-sans-espaces
$ for _f in *; do echo ${_f}; done
un fichier avec des espaces
un-fichier-sans-espaces
$ for _f in *; do printf "${_f}\n"; done
un fichier avec des espaces
un-fichier-sans-espaces
$ for _f in *; do printf "%s\n" ${_f}; done
un
fichier
avec
des
espaces
un-fichier-sans-espaces

#
# Whaaaaat?! o_O
#

Si quelqu’un a une explication rationnelle, je suis preneur. :-°

+0 -0

Ah, peut-être pour le coup de la chaîne vide (mais je saurais pas dire si c’est vraiment le cas). Par contre, IFS=$'\0' (j’ai pas testé juste IFS='') ne correspond pas du tout à remettre la valeur par défaut. C’est fourbe, ceci dit. Et le fait que ça passe, c’est ptêt que le null byte est utilisé pour séparer les fichiers (je sais pas du tout, je suppose juste).

Pour tes printf c’est assez évident si tu fait l’expansion :

echo un fichier avec des espaces
printf "un fichier avec des espaces\n"
printf "%s\n" un fichier avec des espaces
+1 -0

Ah, peut-être pour le coup de la chaîne vide (mais je saurais pas dire si c’est vraiment le cas). Par contre, IFS=$'\0' (j’ai pas testé juste IFS='') ne correspond pas du tout à remettre la valeur par défaut. C’est fourbe, ceci dit. Et le fait que ça passe, c’est ptêt que le null byte est utilisé pour séparer les fichiers (je sais pas du tout, je suppose juste).

Breizh

En effet, au temps pour moi, utiliser le multiplet nul est seulement équivalent à assigner une chaîne vide.

Pour tes printf c’est assez évident si tu fait l’expansion :

echo un fichier avec des espaces
printf "un fichier avec des espaces\n"
printf "%s\n" un fichier avec des espaces

Breizh

Si c’était le cas, la dernière boucle retournerai alors ceci.

un
un-fichier-sans-espaces

Sauf que ce n’est pas le cas, d’où mon interrogation. ^^

Édit : ah, non, ok, au temps pour moi, cela semble être un comportement de printf que je ne connaissais pas. o_O

+0 -0

Ok, donc si je saisis bien le fonctionnement global, le mécanisme de séparation n’a lieu que lors d’une substitution, ce qui exclut les expensions de nom de fichiers. Autrement dit, il n’y a visiblement même pas besoin de chipoter avec IFS pour ça.

+0 -0

Du coup, version corrigée :

#!/bin/bash

# Active l'option globstar pour pouvoir utiliser le **
shopt -s globstar

# Parcours tous les dossiers, sous-dossiers et leurs contenus
for i in **
do
    # Vérifie si $i est un fichier et si oui l'affiche
    [[ -f "$i" ]] && echo "$i"
done

Beaucoup plus court et clair, et visiblement ça marche après test. Comme quoi on en apprends tous les jours ^^

+2 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte