Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/ericgj/ing
Vanilla ruby command-line scripting
https://github.com/ericgj/ing
Last synced: about 2 months ago
JSON representation
Vanilla ruby command-line scripting
- Host: GitHub
- URL: https://github.com/ericgj/ing
- Owner: ericgj
- License: other
- Created: 2012-09-13T01:02:26.000Z (over 12 years ago)
- Default Branch: master
- Last Pushed: 2012-10-07T05:18:02.000Z (over 12 years ago)
- Last Synced: 2024-10-31T14:29:26.608Z (2 months ago)
- Language: Ruby
- Size: 512 KB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this 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](http://trollop.rubyforge.org/)) that maps
the command line to your ruby classes and methods, using simple conventions.For example, this:
```bash
ing some:task run something --verbose
```in the most typical scenario, routes to:
```ruby
Some::Task.new(:verbose => true).run("something")
```or if Some::Task is not a class but a proc or other 'callable', routes to:
```ruby
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:```ruby
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](ing/blob/master/SYNTAX.md)
## 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](ing/blob/master/completions/ing.bash) 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`):```ruby
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](ing/blob/master/examples) directory.[MORE](ing/blob/master/TASKS.md)
### Option arguments
Your tasks (ing subcommands) can specify what options they take by defining a
class method `specify_options`. For example:```ruby
class Cleanupdef 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](ing/blob/master/OPTIONS.md)
### 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:```ruby
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:
```ruby
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.```ruby
class MyGeneratordef 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](ing/blob/master/GENERATORS.md)
## 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.