I have written this to help me batch-encode some videos:

find -name '*.mkv' -exec HandBrakeCLI -Z "Android Tablet" \
  -c "1-3" -m -O --x264-tune film -E copy --decomb -s 1 -i {} \
  -o `echo {} | tr mkv m4v` \;

However, it fails catastrophically -- it takes each 700-900MB file and replaces it with a 36KB file.

Notably, HandBrake is able to read and print the properties of the video file; it is only when it moves on to encode it that it fails because the file is invalid. So it appears that the overwriting occurs when it gets to...

-o `echo {} | tr mkv m4v`

...but I am not sure why it should. I am simply trying to change the output filename from whatever.mkv to whatever.m4v

The filenames do not have spaces. They are of the form 12-the-revenge.mkv, for instance.

| improve this question | | | | |
  • The command tr mkv m4v translates every "k" in the input to "4" leaving other characters unchanged; thus "kicking.mkv" would become "4ic4ing.m4v" which is probably not what you intend. – msw Aug 14 '13 at 3:27
  • What do you get when you run the command on one file (without the find) using the same set of options? I'm guessing that you get a 36KB file, and if you add --verbose it might give you some information about why it is failing. – msw Aug 14 '13 at 3:46

The backticked expression: echo {} | tr mkv m4v (which is not what you want, for a variety of reasons; see below) is expanded once, when the find command is parsed. If you're using bash, it will normally be expanded to {}:

$ echo {} | tr mkv m4v

And, indeed, that happens on every shell installed on my machine except fish, which outputs an empty line. Assuming you're not using fish, then the arguments find is actually seeing will be:

find -name '*.mkv' -exec HandBrakeCLI -Z "Android Tablet" \
-c "1-3" -m -O --x264-tune film -E copy --decomb -s 1 -i {} \
-o {} \;

In short, HandBrakeCLI is being given the same file for both input and output, and I think that's what you're seeing, although your description of the symptoms is not very precise.

Unfortunately, there's no easy way to get find to do the extension replacement you want it to do within an -exec action. You could do it by passing the command to bash -c, but that will involve extra, confusing quoting on the command line. A cleaner and more readable solution is to create a shell script file which can iterate through it's arguments:

File: mkvtom4v.sh (Make sure you chmod a+x mkvtom4v.sh)

for file in "$@"; do
  HandBrakeCLI -Z "Android Tablet" -c "1-3" -m -O \
               --x264-tune film -E copy --decomb -s 1 \
               -i "$file" -o "${file/%.mkv/.m4v}"

And then invoke it with find:

find /path/to/directory -name '*.mkv' -exec /path/to/mkvtom4v.sh {} +

Some notes:

  1. Don't use backticks (some command); they have been deprecated for many years. Use $(some command), which is more readable and allows nesting.

  2. tr is definitely not what you want. It does character by character translation. For example, tr aeiou AEIOU would make all vOwEls UppErcAsE. tr mkv m4v will change every k to a 4. See man tr (or info tr) for more details.

  3. "${file/%.mkv/.m4v}" is bash's idiosyncratic search-and-replace syntax. In this case, it means: "Take the value of $file, and if it ends with .mkv (the % means "ends with" in this context), then replace it with .m4v". There are lots of other bashisms for editing the value of a variable. man bash and search for "Parameter Expansion".

  4. With GNU find, you can use {} + at the end of an -exec command (instead of \;). It will be replaced by as many found filenames as possible. See info find for more details.

| improve this answer | | | | |
  • OK, so as I understand it, the problem was that tr was run before find, so the output file was just {} and hence the same as the input? And I had a feeling that I misremembered the operation of tr, but it worked when I tested it outside find so I stupidly went with it. Anyway, your solution works, thanks! – user45084 Aug 14 '13 at 5:06
  • @user45084: Yep ("HandBrakeCLI is being given the same file for both input and output"). find will replace both instances of {} with the filename, in a single-file -exec. – rici Aug 14 '13 at 6:00
  • @gilles: the shell script creates one bash process (for up to hundreds of files, probably). A single file -exec bash -c creates an extra bash per file. But thanks for the edit, anyway: its clearer than mine was. – rici Aug 14 '13 at 22:28
  • @rici That's purely a matter of using + vs ; at the end of the -exec clause. It's unrelated with the shell program being in a -c argument or in a script. – Gilles 'SO- stop being evil' Aug 14 '13 at 22:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.