clive 0.8.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/LICENSE +1 -1
  2. data/README.md +328 -227
  3. data/lib/clive.rb +130 -50
  4. data/lib/clive/argument.rb +170 -0
  5. data/lib/clive/arguments.rb +139 -0
  6. data/lib/clive/arguments/parser.rb +210 -0
  7. data/lib/clive/base.rb +189 -0
  8. data/lib/clive/command.rb +342 -444
  9. data/lib/clive/error.rb +66 -0
  10. data/lib/clive/formatter.rb +57 -141
  11. data/lib/clive/formatter/colour.rb +37 -0
  12. data/lib/clive/formatter/plain.rb +172 -0
  13. data/lib/clive/option.rb +185 -75
  14. data/lib/clive/option/runner.rb +163 -0
  15. data/lib/clive/output.rb +141 -16
  16. data/lib/clive/parser.rb +180 -87
  17. data/lib/clive/struct_hash.rb +109 -0
  18. data/lib/clive/type.rb +117 -0
  19. data/lib/clive/type/definitions.rb +170 -0
  20. data/lib/clive/type/lookup.rb +23 -0
  21. data/lib/clive/version.rb +3 -3
  22. data/spec/clive/a_cli_spec.rb +245 -0
  23. data/spec/clive/argument_spec.rb +148 -0
  24. data/spec/clive/arguments/parser_spec.rb +35 -0
  25. data/spec/clive/arguments_spec.rb +191 -0
  26. data/spec/clive/command_spec.rb +276 -209
  27. data/spec/clive/formatter/colour_spec.rb +129 -0
  28. data/spec/clive/formatter/plain_spec.rb +129 -0
  29. data/spec/clive/option/runner_spec.rb +92 -0
  30. data/spec/clive/option_spec.rb +149 -23
  31. data/spec/clive/output_spec.rb +86 -2
  32. data/spec/clive/parser_spec.rb +201 -81
  33. data/spec/clive/struct_hash_spec.rb +82 -0
  34. data/spec/clive/type/definitions_spec.rb +312 -0
  35. data/spec/clive/type_spec.rb +107 -0
  36. data/spec/clive_spec.rb +60 -0
  37. data/spec/extras/expectations.rb +86 -0
  38. data/spec/extras/focus.rb +22 -0
  39. data/spec/helper.rb +35 -0
  40. metadata +56 -36
  41. data/lib/clive/bool.rb +0 -67
  42. data/lib/clive/exceptions.rb +0 -54
  43. data/lib/clive/flag.rb +0 -199
  44. data/lib/clive/switch.rb +0 -31
  45. data/lib/clive/tokens.rb +0 -141
  46. data/spec/clive/bool_spec.rb +0 -54
  47. data/spec/clive/flag_spec.rb +0 -117
  48. data/spec/clive/formatter_spec.rb +0 -108
  49. data/spec/clive/switch_spec.rb +0 -14
  50. data/spec/clive/tokens_spec.rb +0 -38
  51. data/spec/shared_specs.rb +0 -16
  52. data/spec/spec_helper.rb +0 -12
@@ -1,104 +1,197 @@
1
- module Clive
2
-
3
- # A module wrapping the command line parsing of clive. In the future this
4
- # will be the only way of using clive.
5
- #
6
- # @example
7
- #
8
- # require 'clive'
9
- #
10
- # class CLI
11
- # include Clive::Parser
12
- # option_hash :opts
13
- #
14
- # switch :v, :verbose, "Run verbosely" do
15
- # opts[:verbose] = true
16
- # end
17
- # end
18
- #
19
- # CLI.parse ARGV
20
- # p CLI.opts
21
- #
22
- module Parser
23
-
24
- # When the module is included we need to keep track of the new class it
25
- # is now in and we need to create a new base command. So here instance
26
- # variables are set directly in the new class, and the class is made to
27
- # extend the methods in Parser so they are available as class methods.
28
- #
29
- def self.included(klass)
30
- klass.instance_variable_set("@klass", klass)
31
- klass.extend(self)
32
- klass.instance_variable_set "@base", Clive::Command.setup(klass)
1
+ class Clive
2
+
3
+ class Parser
4
+
5
+ class MissingArgumentError < Error
6
+ reason 'missing argument for #0, found #1, needed #2'
7
+ end
8
+
9
+ class MissingOptionError < Error
10
+ reason 'option could not be found: #0'
33
11
  end
34
-
35
- # @return [Clive::Command]
36
- # The base command to forward method calls to.
12
+
13
+ DEFAULTS = {
14
+ :state => ::Clive::StructHash
15
+ }
16
+
17
+ # @param base [Command]
37
18
  #
38
- def base; @base; end
39
-
40
- # @see Clive::Command#run
41
- def parse(argv)
42
- base.run(argv)
19
+ # @param config [Hash]
20
+ # @option config [.new, #[], #[]=, #alias] :state
21
+ # What class the state should be
22
+ def initialize(base, config)
23
+ @base = base
24
+ @config = DEFAULTS.merge(config)
43
25
  end
44
-
45
- # @see Clive::Command#flag
46
- def flag(*args, &block)
47
- base.flag(*args, &block)
26
+
27
+ # The parser should work how you expect. It allows you to put global options before and after
28
+ # a command section (if it exists, which it doesn't), so you have something like.
29
+ #
30
+ # my_app.rb [global options] ([command] [options] [args]) [g. options] [g. args] [g. options] etc.
31
+ # | global section | command section | global section
32
+ #
33
+ # Only one command can be run, if you attempt to use two the other will be caught as an argument.
34
+ #
35
+ # @param argv [Array]
36
+ # The input to parse from the command line, usually ARGV.
37
+ #
38
+ # @param pre_state [Hash]
39
+ # A pre-populated state to be used.
40
+ #
41
+ def parse(argv, pre_state)
42
+ @argv = argv
43
+ @i = 0
44
+
45
+ @state = @config[:state].new(pre_state)
46
+ @state.store :args, []
47
+
48
+ # Pull out 'help' command immediately if found
49
+ if @argv[0] == 'help'
50
+ if @argv[1]
51
+ if @base.has?(@argv[1])
52
+ command = @base.find(@argv[1])
53
+ command.run_block({})
54
+ puts command.help
55
+ else
56
+ puts "Error: command #{@argv[1]} could not be found. Try `help` to see the available commands."
57
+ end
58
+ else
59
+ puts @base.help
60
+ end
61
+ end
62
+
63
+ until ended?
64
+ # does +curr+ exist? (and also check that if it is a command a command hasn't been run yet
65
+ if @base.has?(curr) && ((@base.find(curr).kind_of?(Command) && !command_ran?) || @base.find(curr).kind_of?(Option))
66
+
67
+ found = @base.find(curr)
68
+
69
+ # is it a command?
70
+ if found.kind_of?(Command)
71
+ @command_ran = true
72
+ @state.store found.names, found.run_block(@config[:state].new)
73
+
74
+ inc
75
+ args = []
76
+
77
+ until ended?
78
+ if found.has?(curr)
79
+ run_option found.find(curr), found
80
+ else
81
+ break unless found.args.possible?(args + [curr])
82
+ args << curr
83
+ end
84
+ inc
85
+ end
86
+ dec
87
+
88
+ found.run @state, validate_arguments(found, args), found
89
+
90
+ # otherwise it is an option
91
+ else
92
+ run_option found
93
+ end
94
+
95
+ # it's a no- option
96
+ elsif curr[0..4] == '--no-' && @base.find("--#{curr[5..-1]}").config[:boolean] == true
97
+ @base.find("--#{curr[5..-1]}").run @state, [false]
98
+
99
+ # it's one (or more) short options
100
+ elsif curr[0..0] == '-' && curr.size > 2 && @base.has?("-#{curr[1..1]}")
101
+ currs = curr[1..-1].split('').map {|i| "-#{i}" }
102
+
103
+ currs.each do |c|
104
+ opt = @base.find(c)
105
+ raise MissingOptionError.new(c) unless opt
106
+
107
+ if c == currs.last
108
+ run_option opt
109
+ else
110
+ # can't take any arguments as an option is next to it
111
+ if opt.args.min > 0
112
+ raise MissingArgumentError.new(opt, [], opt.args)
113
+ else
114
+ opt.run @state, [true]
115
+ end
116
+ end
117
+ end
118
+
119
+ # otherwise it is an argument
120
+ else
121
+ @state.args << curr
122
+ end
123
+
124
+ inc
125
+ end
126
+
127
+ @state
48
128
  end
49
-
50
- # @see Clive::Command#switch
51
- def switch(*args, &block)
52
- base.switch(*args, &block)
129
+
130
+
131
+ private
132
+
133
+ def run_option(opt, within=nil)
134
+ args = opt.args.max > 0 ? do_arguments_for(opt) : [true]
135
+ opt.run @state, args, within
53
136
  end
54
-
55
- # @see Clive::Command#command
56
- def command(*args, &block)
57
- base.command(*args, &block)
137
+
138
+ # Increment the index
139
+ def inc
140
+ @i += 1
58
141
  end
59
-
60
- # @see Clive::Command#bool
61
- def bool(*args, &block)
62
- base.bool(*args, &block)
142
+
143
+ # Decrement the index
144
+ def dec
145
+ @i -= 1
63
146
  end
64
-
65
- # @see Clive::Command#desc
66
- def desc(*args)
67
- base.desc(*args)
147
+
148
+ # @return [String] The current token
149
+ def curr
150
+ @argv[@i]
68
151
  end
69
-
70
- # @see Clive::Command#help_formatter
71
- def help_formatter(*args, &block)
72
- base.help_formatter(*args, &block)
152
+
153
+ # Whether the index is at the end of the argv
154
+ def ended?
155
+ @i >= @argv.size
73
156
  end
74
-
75
- # This is a bit nicer, I think, for defining CLIs.
76
- def option_var(name, value=nil)
77
- if value
78
- @klass.class_attr_accessor name => value
79
- else
80
- @klass.class_attr_accessor name
81
- end
157
+
158
+ def command_ran?
159
+ @command_ran || false
82
160
  end
83
-
84
- # Create a new hash which is accessible to the options in the new class
85
- # but can also be accessed from outside the class. Defines getters and
86
- # setters for the symbols given, and sets their initial value to +{}+.
87
- #
88
- # @param args [Symbol]
89
- #
90
- def option_hash(*args)
91
- args.each do |arg|
92
- option_var(arg, {})
161
+
162
+ # Returns the finished argument list for +opt+ which can then be pushed to the state.
163
+ def do_arguments_for(opt)
164
+ arg_list = collect_arguments(opt)
165
+ arg_list = validate_arguments(opt, arg_list)
166
+
167
+ arg_list
168
+ end
169
+
170
+ # Collects the arguments for +opt+.
171
+ def collect_arguments(opt)
172
+ inc
173
+ arg_list = []
174
+ while !ended? && arg_list.size < opt.args.max
175
+ break unless opt.args.possible?(arg_list + [curr])
176
+ arg_list << curr
177
+ inc
93
178
  end
179
+ dec
180
+ arg_list
94
181
  end
95
-
96
- def option_array(*args)
97
- args.each do |arg|
98
- option_var(arg, [])
182
+
183
+ # Makes sure the found list of arguments is valid, if not raises
184
+ # MissingArgumentError. Returns the valid argument list with the arguments
185
+ # as the correct type, in the correct positions and with default values
186
+ # inserted if necessary.
187
+ def validate_arguments(opt, arg_list)
188
+ # If we don't have enough args
189
+ unless opt.args.valid?(arg_list)
190
+ raise MissingArgumentError.new(opt, arg_list, opt.args.to_s)
99
191
  end
192
+
193
+ opt.args.create_valid(arg_list)
100
194
  end
101
- alias_method :option_list, :option_array
102
195
 
103
196
  end
104
197
  end
@@ -0,0 +1,109 @@
1
+ class Clive
2
+
3
+ # A Struct-like Hash (or Hash-like Struct)
4
+ #
5
+ # sh = StructHash.new(:a => 1)
6
+ # sh.a #=> 1
7
+ # sh[:a] #=> 1
8
+ #
9
+ # sh.set 42, [:answer, :life]
10
+ # sh.answer #=> 42
11
+ # sh.life #=> 42
12
+ #
13
+ # sh.to_h
14
+ # #=> {:a => 1, :answer => 42}
15
+ # sh.to_struct('Thing')
16
+ # #=> #<struct Struct::Thing @a=1 @answer=42>
17
+ #
18
+ class StructHash
19
+
20
+ alias_method :__respond_to?, :respond_to?
21
+
22
+ skip_methods = %w(object_id respond_to_missing? inspect === to_s class)
23
+ instance_methods.each do |m|
24
+ undef_method m unless skip_methods.include?(m.to_s) || m =~ /^__/
25
+ end
26
+
27
+ def initialize(kvs={})
28
+ @data = kvs
29
+ @aliases = Hash[ kvs.map {|k,v| [k, k] } ]
30
+ end
31
+
32
+ # Sets a value in the StructHash, this can be set with multiple keys but the
33
+ # first will be set as the most important key, the others will not show up in
34
+ # #to_h or #to_struct.
35
+ #
36
+ # @param keys [#to_sym, Array<#to_sym>]
37
+ # @param val
38
+ def store(keys, val)
39
+ keys = Array(keys).map(&:to_sym)
40
+
41
+ keys.each do |key|
42
+ @aliases[key] = keys.first
43
+ end
44
+
45
+ @data[keys.first] = val
46
+ end
47
+
48
+ # Gets the value from the StructHash corresponding to the key given.
49
+ #
50
+ # @param key [Symbol]
51
+ def fetch(key)
52
+ @data.fetch @aliases[key]
53
+ end
54
+ alias_method :[], :fetch
55
+
56
+ # Checks whether the StructHash contains an entry for the key given.
57
+ def key?(key)
58
+ @aliases.key? key
59
+ end
60
+
61
+ # @return [Hash] The data without the +:args+ key.
62
+ def data
63
+ @data.reject {|k,v| k == :args }
64
+ end
65
+
66
+ # Returns a hash representation of the StructHash instance, using only the
67
+ # important keys. This acts recursively, so any contained StructHashes
68
+ # will have #to_h called on them.
69
+ #
70
+ # @return [Hash]
71
+ def to_h
72
+ Hash[ data.map {|k,v| v.is_a?(StructHash) ? [k, v.to_h] : [k, v] } ]
73
+ end
74
+ alias_method :to_hash, :to_h
75
+
76
+ # Returns a struct representation of the StructHash instance, using only the
77
+ # important keys. This does not modify any contained StructHash instances
78
+ # like #to_h, but leaves them as they are.
79
+ #
80
+ # @return [Struct]
81
+ def to_struct(name=nil)
82
+ Struct.new(name, *data.keys).new *data.values
83
+ end
84
+
85
+ # Checks whether the method corresponds to a key, if so gets the value.
86
+ # Checks whether the method ends with '?', then checks if the key exists.
87
+ # Otherwise calls super.
88
+ def method_missing(sym, *args, &block)
89
+ if key?(sym)
90
+ fetch sym
91
+ elsif sym.to_s[-1..-1] == "?"
92
+ key? sym.to_s[0..-2].to_sym
93
+ else
94
+ super sym, *args, &block
95
+ end
96
+ end
97
+
98
+ def respond_to?(sym, include_private=false)
99
+ return true if key?(sym)
100
+ return true if sym.to_s[-1..-1] == "?" && key?(sym.to_s[0..-2].to_sym)
101
+ return __respond_to?(sym, include_private)
102
+ end
103
+
104
+ def ==(other)
105
+ to_h == other.respond_to?(:to_h) ? other.to_h : other
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,117 @@
1
+ class Clive
2
+ class Type
3
+
4
+ # @param arg [::String]
5
+ def valid?(arg)
6
+ false
7
+ end
8
+
9
+ # @param arg [::String]
10
+ def typecast(arg)
11
+ nil
12
+ end
13
+
14
+ class << self
15
+
16
+ # Find the class for +name+.
17
+ # @param name [::String]
18
+ def find_class(name)
19
+ name = name.split('::').last
20
+ Clive::Type.const_get(name) if Clive::Type.const_defined?(name)
21
+ end
22
+
23
+ # Shorthand to define #valid? for subclasses of {Type}, pass a
24
+ # regular expression that should be matched or a symbol for a
25
+ # method which will be called on the argument that returns either
26
+ # +true+ (valid) or +false+ (invalid).
27
+ #
28
+ # @param other [#to_proc, ::Regexp]
29
+ #
30
+ # @example With a regular expression
31
+ #
32
+ # class YesNo < Type
33
+ # match /yes|no/
34
+ # # ...
35
+ # end
36
+ #
37
+ # @example With a method symbol
38
+ #
39
+ # class String
40
+ # def five?
41
+ # size == 5
42
+ # end
43
+ # end
44
+ #
45
+ # class FiveChars < Type
46
+ # match :five?
47
+ # # ...
48
+ # end
49
+ #
50
+ def match(other)
51
+ if other.respond_to?(:to_proc)
52
+ @valid = other.to_proc
53
+ else
54
+ @valid = proc {|arg| other =~ arg.to_s }
55
+ end
56
+ end
57
+
58
+ # Similar to {.match} but opposite, so where {.match} would be valid
59
+ # refute is invalid.
60
+ #
61
+ # @param other [#to_proc, ::Regexp]
62
+ def refute(other)
63
+ if other.respond_to?(:to_proc)
64
+ @valid = proc {|arg| !arg.send(other) }
65
+ else
66
+ @valid = proc {|arg| other !~ arg.to_s }
67
+ end
68
+ end
69
+
70
+ # Shorthand to define a method which is called on the string argument
71
+ # to return the correct type.
72
+ #
73
+ # @param sym [::Symbol]
74
+ #
75
+ # @example
76
+ #
77
+ # class Symbol < Type
78
+ # # ...
79
+ # cast :to_sym
80
+ # end
81
+ #
82
+ def cast(sym, *args)
83
+ @cast = [sym, args]
84
+ end
85
+
86
+ # Checks whether the +arg+ passed is valid, if {.match} or {.refute}
87
+ # have been called it uses the Proc created by them otherwise calls
88
+ # {#valid?}.
89
+ #
90
+ # @param arg [::String]
91
+ def valid?(arg)
92
+ if @valid
93
+ @valid.call arg
94
+ else
95
+ new.valid? arg
96
+ end
97
+ end
98
+
99
+ # Casts the +arg+ to the correct type, if {.cast} has been called it
100
+ # uses the proc created otherwise it calls {#typecast}.
101
+ #
102
+ # @param arg [::String]
103
+ def typecast(arg)
104
+ if @cast
105
+ arg.send @cast[0], *@cast[1]
106
+ else
107
+ new.typecast arg
108
+ end
109
+ end
110
+
111
+ end
112
+
113
+ end
114
+ end
115
+
116
+ require 'clive/type/definitions'
117
+ require 'clive/type/lookup'