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' {} \;