johnthethird
12/17/2009 - 5:12 AM

canine.rb

Canine

Canine is a library for creating basic Ruby "binaries," a la the rails, monk, and jekyll executables. It's as easy as passing a block: require 'canine' Canine.new do # code here end And it will auto-magically run.

Now, the things you can do are pretty simple:

  • command name, [desc], &block => creates a new command with the given name. The block can take as many arguments as you want, provided you define them with options later in the script
  • command name, autocommand-name => links an auto-command to the given name. At the moment, there's only one, commands. If more are added, they'll be in the @auto hash
  • no_command desc, &block => if you want a command to be run when arguments are simply passed (as in rails <myapp>), pass a description and a block to this method
  • no_command name => should you want the commandless process to be a command you already created, pass the name to the method
  • options hash => set up the options for the latest command that you created, in an 'name' => 'description' format
  • default_command name => set the command you want to run when no arguments are passed the the script. This is required.

The command blocks themselves can do anything. You can import anything you really need, from fileutils to rake, and use them within the commands.

class Canine
  VERSION = '1.3'
  def initialize(&block)
    @commands = Hash.new
    @default = @latest = :commands
    @empty = nil
    @auto = {
      :commands => hash_command("commands","Show a list of commands",Proc.new {
        @commands.each { |cmd| c = cmd[1]
          name = c[:name].to_s
          name += ' [' + c[:args].map { |i| i[0] }.join('] [') + ']' if c[:args].length > 0
          puts "  %-20s %-30s" % [name,c[:desc]]
        }
      })
    }
    if block then
      self.instance_exec &block
      self.run
    end
  end
  def command(name,desc="",&block)
    if block then
      @commands[intern(name)] = hash_command name, desc, block
      @latest = intern(name)
    else
      raise ArgumentError, "No such command '%s'" % desc unless @auto.has_key? intern(desc)
      @commands[intern(name)] = auto_command(name,desc)
    end
  end
  def no_command desc, &block
    if block then
      command('<no command>',desc,&block)
      @empty = block
    else
      @empty = @commands[intern(desc)][:block]
      command('<no command>',"Same as %s" % desc,&@empty)
    end
  end
  def options(arghash)
    arghash.each_pair { |key, value|
      @commands[@latest][:args].push [key, value]
    }  
  end
  def default_command cmd
    @default = intern(cmd)
  end
  def invoke(sym, *args)
    sym = intern(sym)
    raise ArgumentError, "No such command '%s'" % sym unless @commands.has_key? sym
    raise ArgumentError, "Not enough arguments for command '%s' (%s for %s)" % [sym, args.length,@commands[sym][:args].length] if args.length < @commands[sym][:args].length
    self.instance_exec *args, &@commands[sym][:block]
  end
  def hash_command(name,desc,block,args=[])
    return {:name => name, :desc => desc, :block => block, :args => args}
  end
  def intern(n)
    (Symbol === n) ? n : n.intern
  end
  def auto_command(name,real_name)
    @auto[intern(real_name)].merge({:name => name})
  end
  def run
    cmd = @default
    args = Array.new
    ARGV.each { |arg|
      if cmd == @default then
        cmd = arg.intern
        if !@commands.include?(cmd) then
          if @empty != nil then
            cmd = "<no command>";
            args.push(arg)
          else
            puts "No such command '%s'" % cmd.to_s
            Process.exit
          end
        end
        next
      else
        args.push(arg)
      end
    }
    ARGV.shift while ARGV.length > 0
    begin
      invoke cmd, *args
    rescue Exception => e
      puts e.message
    end
  end
end