ame 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README ADDED
@@ -0,0 +1,4 @@
1
+ # Ame #
2
+
3
+ Ame is a simple command-line parser and build system. Its aim is to replace
4
+ the need for other command-line parsers and Rake.
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'lookout/rake/tasks'
4
+ require 'yard'
5
+
6
+ Lookout::Rake::Tasks::Test.new
7
+ Lookout::Rake::Tasks::Gem.new
8
+ Lookout::Rake::Tasks::Tags.new
9
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Ame
4
+ AbortAllProcessing = :AmeAbortAllProcessing
5
+ AbortProcessing = :AmeAbortProcessing
6
+
7
+ Error = Class.new(StandardError)
8
+ UnrecognizedMethod = Class.new(Error)
9
+ MalformedArgument = Class.new(Error)
10
+ MissingArgument = Class.new(Error)
11
+ SuperfluousArgument = Class.new(Error)
12
+ UnrecognizedOption = Class.new(Error)
13
+
14
+ autoload :Argument, 'ame/argument'
15
+ autoload :Arguments, 'ame/arguments'
16
+ autoload :Class, 'ame/class'
17
+ autoload :Help, 'ame/help'
18
+ autoload :Method, 'ame/method'
19
+ autoload :Methods, 'ame/methods'
20
+ autoload :Option, 'ame/option'
21
+ autoload :Options, 'ame/options'
22
+ autoload :Root, 'ame/root'
23
+ autoload :Splat, 'ame/splat'
24
+ autoload :Types, 'ame/types'
25
+ autoload :Version, 'ame/version'
26
+ end
@@ -0,0 +1,56 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ class Ame::Argument
4
+ def initialize(name, description, options = {}, &validate)
5
+ @name, @description, @validate = name.to_sym, description, validate || DefaultValidate
6
+ @optional = options.fetch(:optional, false)
7
+ @type = Ame::Types[[options[:type], options[:default], String].find{ |o| !o.nil? }]
8
+ set_default options[:default], options[:type] if options.include? :default
9
+ end
10
+
11
+ def arity
12
+ 1
13
+ end
14
+
15
+ def process(options, processed, argument)
16
+ raise Ame::MissingArgument, 'missing argument: %s' % self if required? and argument.nil?
17
+ validate(options, processed, argument)
18
+ end
19
+
20
+ attr_reader :name, :description, :default
21
+
22
+ def optional?
23
+ @optional
24
+ end
25
+
26
+ def required?
27
+ not optional?
28
+ end
29
+
30
+ def to_s
31
+ name.to_s.upcase
32
+ end
33
+
34
+ private
35
+
36
+ DefaultValidate = proc{ |options, processed, argument| argument }
37
+
38
+ def set_default(value, type)
39
+ raise ArgumentError,
40
+ 'default value can only be set if optional' unless optional?
41
+ raise ArgumentError,
42
+ 'default value %s is not of type %s' %
43
+ [value, type] unless value.nil? or type.nil? or value.is_a? type
44
+ @default = value
45
+ end
46
+
47
+ def parse(argument)
48
+ argument.nil? ? default : @type.parse(argument)
49
+ end
50
+
51
+ def validate(options, processed, argument)
52
+ @validate.call(options, processed, parse(argument))
53
+ rescue Ame::MalformedArgument, ArgumentError, TypeError => e
54
+ raise Ame::MalformedArgument, '%s: %s' % [self, e]
55
+ end
56
+ end
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ class Ame::Arguments
4
+ include Enumerable
5
+
6
+ def initialize
7
+ @arguments = []
8
+ @splat = nil
9
+ end
10
+
11
+ def argument(name, description, options = {}, &block)
12
+ argument = Ame::Argument.new(name, description, options, &block)
13
+ raise ArgumentError,
14
+ 'argument %s must come before splat argument %s' %
15
+ [argument.name, splat.name] if @splat
16
+ raise ArgumentError,
17
+ 'optional argument %s may not precede required argument %s' %
18
+ [first_optional.name, argument.name] if argument.required? and first_optional
19
+ @arguments << argument
20
+ self
21
+ end
22
+
23
+ def splat(name = nil, description = nil, options = {}, &validate)
24
+ return @splat unless name
25
+ splat = Ame::Splat.new(name, description, options, &validate)
26
+ raise ArgumentError,
27
+ 'splat argument %s already defined: %s' % [@splat.name, splat.name] if @splat
28
+ raise ArgumentError,
29
+ 'optional argument %s may not precede required splat argument %s' %
30
+ [first_optional.name, splat.name] if splat.required? and first_optional
31
+ @splat = splat
32
+ self
33
+ end
34
+
35
+ def arity
36
+ required = @arguments.select{ |a| a.required? }.size +
37
+ (@splat && @splat.required? ? 1 : 0)
38
+ @splat || first_optional ? -required - 1 : required
39
+ end
40
+
41
+ def process(options, arguments)
42
+ unprocessed = arguments.dup
43
+ reduce([]){ |processed, argument|
44
+ processed << argument.process(options, processed,
45
+ argument.arity < 0 ? unprocessed : unprocessed.shift)
46
+ }.tap{
47
+ raise Ame::SuperfluousArgument,
48
+ 'superfluous arguments: %s' % unprocessed.join(' ') unless unprocessed.empty?
49
+ }
50
+ end
51
+
52
+ def each
53
+ @arguments.each do |argument|
54
+ yield argument
55
+ end
56
+ yield @splat if @splat
57
+ self
58
+ end
59
+
60
+ private
61
+
62
+ def first_optional
63
+ @arguments.find{ |a| a.optional? }
64
+ end
65
+ end
@@ -0,0 +1,117 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ class Ame::Class
4
+ class << self
5
+ def basename(basename = nil)
6
+ @basename = basename if basename
7
+ return @basename if defined? @basename
8
+ name.split('::').last.scan(/[[:upper:]][[:lower:]]*/).join('-').downcase
9
+ end
10
+
11
+ def fullname
12
+ [].tap{ |names|
13
+ klass = self
14
+ until klass.nil? or klass.basename.empty?
15
+ names << klass.basename
16
+ klass = klass.parent
17
+ end
18
+ }.reverse.join(' ')
19
+ end
20
+
21
+ def description(description = nil)
22
+ return method.description(description) if description
23
+ defined?(@description) ? @description : ''
24
+ end
25
+
26
+ def help_for_dispatch(method, subclass)
27
+ parent.help_for_dispatch(method, subclass)
28
+ end
29
+
30
+ def help_for_method(method)
31
+ parent.help_for_method(method)
32
+ end
33
+
34
+ def methods
35
+ @methods ||= Ame::Methods.new
36
+ end
37
+
38
+ def dispatch(klass, options = {})
39
+ klass.parent = self
40
+ description klass.description
41
+ options_must_precede_arguments
42
+ dispatch = method
43
+ option :help, 'Display help for this method', :ignore => true do
44
+ help_for_dispatch dispatch, klass
45
+ throw Ame::AbortAllProcessing
46
+ end unless method.options.include? :help
47
+ method.arguments.arity.zero? or
48
+ raise ArgumentError,
49
+ 'arguments may not be defined for a dispatch: %s' % klass
50
+ argument :method, 'Method to run', options.include?(:default) ?
51
+ {:optional => true, :default => options[:default]} :
52
+ {}
53
+ splat :arguments, 'Arguments to pass to METHOD', :optional => true
54
+ define_method Ame::Method.ruby_name(klass.basename) do |method, arguments|
55
+ klass.new.process method, arguments
56
+ end
57
+ end
58
+
59
+ protected
60
+
61
+ attr_accessor :parent
62
+
63
+ private
64
+
65
+ def options_must_precede_arguments
66
+ method.options_must_precede_arguments
67
+ self
68
+ end
69
+
70
+ def option(name, description, options = {}, &validate)
71
+ method.option name, description, options, &validate
72
+ self
73
+ end
74
+
75
+ def argument(name, description, options = {}, &validate)
76
+ method.argument name, description, options, &validate
77
+ self
78
+ end
79
+
80
+ def splat(name, description, options = {}, &validate)
81
+ method.splat name, description, options, &validate
82
+ self
83
+ end
84
+
85
+ def method
86
+ @method ||= Ame::Method.new(self)
87
+ end
88
+
89
+ def method_added(name)
90
+ if name == :initialize
91
+ @description = method.define(name).description
92
+ elsif [:process, :call].include? name
93
+ method.valid? and
94
+ raise NameError, 'method name reserved by Ame: %s' % name
95
+ elsif public_method_defined? name
96
+ methods << method.define(name)
97
+ elsif method.valid?
98
+ raise ArgumentError, 'non-public method cannot be used by Ame: %s' % name
99
+ end
100
+ @method = Ame::Method.new(self)
101
+ end
102
+ end
103
+
104
+ def process(name, arguments = [])
105
+ catch Ame::AbortProcessing do
106
+ self.class.methods[name].process self, arguments
107
+ end
108
+ self
109
+ end
110
+
111
+ def call(name, arguments = nil, options = nil)
112
+ catch Ame::AbortProcessing do
113
+ self.class.methods[name].call self, arguments, options
114
+ end
115
+ self
116
+ end
117
+ end
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Ame::Help
4
+ autoload :Console, 'ame/help/console'
5
+ end
@@ -0,0 +1,96 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ class Ame::Help::Console
4
+ def initialize(io = $stdout, exit_on_error = true)
5
+ @io, @exit_on_error = io, exit_on_error
6
+ end
7
+
8
+ def for_dispatch(method, subclass)
9
+ @io.puts for_method_s(method).tap{ |result|
10
+ append_group result, 'Methods', :method, subclass.methods.sort_by{ |m| method(m) }
11
+ }
12
+ end
13
+
14
+ def for_method(method)
15
+ @io.puts for_method_s(method)
16
+ end
17
+
18
+ def version(klass, method)
19
+ @io.puts '%s %s' % [method.name, klass.const_get(:Version)]
20
+ end
21
+
22
+ def for_error(method, error)
23
+ (@io == $stdout ? $stderr : @io).puts '%s: %s' % [method, error]
24
+ if @exit_on_error
25
+ exit 1
26
+ else
27
+ raise error
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def for_method_s(method)
34
+ ['Usage:'].tap{ |result|
35
+ append result, ' ', method.qualified_name
36
+ append result, ' ', options_usage(method.options)
37
+ append result, ' ', arguments_usage(method.arguments)
38
+ result << "\n"
39
+ append result, ' ', method.description
40
+ append_group result, 'Arguments', :argument, method.arguments
41
+ append_group result, 'Options', :option, method.options.sort_by{ |o| (o.short or o.long).to_s }
42
+ }.join('')
43
+ end
44
+
45
+ def append(result, prefix, string)
46
+ result << prefix << string unless string.empty?
47
+ end
48
+
49
+ def append_group(result, heading, display, objects)
50
+ longest = objects.map{ |o| send(display, o).length }.max
51
+ append result, "\n\n%s:\n" % heading,
52
+ objects.map{ |o| ' %-*s %s' % [longest, send(display, o), o.description] }.join("\n")
53
+ end
54
+
55
+ def options_usage(options)
56
+ options.count > 0 ? '[OPTIONS]...' : ''
57
+ end
58
+
59
+ def arguments_usage(arguments)
60
+ arguments.map{ |a|
61
+ if a.optional? and a.arity < 0 then '[%s]...'
62
+ elsif a.optional? then '[%s]'
63
+ elsif a.arity < 0 then '%s...'
64
+ else '%s'
65
+ end % a
66
+ }.join(' ')
67
+ end
68
+
69
+ def argument(argument)
70
+ result = argument.to_s
71
+ result << '=%s' % argument.default if argument.default
72
+ result = '[%s]' % result if argument.optional?
73
+ result << '...' if argument.arity < 0
74
+ result
75
+ end
76
+
77
+ def option(option)
78
+ if not option.long and option.argument_name.empty?
79
+ '-%s' % option.short
80
+ elsif not option.long
81
+ '-%s=%s' % [option.short, option.argument_name.upcase]
82
+ elsif option.short and option.argument_name.empty?
83
+ '-%s, --%s' % [option.short, option.long]
84
+ elsif option.short
85
+ '-%s, --%s=%s' % [option.short, option.long, option.argument_name.upcase]
86
+ elsif option.argument_name.empty?
87
+ ' --%s' % option.long
88
+ else
89
+ ' --%s=%s' % [option.long, option.argument_name.upcase]
90
+ end
91
+ end
92
+
93
+ def method(method)
94
+ method.name.to_s
95
+ end
96
+ end
@@ -0,0 +1,94 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ class Ame::Method
4
+ class << self
5
+ def ruby_name(name)
6
+ name.to_s.gsub('-', '_').to_sym
7
+ end
8
+
9
+ def name(name)
10
+ name.to_s.gsub('_', '-').to_sym
11
+ end
12
+ end
13
+
14
+ def initialize(klass)
15
+ @class = klass
16
+ @description = nil
17
+ end
18
+
19
+ def description(description = nil)
20
+ return @description unless description
21
+ @description = description
22
+ self
23
+ end
24
+
25
+ def options_must_precede_arguments
26
+ self.options.options_must_precede_arguments
27
+ self
28
+ end
29
+
30
+ def option(name, description, options = {}, &validate)
31
+ self.options.option name, description, options, &validate
32
+ self
33
+ end
34
+
35
+ def argument(name, description, options = {}, &validate)
36
+ arguments.argument name, description, options, &validate
37
+ self
38
+ end
39
+
40
+ def splat(name, description, options = {}, &validate)
41
+ arguments.splat name, description, options, &validate
42
+ self
43
+ end
44
+
45
+ def arity
46
+ arguments.arity
47
+ end
48
+
49
+ def define(name)
50
+ self.name = name
51
+ option :help, 'Display help for this method', :ignore => true do
52
+ @class.help_for_method self
53
+ throw Ame::AbortAllProcessing
54
+ end unless options.include? :help
55
+ self
56
+ end
57
+
58
+ def valid?
59
+ not description.nil?
60
+ end
61
+
62
+ def process(instance, arguments)
63
+ options, remainder = self.options.process(arguments)
64
+ call(instance, self.arguments.process(options, remainder), options)
65
+ end
66
+
67
+ def call(instance, arguments = nil, options = nil)
68
+ options, remainder = self.options.process([]) unless options
69
+ arguments ||= self.arguments.process(options, [])
70
+ instance.send ruby_name, *(arguments + (options.empty? ? [] : [options]))
71
+ self
72
+ end
73
+
74
+ attr_reader :name, :ruby_name
75
+
76
+ def qualified_name
77
+ [@class.fullname, name.to_s].reject{ |n| n.empty? }.join(' ')
78
+ end
79
+
80
+ def options
81
+ @options ||= Ame::Options.new
82
+ end
83
+
84
+ def arguments
85
+ @arguments ||= Ame::Arguments.new
86
+ end
87
+
88
+ private
89
+
90
+ def name=(name)
91
+ @ruby_name = name
92
+ @name = self.class.name(name)
93
+ end
94
+ end