I want to copy mp3 files from a directory to a flash drive. They are stored in a directory structure such as this: Artist/Album/Track.mp3
. Since my car's mp3 player sucks, I want to have all my mp3 files in the root directory of the flash drive, with the filename in the format Artist-Album-Track.mp3
. How can I copy files from the directory to the flash drive, whilst adding the path into the filename?

- 14,404
- 21
- 67
- 93
-
If any of the answers solved your problem, please accept it by clicking the checkmark next to it. Thank you! – Jeff Schaller Jul 22 '18 at 13:52
4 Answers
A simple for
loop could do it, with parameter expansions to extract the various elements:
for file in */*/*.mp3;
do
artist=${file%%/*}
rest=${file#*/}
album=${rest%%/*}
track=${rest#*/*}
cp -- "$file" /flash/drive/"${artist}-${album}-${track}"
done
The first expansion strips everything from the end of the file up to and through the first forward-slash -- generating the artist.
The second expansion strips the leading characters up to and through the first forward-slash -- stripping the artist off of the path.
The third expansion is like the first, stripping the filename off of the remaining path, leaving the album.
The fourth expansion is like the second, stripping the album off, leaving just the filename.
Then we piece it all back together with dashes and cp it to the desired /flash/drive path.

- 67,283
- 35
- 116
- 255
I believe it is a bad practice to manipulate multiple strings with shell parameter expansions and variable assignments.
root_dir="~/songs"
for fn in */*/*.mp3
do
cp -v "$fn" "${root_dir}/${fn////-}" # replace all slashes by dashes
done
Output:
~/songs/Artist-Album-Track.mp3
-
3
-
2@Fólkvangr That would iterate over the pathnames as a single string (one iteration). – Kusalananda Jul 08 '18 at 13:24
-
1Note that in the current answer, the globs in
"*/*/*.mp3"
are not expanded as they are quoted. Should be*/*/*.mp3
with no quotes. – Kusalananda Jul 08 '18 at 13:52
Using find
and bash
to copy the files out of $mp3dir
to $destdir
:
mp3dir="$HOME/my_music"
destdir="/mnt/my_mp3_player"
find "$mp3dir" -type f -name '*.mp3' -exec bash -c '
mp3dir=$1; destdir=$2; shift 2
for pathname do
dest=${pathname#$mp3dir/}
dest="$destdir/${dest//\//-}"
if [ -f "$dest" ]; then
printf "%s exist, skipping %s\n" "$dest" "$pathname" >&2
else
cp "$pathname" "$dest"
fi
done' bash "$mp3dir" "$destdir" {} +
This looks for any regular file in or below $mp3dir
whose names end with .mp3
. For batches of these, a short script is executed. The couple of parameters to the script is $mp3dir
and $destdir
and the rest are pathnames of MP3 files.
The script picks out two directory names from the first two command line arguments and then loops over the rest of the arguments, constructing the destination pathname as $dest
for each MP3 file. This is done using a bash
parameter substitution that replaces all slashes in the pathname with dashes after removing the initial $mp3dir
bit of the pathname.
If $dest
already exists, a message about this is printed, otherwise the file is copied.
Related:

- 333,661
-
Smart to just replace slashes with dashes! What was I thinking... – Jeff Schaller Jul 08 '18 at 13:25
Background
I was curious if I could make a solution that achieves two goals:
- easier to read
- easier to debug
Building off of @kusalananda's wonderful answer to this other U&L Q&A titled: Understanding the -exec option of find
, I think I've come up with something that solves your issue.
To start here's my sample directory structure that mimics yours.
$ mkdir -p Artist{1..5}/Album{1..5}
$ touch Artist{1..5}/Album{1..5}/Track{1..5}.mp3
This results in the following:
$ find Artist* -type f | head
Artist1/Album1/Track1.mp3
Artist1/Album1/Track2.mp3
Artist1/Album1/Track3.mp3
Artist1/Album1/Track4.mp3
Artist1/Album1/Track5.mp3
Artist1/Album2/Track1.mp3
Artist1/Album2/Track2.mp3
Artist1/Album2/Track3.mp3
Artist1/Album2/Track4.mp3
Artist1/Album2/Track5.mp3
Now to copy your mp3 files into ./targetDir
:
$ find . -type f -name '*.mp3' -exec sh -c \
'cp {} targetDir/$(echo "{}" | sed "s#\./##g;s#/#-#g")' \;
Which results in this:
$ ls targetDir/ | head
Artist1-Album1-Track1.mp3
Artist1-Album1-Track2.mp3
Artist1-Album1-Track3.mp3
Artist1-Album1-Track4.mp3
Artist1-Album1-Track5.mp3
Artist1-Album2-Track1.mp3
Artist1-Album2-Track2.mp3
Artist1-Album2-Track3.mp3
Artist1-Album2-Track4.mp3
Artist1-Album2-Track5.mp3
I like this approach, because I can wrap the cp ...
command in a echo
first so I can verify the output before committing to doing the work:
$ find . -type f -name '*.mp3' -exec sh -c \
'echo "cp {} targetDir/$(echo "{}" | sed "s#\./##g;s#/#-#g")"' \;
cp ./Artist1/Album1/Track1.mp3 targetDir/Artist1-Album1-Track1.mp3
cp ./Artist1/Album1/Track2.mp3 targetDir/Artist1-Album1-Track2.mp3
cp ./Artist1/Album1/Track3.mp3 targetDir/Artist1-Album1-Track3.mp3
cp ./Artist1/Album1/Track4.mp3 targetDir/Artist1-Album1-Track4.mp3
cp ./Artist1/Album1/Track5.mp3 targetDir/Artist1-Album1-Track5.mp3
cp ./Artist1/Album2/Track1.mp3 targetDir/Artist1-Album2-Track1.mp3
cp ./Artist1/Album2/Track2.mp3 targetDir/Artist1-Album2-Track2.mp3
cp ./Artist1/Album2/Track3.mp3 targetDir/Artist1-Album2-Track3.mp3
cp ./Artist1/Album2/Track4.mp3 targetDir/Artist1-Album2-Track4.mp3
...
How it works
This solution takes the output of find
and then runs the following shell command:
cp {} targetDir/$(echo "{}" | sed "s#\./##g;s#/#-#g")'
This will cp
the file from Artist../Album../Track..
to targetDir/..
and reformulate the name so that it has dashes (-
) where ever there's a forward slash (/
).
NOTE: I added 2 operations to the sed
. The first strips any prefix ./
that may exist if you use find . ...
instead of find Artist* ..
.
Alternatives?
I think doing this using find
and cp
is still not the ideal solution. I too maintain directories of MP3 files and I think doing something like this with rsync
and through lists that you provide rsync
might be a more useful implementation, longer term since you can use it to update, rather than re-copy each time you run it.
$ man rsync
...
--files-from=FILE read list of source-file names from FILE
Just a thought.

- 369,824