« | Main | »

December 01, 2009

I find myself writing tons and tons of scripts, even for things I know I'll probably do only once. So, code.google.com/p/binfiles is a google project where I'm collecting them all in one place. They all use the app gem to do some task, and hopefully, out of this I get some maintainability of all these files.

The basic concepts are these:
  • I put all of these files in ~/bin, you can put them where you want. But, if you don't there could be a hiccup caused from relying that they are in ~/bin.
  • If I want to do some task, that task will probably be done repeatedly, but don't want to invest tons of time into actually writing the overhead that makes maintainable/understandable scripts (e.g. argument parsing, help printing, convenient methods abstracted out to base classes, etc) I would first invoke newapp with the name of the app(s) as argument(s) and some other options, and it will create the file, make it executable, and try to add it to SVN. Here are the full set of options you'd see if you type pass the one of the two help arguments, -h or -help, or simply invoke it with no arguments.

    % newapp
    Produces starter code for commandline apps using the 'app' gem
    Usage: newapp <options> <arguments>
    where options include:
      -h | -help          Print this message
      -v | -verbose       Print debugging messages
      -d | -describe      Print a desription of this program
      -e | -explainargs   Print a desription of the argument types
      -b | -bindir        Use ~/bin for you destintion directory.
      -n | -motions       Just go through the motions
      -f | -force         Force override of existing files
      -t | -text      <s> New description of the app, 's'
    and where arguments are 'app names'
          
    After that a new file will be created for every name you pass as an argument. For example:

    % newapp a_sample_app -t "A sample app for this blog post"
    % cat a_sample_app
    #!/usr/bin/env ruby
    #
    # A sample app for this blog post
    #
    
    require 'rubygems'
    require 'app'
    
    class ASampleApp < App
    
      def initialize
        super
      end
    
      def description    
        "A sample app for this blog post"
      end
    
      def default_arguments
        []
      end
    
      def argument_explanations
        []
      end
        
      def get_options
      end
      
      def process_arg(arg)
        throw 'Implementation missing for method ' + 
          q(self.class.name + '.process_arg')
    
        # OK
        0
      end
    
    end
    
    ASampleApp.new.main ARGV
    		   
  • First, something about the arguments...all apps that are subclasses of App support the following arguments:

      -h | -help            Print this message
      -v | -verbose         Print debugging messages
      -d | -describe        Print a desription of this program
      -e | -explainargs     Print a desription of the argument types
      -b | -bindir          Use /users/jeff/bin for you destintion directory.
      -n | -motions         Just go through the motions
          
    To add more arguments implement the method get_options to return a list of Options, for example:

      def get_options
        [new_force_opt,
         new_text_opt,
         new_opt('-j','-jeff',"Print jeff's name"),
         new_long_opt('-s','-string',"Use the passed in",true)]
      end
          
    Here, the two methods that create Options are new_opt(short_opt,long_opt,description) and new_long_opt(short_opt,long_opt,text,required), the other true are shortcuts for commonly-used options.
  • Within your code, member variables are set by reflection, so for the third options from above, you could refer to the jeff option with @opt_j or @opt_jeff.
  • There is a special method for side-effecting code -- like file creation or deletion -- called block(msg) that takes as arguments a string msg and a thunk. If the user passes in -n or -motions, as in make, this method simply prints out the message. These options control whether any actually 'happens'. In normal mode, the thunk is run and msg is printed if the user has passed verbose options. I really like this one, probably the most, and here's an example of how you would use this to delete a file, f:

    f = ...
    block 'Deleting file ' + f do
      File.delete f
    end
        
    This way it's easier to write code that you can just see what it will do first, before doing things that could change your machine.
  • I kinda rambled there, but the idea is that the migration from little scripts to real programs shouldn't be painful. In fact, it should be fun, and these are fun to write (OK, I may have lost a lot of readers there). So, you should never have to parse arguments, or write help screens, or comment out nasty stuff that could screw things up. You just really implement one method -- parse_arg -- and do your stuff in there.

    Posted by jeff at December 1, 2009 07:53 PM