Batch convert video files with grep, xargs, sed and ffmpeg

I had a bunch of video files that I needed to convert from mkv to mp4 so that they would work on a media player I was testing. It presented a good opportunity to show how you can tie together a bunch of useful Linux command line tools to do the job.

tl;dr; I just want to convert files

You can do this with a loop, but the real purpose of this article is to explore a combination of tools including pipe, grep, find, xargs, and sed. Here is the loop:

for i in *.avi; do ffmpeg -i "$i" "${i%.*}.mp4"; done
The loop version

I want to explore some pipes

This command that will take all of the mkv files in the current directory, convert them to mp4 files in a sub-directory called mp4 (you will need to create this before-hand).

grep -l . *mkv | xargs -i echo "{} mp4/{}" |  sed 's/.mkv$/.mp4/g' | xargs -n 2 ffmpeg -i
The pipe version

The command can be broken down into 4 steps each passed onto the next using pipes |.

Get a list of all mkv files

grep -l . *mkv

Using this command will show all the mkv files in the current directory on stdout. The -l flag tells grep to only display the file names.

video001.mkv
video002.mkv
video003.mkv
video004.mkv
video005.mkv

You could also do this with find * -name '*.mkv'

Transform each line into source & destination

xargs -i echo "{} mp4/{}"

xargs takes the previous output (via stdin thanks to the pipe) and puts everything on a single line. Allowing you to convert vertical lists into horizontal parameters. The default command for xargs is echo For example:

find * -name '*.mkv' | xargs
video001.mkv video002.mkv video003.mkv video004.mkv video005.mkv

We can change this behavior by using -i to take control over each parameter and use it in more complex ways. In this scenario we print the filename twice, but with the second one inside the mp4 directory:

find * -name '*.mkv' | xargs -i echo "{} mp4/{}"
video001.mkv mp4/video001.mkv
video002.mkv mp4/video002.mkv
video003.mkv mp4/video003.mkv
video004.mkv mp4/video004.mkv
video005.mkv mp4/video005.mkv

Change the destination parameter to MP4

sed 's/.mkv$/.mp4/g'

With the previous command we're getting closer to what we need. Then next thing to tackle is to change the destination file extension to mp4. We can do this with sed and some regex. The trick with this one is $ which will only match with the end of the line. This will leave the source filename with mkv, but change the destination:

find * -name '*.mkv' | xargs -i echo "{} mp4/{}" | sed 's/.mkv$/.mp4/g'
video001.mkv mp4/video001.mp4
video002.mkv mp4/video002.mp4
video003.mkv mp4/video003.mp4
video004.mkv mp4/video004.mp4
video005.mkv mp4/video005.mp4

Use FFMPEG to do the conversion

xargs -n 2 ffmpeg -i

Finally we get to the ffmpeg step. This command is of the form ffmpeg -i <input_file> <output_file>. We use xargs here to control the parameters. We need -n 2 to tell xargs that we only want two parameters per ffmpeg execution.

This will execute the following commands one after the other:

ffmpeg -i video001.mkv mp4/video001.mp4
ffmpeg -i video002.mkv mp4/video002.mp4
ffmpeg -i video003.mkv mp4/video003.mp4
ffmpeg -i video004.mkv mp4/video004.mp4
ffmpeg -i video005.mkv mp4/video005.mp4

Conclusion

Using pipes can be very powerful. The composability adds flexibility. Instead of starting with the list of files in a directory you could grab the list from inside a csv, or from a network stream. You can use sed for all kinds of find/replace style operations, and you can use xargs to control how previous stages are handled in the next stage.