Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config tasks proposal #24

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/rake.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
require 'rake/file_task' require 'rake/file_task'
require 'rake/file_creation_task' require 'rake/file_creation_task'
require 'rake/multi_task' require 'rake/multi_task'
require 'rake/config_task'
require 'rake/dsl_definition' require 'rake/dsl_definition'
require 'rake/file_utils_ext' require 'rake/file_utils_ext'
require 'rake/file_list' require 'rake/file_list'
Expand Down
31 changes: 29 additions & 2 deletions lib/rake/application.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ def parse_task_string(string)
end end
[name, args] [name, args]
end end

def format_task_string(args)
"#{args.shift}[#{args.join(',')}]"
end


# Provide standard exception handling for the given block. # Provide standard exception handling for the given block.
def standard_exception_handling def standard_exception_handling
Expand Down Expand Up @@ -421,6 +425,12 @@ def handle_options
end end


standard_rake_options.each { |args| opts.on(*args) } standard_rake_options.each { |args| opts.on(*args) }

opts.on_tail("--", "Turn on arg syntax ('-- a b c' => 'a[b,c]')") do
# restores option break to ARGV if found, allowing arg syntax
throw :terminate, "--"
end

opts.environment('RAKEOPT') opts.environment('RAKEOPT')
end.parse! end.parse!


Expand Down Expand Up @@ -532,13 +542,30 @@ def standard_system_dir #:nodoc:
# Environmental assignments are processed at this time as well. # Environmental assignments are processed at this time as well.
def collect_tasks def collect_tasks
@top_level_tasks = [] @top_level_tasks = []

current = nil
ARGV.each do |arg| ARGV.each do |arg|
if arg =~ /^(\w+)=(.*)$/ case arg
when '--'
if current && !current.empty?
@top_level_tasks << format_task_string(current)
end
current = []
when /^(\w+)=(.*)$/
ENV[$1] = $2 ENV[$1] = $2
else else
@top_level_tasks << arg unless arg =~ /^-/ if current
current << arg
else
@top_level_tasks << arg unless arg =~ /^-/
end
end end
end end

if current && !current.empty?
@top_level_tasks << format_task_string(current)
end

@top_level_tasks.push("default") if @top_level_tasks.size == 0 @top_level_tasks.push("default") if @top_level_tasks.size == 0
end end


Expand Down
255 changes: 255 additions & 0 deletions lib/rake/config_task.rb
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,255 @@
require 'rake/task.rb'
require 'optparse'

module Rake
# #########################################################################
# = ConfigTask
#
# Config tasks allow the creation of tasks that can recieve simple
# configurations using command line options. The intention is to allow
# command-like tasks to be defined and used in a natural way (ie using a 'name
# --flag arg' syntax). For clarity ConfigTasks are declared using 'tasc' and
# referred to as 'tascs'.
#
# Tasc configs are declared using an options string and accessed on the tasc
# itself. Numeric types are cast to appropriate values.
#
# require 'rake'
#
# desc "welcome a thing"
# tasc :welcome, :thing, %{
# -m,--message [hello] : A welcome message
# -n [1] : Number of times to repeat
# } do |config, args|
# config.n.times do
# puts "#{config.message} #{args.thing}"
# end
# end
#
# Then from the command line, invoke after '--':
#
# % rake -- welcome world
# hello world
# % rake -- welcome --message goodnight -n 3 moon
# goodnight moon
# goodnight moon
# goodnight moon
# % rake -- welcome --help
# Usage: rake -- welcome [options] object
# -m, --message [hello] A welcome message
# -n [1] Number of times to repeat
# -h, --help Display this help message.
#
# Unlike typical tasks which only run once, tascs are reset after each run, so
# that they can be invoked multiple times:
#
# % rake -- welcome world -- welcome moon -m goodnight
# hello world
# goodnight moon
#
# Tascs may participate in dependency workflows, although it gets a little
# peculiar when other tasks depend upon the tasc. Below is an explanation.
# TL;DR; -- tascs may have dependencies, but other tasks/tascs should not depend
# upon a tasc.
#
# == Dependency Discussion
#
# Normally tasks are designed to be unaware of context (for lack of a better
# word). Once you introduce arguments/configs, then suddenly it matters when the
# arguments/configs 'get to' a task. For example:
#
# require 'rake'
#
# task(:a) { puts 'a' }
# task(:b => :a) { puts 'b' }
#
# tasc(:x, [:letter, 'x']) {|t| puts t.letter }
# tasc(:y, [:letter, 'y'], :needs => :x) {|t| puts t.letter }
#
# There is no order issue for the tasks, for which there is no context and
# therefore they can align into a one-true execution order regardless of
# declaration order.
#
# % rake a b
# a
# b
# % rake b a
# a
# b
#
# A problem arises, however with tascs that do have a context. Now it matters
# what order things get declared in. For example:
#
# % rake -- x --letter a -- y --letter b
# a
# a
# b
# % rake -- y --letter b -- x --letter a
# x
# b
# a
#
# You can see that declaration order matters for tascs in a way it does not for
# tasks. The problem is not caused directly by the decision to make tascs run
# multiple times; it's caused by the context which gets interwoven to all
# tasks/tascs via dependencies. For example, pretend tascs only executed once...
# which arguments/configurations should win in this case?
#
# % rake -- welcome world -- welcome -m goodnight
# # hello world ?
# # goodnight ?
# # goodnight world ?
#
# All of this can be avoided by only using tascs as end-points for dependency
# workflows and never as prerequisites. This is fine:
#
# require 'rake'
#
# task(:a) { print 'a' }
# task(:b => :a) { print 'b' }
# tasc(:x, [:letter, 'x'], :needs => [:b, :a]) {|t| puts t.letter }
#
# Now:
#
# % rake -- x --letter c
# abc
# % rake a b -- x --letter c
# abc
# % rake b a -- x --letter c
# abc
#
class ConfigTask < Task

def parser
@parser ||= OptionParser.new do |opts|
opts.on_tail("-h", "--help", "Display this help message.") do
puts opts
exit
end
end
end

def invoke(*args)
parser.parse!(args)
super(*args)
end

def invoke_with_call_chain(*args)
super
reenable
end

def reenable
@config = nil
super
end

def config
@configs ||= default_config.dup
end

def default_config
@default_config ||= {}
end

def [](key)
config[key.to_sym]
end

def method_missing(sym, *args, &block)
sym = sym.to_sym
config.has_key?(sym) ? config[sym] : super
end

def set_arg_names(args)
while options = parse_options(args.last)
set_options(options)
args.pop
end
@arg_names = args.map { |a| a.to_sym }
parser.banner = "Usage: rake -- #{name} [options] #{@arg_names.join(' ')}"
@arg_names
end

def parse_options(obj)
case obj
when Array then [obj]
when String then parse_options_string(obj)
else nil
end
end

def parse_options_string(string)
string = string.strip
return nil unless string[0] == ?-

string.split(/\s*\n\s*/).collect do |str|
flags, desc = str.split(':', 2)
flags = flags.split(',').collect! {|arg| arg.strip }

key = guess_key(flags)
default = flags.last =~ /\s+\[(.*)\]/ ? guess_default($1) : guess_bool_default(flags)

[key, default] + flags + [desc.to_s.strip]
end
end

def guess_key(flags)
keys = flags.collect do |flag|
case flag.split(' ').first
when /\A-([^-])\z/ then $1
when /\A--\[no-\](.*)\z/ then $1
when /\A--(.*)\z/ then $1
else nil
end
end
keys.compact.sort_by {|key| key.length }.last
end

def guess_default(str)
case str
when /\A(\d+)\z/ then str.to_i
when /\A(\d+\.\d+)\z/ then str.to_f
else str
end
end

def guess_bool_default(flags)
flags.any? {|flag| flag =~ /\A--\[no-\]/ ? true : false }
end

def set_options(options)
options.each do |(key, default, *option)|
default = false if default.nil?
option = guess_option(key, default) if option.empty?

default_config[key.to_sym] = default
parser.on(*option) do |value|
config[key.to_sym] = parse_config_value(default, value)
end
end
end

def guess_option(key, default)
n = key.to_s.length

case default
when false
n == 1 ? ["-#{key}"] : ["--#{key}"]
when true
["--[no-]#{key}"]
else
n == 1 ? ["-#{key} [#{key.to_s.upcase}]"] : ["--#{key} [#{key.to_s.upcase}]"]
end
end

def parse_config_value(default, value)
case default
when String then value.to_s
when Integer then value.to_i
when Float then value.to_f
else value
end
end
end
end
56 changes: 56 additions & 0 deletions lib/rake/dsl_definition.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -23,6 +23,62 @@ def task(*args, &block)
Rake::Task.define_task(*args, &block) Rake::Task.define_task(*args, &block)
end end


# Declare a config task. Configs are defined as arrays of [key, default]
# pairs that follow the argument list. The default value may be a String,
# Integer, Float, true, or false; these values will be cast appropriately.
#
# Example:
#
# tasc :welcome, :object, [:msg, 'hello'], [:n, 1] do |config, args|
# config.n.times do
# puts "#{config.msg} #{args.object}"
# end
# end
#
# OptionParser arguments may be specified after the default, if desired:
#
# Example:
#
# tasc(:welcome, :object,
# [:msg, 'hello', '-m', '--message', 'A welcome message']
# [:n, 1, '-n', 'Number of times to repeat']
# ) do |config, args|
# config.n.times do
# puts "#{config.msg} #{args.object}"
# end
# end
#
# Configs may also be declared as a string, which allows documentation and
# compact specification of shorts. In this syntax the config key will be
# equal to the long, if available, or the short if not. The config type
# will be guessed from the default string.
#
# Example:
#
# tasc :welcome, :object, %{
# -m,--message [hello] : A welcome message
# -n [1] : Number of times to repeat
# } do |config, args|
# config.n.times do
# puts "#{config.message} #{args.object}"
# end
# end
#
# Prerequisites may still be declared using a trailing hash in the form
# {:needs => [prequisites]}.
def tasc(*args, &block)
# adding an explicit needs hash ensures args will be equivalent to one
# of these patterns (thereby making argument resolution predictable)
#
# tasc :name => :need
# tasc :name => [:needs]
# tasc :name, args_or_configs..., {:needs => [:needs]}
#
unless args.last.kind_of?(Hash)
args << {:needs => []}
end
Rake::ConfigTask.define_task(*args, &block)
end


# Declare a file task. # Declare a file task.
# #
Expand Down
Loading