Manipuler le nom du fichier à partir de la commande find

10

Je suis relativement nouveau dans Bash et j'essaie de faire quelque chose qui à première vue semblait assez simple - exécuter la recherche sur une hiérarchie de répertoires pour obtenir tous les fichiers * .wma, diriger cette sortie vers une commande où je les convertis en mp3 et enregistrez le fichier converti au format .mp3. Ma pensée était que la commande devrait ressembler à ce qui suit (j'ai laissé la commande de conversion audio et j'utilise plutôt echo pour l'illustration):

$ find ./ -name '*.wma' -type f -print0 | xargs -0 -I f echo ${f%.*}.mp3

Si je comprends bien, l'argument -print0 me permettra de gérer les noms de fichiers qui ont des espaces (ce que beaucoup d'entre eux font car ce sont des fichiers musicaux). Je m'attends ensuite (à la suite de xargs) à ce que chaque chemin de fichier de find soit capturé en f, et qu'en utilisant la sous-chaîne match / delete à la fin de la chaîne, je devrais faire écho au chemin de fichier d'origine avec un mp3 extension au lieu de wma. Cependant, au lieu de ce résultat, je vois ce qui suit:

*.mp3
*.mp3
*.mp3
*.mp3
*.mp3
*.mp3
*.mp3
*.mp3
*.mp3
...

Donc ma question (en dehors de la spécification `` qu'est-ce que je fais mal ici '') est la suivante: les valeurs qui sont le résultat d'une opération de tuyau doivent-elles être traitées différemment dans les opérations de manipulation de chaînes que celles qui sont le résultat d'une affectation de variable ?

Howard Dierking
la source
1
Il n'y a aucune raison de l'utiliser xargsavec find. Il est livré avec une -execoption. Pouvez-vous simplement ajouter la commande que vous allez utiliser à votre question, et quelqu'un peut vous montrer la findcommande correcte ?
ixtmixilix
oui (comme je l'ai noté ci-dessous), tant que je peux encore faire la manipulation de chaîne sur chaque résultat de la commande find (par exemple le {}membre)
Howard Dierking
en fait, il y a des cas de bord où xargsest plus approprié que exec. Voir ce stackpost stackoverflow.com/questions/896808/find-exec-cmd-vs-xargs pour un cas d'espèce.
d34dh0r53
@ d34dh0r53 Quel cas de bord? Le fil auquel vous vous connectez ne pointe aucun.
Gilles 'SO- arrête d'être méchant'

Réponses:

8

Comme d'autres réponses l'ont déjà identifié, ${f%.*}est développé par le shell avant d'exécuter la xargscommande. Vous avez besoin que cette expansion se produise une fois pour chaque nom de fichier, avec la variable shell fdéfinie sur le nom du fichier (le passage -I fne fait pas cela: xargsn'a aucune notion de variable shell, il recherche la chaîne fdans la commande, donc si vous utilisé par exemple, xargs -I e echo …il aurait exécuté des commandes comme ./somedir/somefile.wmacho .mp3).

En gardant cette approche, dites xargsd'invoquer un shell qui peut effectuer l'expansion. Mieux, dites find- xargsest un outil largement obsolète et difficile à utiliser correctement, car les versions modernes de find ont une construction qui fait le même travail (et plus) avec moins de difficultés de plomberie. Au lieu de find … -print0 | xargs -0 command …, exécutez find … -exec command … {} +.

find . -name '*.wma' -type f -exec sh -c 'for f; do echo "${f%.*}.mp3"; done' _ {} +

L'argument _est $0dans le shell; les noms de fichiers sont passés comme arguments positionnels qui for f; do …bouclent. Une version plus simple de cette commande exécute un shell séparé pour chaque fichier, ce qui est équivalent mais légèrement plus lent:

find . -name '*.wma' -type f -exec sh -c 'echo "${0%.*}.mp3"' {} \;

Vous n'avez pas vraiment besoin d'utiliser findici, en supposant que vous exécutez un shell raisonnablement récent (ksh93, bash ≥4.0 ou zsh). En bash, insérez shopt -s globstarvotre .bashrcpour activer le **/modèle glob à récurrer dans les sous-répertoires (en ksh, c'est set -o globstar). Ensuite, vous pouvez exécuter

for f in **/*.wma; do
  echo "${f%.*}.mp3"
done

(Si vous avez des répertoires appelés *.wma, ajoutez-les [ -f "$f" ] || continueau début de la boucle.)

Gilles 'SO- arrête d'être méchant'
la source
de grandes explications et m'ont indiqué une direction que je n'avais même pas réalisée (mettre à niveau mon shell bash pour obtenir la fonctionnalité globstar) - au final, le globbing récursif était la solution qui a gardé la commande simple et accompli tout ce que j'essayais de faire . Merci!
Howard Dierking
6

Dans le cas de l'évaluation de votre solution de $ {f%. *}. Mp3 se fait dans le shell dans lequel vous invoquez la commande entière, pas dans le shell forké par les xargs . Et dans votre shell il n'y a pas de variable f , elle est remplacée par une chaîne vide.

Solution utilisant xargs avec -I f :

% cat 1.sh 
#!/bin/sh
f=$1
echo ${f%.*}.mp3
% /bin/ls -1
1.sh
aaa.wma
bbbb.wma
ccccc.wma
% find ./ -name '*.wma' -type f -print0 | xargs -0 -I f ./1.sh f
./aaa.mp3
./ccccc.mp3
./bbbb.mp3

Mais je le ferais de cette façon:

% find ./ -name '*.wma' -type f | sed 's,\.wma$,.mp3,'
0xFF
la source