Converting `for file in` to `find` so that my script can apply recursively
With POSIX find:
find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o \
-name '*ogg' -o -name '*mov' \) -exec sh -c '
for file do
target="${file%.*}.mkv"
echo ffmpeg -i "$file" "$target"
done' sh {} +
Replace echo
with whatever command you want to use.
If you have GNU find or BSD find, you can use -regex
:
find . -regex '.*\.\(mkv\|avi\|mp4\|flv\|ogg\|mov\)'
You've got this code:
for file in *.mkv *avi *mp4 *flv *ogg *mov; do target="${file%.*}.mkv" ffmpeg -i "$file" "$target" && rm -rf "$file" done
which runs in the current directory. To turn it into a recursive process you have a couple of choices. The easiest (IMO) is to use find
as you suggested. The syntax for find
is very "un-UNIX-like" but the principle here is that each argument can be applied with AND or OR conditions. Here, we're going to say "If this-filename matches OR that-filename matches Then print-it". The filename patterns are quoted so that the shell can't get hold of them (remember that the shell is responsible for expanding all unquoted patterns, so if you had an unquoted pattern of *.mp4
and you had janeeyre.mp4
in your current directory, the shell would replace *.mp4
with the match, and find
would see -name janeeyre.mp4
instead of your desired -name *.mp4
; it gets worse if *.mp4
matches multiple names...). The brackets are prefixed with \
also to keep the shell from trying to action them as subshell markers (we could quote the brackets instead, if preferred: '('
).
find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o -name '*ogg' -o -name '*mov' \) -print
The output of this needs to be fed into the input of a while
loop that processes each file in turn:
while IFS= read file ## IFS= prevents "read" stripping whitespace
do
target="${file%.*}.mkv"
ffmpeg -i "$file" "$target" && rm -rf "$file"
done
Now all that's left is to join the two parts together with a pipe |
so that the output of the find
becomes the input of the while
loop.
While you're testing this code I'd recommend you prefix both ffmpeg
and rm
with echo
so you can see what would be executed - and with what paths.
Here is the final result, including the echo
statements I recommend for testing:
find . \( -name '*.mkv' -o -name '*avi' -o -name '*mp4' -o -name '*flv' -o -name '*ogg' -o -name '*mov' \) -print |
while IFS= read file ## IFS= prevents "read" stripping whitespace
do
target="${file%.*}.mkv"
echo ffmpeg -i "$file" "$target" && echo rm -rf "$file"
done
Example snippet without piping (assumes you are giving the path as argument):
#!/bin/bash
backup_dir=/backup/
OIFS="$IFS"
IFS=$'\n'
files="$(find "$1" -type f -name '*.mkv' -or -name '*.avi' -or -name '*.mp4' -or -name '*.ogg' -or -name '*.mov' -or -name '*.flv')"
for f in $files; do
# get path
d="${f%/*}"
# get filename
b="$(basename "$f")"
ttarget="${b%.*}.mkv"
# this is your final target
target="$d/$ttarget"
echo $target
# mv $f "$backup_dir"
done
IFS="$OIFS"
The shell reads the IFS
variable, which is set to (space
, tab
, newline
) by default. Then it looks at each character in the output of find
. So if it finds space
it thinks it is end of the filename (file containing spaces for example "Sin City.avi" ill be treated as two files "Sin" and "City.avi"). So with IFS=$'\n' we are telling to split the input on newlines
. And finally we restore old (default) IFS
which is saved in $OIFS
variable.
Or as suggested in comments may be better approach could be:
#!/bin/bash
backup_dir=/backup/
find "$1" -type f \( -name '*.mkv' -or -name '*.avi' -or -name '*.mp4' -or -name '*.ogg' -or -name '*.mov' -or -name '*.flv' \) -print0 | while IFS= read -r -d '' f
do
# get path
d="${f%/*}"
# get filename
b="$(basename "$f")"
ttarget="${b%.*}.mkv"
# this is your final target
target="$d/$ttarget"
echo $target
# mv $f "$backup_dir"
done