2009-10-01

Bash completion for playing movie

Sometimes I play movies (mostly tv series) on my Ubuntu installation. The movie player I have got used to the most, and which can handle pretty much everything I throw at it, is mplayer.

I mainly use mplayer from the command line (I have GUIs for it installed too but don't use them much) and about a week ago I got tired of having to cd to my movie folder to play movies, or to type long path names, each time I wanted to watch a movie. I knew that bash has programmable completion so I set out to see how I could use that to get tab completion for all my movies regardless of where I am in the file system.

Getting it to work was a small adventure, let me tell you that. I started by looking under /etc/bash_completion and borrowed some snippets from there but soon I needed help so I went over to the gnu.bash.bug newsgroup and started a new thread.

It took a while to understand how it all fits together. Bash is very competent and the manual has a lot of details but in my opinion it is very terse and does not explain things in more detail that is really really necessary. If you don't grok everything about all the expansion and completion facilities it can be hard to dive in and get something like this working.

Anyway, with a lot of help I finally got it to work, and really well I must say. The main issues I had had to do with how to handle space and other metacharacters in file names, quoting/escaping them the correct way etc, and how to handle movie files in subfolders.

The solution concists of two parts: the programmable completion, consisting of one bash function and an accompanying call to the complete command, and a small bash script to start mplayer.

The bash function is responsible for finding all movie files and match them against the current user input. The work horse is the find program, for finding the movie files, in combination with grep, to filter the matches (it can theoretically be done using find only but then the solution does not become exactly how I want it).

The bash script is more or less just a wrapper around the mplayer program but it also prepends the movie folder path to the argument sent in. You will understand later.

Here is the bash function:
_mm() {
local cur files
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"

if [ "$1" == "substring" ]; then
pattern="${cur}"
else
pattern="^${cur}"
fi

files=$(find /home/mathias/Videos/movies/ -iname "*.avi" -type f -printf "%P\n" | grep -i "$pattern" | while read file; do
printf "%q\n" "$file"
done)
local IFS=$'\n'
COMPREPLY=(${files})
}

_ms() {
_mm "substring"
}

It's not very complicated once you get it working but the devil are in the details so I should comment it anyway:

First we set up some local variables.

Next we make sure the magic COMPREPLY array is made empty before we fill it with values used for the completion.

After that we need to get hold of the currently entered text on the command line, if any, and place this into the cur variable.

Next up is an if statement that will build the pattern to look for. This is not strictly needed but I wanted to be able to have one command that did prefix matching and one for substring matching, i.e. matching the beginning or anywhere in file names, respectively.

The pattern is finally used in the call to grep which filters all files found by find. I made it simple and only pick out avi files, since all my movies are in avi format. The P in the format string makes find skip printing the leading part of the path (otherwise that would be part of all completion candidates). The last part of the pipe is for shell quoting/escaping spaces and other shell metacharacters.

In the files variable all matching file names are separated by a newline so before we let the shell split it into array elements to be placed in COMPREPLY we must change the IFS field separator to a newline.

The last line in the function puts the completion candidates in COMPREPLY from where bash then reads it when the user asks to complete filenames.

The helper function _ms is used to get a substring matching version of the completion.

Next comes the calls to the complete command which is what activates the completion for various commands. The -F parameter says to get the completion candodates by calling a function and the _mm is the name of the function. The last parameter is the command name for which we want to add the completion:
complete -F _mm mp
complete -F _mm mm
complete -F _ms ms

Both the mp and ms commands are just symlinks to the main script, mm. They are only needed to control the two variants of completion (substring and prefix). I put both the functions and calls to complete in a file called ~/bin/bash_completion, together with other completion stuff, and source this file from my ~/.bashrc, like so:
. ~/bin/bash_completion
The mm command, a bash script which I also put in my ~/bin folder, is very simple and looks like this:
#!/bin/bash
mplayer "/home/mathias/Videos/movies/$1"
The reason we prefix the filename with the root movie folder is that the completion function is made not to include it, for ease of typing.

Okay, I hope this can be as useful to others as it is to me. The technique, of course, could be applied to any similar needs you might have.

If you like, check out the discussion I started over at gnu.bash.bug,
it contains a lot more details which made this hack of mine end up the way it did

Happy completing!