clive 0.8.1 → 1.0.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.
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'