Project

ing

0.0
No commit activity in last 3 years
No release in over 3 years
An alternative to Rake and Thor, Ing has a command-line syntax similar to Thor's, and it incorporates Thor's (Rails') generator methods and shell conventions. But unlike Thor or Rake, it does not define its own DSL. Your tasks correspond to plain ruby classes and methods. Ing just handles routing from the command line to them, and setting options. Your classes (or even Procs) do the rest.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

 Project Readme

Ing

Vanilla ruby command-line scripting.

or gratuitous backronym: I Need a Generator!

Ing is a task scripting micro-toolkit designed around the following opinions:

  • Ruby itself is a domain-specific language for scripting (among other things), it has great facilities for dealing with filesystems, processes, network IO, interacting with the shell, etc;

  • In addition, Ruby's object model gives you most of what you need for organizing your code into tasks to be run from the command line, for dependency management, handling errors, etc.

  • Sometimes the functionality your tasks implement you want to make use of within other programs, not only from the shell. You don't want to have to either (a) wade through the scripting framework to figure out how to get to it, or (b) refactor your tasks into separate modules and classes.

  • In particular, you want to be able to test your tasks independently of the framework.

  • A framework (any framework, in any context) should not encourage bad design at the expense of supposed simplicity of the interface. A framework should get out of the way as much as possible.

Introduction

The core of what Ing provides is a router and built-in option parser (using the venerable and excellent Trollop) that maps the command line to your ruby classes and methods, using simple conventions.

For example, this:

ing some:task run something --verbose

in the most typical scenario, routes to:

Some::Task.new(:verbose => true).run("something")

or if Some::Task is not a class but a proc or other 'callable', routes to:

Some::Task.call(:run, "something", :verbose => true)

As you can see, although the implementation is completely different, the command-line syntax is similar to Thor's.

In addition, Ing includes Thor's (Rails') generator methods and conventions so you can do things like this within your tasks:

if yes? 'process foo files?', :yellow
  inside('foo') { create_file '%foo_file%.rb' }
end

Unlike Thor or Rake, Ing does not define its own DSL. Your tasks correspond to plain ruby objects and methods. Ing just handles routing from the command line to them, and setting options. Your classes or procs do the rest.

As we will see, there are some base classes your tasks can inherit from that cut down on boilerplate code for common scenarios, but they are there only for convenience: your task classes/procs are not required to be coupled to the framework at all.

MORE

Installation

gem install ing

To generate a default ing.rb file (similar to Rakefile or Thorfile), that loads from a tasks directory:

ing setup

Usage

Built-in commands

Ing has some built-in commands. You can see what they are (so far) with ing list -n ing:commands. And you can get help on a command with ing help ....

NEW: Ing now provides bash auto-completion (hooray!). Copy the script to your OS' bash-completions directory, or source it manually, and tab away.

Generator tasks

The most significant built-in Ing command is generate or g, which simplifies a common and familiar use-case (at the expense of some file- system conventions):

ing generate some:task --force

Unlike Thor/Rails generators, these don't need to be packaged up as gems and preloaded into ruby. They can be parsed as either:

  1. A file relative to a root dir: e.g. some/task, or
  2. A subdirectory of the root dir, in which case it attempts to preload ing.rb within that subdirectory: e.g. some/task/ing.rb

So the command above is then dispatched as normal to Some::Task.new(:force => true).call (#call is used if no method is specified). So you should put the task code within that namespace in the preloaded file.

(By default, the generator root directory is specified by ENV['ING_GENERATORS_ROOT'] or failing that, ~/.ing/generators.)

MORE

A simple example of a plain old ruby task

Let's say you want to run your project's tests with a command like ing test. The default is to run the whole suite; but if you just want unit tests you can say ing test unit. This is what it would look like (in ./ing.rb):

class Test

  # no options passed, but you need the constructor
  def initialize(options); end
  
  # `ing test`
  def call(*args)
    suite
  end
  
  # `ing test suite`
  def suite
    unit; functional; acceptance
  end

  # `ing test unit`
  def unit
    type 'unit'
  end

  # `ing test functional`
  def functional
    type 'functional'
  end

  # `ing test acceptance`
  def acceptance
    type 'acceptance'
  end
    
  def type(dir)
    Dir["./test/#{dir}/*.rb"].each { |f| require_relative f }
  end
  
end

As you can see, the second arg corresponds to the method name. call is what gets called when there is no second arg. Organizing the methods like this means you can also do ing test type custom: extra non-option arguments are passed into the method as parameters.

Note in most real cases you would want to namespace your tasks, and not use a top-level class named Test (which would fail in some ruby versions in fact). This is just to give you a flavor.

For more worked examples of ing tasks, see the examples directory.

MORE

Option arguments

Your tasks (ing subcommands) can specify what options they take by defining a class method specify_options. For example:

class Cleanup

  def self.specify_options(spec)
    spec.text "Clean up your path"
    spec.text "\nUsage:"
    spec.text "ing cleanup [OPTIONS]"
    spec.text "\nOptions:"
    spec.opt :quiet, "Run silently"
    spec.opt :path,  "Path to clean up", :type => :string, :default => '.'
  end
    
  attr_accessor :options
  
  def initialize(options)
    self.options = options
  end
  
  # ...
end

The syntax used in self.specify_options is Trollop - in fact what you are doing is building a Trollop::Parser which then sends the parsed options into your constructor.

In general your constructor should just save the options to an instance variable like this, but in some cases you might want to do further processing of the passed options.

MORE

Using the Ing::Task base class

To save some boilerplate, and to allow more flexible options specification, as well as a few more conveniences, you can inherit from Ing::Task and rewrite this example as:

class Cleanup < Ing::Task
  desc "Clean up your path"
  usage "ing cleanup [OPTIONS]"
  opt :quiet, "Run silently"
  opt :path,  "Path to clean up", :type => :string, :default => '.'

  # ...
end

This gives you a slightly more automated help message, with the description lines followed by usage followed by options, and with headers for each section.

Ing::Task also lets you inherit options. Say you have another task:

class BigCleanup < Cleanup
  opt :servers, "On servers", :type => :string, :multi => true
end

This task will have the two options from its superclass as well as its own. (Note the description and usage lines are not inherited this way, only the options).

Generator tasks

If you want to use Thor-ish generator methods, your task classes need a few more things added to their interface. Basically, it should look something like this.

class MyGenerator

  def self.specify_options(spec)
    # ...
  end
  
  include Ing::Files
  
  attr_accessor :destination_root, :source_root, :options, :shell
  
  # default == execution from within your project directory
  def destination_root
    @destination_root ||= Dir.pwd
  end
  
  # default == current file is within root directory of generator files
  def source_root
    @source_root ||= File.expand_path(File.dirname(__FILE__))
  end
  
  def initialize(options)
    self.options = options
  end
  
  # ...
end

The generator methods need :destination_root, :source_root, and :shell. Also, include Ing::Files after you specify any options (this is because Ing::Files adds several options automatically).

If you prefer, you can inherit from Ing::Generator, which gives you all of the above defaults more or less, plus the functionality of Ing::Task.

Like Ing::Task, Ing::Generator is simply a convenience for common scenarios.

MORE

Standalone executables

You can use Ing to generate 'standalone' executables from your tasks, so you can call it directly from the command line and also redistribute it (as a gem). For more details see ing help gemify.

Motivation

I wanted to use Thor's generator methods and shell conventions to write my own generators. But I didn't want to fight against Thor's hijacking of ruby classes.

I love Rake, but find it much too easy to write horribly unmaintainable code in its DSL, and always fight with its nonstandard command-line syntax.

Q & A

But what about task dependency resolution?

That's what require and ||= are for ;)

Seriously, you do have Ing.invoke Some::Task, :some_method if you want a declarative way to say, from any point in your codebase, that you only want the depended-on task to run only if it hasn't already.

But before you do, please consider:

  • If your case is invoking a task only once within the same module, you should probably simply design your methods so they are called that way in plain ruby.

  • If your case is running some bit of setup code that is shared among several tasks that would otherwise not be executed as a task itself, Ing.invoke is overkill. The code should be refactored so that it's accessible to the several tasks, but not implemented as a task itself.

Ing.invoke is there for cases of multi-step tasks where you want access to both the complete task and the sub-steps: such as mult-step compilation, the classic use-case for make.

In fact, if you find yourself needing to use Ing.invoke a lot, perhaps you should just use make, since the DSL is optimized for exactly this kind of task.