ame 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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