require "sickle/version" require 'optparse' module Sickle class << self def push_desc(desc) @__desc = desc end def pop_desc d = @__desc @__desc = nil d end def push_option(name, opts) @__options ||= {} @__options[name] = Option.new(name, opts) end def pop_options o = @__options || {} @__options = {} o end def push_namespace(n) namespace << n end def pop_namespace namespace.pop end def namespace @__namespace ||= [] end end module Runner def self.included(base) base.extend(Sickle::ClassMethods) if base.is_a?(Class) base.send(:include, Sickle::Help) base.method_added(:help) end end def options @__options ||= {} end end class Command attr_accessor :meth, :name, :desc, :options def initialize(meth, name, desc, options) @meth, @name, @desc, @options = meth, name, desc, options end end class Option attr_accessor :name, :opts, :default def initialize(name, opts) @name, @opts = name, opts @default = opts.has_key?(:default) ? opts[:default] : nil if @default == true || @default == false @type = :boolean else @type = @default.class.to_s.downcase.to_sym end end def register(parser, results) if @type == :boolean parser.on("--#{cli_name}", opts[:desc]) do results[@name] = true end else parser.on("--#{cli_name} #{@name.upcase}") do |v| results[@name] = coerce(v) end end end def cli_name @name.to_s.tr("_", "-") end def coerce(value) case @default when Fixnum value.to_i when Float value.to_f else value end end end module Help def self.included(base) base.class_eval do desc "Display help" def help(command = nil) if command __display_help_for_command(command) else __display_help end end end end def __display_help_for_command(name) if cmd = self.class.__commands[name] puts "USAGE:" u, _ = __display_command_usage(name, cmd) puts " #{$0} #{u}" puts puts "DESCRIPTION:" puts cmd.desc.split("\n").map {|e| " #{e}"}.join("\n") puts unless cmd.options.empty? puts "OPTIONS:" cmd.options.each do |_, opt| puts __display_option(opt) end end __display_global_options else puts "\e[31mCommand '#{name}' not found\e[0m" end end def __display_command_usage(name, command) params = command.meth.parameters.map do |(r, p)| r == :req ? p.upcase : "[#{p.upcase}]" end ["#{name.gsub("_", "-")} #{params.join(" ")}", command] end def __display_help puts "USAGE:" puts " #{$0} COMMAND [ARG1, ARG2, ...] [OPTIONS]" puts puts "TASKS:" cmds = self.class.__commands.sort.map do |name, command| __display_command_usage(name, command) end max = cmds.map {|a| a[0].length }.max cmds.each do |(cmd, c)| desc = c.desc ? "# #{c.desc}" : "" puts " #{cmd.ljust(max)} #{desc}" end __display_global_options end def __display_global_options unless self.class.__global_options.empty? puts puts "GLOBAL OPTIONS:" self.class.__global_options.sort.each do |name, opt| puts __display_option(opt) end end end def __display_option(opt) " --#{opt.cli_name} (default: #{opt.default})" end end module ClassMethods def included(base) __commands.each do |name, command| name = (Sickle.namespace + [name]).join(":") base.__commands[name] = command end end def desc(label) Sickle.push_desc(label) end def global_option(name, opts = {}) __global_options[name.to_s] = Option.new(name, opts) end def global_flag(name) global_option(name, :default => false) end def option(name, opts = {}) Sickle.push_option(name, opts) end def flag(name) option(name, :default => false) end def include_modules(hash) hash.each do |key, value| Sickle.push_namespace(key) send(:include, value) Sickle.pop_namespace end end def __commands @__commands ||= {} end def __global_options @__global_options ||= {} end def run(argv) # puts "ARGV: #{argv.inspect}" if command_name = argv.shift command_name = command_name.gsub("-", "_") if command = __commands[command_name] all = __global_options.values + command.options.values results = {} args = OptionParser.new do |parser| all.each do |option| option.register(parser, results) end end.parse!(argv) all.each do |o| results[o.name] ||= o.default end # puts "args: #{args.inspect}" # puts "results: #{results.inspect}" obj = self.new obj.instance_variable_set(:@__options, results) command.meth.bind(obj).call(*args) else puts "\e[31mCommand '#{command_name}' not found\e[0m" puts run(["help"]) end else run(["help"]) end end def method_added(a) meth = instance_method(a) if desc = Sickle.pop_desc __commands[a.to_s] = Command.new(meth, a, desc, Sickle.pop_options) end end end end