Drawing a Blank
by Mike Fulcher

Bash alias with subdirectory tab completion
Friday, September 20, 2013

I’m always on the command line, so I love aliases and functions that provide quick shortcuts to common actions. Since the actions I do most often are cd-ing into the directories of the projects I’m currently working on, I’ve put together a handy script to make this process as easy as possible – especially when you have (as I do) multiple projects which are organised in subdirectories. What makes this tab completion script particularly useful is that it supports deep nesting (just like standard Bash path completion) and it’s case-insensitive.

The script

I’ve written the script in Ruby and placed this in ~/.bash/dev_completion (but you can put it anywhere you like).

#!/usr/bin/env ruby

class DevCompletion

  # Cache the full typed command (eg "dev Web").
  def initialize(command)
    @command = command
  end

  # Prepend the current path to each directory name,
  # filter only paths matching the current typed path,
  # then tidy up the output.
  def matches
    @matches ||= directories.map do |directory|
      target_directory ? "#{target_directory}/#{directory}" : directory
    end.select do |directory|
      directory.downcase[0, typed.length] == typed.downcase
    end.map do |path|
      # This should be done last.
      path.gsub("\\", '')
    end
  end

  # Cache the typed path (without the leading "dev<space>" part).
  def typed
    @typed ||= @command[/\s(.+?)$/, 1] || ''
  end

  # Cache the target directory.
  # Eg "dev Web/<tab>" or "dev Web/my_p<tab>" will set the target directory to "Web".
  def target_directory
    @target_directory ||= if typed =~ /\//
      split  = typed.split('/')
      joined = split[0...-1].join('/')
      joined.empty? ? split[0] : joined
    end
  end

  # Retrieve a (formatted) list of all directories within the target directory.
  def directories
    @directories ||= `cd ~/Dev/#{target_directory} ; ls -d */ | tr '' ' '`.split("\n")
  end

end

# Instantiate a new instance passing in the full typed command and printing the matches.
puts DevCompletion.new(ENV["COMP_LINE"]).matches
exit 0

This script assumes your projects are located in ~/Dev/, so you’ll need to change the path (found within the directories method) to the directory of your choosing. Then to create the dev function and activate tab completion, add the following to your ~/.bash_profile:

function dev() { cd "$HOME/Dev/$1"; }
complete -C ~/.bash/dev_completion -o filenames -o nospace dev

Again, you’ll need to change the directory to suit your own setup.

The first line defines the main function, which in this case is called “dev”. The function is very simple: called without any arguments it just cds into the ~/Dev directory; or an argument can be supplied which supplies a path (relative to ~/Dev) to cd into.

The eagle-eyed among you will notice that I’ve written cd "$HOME/Dev/$1" as opposed to cd ~/Dev/$1, and there’s a good reason for this: in the former, Bash will properly escape the $1 variable; in the latter it won’t. Escaping the path is important due to paths with spaces, which in Bash need to be escaped with a single preceding backslash.

The second line connects the dev function to the completion script. -o filenames is again to help with properly escaping spaces in paths, and -o nospace is to prevent Bash from adding a trailing space when completing a directory name.

Usage

Assuming a directory structure like this…

~/
  Dev/
    Client Projects/
    Experiments/
    Plugins/
      jquery-switch/
      bootstrap-sortable/

…the script will allow you to do things like this:

pwd
#> /Users/Mike

dev
pwd
#> /Users/Mike/Dev

dev plu<tab> #=> dev Plugins/
pwd
#> /Users/Mike/Dev/Plugins

dev Plugins/<tab><tab>
#> jquery-switch      bootstrap-sortable

dev Plugins/jq<tab> #=> dev Plugins/jquery-switch/
pwd
#> /Users/Mike/Dev/Plugins/jquery-switch

dev cli<tab> #=> dev Client\ Projects/
pwd
#> /Users/Mike/Dev/Client Projects

Enjoy!