Get to Know a Gem: Main

Chapter 1

Introduction

Main is a great library to succinctly write command line applications in Ruby. It's written by Ara T. Howard, a prolific member of the Ruby community, who's written over a hundred Ruby Gems. Ara's style of writing code is unique compared to the conventional coding techniques we come across in Rails or most Gems. It took me a while to get my head around exactly how the code is organized, but once I did I couldn't help but admire the way in which Main is written. To the end user Main is simple to use, but under the hood it is a very well crafted and intricate library using all sorts of techniques that I've never encountered before. In many ways, Ara's style comes across as someone that has command of not only the Ruby language but core concepts and isn't afraid to override base methods such as #new or #allocate. Main is the quintessence of confident Ruby.

As Main has so many interesting components to explain, I will spread it across several blog posts over the coming weeks.

Part 1: Main {}

Getting started with Main is trivial. In the project directory under the Samples folder you'll find several examples of how to use Main. For the purposes of this post, I'll be focusing on a.rb:

Main {
  argument('foo'){
  required                    
  cast :int                   
  validate{|foo| foo == 42}   
  description 'the foo param'
  	  }

 def run
   p params['foo'].given?
   p params['foo'].value
end
} 

This Main program will take an argument foo cast it as an Int validate that it is equal to 42 and then proceed to call #run. All Main requires is that you implement #run, but otherwise has no requirement to get a command line app running.

So what's going on in the source code?

First that initial Main {} is a method found in /main/program/factories.rb:

module Kernel
private
  def Main(*args, &block) 
    Main.run(*args, &block)
 end
alias_method 'main', 'Main'
end

The module Kernel is actually Standard Ruby included by Object. So Main is smartly extending a core Ruby module so that Main can be called without needing to write a class, or inlude a seperate module. Clearly, this is an excellent example of extending Ruby.

As you may have noticed. #Main takes both arguments and a block. However, in the case of a.rb we are only passing a block, the contents inside the brackets {}.

Part 2: Main.run

The Kernel#Main method proceeds to call Main.run passing along any args and the block. The implementation of this method you can find in that very factories.rb. #run is simply a class method on the Main module.

def Main.run(*args, &block)
  program = factory(&block).build(*args)
  main = program.new()
  main.run()
end

On the first line factory(&block).build(*args) are the key two methods called that understand a user's configuration of main within the Main{} block, and return an instance of Factory. So let's delve into that first #factory call.

The initial #factory method is actually implemented in the Main module:

def Main.factory(&block)
  Program.factory(&block)
end

Program is the protagonist of Main most of the methods discussed from here on will mostly be implemented there. The code for Program#factory you can find in /main/program/class_methods.rb:

def factory(&block)
  Factory.new(&block)
end
alias_method 'create', 'factory'

Factory is a class that lives inside the ClassMethods module, it is responsible for generating an instance that can execute the block passed to the initial Main{} method. On initialize, Factory#new sets &block to an instance var @block:

class Factory
  def initialize(&block)       
    @block = block || lambda{}
  end

Part 3: Factory#build

Now that we have an factory instance, Main then calls build(*args) on it. This is where a significant amount of setup is done:

def build(*args, &block)
  argv = (args.shift || ARGV).map{|arg| arg.dup}
  env = (args.shift || ENV).to_hash.dup
  opts = (args.shift || {}).to_hash.dup

  factory = self

  program = Class.new(Program)

  program.factory = factory
  program.argv = argv
  program.env = env
  program.opts = opts

  program.module_eval(&factory)

  program.module_eval do
    dynamically_extend_via_commandline_modes!
    program.set_default_options!
    define_method(:run, &block) if block
    wrap_run!
  end
  program
end

Let's examine this in small pieces, beginning with the first three lines:

argv = (args.shift || ARGV).map{|arg| arg.dup}
env = (args.shift || ENV).to_hash.dup
opts = (args.shift || {}).to_hash.dup

Main first creates duplicates of any arguments passed to the application. Clearly, the code relies on a convention that argv, env, and opts will be in order. In the case of argv and env Main falls back to simple picking up whatever was passed in via the terminal in ARGV and ENV .

Next the setup of a Program class:

program = Class.new(Program)

program.factory = factory
program.argv = argv
program.env = env
program.opts = opts

I was vaguely aware of using Class.new, you can read more about it here. Main creates an instance of the class Program and sets the factory, argv, env and opts to it. These attributes are defined above in ClassMethods:

fattr('argv')
fattr('env')
fattr('opts')
…
def factory=(factory)
  @factory = factory
end

fattr is a call to the Fattr gem, a library used to quickly add attributes to a class that does more than attr_accessor. It's also written by Ara.

Having setup the basic attributes of this Program class, Main then does some meta-programming:

factory = self

program.module_eval(&factory)

program.module_eval do
  dynamically_extend_via_commandline_modes!
  program.set_default_options!
  define_method(:run, &block) if block
  wrap_run!
end

On that second line we see a call to module_eval passing in our factory instance as a block. But isn't factory a class and not a Proc? Indeed, but remember that @block instance variable we assigned earlier, you'll find it being returned by Factory#to_proc

def to_proc
  @block
end

I was vaguely aware this could be done, but had never seen the use of #to_proc used in the wild, this is a great example of when to do so. So why module_eval? In case you don't remeber the difference between, class_eval, module_eval, and instance_eval (I know I didn't) jog your memory here. Because program is a Class, Main wants that initial block in a.rb to be evaluated in the context of the class and provide instance methods on the class.

For now I'm not going to delve into what's happening when Main calls that module_eval, that will be coming up in later posts. Similarly that second call to module_eval

program.module_eval do
  dynamically_extend_via_commandline_modes!
  program.set_default_options!
  define_method(:run, &block) if block
  wrap_run!
end

The key method to pay attention to is #wrap_run!. This is responsible for aliasing the user defined #run to #run! and defining a new #run method:

def wrap_run!
  evaluate do
    alias_method 'run!', 'run'

    def run()
      exit_status =
        catch :exit do
          begin
            parse_parameters

            if help?
              puts(usage.to_s)
              exit
            end

            pre_run
            before_run
            run!
            after_run
            post_run

            finalize
          rescue Object => exception
            self.exit_status ||= exception.status if exception.respond_to?(:status)
            handle_exception(exception)
          end
          nil
        end
      self.exit_status ||= (exit_status || exit_success)
      handle_exit(self.exit_status)
    end
  end
  end

Clearly a lot is going on here, some of which I'll cover later. You'll notice the call to catch that is sent a large block of code. This is a neat way that Main handles your entire applications run time. As per the documentation:

'throw :exit, 42' where 42 is the desired exit status.  throw without a
status simply exits with 0.'

So at any point in the execution of a user's application if throw :exit is sent, this is where it is caught and handled. This is another excellent convention Main introduces to make it much easier for users to focus on the logic of their applications rather than boilerplate code.

Within the catch block, Main sets up an execution path that is similar to ActiveRecord Callbacks. So that single #run method is sandwiched amongst before and after calls. For the time being, I'll leave it at that. Feel free to dive into the implementation of any of those methods, but I will be covering them later on.

Part 4: Program.new

If you can't recall, all of Part 3 above covered this line from factories.rb:

program = factory(&block).build(*args)

Now that we have a Class for our Command line application configured it makes sense to actually create an instance. Main does just this in the next line of Main#run

program.new()

You're probably thinking that this is a generic call to #new, rather in ClassMethods Main overrides new:

def new()
 puts "ClassMethods#new"
 instance = allocate
 instance.instance_eval do
   pre_initialize()
   before_initialize()
   main_initialize()
   initialize()
   after_initialize()
   post_initialize()
 end
 instance
end

Main's confidence is on full display here. Those _initialize methods are all defined in the InstanceMethods module. Main uses these methods to speak directly with the ObjectSpace module to handle it's own Garbage Collection, hookup IO direction of iput, output and errors and finally define a logger or use one if it's already defined. For the average Ruby application this is far more 'low-level', but reading through this overriden method is insightful into the more daunting parts of instantiating an object.

Conclusion: main.run

Finally we end up with an instance of our program, main. We call #run on it, which if you remember calls the run method that was defined by wrap_run! thereby eventually calling the def run(); end that we see defined in a.rb

That's the Main Gem from the highest level. There's a ton more going on that I will detail in my next few posts. Look forward to any comments or critiques you might have.

About Me

I'm Ruby Developer working in the Bay Area with previous experience in C#, and Java. I have a particular interest in software design principles, and metaprogramming.

  • kavinderd@gmail.com
  • RSS Feed
comments powered by Disqus