Faire en sorte que le script BASH `for` gère les noms de fichiers avec des espaces (ou solution de contournement)

12

Bien que j'utilise BASH depuis plusieurs années, mon expérience avec les scripts BASH est relativement limitée.

Mon code est comme ci-dessous. Il doit récupérer l'intégralité de la structure du répertoire à partir du répertoire actuel et la répliquer dans $OUTDIR.

for DIR in `find . -type d -printf "\"%P\"\040"`
do
  echo mkdir -p \"${OUTPATH}${DIR}\"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done

Le problème est, voici un exemple de ma structure de fichiers:

$ ls
Expect The Impossible-Stellar Kart
Five Iron Frenzy - Cheeses...
Five Score and Seven Years Ago-Relient K
Hello-After Edmund
I Will Go-Starfield
Learning to Breathe-Switchfoot
MMHMM-Relient K

Notez les espaces: -S Et forprend les paramètres mot par mot, donc la sortie de mon script ressemble à ceci:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Learning"
Created Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot"
Created Breathe-Switchfoot

Mais j'en ai besoin pour récupérer des noms de fichiers entiers (une ligne à la fois) de la sortie de find. J'ai également essayé de findmettre des guillemets autour de chaque nom de fichier. Mais cela n'aide pas.

for DIR in `find . -type d -printf "\"%P\"\040"`

Et sortie avec cette ligne modifiée:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"""
Created ""
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"Learning"
Created "Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot""
Created Breathe-Switchfoot"

Maintenant, j'ai besoin d'un moyen que je puisse parcourir comme ceci, car je souhaite également exécuter une commande plus compliquée impliquant gstreamerchaque fichier dans une structure similaire suivante. Comment dois-je faire cela?

Edit: J'ai besoin d'une structure de code qui me permettra d'exécuter plusieurs lignes de code pour chaque répertoire / fichier / boucle. Désolé si je n'ai pas été clair.

Solution: j'ai d'abord essayé:

find . -type d | while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done

Cela a très bien fonctionné pour la plupart. Cependant, j'ai constaté plus tard que, puisque le canal entraînait l'exécution de la boucle while dans un sous-shell, toutes les variables définies dans la boucle étaient ultérieurement indisponibles, ce qui rendait l'implémentation d'un compteur d'erreurs assez difficile. Ma solution finale (à partir de cette réponse sur SO ):

while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done < <(find . -type d)

Cela m'a plus tard permis d'incrémenter conditionnellement des variables dans la boucle qui resteraient disponibles plus tard dans le script.

Samuel Jaeschke
la source
Why_would_you_ever_need_a_space_in_a_file_name?
Kevin Panko
C'est vrai, pas ma préférence. Cependant, pour supprimer des espaces, vous devez d'abord gérer les fichiers avec des espaces;)
Samuel Jaeschke
1
En fait, les noms de fichiers doivent autoriser les espaces. /J'autoriserais tout sauf les caractères non imprimables. Mais tout est permis sauf /et \0vous devez donc les autoriser.
Kevin Panko

Réponses:

11

Vous devez diriger le finddans une whileboucle.

find ... | while read -r dir
do
    something with "$dir"
done

De plus, vous n'aurez pas besoin de l'utiliser -printfdans ce cas.

Vous pouvez faire cette preuve contre les fichiers avec des retours à la ligne dans leurs noms, si vous le souhaitez, en utilisant un délimiteur nullbyte (c'est le seul caractère qui ne peut pas apparaître dans un chemin de fichier * nix):

find ... -print0 | while read -d '' -r dir
do
    something with "$dir"
done

Vous constaterez également que l'utilisation $()au lieu de backticks est plus polyvalente et plus facile. Ils peuvent être imbriqués beaucoup plus facilement et les devis peuvent être effectués beaucoup plus facilement. Cet exemple artificiel illustrera ces points:

echo "$(echo "$(echo "hello")")"

Essayez de le faire avec des contre-coups.

En pause jusqu'à nouvel ordre.
la source
2
De plus, plutôt que "$dir", il est préférable d'utiliser "${dir}"- il est facile de faire la différence entre $ {dir} nom et $ {dirname}, mais $ dirname peut être interprété dans les deux sens.
James Polley
L'important ici est de readlire une ligne entière ${dir}, donc l'IFS n'a pas d'importance.
James Polley
1
Merci d'avoir trouvé la faute de frappe $ / ". Les accolades ne sont pas nécessaires s'il n'y a rien après le nom de la variable.
pause jusqu'à nouvel ordre.
4
Cela gérera les chemins d'accès avec des espaces (U + 0020), mais il échouera toujours à gérer correctement les chemins d'accès avec des sauts de ligne (U + 000A). Je préfère find … -print0 | xargs -0 …parce que le délimiteur qu'il utilise correspond exactement au seul caractère qui n'est pas autorisé dans les noms de chemin POSIX: NUL (U + 0000).
Chris Johnsen
2
Parfait! Exactement ce que je cherchais. Il ne m'est jamais venu à l'esprit que vous pourriez être en mesure de le faire while. @Chris Johnsen: C'est vrai, mais même les programmes d'extraction de musique n'ont pas tendance à mettre des sauts de ligne dans leurs noms de fichiers. Et s'ils le font, je veux savoir (c'est-à-dire: quelque chose ne va pas) et m'en débarrasser immédiatement ...
Samuel Jaeschke
8

Voir cette réponse que j'ai écrite il y a quelques jours pour un exemple de script qui gère les noms de fichiers avec des espaces.

Il existe cependant un moyen un peu plus compliqué (mais plus concis) de réaliser ce que vous essayez de faire:

find . -type d -print0 | xargs -0 -I {} mkdir -p ../theredir/{}

-print0indique à find de séparer les arguments par un null; le -0 à xargs lui dit d'attendre des arguments séparés par des nulls. Cela signifie qu'il gère très bien les espaces.

-I {}indique à xargs de remplacer la chaîne {}par le nom de fichier. Cela implique également qu'un seul nom de fichier doit être utilisé par ligne de commande (xargs remplira normalement autant de fichiers que possible sur la ligne)

Le reste devrait être évident.

James Polley
la source
La suggestion de Dennis Williamson est cependant (en dehors des fautes de frappe) beaucoup plus lisible, et donc préférable dans presque tous les sens.
James Polley
Fonctionne, pour mkdir, mais désolé, j'aurais dû être plus clair - je souhaite exécuter une série de commandes pour chaque fichier. Vous voyez, pour ma routine similaire plus tard, je souhaite générer un nom de fichier de sortie basé sur le nom de fichier d'entrée (ce qui implique la suppression de l'extension .ogg et l'ajout de .mp3), puis j'utilise ces multiples variables dans ma hiérarchie lors de l'appel à gst-launch.
Samuel Jaeschke
5

Le problème que vous rencontrez est que l'instruction for répond à la recherche en tant qu'arguments distincts. Le délimiteur d'espace. Vous devez utiliser la variable IFS de bash pour ne pas diviser l'espace.

Voici un lien qui explique comment procéder.

La variable interne IFS

Une façon de contourner ce problème consiste à modifier la variable IFS interne (séparateur de champ interne) de Bash afin qu'elle divise les champs par autre chose que les espaces par défaut (espace, tabulation, nouvelle ligne), dans ce cas, une virgule.

#!/bin/bash
IFS=$';'

for I in `find -type d -printf \"%P\"\;`
do
   echo "== $I =="
done

Définissez votre recherche pour sortir votre délimiteur de champ après le% P et définissez votre IFS de manière appropriée. J'ai choisi le point-virgule car il est très peu probable qu'il se trouve dans vos noms de fichiers.

L'autre alternative est d'appeler mkdir à partir de la recherche directement via -execdo, vous pouvez ignorer la boucle for. C'est si vous n'avez pas besoin d'effectuer d'analyse syntaxique supplémentaire.

Darren Hall
la source
Que faire si le nom de fichier contient l'IFS? Ensuite, vous devez en choisir un autre. Mais alors, et si ...
Pause jusqu'à nouvel ordre.
3
Vous pouvez choisir /POSIX et :les systèmes de fichiers DOS. Il existe des caractères illégaux pour différents systèmes de fichiers que vous pouvez sélectionner pour l'IFS. Rien de plus compliqué et vous feriez mieux d'utiliser perl.
Darren Hall
2
Le problème avec / est que c'est le délimiteur de répertoire et findrenvoie les noms de fichiers avec des chemins incluant une barre oblique. Essayez de changer le point-virgule dans votre script en barre oblique et l'écho imprimera le répertoire et le nom de fichier sur des lignes distinctes.
pause jusqu'à nouvel ordre.
Cela semble également très utile. J'ai whileopté pour l' option pipe , mais cela semble également tout à fait réalisable. Oui, dans ma structure similaire plus tard, j'avais besoin de faire une analyse plus approfondie. (Le nom du fichier d'entrée serait .ogg, qui serait transmis comme filesrcdans le pipeline gst, mais un équivalent se terminant par .mp3 basé dans le répertoire de sortie serait généré et également transmis au pipeline en tant que filesink, et cela doit bien sûr être fait pour chaque fichier, ainsi que certains echoà l'utilisateur.)
Samuel Jaeschke
4

Si le corps de votre boucle est plus d'une seule commande, il est possible d'utiliser xargs pour piloter un script shell:

export OUTPATH=/some/where/else/
find . -type d -print0 | xargs -0 bash -c 'for DIR in "$@"; do
  printf "mkdir -p %q\\n" "${OUTPATH}${DIR}"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done' -

Assurez-vous d'inclure le tiret de fin (ou un autre «mot») si le shell est de la variété Bourne / POSIX (il est utilisé pour définir $ 0 dans le script shell). De plus, il faut être prudent avec les guillemets, car le script shell est écrit à l'intérieur d'une chaîne entre guillemets au lieu d'être directement à l'invite.

Chris Johnsen
la source
Un autre concept intéressant. Merci - je suis sûr que je trouverai une utilisation pour cela plus tard :)
Samuel Jaeschke
1

dans votre question mise à jour, vous avez

mkdir -p \"${OUTPATH}${DIR}\"

ce devrait être

mkdir -p "${OUTPATH}${DIR}"
user23307
la source
Merci. Fixé. Il lisait également FILENAME au lieu de DIR - copier-coller: P
Samuel Jaeschke
1
find . -type d -exec mkdir -p "{}\040" ';' -exec echo "Created {}\040" ';'
Vouze
la source
0

ou pour rendre le tout beaucoup moins compliqué:

% rsync -av --include='*/' --exclude='*' SRC DST

cela reproduit la structure de répertoires de SRC en DST.

akira
la source
Non, j'ai besoin d'une structure itérative comme celle-ci, ce qui me permet d'exécuter plusieurs lignes de code pour chaque fichier. "Maintenant, j'ai besoin d'un moyen de parcourir comme ceci, car je souhaite également exécuter une commande plus compliquée impliquant gstreamer sur chaque fichier dans une structure similaire suivante." Désolé si je n'ai pas été clair.
Samuel Jaeschke
la commande que j'ai donnée résout le problème que vous avez demandé, peu importe si ce n'est qu'une partie d'un plus grand «pipeline» de votre côté. pour quelqu'un d'autre ayant le problème décrit dans la question, l'approche rsync fonctionnera. donc, pas besoin d'être désolé pour un manque de clarté potentiel :)
akira
Oui. Non, je veux dire que j'utiliserais une structure similaire while... do... doneplus tard pour effectuer un traitement similaire à partir de find, ce qui nécessiterait l'exécution de plusieurs lignes de code sur chaque fichier (modification de chaîne, écho, lancement gst, etc. ) et rsyncn'y parviendrait pas. C'est pourquoi j'ai spécifié que j'avais besoin de pouvoir exécuter un ensemble de commandes plus compliqué dans une structure similaire. Mon script utilise cette structure de boucle deux fois, donc pour la question, j'ai posté celle avec moins de crud au milieu.
Samuel Jaeschke
0

Si vous avez installé GNU Parallel http: // www.gnu.org/software/parallel/, vous pouvez le faire:

find . -type d | parallel echo making {} ";" mkdir -p /tmp/outdir/{} ";" echo made {}

Regardez la vidéo d'introduction pour GNU Parallel pour en savoir plus: http://www.youtube.com/watch?v=OpaiGYxkSuQ

Ole Tange
la source