2

Directory "$d" contains a few thousand e-mail files with the .txt extension. To open them in my e-mail client, I need to rename them to .eml

Will this command rename them correctly:

find "${d}" -type f -name '*.txt' | while read f; do mv -vn "${f}" "${f%.*}".eml; done

or is there a better, more robust way to do this?

I could not think of an elegant way of doing this using:

-exec ...{}... \;

4 Answers4

5

Your solution is generally ok, but it will break on newlines. Here is a slightly more robust bash4+ solution:

shopt -s globstar nullglob
for file in **/*.txt; do
    mv "$file" "${file%.*}.eml"
done
Chris Down
  • 125,559
  • 25
  • 270
  • 266
1

I think you should be fine with

find "$d" -name \*.txt -exec rename .txt .eml {} \;

or even

for f in *.txt; do rename .txt .eml "$f"; done

if all the files are in the same directory.

0

Yes, your command will work, assuming you're using bash or a shell with similar syntax. In the future when you're contemplating a big command like this, remember that you can use echo to preview the resulting command lines. I.e. you could put echo in front of mv, run the pipeline and see what the commands are going to be. If they look OK, remove echo and run the command for real.

Kyle Jones
  • 15,015
0

In zsh, zmv makes this easy. Put autoload -U zmv in your ~/.zshrc, then use one of several ways of specifying a replacement text for **/*.txt which matches files with the extension txt in the current directory and subdirectories:

zmv '**/*.txt' '$f:r.eml'
zmv '**/*.txt' '${f%.*}.eml'
zmv '(**/)(*).txt' '$1$2.eml'
zmv -w '**/*.txt' '$1$2.eml'

If you don't have zsh, but you have bash ≥4 or ksh93, you can use the ** to traverse subdirectories recursively and then loop over the matches. You'll need to activate the ** glob pattern first with shopt -s globstar in bash (put it in your ~/.bashrc) or set -o globstar in ksh (put it in your ~/.kshrc). This also works in zsh (no prior setup needed).

for f in **/*.txt; do mv -- "$f" "${f%.*}.eml"; done

Note that this renames all files, not just regular files. If you have directories or other non-regular files and you want to leave them untouched:

zmv -Q '**/*.txt(.)' '$f:r.eml'
for f in **/*.txt; do [[ -f $f ]] && mv -- "$f" "${f%.*}.eml"; done

With no shell feature beyond POSIX, you'll need to invoke find to traverse subdirectories. Your solution is brittle because it breaks on files containing backslashes, trailing whitespace or newlines. You can fix the trailing whitespace and backslashes issue with while IFS= read -r f; do … but piping the output of find inherently breaks on newlines. Use -exec instead. On Linux, you can use the rename utility (whichever your distribution carries). On Debian, Ubuntu and derivatives:

rename 's/\.txt$/.eml/' **/*.txt
find . -name '*.txt' -type f -exec rename 's/\.txt$/.eml/' {} +

On other distributions, as long as none of the file names contain .txt in the middle (because rename substitutes the first occurence of the source string):

rename .txt .eml **/*.txt
find . -name '*.txt' -type f -exec rename .txt .eml {} +

With only POSIX features throughout, invoke a shell to perform the transformation on the name.

find . -name '*.txt' -type f -exec sh -c 'for f; do mv "$f" "${f%.*}.eml"; done' _ {} +

If your find is too old to support -exec … +, you'll need to invoke one shell per file, which makes for simpler but slower code.

find . -name '*.txt' -type f -exec sh -c 'mv "$0" "${0%.*}.eml"; done' {} \;