Take the 2-minute tour ×
Super User is a question and answer site for computer enthusiasts and power users. It's 100% free, no registration required.

I have a directory structure composed by:

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

I implemented a script to strip my mp3 bitrate to 128 kbps using lame (which works on a single file at time). My script looks like this 'normalize_mp3.sh':

#!/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

This works fine, if I go folder by folder and execute this command.

But I'd like to have a "global" command, like in 4DOS so I can run:

$ cd iTunes/Music
$ global normalize_mp3.sh

and the global command would traverse all subdirs and execute the normalize_mp3.sh to strip all my mp3 in all subfolders.

Anyone knows if there is a unix equivalent to the 4dos global command? I tried to play with find -exec but I just managed to get an headache.

share|improve this question

4 Answers 4

up vote 2 down vote accepted
+50

This is a very basic implementation of the described global command for bash:

global() {
  shopt -s globstar
  origdir="$PWD"
  for i in **/; do
    cd "$i"
    echo -n "${PWD}: "
    eval "$@"
    echo
    cd "$origdir"
  done
}
  • shopt -s globstar enables recursive globbing with ** (I think you'll need bash version >= 4)
  • **/ finds all directories below the current working dir, so $i loops over those
  • I included a echo so you can see in which dir the loop currently is
  • eval "$@" evaluates all arguments of the function
  • cd $origdir goes back to the original directory. (At first I had an IMHO more elegant approach with pushd / popd, but I think cd back to $origdir is more bullet-proof in case the command "$@" itself changes the directory (which of course is a bad idea!)

This global command takes commands with arguments like global touch foo. If you want to use more advanced shell syntax like redirects or variables, you have to enclose the command in single-quotes '...'. But if your desire is to run your script normalize_mp3.sh in every directory the usage is as simple as

global normalize_mp3.sh

Be aware, that the start of the first command can need some time to gather all subdirs if there are a lot of them.

And, of course, please test with an up-to-date backup of your files!


Here is how it works (I can only test with Linux, but I assume that bash is working on OSX the same way):

$ 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
...
share|improve this answer
    
in my understanding this is not really recursive, but thanks for trying, you've been the only one: you get +50 –  Luigi R. Viggiano Jun 30 '13 at 21:13
    
Still I feel the question is quite interesting... –  Luigi R. Viggiano Jun 30 '13 at 21:13
    
What do you understand by "really recursive"? This command does "traverse all subdirs" and executes a command in every of those dirs. Is it not working with your setup? To get it working you must use a recent bash version (>= 4.0) -- I suppose bash is working on OSX the same way as on Linux?! –  mpy Jul 1 '13 at 9:57
    
In the meantime I have an idea how to incorporate variables etc. in the command (I'll improve my answer later) -- it's so simple, but sometimes you simply have to stand back and get a glance of the whole. –  mpy Jul 1 '13 at 9:59
    
That's fantastic. I wonder why something like this is missing in unix utils. You should improve this tool and open source this. –  Luigi R. Viggiano Jul 2 '13 at 22:33

Other options:

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
share|improve this answer
    
+1 This definitely is a task for find. Even if you don't want to modify your script, you can limit the find command to only output directories and have your script run through them. –  Squeezy Jun 26 '13 at 19:10

Try something like:

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

There is no need for setting IFS. You need to put double quotes around $f though.. I don't know if lame has a return code if something goes wrong. Perhaps first try it out with printf "%s\n" "$f" first instead of the lame command..

share|improve this answer

First of all, have a slightly leaner script that loops through the arguments provided to the script.

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

To have it accessible from anywhere, put the script somewhere in your $PATH. On Linux, I'd use $HOME/bin, which is added to the $PATH by default; I suspect this is true of OSX too.

Use it on a single directory of mp3s with:

downrate_mp3 ./*.mp3

I prefer to prefix these globs with ./, since it is legal to start filenames with a -, which can cause problems as many programs (including mv) expects arguments beginning with a - to be an option, rather than a file. For recursiveness, the best way is to use globstar (requires bash 4+, but I think that's what recent OSX has):

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

Alternatively, you can use find with -exec, as you've already tried:

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

The reason this wouldn't work with your existing command is that find is essentially trying to use the big list of files as arguments to the script -- and since the script in your question doesn't do anything with arguments, this obviously wouldn't work.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.