Como executar um comando em todos os subdiretórios usando o Bash

2

Eu tenho uma estrutura de diretórios composta por:

iTunes/Music/${author}/${album}/${song.mp3}

Eu implementei um script para remover a taxa de bits do meu mp3 para 128 kbps usando o lame (que funciona em um único arquivo por vez).

Meu 'normalize_mp3.sh' é assim:

#!/bin/bash
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in *.mp3
do
  lame --cbr $f __out.mp3
  mv __out.mp3 $f
done
IFS=$SAVEIFS

Isso funciona bem se eu for pasta por pasta e executar este comando, mas gostaria de ter um comando "global", como em 4DOS, para que eu possa executar:

$ cd iTunes/Music
$ global normalize_mp3.sh

e o comando global iria percorrer todos os subdiretórios e executar o normalize_mp3.sh para remover todos os meus mp3 em todas as subpastas.

Alguém sabe se existe um equivalente Unix ao comando 4DOS global ? Eu tentei jogar com find -exec , mas acabei tendo dor de cabeça.

    
por Luigi R. Viggiano 16.06.2013 / 15:58

4 respostas

2

Esta é uma implementação muito básica do comando global descrito para bash :

global() {
  shopt -s globstar
  origdir="$PWD"
  for i in **/; do
    cd "$i"
    echo -n "${PWD}: "
    eval "$@"
    echo
    cd "$origdir"
  done
}
  • shopt -s globstar ativa a globalização recursiva com ** (acho que você precisará da versão bash > = 4)
  • **/ encontra todos os diretórios abaixo do diretório de trabalho atual, portanto, $i faz um loop sobre esses
  • Eu incluí um echo para que você possa ver em que dir o loop é atualmente
  • eval "$@" avalia todos os argumentos da função
  • cd $origdir retorna ao diretório original. (No começo eu tinha uma abordagem mais elegante do IMHO com pushd / popd , mas acho que cd de volta para $origdir é mais à prova de balas caso o comando "$@" mude o diretório (o que obviamente é um má ideia!)

Esse comando global aceita comandos com argumentos como global touch foo . Se você quiser usar uma sintaxe shell mais avançada, como redirecionamentos ou variáveis, você precisa colocar o comando entre aspas simples '...' . Mas se o seu desejo é executar o seu script normalize_mp3.sh em todos os diretórios, o uso é tão simples como

global normalize_mp3.sh

Esteja ciente de que o início do primeiro comando pode precisar de algum tempo para reunir todos os subdiretórios se houver muitos deles.

E, claro, por favor teste com um backup atualizado dos seus arquivos!

Aqui está como funciona (eu só posso testar com o Linux, mas eu assumo que o bash está trabalhando no OSX da mesma forma):

$ echo $BASH_VERSION 
4.1.5(1)-release
$ cd /var
$ tree -d | head
.
├── backups
├── cache
│   ├── apt
│   │   ├── apt-file
│   │   └── archives
│   │       └── partial
│   ├── cups
│   │   └── rss
│   ├── debconf
$ global 'echo -n This command is executed in $PWD at $(date); sleep 1'
/var/backups: This command is executed in /var/backups at Mo 1. Jul 12:11:00 CEST 2013
/var/cache: This command is executed in /var/cache at Mo 1. Jul 12:11:01 CEST 2013
/var/cache/apt: This command is executed in /var/cache/apt at Mo 1. Jul 12:11:02 CEST 2013
/var/cache/apt/apt-file: This command is executed in /var/cache/apt/apt-file at Mo 1. Jul 12:11:03 CEST 2013
/var/cache/apt/archives: This command is executed in /var/cache/apt/archives at Mo 1. Jul 12:11:04 CEST 2013
/var/cache/apt/archives/partial: This command is executed in /var/cache/apt/archives/partial at Mo 1. Jul 12:11:05 CEST 2013
/var/cache/cups: This command is executed in /var/cache/cups at Mo 1. Jul 12:11:07 CEST 2013
/var/cache/cups/rss: This command is executed in /var/cache/cups/rss at Mo 1. Jul 12:11:08 CEST 2013
/var/cache/debconf: This command is executed in /var/cache/debconf at Mo 1. Jul 12:11:09 CEST 2013
...
    
por 25.06.2013 / 22:32
5

Outras opções:

find ~/Music -name \*.mp3 | while IFS= read -r f; do
  lame --cbr "$f" "$f"_temp
  mv "$f"_temp "$f"
done
mdfind 'kMDItemAudioBitRate>128000' -onlyin ~/Music |\
parallel lame --cbr {} {}_temp \; mv {}_temp {}
f() { lame --cbr "$1" "$1"_temp; mv "$1"_temp "$1"; }
export -f f
mdfind 'kMDItemAudioBitRate>128000' -onlyin ~/Music | parallel f
shopt -s globstar # bash 4.0 or later
for f in ~/Music/**/*.mp3; do lame --cbr "$f" "$f"_temp; mv "$f"_temp "$f"; done
    
por 16.06.2013 / 19:06
1

Tente algo como:

#!/bin/bash
# first cd to iTunes/Music
for f in */*/*.mp3
do 
  lame --cbr "$f" __out.mp3 && mv __out.mp3 "$f"
done

Não há necessidade de definir IFS . Você precisa colocar aspas duplas em torno de $f embora .. Eu não sei se o coxo tem um código de retorno se algo der errado. Talvez primeiro experimentá-lo com printf "%s\n" "$f" first em vez do comando coxo ..

    
por 16.06.2013 / 16:27
1

Primeiro, tenha um script ligeiramente mais enxuto que percorre os argumentos fornecidos para o script .

#! /usr/bin/env bash
for f do
  lame --cbr "$f" "$f_out.mp3"
  mv "$f_out.mp3" "$f"
done

Para que seja acessível de qualquer lugar, coloque o script em algum lugar em seu $ PATH. No Linux, eu usaria $HOME/bin , que é adicionado ao $ PATH por padrão; Eu suspeito que isso seja verdade para o OSX também.

Use-o em um único diretório de mp3s com:

downrate_mp3 ./*.mp3

Eu prefiro prefixar esses globs com ./ , já que é legal iniciar nomes de arquivos com - , o que pode causar problemas, pois muitos programas (incluindo mv ) esperam que argumentos começando com - sejam uma opção, em vez de um arquivo. Para recursividade, a melhor maneira é usar globstar (requer o bash 4+, mas acho que é isso que o recente OSX tem):

shopt -s globstar
downrate_mp3 ./**/*.mp3

Em alternativa, pode utilizar a localização com -exec , como já tentou:

find iTunes/Music -type f -name '*.mp3' -exec downrate_mp3 {} +

A razão pela qual isso não funcionaria com o comando existente é que find está essencialmente tentando usar a grande lista de arquivos como argumentos para o script - e como o script em sua pergunta não faz nada com argumentos , isso obviamente não funcionaria.

    
por 13.04.2017 / 14:37