Some things to Consider for Console Application Development

It is your current project and you have been induging in secret thought plans about it for weeks. Planning happens in the shower, on the bus. It happens when you talk to relatives on the phone, or standing in line for coffee. At any spare moment it can be opportunistically slipped into your personal awareness space. The Master Plan is unrolled on a mental table and worked-upon. There are notebooks with diagrams at home. Perhaps functional specs, or at least sketchy attempts to define.

Some questions you may be asking:

"What about logging? I can't see what my code is doing."

"I'm not going to parse all these options on a command-line. I need a configuration file."

"I think I need a driver for all this."

So we are missing pretty standard things. Logging, Driver, Configuration, Argument Handling. These small things matter. You can't go on without them.

Let's address one at a time:

Logging

Ruby system libraries support Logger and are fully thread-safe in the latest version. The documentation, as usual is ungenerous, but the facility can be included easily with some modifications shown below. For normal use, you shouldn't need to download dependency gems. The built-in logger is top-notch, and better for general purpose than most other 3rd-party logging frameworks.

# from http://apidock.com/ruby/Logger (suggestion by tadman)

require 'logger'

class MyProjectLogger < Logger

   def initialize(file)
     super(file)
   end

   def format_message(severity, datetime, progname, msg)
     "[%s %s] %s\n" % [ severity, datetime.strftime("%H:%M"), msg ]
   end
end

Driver

You need to have one. It needs to be in a separate file. You will be running it all the time. I like to pretend it's C, so I put in a main() method.

Use require_relative to include custom modules and classes in the project directory and subdirectories. You can log directly using Log. It's wise to keep this ultra-simple. A driver is a place to move code out of and into classes or modules. Whatever is left behind should just be a skeleton of commands. You could say a driver is a client that execrises the API in some way, and you'd be correct. But don't think of it like that. It's the main operational recipe of your program, and deals with phases of execution. Big-picture stuff only.

require_relative 'MyProjectLogger.rb'
require_relative 'MyProjectArgs.rb'

Log = MyProjectLogger.new(STDERR)
Log.level = MyProjectLogger::DEBUG

def main()

   ## cleanup old junk, check for duplicate instances of program, etc.

   ## obtain and validate command line arguments
   args = MyProjectArgs.new(ARGV)
   args.run

   ## read configuration file options

   ## start modules, threads, whatever and wait on some condition
   ## loop or sleep or something...
end

main # <--- Where program begins.

Argument Handler

You could use frameworks, but if you keep your arguments out of the command line and in a configuration file, you only have to deal with a simple argument validation class. The whole purpose of this class is to exit early on invalid user input. One of the arguments you handle here should always be the path to your configuration file. I also like a mode= option. It's like an "anything else I might want to add later" card.

class MyProjectArgs

   attr_reader :argv, :mode, :config

   def initialize(argv)
      @argv = argv
      @mode = nil
      @config = nil
   end

   def run()

      argv.each do |arg|
         case arg

            when /mode=(.+)/i
               @mode = $1
               if mode != "standalone"
                  Log.error "Wrong mode assigned [#{arg}]"
               end

            when /config=(.+)/i
               if not File.exist? $1
                  Log.error "Config file [#{$1}]doesn't exist, [#{arg}]"
               else
                  @config=$1
               end
         end
      end
   end
end

Configuration File

This can be tough because it is a database. Which means design. On one hand, you ideally need <variable> = <value> fields. On the other hand, you also need sections of configuration options, and defaults. I typically think of Windows .ini files as being the easiest, most straightforward way of doing this, but consider this possibility: using a Ruby Module to store values which can be later accessed in a struct.

Ruby has such a clean syntax, its code is almost like configuration. Why not leverage the mechanisms of modules to play the role of a simple configuration file with sections of keys/values?

# source: http://aphyr.com/posts/173-monkeypatching-is-for-wimps-use-set-trace-func

require 'ostruct'

opts = OpenStruct.new

module Institutions
  def Zoo
    date_open="March"
    date_closed="Feb"
    a = "Value for opt.zoo.aa"
    b = "Value for opt.zoo.b"
    c = "Value for opt.zoo.c"
    d = "Value for opt.zoo.d"
  end

  def Railroad
    date_open="1970"
    date_closed="1980"
    a = 1000001
    b = 2000002
    c = 3000003
    d = 4000004
  end
end

class A
   extend Institutions
end

# get sections ("def <section>" parts of code) and deepen the OpenStruct

funcs = Institutions.instance_methods false
funcs.each { |name| opts.send( "#{name}=", OpenStruct.new ) }

# trace each assignment inconfig file as it is "executed", filtering junk

set_trace_func proc { |event, file, line, id, binding, classname| # trace on
   if opts[id.to_s]
      locals = binding.eval( "local_variables" )
      locals.each { |var|
         val = binding.eval( var.to_s )
         section = opts.send( id )
         section.send( "#{var}=", val ) # assign to correct section in OpenStruct
      }
   end
}

# "run" each section of configuration file
funcs.each { |meth| A.send meth }

# trace off
set_trace_func(nil)


# some proof of concept it works...
puts opts.Zoo.date_open
puts opts.Railroad.date_open
puts opts.Zoo.a
puts opts.Railroad.a

Above, we have some blobs of monkeypatch code that handles simple configuration options contained in a module that has sections of configuration options. A demo configuration module called Institutions is defined, and contains sections Zoo and Railroad. Within each section are values assigned to keys. Class A uses the config module by extending it. The configuration logic acts upon Class A to get the options out of the file. The options are then directly accessible by: opts.<section>.<variable>

We load the module, grab the sections and drill separate rooms for each in the OpenStruct, flip on set_trace_func and run each sectionof the config file, transferring the loot from the module into the OpenStruct. We then flip off the trace, and voila! All configuration options are conveniently available in dot-notation!

This of course is very limited, but the concept is comparatively clean and sensible. No external dependencies. No explicit file reads or decisions to make about format.

Now the Munchkins are happy. We can move on with the project!