Bash script to apply operation to each file found by find - bash

I'm trying to execute an operation to each file found by find - with a specific file extension (wma). For example, in python, I would simply write the following script:
for file in os.listdir('.'):
if file.endswith('wma'):
name = file[:-4]
command = "ffmpeg -i '{0}.wma' '{0}.mp3'".format(name)
os.system(command)
I know I need to execute something similar to
find -type f -name "*.wma" \
exec ffmpeg -i {}.wma {}.mp3;
But obviously this isn't working or else I wouldn't be asking this question =]

Most of the time it's better to use read when parsing input than doing word splitting with for and depending on IFS as there's risk with unexpected pathname expansion.
while IFS= read -u 4 -r LINE; do
ffmpeg -i "$LINE" "${LINE%.*}.mp3"
done 4< <(exec find -type f -name '*.wma')
Or use readarray (Bash 4.0+)
readarray -t FILES < <(exec find -type f -name '*.wma')
for FILE in "${FILES[#]}"; do
ffmpeg -i "$FILE" "${FILE%.*}.mp3"
done

Sticking to the basics always gets the job done (does not handle spaces in filenames):
for f in $(find "." -type f -name "*.wma"); do ffmpeg -i "$f" "${f//wma/mp3}"; done
Starting from konsolebox's suggestions below, I've come up with this complete version:
find "." -type f -name "*.wma" | while read -d $'\n' f; do ffmpeg -i "$f" "${f//wma/mp3}"; done

Related

How do handle filename space and Character with FIND command

Good day all,
I am working on a bash script to merge multiple mp3 files to one. The code is working fine but cannot handle file name with space or Characters. Below is the code. Can you please tell me what I'm doing wrong. Thanks
for file in $(find . -type f -name "*.mp3" -print0 | xargs -0 ); do
ffmpeg -i "concat:intro.mp3|"$file"|outro.mp3" -acodec copy "${file%.mp3}-".mp3;
done
find has an -exec flag that allows you to call scripts with the search results.
e.g. creating a simple helper-script addxtros.sh:
#!/bin/sh
infile=$1
outfile=${infile%.mp3}-.mp3
ffmpeg -i "concat:intro.mp3|${infile}|outro.mp3" -acodec copy "${outfile}"
you can use it like:
find . -type f -name "*.mp3" -exec ./addxtros.sh {} ";"
read builtin with -d followed by empty argument to use NUL caracter as input record delimiter
while IFS= read -r -d '' file; do
ffmpeg -i "concat:intro.mp3|$file|outro.mp3" -acodec copy "${file%.mp3}-".mp3;
done < <(find . -type f -name "*.mp3" -print0)
You can use find together with bash -c command (that allows passing 2 arguments to ffmpeg):
find . -type f -name "*.mp3" -exec bash -c 'ffmpeg -i "concat:intro.mp3|$1|outro.mp3" -acodec copy "${1%.mp3}-.mp3"' _ {} \;

while read with spaces in filenames

On this .ogg files
$ tree
.
├── Disc 1 - 01 - Procrastination.ogg
├── Disc 1 - 02 - À carreaux !.ogg
├── Disc 1 - 03 - Météo marine.ogg
└── mp3
I try with a while loop to ffmpeg convert them to mp3 keeping spaces in filenames::
$ ls *.ogg | while read line; do ffmpeg -i "$line" mp3/"$line".mp3 ; done
But I get this error::
$ ls *.ogg | while read line; do ffmpeg -i "$line" mp3/"$line".mp3 ; done
...
Parse error, at least 3 arguments were expected, only 0 given
in string ' 1 - 02 - À carreaux !.ogg' ...
...
This report bash ffmpeg find and spaces in filenames even if it look similar is for a more complicate script and has no answer.
This ffmpeg not working with filenames that have whitespace only fix it when output is a http:// URL
Use find -print0 to get the NUL-separated list of files, instead of parsing ls output which is never a good idea:
#!/bin/bash
while read -d '' -r file; do
ffmpeg -i "$file" mp3/"$file".mp3 </dev/null
done < <(find . -type f -name '*.ogg' -print0)
You can use a simple glob to do this as well:
shopt -s nullglob # make glob expand to nothing in case there are no matching files
for file in *.ogg; do
ffmpeg -i "$file" mp3/"$file".mp3
done
See:
Why you shouldn't parse the output of ls(1)
You don't need a loop here; let find execute the command for you.
find . -type f -name '*.ogg' -exec ffmpeg -i {} mp3/{}.mp3 \;
Or, if you want to strip the .ogg extension from the result:
find . -type f -name '*.ogg' -exec sh -c 'ffmpeg -i "$1" mp3/"${1%.ogg}.mp3"' _ {} \;
Conversely, you can skip find altogether:
shopt -s extglob
for f in **/*.ogg; do
[[ -f $f ]] || continue
ffmpeg -i "$f" mp3/"${f%.ogg}.mp3"
done

Run script on multiple files

I would like to execute a script on a batch of files all of which have .xml extension.
Inspired by previous posts, I tried the following:
for file in *.xml; do ./script.sh <"$file"; done
And
for i in $(\ls -d *.xml)
do
./script.sh -i /path/*.xml -d /output_folder $i
done
Both of these run the script many times but only on the first .xml file in that folder. So I end up with a dozen output files but all of them are file1.txt, file1.txt_1, file1.txt_2 etc. The loop stops randomly, sometimes after 3 iterations, sometimes after 28.
Any help would be appreciated,
Thank you,
TP
for f in /input_path/*.xml; do
./interproscan.sh -mode convert -f raw -i "$f" -d /output_path
done
More simple and safe method is this:
while IFS= read -r -d $'\0'; do
./interproscan.sh -mode convert -f raw -i "$REPLY" -d /output_path
done < <(find . -iname "*.xml" -print0)
NOTE
1) Using iname you search using case insensitive.
2) "$variable" help you if filename have some space.
Instead of looping though the files you could use find's -exec option. It will execute the command on each file, replacing {} with the file's path. Note you must end the command with an escaped semicolon (\;).
Something like this could work for you:
find . -name "*.xml" -exec ./script.sh -i /path/*.xml -d /output_folder {} \;
But you are limited that you can only insert the {} once, alternitlvity to do it with a loop you could do this:
xmlFiles=( $(find . -name "*.xml") )
for i in ${xmlFiles[#]}
do
./script.sh -i /path/*.xml -d /output_folder $i
done

Bash loop over directory tree with ffmpeg, wrong spaces? [duplicate]

This question already has answers here:
Iterate over a list of files with spaces
(12 answers)
How to loop through file names returned by find?
(17 answers)
Closed 5 years ago.
I am trying to interate over multiple video files that are grouped in directories, and ffmpeg returns errors about paths, it seems like paths are broken, end at first space. Can you point me too what is the problem here? Files and directories have spaces.
$ for f in $(find -type f -name *.mkv); do ffmpeg -n -i "$f" -c:v copy "~/Pobrane/$f" ; done
Loop splits paths by space and takes words as entries. How to fix this?
$ for f in $(find -type f -name *.mkv); do echo "$f"; done
./homeworks/2017-04-03
00-54-57
homework3b.mkv
./homeworks/2017-04-03
00-21-36
homework1.mkv
./homeworks/2017-04-03
Replacing the Loop
Use
find -type f -name '*.mkv' -exec ffmpeg -n -i {} -c:v copy ~/Pobrane/{} \;
-exec executes the following ffmpeg command for each of the found paths, replacing {} with the current path. The ; informs find that the ffmpeg command ends there.
Quote '*.mkv' in order to pass the literal string to find, which then searches for files ending with *.mkv. If you do not quote the string and have some mkv files laying around in your working directory, the shell will expand the unquoted *.mkv resulting in find -type f -name firstFile.mkv secondFile.mkv ... before starting find.
Do not quote ~. The unquoted ~ expands to your home directory (probably /home/yourname) but the quoted '~' is a directory/file with the literal name ~ .
Creating Parent Directories
How would I add mkdir -p before the ffmpeg call?
You could wrap the mkdir and ffmpeg in one function and execute the function:
myFunction() {
mkdir -p "$(dirname "$1")"
ffmpeg -n -i "$1" -c:v copy ~/Pobrane/"$1"
}
export -f myFunction
find -type f -name '*.mkv' -exec bash -c 'myFunction "$0"' {} \;
or use a loop:
find . -type f -iname "*.txt" -print0 | while IFS= read -r -d $'\0' file; do
mkdir -p "$(dirname "$file")"
ffmpeg -n -i "$file" -c:v copy ~/Pobrane/"$file"
done

while loop stops after first iteration in BASH [duplicate]

This question already has answers here:
While loop stops reading after the first line in Bash
(5 answers)
Closed 1 year ago.
I wrote the script that has to convert *.avi files to mp4 format.
However "while" loop stops after first iteration.
#!/bin/bash
shopt -s lastpipe
cd <some_directory>
find . -name *.avi -type f |
while read -r avi
do
/usr/bin/HandBrakeCLI -i "${avi}" -o "${avi%.avi}.mp4" -f mp4 -m -O -e x264 -q 20 --vfr \
# &> /dev/null
if [ "$?" -eq 0 ]
then
echo "${avi} was converted successfully"
rm "${avi}"
else
echo "${avi} was not converted"
break
fi
done
This part is wrong: find . -name *.avi -type f
The shell is expanding the wildcard before find starts, so the find command looks like:
find . -name a.avi b.avi.c.avi d.avi ... -type f
I'm surprised you didn't notice an error message, like "find: paths must precede expression: b.avi"
You need to protect the asterisk from the shell so find can to its own expansion. Pick one of
find . -name \*.avi -type f
find . -name '*.avi' -type f
You don't mention if you're on a GNU system or not. You're while loop is at risk of being tripped up by filenames with leading or trailing whitespace. Try this:
find . -name \*.avi -type f -print0 | while read -rd '' avi; do ...
HandBrakeCLI could also be reading input that it makes your loop end after the first instance is called. Since you're using bash, you can use process substitution with redirected input to another file descriptor. In this example with use 4:
while read -ru 4 avi; do
...
done 4< <(exec find . -name *.avi -type f)
My preferred version too is to use readarray. It's quite enough if you don't target irregular filenames where they have newlines:
readarray -t files < <(exec find . -name *.avi -type f)
for avi in "${files[#]}"; do
...
done
Another way perhaps is to redirect input of HandBrakeCLI to /dev/null:
</dev/null /usr/bin/HandBrakeCLI ...
Other suggestions:
Quote your -name pattern: '*.avi'
Use IFS= to prevent stripping of leading and trailing spaces: IFS= while read ...

Resources