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
@@ -0,0 +1,210 @@
1
+ class Clive
2
+ class Arguments
3
+ class Parser
4
+
5
+ # Raised when the argument string passed to {Option} is wrong.
6
+ class InvalidArgumentStringError < Error
7
+ reason 'Invalid argument string format: #1'
8
+ end
9
+
10
+ # Valid key names for creating arguments passed to Option#initialize and
11
+ # standard names to map them to.
12
+ KEYS = {
13
+ :arg => [:args],
14
+ :type => [:types, :kind, :as],
15
+ :match => [:matches],
16
+ :within => [:withins, :in],
17
+ :default => [:defaults],
18
+ :constraint => [:constraints]
19
+ }.inject({}) {|hsh, (k,v)|
20
+ (v + [k]).each {|key|
21
+ hsh[key] = k
22
+ }
23
+ hsh
24
+ }
25
+
26
+ # @param opts [Hash]
27
+ def initialize(opts)
28
+ @opts = normalise_key_names(opts, KEYS) || {}
29
+ end
30
+
31
+ # This turns the arguments string and other options into a nicely formatted
32
+ # hash.
33
+ #
34
+ # @return [Array<Hash>]
35
+ def to_a
36
+ multiple = to_arrays(@opts.dup)
37
+ args = split_into_hashes(multiple)
38
+
39
+ if @opts.has_key?(:arg)
40
+ # Parse the argument string and merge in previous options from +singles+.
41
+ args = parse_args_string(args, @opts[:arg])
42
+ end
43
+
44
+ infer_args(args)
45
+ end
46
+
47
+ # @return [Array<Argument>]
48
+ def to_args
49
+ to_a.map do |arg|
50
+ Argument.new(arg.delete(:name) || 'arg', arg)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # Infer arguments that haven't been explicitly defined by name. This allows you
57
+ # to just say "it" should be within the range +1..5+ and have an argument
58
+ # created without having to pass +:arg => '<choice>'+.
59
+ def infer_args(opts)
60
+ opts.map do |hash|
61
+ if hash.has_key?(:name)
62
+ hash
63
+ else
64
+ check = [:type, :match, :constraint, :within, :default]
65
+ if check.any? {|key| hash.has_key?(key) }
66
+ hash.merge! :name => 'arg'
67
+ end
68
+
69
+ if hash.has_key?(:default) && !hash.has_key?(:optional)
70
+ hash.merge! :optional => true
71
+ end
72
+
73
+ hash
74
+ end
75
+ end
76
+ end
77
+
78
+ # @param opts [Hash] Hash to rename keys in
79
+ # @param keys [Hash] Map of key names to desired key names
80
+ #
81
+ # @example
82
+ #
83
+ # normalise_key_names({:a => 1, :b => 2}, {:a => :b, :b => :c})
84
+ # #=> {:b => 1, :c => 2}
85
+ #
86
+ def normalise_key_names(opts, keys)
87
+ opts.inject({}) do |hsh, (k,v)|
88
+ hsh[keys[k]] = v if keys.has_key?(k)
89
+ hsh
90
+ end
91
+ end
92
+
93
+ # Adds enough of +pd+ to +obj+ to make it have #size of +max+.
94
+ #
95
+ # @param obj [#size]
96
+ # @param max [Integer]
97
+ # @param pd [Object]
98
+ # @return [Object]
99
+ def pad(obj, max, pd=nil)
100
+ i = (max - obj.size)
101
+ obj + [pd] * (i < 0 ? 0 : i)
102
+ end
103
+
104
+ # @param name [String]
105
+ # @return [String] Argument name without sqaure or angle brackets
106
+ def clean(name)
107
+ name.tr '[<>]', ''
108
+ end
109
+
110
+ # @param hash [Hash<Symbol=>Object, Symbol=>Array>]
111
+ # @return [Hash<Symbol=>Array>]
112
+ def to_arrays(hash)
113
+ # :within is weird. You will generally set it to an Array, but can use
114
+ # anything which responds to #include?. Unfortunately that includes String
115
+ # which for many reasons should be checked against. So new rules...
116
+ #
117
+ # hash[:within] = <#include?>
118
+ # #=> hash[:within] = [<#include?>]
119
+ #
120
+ # hash[:within] = [<#include?>]
121
+ # #=> hash[:within] = [<#include?>]
122
+ #
123
+ # hash[:within] = '1'..'5'
124
+ # #=> hash[:within] = ['1'..'5']
125
+ #
126
+ # hash[:type] = Integer
127
+ # hash[:within] = 1..5
128
+ # #=> hash[:within] = [1..5]
129
+ #
130
+ # hash[:type] = Integer
131
+ # hash[:within] = [1..5, nil]
132
+ # #=> hash[:within] = [1..5, nil]
133
+ #
134
+ if hash[:within].respond_to?(:[]) && hash[:within].respond_to?(:include?)
135
+ if hash[:within].all? {|o|
136
+ ([String] << hash[:type]).flatten.uniq.compact.any? {|t|
137
+ o.is_a?(t)
138
+ }
139
+ }
140
+ hash[:within] = [hash[:within]].compact
141
+
142
+ elsif hash[:within].any? {|o| o.respond_to?(:include?) }
143
+ hash[:within] = hash[:within]
144
+
145
+ else
146
+ hash[:within] = [hash[:within]].compact
147
+ end
148
+ else
149
+ hash[:within] = [hash[:within]].compact
150
+ end
151
+
152
+ # Make all the values Arrays
153
+ Hash[ hash.map {|k,v| [k, Array(v)] } ]
154
+ end
155
+
156
+ # Splits a single hash of arrays into a single array of hashes.
157
+ #
158
+ # @example
159
+ #
160
+ # hash = {:a => [:g, :h, :i], :b => [:x, :y]}
161
+ # split_into_hashes(hash)
162
+ # #=> [
163
+ # # {:a => :b, :b => :x},
164
+ # # {:a => :h, :b => :y},
165
+ # # {:a => :i}
166
+ # # ]
167
+ #
168
+ def split_into_hashes(hash)
169
+ # Find the largest Array...
170
+ max = hash.values.map(&:size).max || 0
171
+
172
+ hash.map {|k, arr| pad(arr, max).map {|i| [k, i] } }.
173
+ transpose.
174
+ map {|i| Hash[ i.reject {|a,b| b == nil || a == :arg } ] }
175
+ end
176
+
177
+ # Parses the string passed in as +:arg+. The string should have the
178
+ # following format:
179
+ #
180
+ # <arg-name> - Indicates a required argument called "arg-name"
181
+ # [...] - Can surround one or more <arg> tokens and means they are
182
+ # optional, eg. "[<optional> <and-another>]"
183
+ #
184
+ # @param hash [Hash<Symbol=>Object>]
185
+ # @param arg_str [String]
186
+ def parse_args_string(hash, arg_str)
187
+ optional = false
188
+ cancelled_optional = false
189
+
190
+ arg_str.split(' ').zip(hash).map do |arg, opts|
191
+ if cancelled_optional
192
+ optional = false
193
+ cancelled_optional = false
194
+ end
195
+
196
+ cancelled_optional = true if arg[-1..-1] == ']'
197
+
198
+ if arg[0..0] == '['
199
+ optional = true
200
+ elsif arg[0..0] != '<'
201
+ raise InvalidArgumentStringError.new(opts[:arg])
202
+ end
203
+
204
+ {:name => clean(arg), :optional => optional}.merge(opts || {})
205
+ end
206
+ end
207
+
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,189 @@
1
+ class Clive
2
+ class Base < Command
3
+
4
+ attr_reader :commands
5
+
6
+ DEFAULTS = {
7
+ :name => File.basename($0),
8
+ :formatter => Formatter::Colour.new,
9
+ :help_command => true,
10
+ :help => true
11
+ }
12
+
13
+ # These options should be copied into each {Command} that is created.
14
+ GLOBAL_OPTIONS = [:name, :formatter, :help]
15
+
16
+ # You don't need to create an instance of this, create a class extending
17
+ # Clive or call Clive.new instead.
18
+ #
19
+ # @param config [Hash] Options to set for this Base, see #run for details
20
+ # of the keys that can be passed.
21
+ def initialize(config={}, &block)
22
+ super([], config, &block)
23
+
24
+ @commands = []
25
+ @header = proc { "Usage: #{@config[:name]} [command] [options]" }
26
+ @footer = ""
27
+ @_group = nil
28
+ @config = DEFAULTS.merge(get_subhash(config, DEFAULTS.keys))
29
+
30
+ # Need to keep a state before #run is called so #set works.
31
+ @state = {}
32
+ instance_exec &block if block
33
+ end
34
+
35
+ # Runs the Clive with the args passed which defaults to +ARGV+.
36
+ #
37
+ # @param args [Array<String>]
38
+ # Command line arguments to run with
39
+ #
40
+ # @param config [Hash]
41
+ # @option config [Boolean] :help
42
+ # Whether to create a help option.
43
+ # @option config [Boolean] :help_command
44
+ # Whether to create a help command.
45
+ # @option config [Formatter] :formatter
46
+ # Help formatter to use.
47
+ # @option config [String] :name
48
+ # Name to use in headers, this is usually better than setting a header as
49
+ # commands will use this to generate their own headers for use in help.
50
+ #
51
+ def run(args=ARGV, config={})
52
+ @config = @config.merge(get_subhash(config, DEFAULTS.keys))
53
+
54
+ add_help_option
55
+ add_help_command
56
+
57
+ Clive::Parser.new(self, config).parse(args, @state)
58
+ end
59
+
60
+
61
+ # @group DSL Methods
62
+
63
+ # @overload command(*names, description=current_desc, opts={}, &block)
64
+ # Creates a new Command. @param names [Array<Symbol>] Names that the
65
+ # command can be called with. @param description [String] Description of
66
+ # the command. @param opts [Hash] Options to be passed to the new
67
+ # Command, see {Command#initialize}.
68
+ #
69
+ # @example
70
+ #
71
+ # class CLI
72
+ # desc 'Creates a new thing'
73
+ # command :create, arg: '<thing>' do
74
+ # # ...
75
+ # end
76
+ # end
77
+ #
78
+ def command(*args, &block)
79
+ ns, d, o = [], current_desc, {}
80
+ args.each do |i|
81
+ case i
82
+ when ::Symbol then ns << i
83
+ when ::String then d = i
84
+ when ::Hash then o = i
85
+ end
86
+ end
87
+ o = DEFAULTS.merge(Hash[global_opts]).merge(o)
88
+ @commands << Command.new(ns, d, o.merge({:group => @_group}), &block)
89
+ end
90
+
91
+ include Clive::StateActions
92
+
93
+ # Set configuration values for the base, as if you passed an options hash
94
+ # to #initialize.
95
+ #
96
+ # @param [Hash] See #initialize
97
+ # @example
98
+ #
99
+ # config arg: '<dir>'
100
+ #
101
+ def config(opts=nil)
102
+ if opts
103
+ @config = @config.merge(get_subhash(opts, DEFAULTS.keys))
104
+ else
105
+ @config
106
+ end
107
+ end
108
+
109
+ # @endgroup
110
+
111
+ # Finds the option or command represented by +arg+, this can the name of a command
112
+ # or an option which should include the correct number of dashes. If the option or
113
+ # command cannot be found +nil+ is returned.
114
+ #
115
+ # @param arg [String]
116
+ # @see Command#find
117
+ # @example
118
+ #
119
+ # c = Clive.new {
120
+ # command :new
121
+ # opt :v, :version
122
+ # }
123
+ #
124
+ # c.find('-v')
125
+ # #=> #<Clive::Option -v, --version>
126
+ # c.find('new')
127
+ # #=> #<Clive::Command new>
128
+ # c.find('test')
129
+ # #=> nil
130
+ #
131
+ def find(arg)
132
+ if arg[0..0] == '-'
133
+ super
134
+ else
135
+ find_command(arg.to_sym)
136
+ end
137
+ end
138
+
139
+ # Finds the command with the name given, if the command cannot be found
140
+ # returns +nil+.
141
+ #
142
+ # @param arg [Symbol, nil]
143
+ # @example
144
+ #
145
+ # c = Clive.new {
146
+ # command :new
147
+ # }
148
+ #
149
+ # c.find_command(:new)
150
+ # #=> #<Clive::Command new>
151
+ #
152
+ def find_command(arg)
153
+ @commands.find {|i| i.names.include?(arg) }
154
+ end
155
+
156
+ # Attempts to find the command with the name given, returns true if the
157
+ # command exits.
158
+ #
159
+ # @param arg [Symbol]
160
+ # @example
161
+ #
162
+ # c = Clive.new {
163
+ # command :new
164
+ # }
165
+ #
166
+ # c.has_command? :new #=> true
167
+ # c.has_command? :create #=> false
168
+ #
169
+ def has_command?(arg)
170
+ !!find_command(arg)
171
+ end
172
+
173
+ private
174
+
175
+ # Options which should be copied into each Command created.
176
+ def global_opts
177
+ @config.find_all {|k,v| GLOBAL_OPTIONS.include?(k) }
178
+ end
179
+
180
+ # Adds the help command, which accepts the name of a command to display help
181
+ # for, to this if it is wanted.
182
+ def add_help_command
183
+ if @config[:help] && @config[:help_command] && !has_command?(:help)
184
+ self.command(:help, 'Display help', :arg => '[<command>]', :tail => true)
185
+ end
186
+ end
187
+
188
+ end
189
+ end
@@ -1,510 +1,408 @@
1
- module Clive
2
-
3
- # A string which describes the command to execute
4
- # eg. git add
5
- # git pull
1
+ class Clive
2
+
3
+ # A command allows you to separate groups of commands under their own
4
+ # namespace. But it can also take arguments like an Option. Instead of
5
+ # executing the block passed to it executes the block passed to {#action}.
6
+ #
7
+ # @example
8
+ #
9
+ # class CLI < Clive
10
+ #
11
+ # command :new, arg: '<dir>' do
12
+ # # options
13
+ # bool :force
14
+ #
15
+ # action do |dir|
16
+ # # code
17
+ # end
18
+ # end
19
+ #
20
+ # end
6
21
  #
7
22
  class Command < Option
8
-
9
- attr_accessor :options, :commands
10
- attr_accessor :argv, :base
11
- attr_reader :names, :current_desc
12
- attr_reader :top_klass
13
-
14
- # Create the base Command instance. Replacement for the #initialize
15
- # overloading.
16
- #
17
- def self.setup(klass, &block)
18
- new([], "", klass, &block)
19
- end
20
-
21
- # Create a new Command instance
22
- #
23
- # @overload initialize(base, &block)
24
- # Creates a new base Command to house everything else
25
- # @param base [Boolean] whether the command is the base
26
- #
27
- # @overload initialize(names, desc, &block)
28
- # Creates a new Command as part of the base Command
29
- # @param names [Symbol] the name of the command
30
- # @param desc [String] the description of the command
31
- #
32
- # @yield A block to run, containing switches, flags and commands
33
- #
34
- def initialize(names, desc, top_klass, &block)
35
- @argv = []
36
- @names = names.map {|i| i.to_s }
37
- @top_klass = top_klass
38
- @desc = desc
39
- @commands = []
40
- @options = []
41
- @block = block
42
- @base = false
43
-
44
- if @names == [] && @desc == ""
45
- @base = true
46
- self.instance_eval(&block) if block_given?
47
- end
48
-
49
- @option_missing = Proc.new {|e| raise NoOptionError.new(e)}
50
-
51
- # Create basic header "Usage: filename [command] [options]
52
- # or "Usage: filename commandname(s) [options]
53
- @header = "Usage: #{File.basename($0, '.*')} " <<
54
- (@base ? "[command]" : @names.join(', ')) <<
55
- " [options]"
56
-
57
- @footer = nil
58
- @current_desc = ""
59
- help_formatter :default
60
-
61
- self.build_help
62
- end
63
-
64
- # @return [Array] all bools in this command
65
- def bools
66
- @options.find_all {|i| i.class == Bool}
67
- end
68
-
69
- # @return [Array] all switches in this command
70
- def switches
71
- @options.find_all {|i| i.class == Switch}
72
- end
73
-
74
- # @return [Array] all flags in this command
75
- def flags
76
- @options.find_all {|i| i.class == Flag}
23
+
24
+ # @return [Array<Option>] List of options created in the Command instance
25
+ attr_reader :options
26
+
27
+ DEFAULTS = {
28
+ :group => nil,
29
+ :head => false,
30
+ :tail => false,
31
+ :runner => Clive::Option::Runner,
32
+
33
+ # these two are copied in from Base, so will be merged over
34
+ :formatter => nil,
35
+ :help => nil,
36
+ :name => nil
37
+ }
38
+
39
+ # @param names [Array[Symbol]]
40
+ # Names that the Command can be ran with.
41
+ #
42
+ # @param desc [String]
43
+ # Description of the Command, this is shown in help and will be wrapped properly.
44
+ #
45
+ # @param config [Hash]
46
+ # @option config [Boolean] :head
47
+ # If option should be at top of help list.
48
+ # @option config [Boolean] :tail
49
+ # If option should be at bottom of help list.
50
+ # @option config [String] :group
51
+ # Name of the group this option belongs to. This is actually set when
52
+ # {Command#group} is used.
53
+ # @option config [Runner] :runner
54
+ # Class to use for running the block passed to #action. This doesn't have
55
+ # to be Option::Runner, but you probably never need to change this.
56
+ # @option config [Formatter] :formatter
57
+ # Help formatter to use for this command, defaults to top-level formatter.
58
+ # @option config [Boolean] :help
59
+ # Whether to add a '-h, --help' option to this command which displays help.
60
+ # @option config [String] :args
61
+ # Arguments that the option takes. See {Argument}.
62
+ # @option config [Type, Array[Type]] :as
63
+ # The class the argument(s) should be cast to. See {Type}.
64
+ # @option config [#match, Array[#match]] :match
65
+ # Regular expression that the argument(s) must match.
66
+ # @option config [#include?, Array[#include?]] :in
67
+ # Collection that argument(s) must be in.
68
+ # @option config [Object] :default
69
+ # Default value that is used if argument is not given.
70
+ #
71
+ def initialize(names=[], description="", config={}, &block)
72
+ @names = names
73
+ @description = description
74
+ @options = []
75
+ @_block = block
76
+
77
+ @args = Arguments.create(get_subhash(config, Arguments::Parser::KEYS))
78
+ @config = DEFAULTS.merge(get_subhash(config, DEFAULTS.keys))
79
+
80
+ # Create basic header "Usage: filename commandname(s) [options]
81
+ @header = proc { "Usage: #{@config[:name]} #{to_s} [options]" }
82
+ @footer = ""
83
+ @_group = nil
84
+
85
+ add_help_option
86
+
87
+ current_desc
77
88
  end
78
-
79
- # Run the block that was passed to find switches, flags, etc.
80
- #
81
- # This should only be called if the command has been called
82
- # as the block could contain other actions to perform only
83
- # when called.
84
- #
85
- def find
86
- return nil if @base || @block.nil?
87
- self.instance_eval(&@block)
88
- @block = nil
89
+
90
+ # @return [Symbol] Single name to use when referring specifically to this command.
91
+ # Use the first name that was passed in.
92
+ def name
93
+ names.first
89
94
  end
90
-
91
- # Gets the type of the option which corresponds with the name given
92
- #
93
- # @param name [String]
94
- # @return [Constant]
95
- #
96
- def type_is?(name)
97
- find_opt(name).class.name || Clive::Command
95
+
96
+ # @return [String]
97
+ def to_s
98
+ names.join(',')
98
99
  end
99
-
100
- def opt_type(name)
101
- case find_opt(name).class.name
102
- when "Clive::Switch"
103
- :switch
104
- when "Clive::Bool"
105
- :switch
106
- when "Clive::Flag"
107
- :flag
108
- when "Clive::Command"
109
- :command
100
+
101
+ # Runs the block that was given to {Command#initialize} within the context of the
102
+ # command. The state hash is passed (and returned) so that {#set} works outside
103
+ # of {Runner} allowing default values to be set.
104
+ #
105
+ # @param state [Hash] The newly created state for the command.
106
+ # @return [Hash] The returned hash is used for the state of the command.
107
+ def run_block(state)
108
+ if @_block
109
+ @state = state
110
+ instance_exec(&@_block)
111
+ @state
110
112
  else
111
- nil
113
+ @state = state
112
114
  end
113
115
  end
114
-
115
- # Finds the option which has the name given
116
+
117
+ # @group DSL Methods
118
+
119
+ # Set the header for {#help}.
120
+ # @param [String]
121
+ # @example
116
122
  #
117
- # @param name [String]
118
- # @return [Clive::Option]
123
+ # header 'Usage: my_app [options] [args]'
119
124
  #
120
- def find_opt(name)
121
- options.find {|i| i.names.include?(name)}
125
+ def header(val)
126
+ @header = val
122
127
  end
123
-
124
- # Checks whether the string given is the name of a Command or not
128
+
129
+ # Set the footer for {#help}.
130
+ # @param [String]
131
+ # @example
125
132
  #
126
- # @param str [String]
127
- # @return [true, false]
133
+ # footer 'For more help visit http://mysite.com/help'
128
134
  #
129
- def is_a_command?(str)
130
- find_command(str).empty?
135
+ def footer(val)
136
+ @footer = val
131
137
  end
132
-
133
- # Finds the command which has the name given
138
+
139
+ # Set configuration values for the command, as if you passed an options hash
140
+ # to #initialize.
134
141
  #
135
- # @param name [String]
136
- # @return [Clive::Command]
142
+ # @param [Hash] See #initialize
143
+ # @example
144
+ #
145
+ # config arg: '<dir>'
137
146
  #
138
- def find_command(str)
139
- commands.find {|i| i.names.include?(str)}
147
+ def config(opts=nil)
148
+ if opts
149
+ @config = @config.merge(get_subhash(opts, DEFAULTS.keys))
150
+ else
151
+ @config
152
+ end
140
153
  end
141
-
142
- # Converts the array of input from the command line into a string of tokens.
143
- # It replaces instances of the names of flags, switches and bools with the
144
- # actual option, but does not affect commands. Instead these are left as +words+.
154
+
155
+ include Clive::StateActions
156
+
157
+ # @overload option(short=nil, long=nil, description=current_desc, opts={}, &block)
158
+ # Creates a new Option in the Command. Either +short+ or +long+ must be set.
159
+ # @param short [Symbol] The short name for the option (:a would become +-a+)
160
+ # @param long [Symbol] The long name for the option (:add would become +--add+)
161
+ # @param description [String] Description of the option
162
+ # @param opts [Hash] Options to create the Option with, see {Option#initialize}
145
163
  #
146
164
  # @example
147
165
  #
148
- # array_to_tokens ['--switch', 'command', '-f', 'arg']
149
- # #=> [[:option, "switch"], [:word, "command"], [:option, "f"], [:word, "arg"]]
150
- #
151
- # @param arr [Array]
152
- # @return [Array]
153
- #
154
- def array_to_tokens(arr)
155
- result = []
156
-
157
- arr.each do |a|
158
- if a[0..1] == "--"
159
- result << [:option, a[2..-1]]
160
-
161
- elsif a[0] == "-"
162
- a[1..-1].split('').each do |i|
163
- result << [:option, i]
164
- end
165
-
166
- else
167
- result << [:word, a]
166
+ # opt :type, arg: '<size>', in: %w(small medium large) do
167
+ # case size
168
+ # when "small" then set(:size, 1)
169
+ # when "medium" then set(:size, 2)
170
+ # when "large" then set(:size, 3)
171
+ # end
172
+ # end
173
+ #
174
+ def option(*args, &block)
175
+ ns, d, o = [], current_desc, {}
176
+ args.each do |i|
177
+ case i
178
+ when ::Symbol then ns << i
179
+ when ::String then d = i
180
+ when ::Hash then o = i
168
181
  end
169
182
  end
170
-
171
- result
183
+ @options << Option.new(ns, d, ({:group => @_group}).merge(o), &block)
172
184
  end
173
-
174
- # Converts the set of tokens returned from #array_to_tokens into a tree.
175
- # This is where we determine whether a +word+ is an argument or command.
185
+ alias_method :opt, :option
186
+
187
+ # @overload boolean(short=nil, long, description=current_desc, opts={}, &block)
188
+ # Creates a new Option in the Command which responds to calls with a 'no-' prefix.
189
+ # +long+ must be set.
190
+ # @param short [Symbol] The short name for the option (:a would become +-a+)
191
+ # @param long [Symbol] The long name for the option (:add would become +--add+)
192
+ # @param description [String] Description of the option
193
+ # @param opts [Hash] Options to create the Option with, see {Option#initialize}
176
194
  #
177
195
  # @example
178
- # tokens_to_tree([[:option, "switch"], [:word, "command"],
179
- # [:option, "f"], [:word, "arg"]])
180
- # #=> [
181
- # # [:switch, #<Clive::Switch [switch]>],
182
- # # [:command, #<Clive::Command [command]>, [
183
- # # [:flag, #<Clive::Flag [f, flag]>, [
184
- # # [:arg, 'arg']
185
- # # ]]
186
- # # ]]
187
- # # ]
188
- #
189
- # @param arr [Array]
190
- # @return [Array]
191
- #
192
- def tokens_to_tree(arr)
193
- tree = []
194
- self.find
195
-
196
- l = arr.size
197
- i = 0
198
- while i < l
199
- a = arr[i]
200
-
201
- if a[0] == :word
202
-
203
- last = tree.last || []
204
-
205
- if last[0] == :flag
206
- last[2] ||= []
207
- end
208
-
209
- if command = find_command(a[1])
210
- if last[0] == :flag
211
- if last[2].size < last[1].arg_size(:mandatory)
212
- last[2] << [:arg, a[1]]
213
- else
214
- tree << [:command, command, command.tokens_to_tree(arr[i+1..-1])]
215
- i = l
216
- end
217
- else
218
- tree << [:command, command, command.tokens_to_tree(arr[i+1..-1])]
219
- i = l
220
- end
221
- else
222
- if last[0] == :flag && last[2].size < last[1].arg_size(:all)
223
- last[2] << [:arg, a[1]]
224
- else
225
- tree << [:arg, a[1]]
226
- end
227
- end
228
- else
229
- tree << [opt_type(a[1]), find_opt(a[1])]
230
- end
231
-
232
- i += 1
233
- end
234
-
235
- tree
236
- end
237
-
238
- # Traverses the tree created by #tokens_to_tree and runs the correct options.
239
- #
240
- # @param tree [Array]
241
- # @return [Array]
242
- # Any unused arguments.
243
- #
244
- def run_tree(tree)
245
- i = 0
246
- l = tree.size
247
- r = []
248
-
249
- while i < l
250
- curr = tree[i]
251
-
252
- case curr[0]
253
- when :command
254
- r << curr[1].run(curr[2])
255
-
256
- when :switch
257
- curr[1].run
258
-
259
- when :flag
260
- args = curr[2].map {|i| i[1] }
261
- if args.size < curr[1].arg_size(:mandatory)
262
- raise MissingArgument.new(curr[1].sort_name)
263
- end
264
- curr[1].run(args)
265
-
266
- when :arg
267
- r << curr[1]
196
+ #
197
+ # bool :auto, 'Auto regenerate on changes'
198
+ #
199
+ # # Usage
200
+ # # --auto sets :auto to true
201
+ # # --no-auto sets :auto to false
202
+ #
203
+ def boolean(*args, &block)
204
+ ns, d, o = [], current_desc, {}
205
+ args.each do |i|
206
+ case i
207
+ when ::Symbol then ns << i
208
+ when ::String then d = i
209
+ when ::Hash then o = i
268
210
  end
269
-
270
- i += 1
271
211
  end
272
- r.flatten
212
+ @options << Option.new(ns, d, ({:group => @_group, :boolean => true}).merge(o), &block)
273
213
  end
274
-
275
-
276
- # Parse the ARGV passed from the command line, and run
214
+ alias_method :bool, :boolean
215
+
216
+ # If an argument is given it will set the description to that, otherwise it will
217
+ # return the description for the command.
218
+ #
219
+ # @param arg [String]
220
+ # @example
277
221
  #
278
- # @param [Array] argv the command line input, usually just +ARGV+
279
- # @return [Array] any arguments that were present in the input but not used
222
+ # description 'Displays the current version'
223
+ # opt(:version) { puts $VERSION }
280
224
  #
281
- def run(argv=[])
282
- to_run = argv
283
- if @base # if not base we will have been passed the parsed tree already
284
- to_run = tokens_to_tree( array_to_tokens(argv) )
285
- end
286
- run_tree(to_run)
287
- end
288
-
289
-
290
- def to_h
291
- {
292
- 'names' => @names,
293
- 'desc' => @desc
294
- }
295
- end
296
-
297
- def method_missing(sym, *args, &block)
298
- if @top_klass.respond_to?(sym)
299
- @top_klass.send(sym, *args)
225
+ def description(arg=nil)
226
+ if arg
227
+ @_last_desc = arg
228
+ else
229
+ @description
300
230
  end
301
231
  end
302
-
303
-
304
-
305
- # @group DSL
306
-
307
- # Add a new command to +@commands+
308
- #
309
- # @overload command(name, ..., desc, &block)
310
- # Creates a new command
311
- # @param [Symbol] name the name(s) of the command, eg. +:add+ for +git add+
312
- # @param [String] desc description of the command
313
- #
314
- # @yield A block to run when the command is called, can contain switches
315
- # and flags
316
- #
317
- def command(*args, &block)
318
- @commands << Command.new(args, @current_desc, @top_klass, &block)
319
- @current_desc = ""
320
- end
321
-
322
- # Add a new switch to +@switches+
323
- # @see Switch#initialize
324
- def switch(*args, &block)
325
- @options << Switch.new(args, @current_desc, &block)
326
- @current_desc = ""
327
- end
328
232
 
329
- # Adds a new flag to +@flags+
330
- # @see Flag#initialize
331
- def flag(*args, &block)
332
- names = []
333
- arg = nil
334
- args.each do |i|
335
- if i.is_a? Symbol
336
- names << i
337
- else
338
- if i[:arg]
339
- arg = i[:arg]
340
- else
341
- arg = i[:args]
342
- end
343
- end
344
- end
345
- @options << Flag.new(names, @current_desc, arg, &block)
346
- @current_desc = ""
347
- end
348
-
349
- # Creates a boolean switch. This is done by adding two switches of
350
- # Bool type to +@switches+, one is created normally the other has
351
- # "no-" appended to the long name and has no short name.
352
- #
353
- # @see Bool#initialize
354
- def bool(*args, &block)
355
- @options << Bool.new(args, @current_desc, true, &block)
356
- @options << Bool.new(args, @current_desc, false, &block)
357
- @current_desc= ""
233
+ # Short version of {#description} which can only set.
234
+ #
235
+ # @param arg [String]
236
+ # @example
237
+ #
238
+ # desc 'Displays the current version'
239
+ # opt(:version) { puts $VERSION }
240
+ #
241
+ def desc(arg)
242
+ @_last_desc = arg
358
243
  end
359
-
360
- # Add a description for the next option in the class. Or acts as an
361
- # accessor for @desc.
244
+
245
+ # The action block is the block which will be executed with any arguments that
246
+ # are found for it. It sets +@block+ so that {Option#run} does not have to be redefined.
362
247
  #
363
248
  # @example
364
249
  #
365
- # class CLI
366
- # include Clive::Parser
250
+ # command :create, arg: '<name>', 'Creates a new project' do
251
+ # bool :bare, "Don't add boilerplate code to created files"
367
252
  #
368
- # desc 'Force build docs'
369
- # switch :force do
370
- # # code
253
+ # action do |name|
254
+ # if get(:bare)
255
+ # # write some empty files
256
+ # else
257
+ # # create some files with stuff in
258
+ # end
371
259
  # end
372
260
  # end
373
261
  #
374
- def desc(str=nil)
375
- if str
376
- @current_desc = str
377
- else
378
- @desc
379
- end
262
+ def action(&block)
263
+ @block = block
380
264
  end
381
-
382
- # Define a block to execute when the option to execute cannot be found.
265
+
266
+ # Set the group name for all options defined after it.
383
267
  #
268
+ # @param name [String]
384
269
  # @example
385
270
  #
386
- # class CLI
387
- # include Clive::Parser
271
+ # group 'Files'
272
+ # opt :move, 'Moves a file', args: '<from> <to>'
273
+ # opt :delete, 'Deletes a file', arg: '<file>'
274
+ # opt :create, 'Creates a file', arg: '<name>'
388
275
  #
389
- # option_missing do |name|
390
- # puts "#{name} couldn't be found"
391
- # end
276
+ # group 'Network'
277
+ # opt :upload, 'Uploads everything'
278
+ # opt :download, 'Downloads everyhting'
392
279
  #
393
- def option_missing(&block)
394
- @option_missing = block
395
- end
396
-
397
- # Set the header
398
- def header(val)
399
- @header = val
400
- end
401
-
402
- # Set the footer
403
- def footer(val)
404
- @footer = val
405
- end
406
-
407
- # @group Help
408
-
409
- # This actually creates a switch with "-h" and "--help" that controls
410
- # the help on this command.
411
- def build_help
412
- @options << Switch.new([:h, :help], "Display help") do
413
- puts self.help
414
- exit 0
415
- end
280
+ def group(name)
281
+ @_group = name
416
282
  end
417
283
 
418
- # Generate the summary for help, show all flags and switches, but do not
419
- # show the flags and switches within each command. Should also prepend the
420
- # header and append the footer if set.
421
- def help
422
- @formatter.format(@header, @footer, @commands, @options)
284
+ # Sugar for +group(nil)+
285
+ def end_group
286
+ group nil
423
287
  end
424
-
425
- # This allows you to define how the output from #help looks.
288
+
289
+ # @endgroup
290
+
291
+ # Finds the option represented by +arg+, this can either be the long name +--opt+
292
+ # or the short name +-o+, if the option can't be found +nil+ is returned.
426
293
  #
427
- # For this you have access to several tokens which are evaluated in an object
428
- # with the correct values, this means you are able to use #join on arrays or
429
- # prepend, etc. The variables (tokens) are:
294
+ # @param arg [String]
295
+ # @return [Option, nil]
296
+ # @example
430
297
  #
431
- # * prepend - a string of spaces as specified when #help_formatter is called
432
- # * names - an array of names for the option
433
- # * spaces - a string of spaces to align the descriptions properly
434
- # * desc - a string of the description for the option
298
+ # a = Command.new [:command] do
299
+ # bool :force
300
+ # end
435
301
  #
436
- # And for flags you have access to:
302
+ # a.find('--force')
303
+ # #=> #<Clive::Option --[no-]force>
437
304
  #
438
- # * args - an array of arguments for the flag
439
- # * options - an array of options to choose from
305
+ def find(arg)
306
+ if arg[0..1] == '--'
307
+ find_option(arg[2..-1].gsub('-', '_').to_sym)
308
+ elsif arg[0..0] == '-'
309
+ find_option(arg[1..-1].to_sym)
310
+ end
311
+ end
312
+ alias_method :[], :find
313
+
314
+ # Attempts to find the option represented by the string +arg+, returns true if
315
+ # it exists and false if not.
440
316
  #
317
+ # @param arg [String]
318
+ # @example
441
319
  #
442
- # @overload help_formatter(args, &block)
443
- # Create a new help formatter to use.
444
- # @param args [Hash]
445
- # @option args [Integer] :width Width before flexible spaces
446
- # @option args [Integer] :prepend Width of spaces to prepend with
320
+ # a = Command.new [:command] do
321
+ # bool :force
322
+ # bool :auto
323
+ # end
447
324
  #
448
- # @overload help_formatter(name)
449
- # Use an existing help formatter.
450
- # @param name [Symbol] name of the formatter (either +:default+ or +:white+)
325
+ # a.has?('--force') #=> true
326
+ # a.has?('--auto') #=> true
327
+ # a.has?('--no-auto') #=> false
328
+ # a.has?('--not-real') #=> false
451
329
  #
330
+ def has?(arg)
331
+ !!find(arg)
332
+ end
333
+
334
+ # Finds the option with the name given by +arg+, this must be in Symbol form so
335
+ # does not have a dash before it. As with {#find} if the option does not exist +nil+
336
+ # will be returned.
452
337
  #
338
+ # @param arg [Symbol]
339
+ # @return [Option, nil]
453
340
  # @example
454
- #
455
- # CLI.help_formatter do |h|
456
- #
457
- # h.switch "{prepend}{names.join(', ')} {spaces}{desc.grey}"
458
- # h.bool "{prepend}{names.join(', ')} {spaces}{desc.grey}"
459
- # h.flag "{prepend}{names.join(', ')} {args.join(' ')} {spaces}{desc.grey}"
460
- # h.command "{prepend}{names.join(', ')} {spaces}{desc.grey}"
461
341
  #
342
+ # a = Command.new [:command] do
343
+ # bool :force
462
344
  # end
463
- #
464
- #
465
- def help_formatter(*args, &block)
466
- if block_given?
467
- width = 30
468
- prepend = 4
469
-
470
- unless args.empty?
471
- args[0].each do |k,v|
472
- case k
473
- when :width
474
- width = v
475
- when :prepend
476
- prepend = v
477
- end
478
- end
479
- end
480
-
481
- @formatter = Formatter.new(width, prepend)
482
- block.call(@formatter)
483
- @formatter
484
- else
485
- case args[0]
486
- when :default
487
- help_formatter(&HELP_FORMATTERS[:default])
488
- when :white
489
- help_formatter(&HELP_FORMATTERS[:white])
345
+ #
346
+ # a.find_option(:force)
347
+ # #=> #<Clive::Option --[no-]force>
348
+ #
349
+ def find_option(arg)
350
+ @options.find {|opt| opt.names.include?(arg) }
351
+ end
352
+
353
+ # Attempts to find the option with the Symbol name given, returns true if the option
354
+ # exists and false if not.
355
+ #
356
+ # @param arg [Symbol]
357
+ def has_option?(arg)
358
+ !!find_option(arg)
359
+ end
360
+
361
+ # @see Formatter
362
+ # @return [String] Help string for this command.
363
+ def help
364
+ f = @config[:formatter]
365
+
366
+ f.header = @header.respond_to?(:call) ? @header.call : @header
367
+ f.footer = @footer.respond_to?(:call) ? @footer.call : @footer
368
+ f.commands = @commands if @commands
369
+ f.options = @options
370
+
371
+ f.to_s
372
+ end
373
+
374
+ private
375
+
376
+ # Sets a value in the state.
377
+ #
378
+ # @param state [#store, #[]]
379
+ # @param args [Array]
380
+ # @param scope [nil]
381
+ def set_state(state, args, scope=nil)
382
+ # scope will always be nil, so ignore it for Option compatibility
383
+ state[name].store :args, (@args.max <= 1 ? args[0] : args)
384
+ state
385
+ end
386
+
387
+ # Adds the '--help' option to the Command instance if it should be added.
388
+ def add_help_option
389
+ if @config[:help] && !(has_option?(:help) || has_option?(:h))
390
+ h = self # bind self so that it can be called in the block
391
+ self.option(:h, :help, "Display this help message", :tail => true) do
392
+ puts h.help
393
+ exit 0
490
394
  end
491
395
  end
492
396
  end
493
-
494
- HELP_FORMATTERS = {
495
- :default => lambda do |h|
496
- h.switch "{prepend}{names.join(', ')} {spaces}{desc.grey}"
497
- h.bool "{prepend}{names.join(', ')} {spaces}{desc.grey}"
498
- h.flag "{prepend}{names.join(', ')} {args} {spaces}{desc.grey} {options.blue.bold}"
499
- h.command "{prepend}{names.join(', ')} {spaces}{desc.grey}"
500
- end,
501
- :white => lambda do |h|
502
- h.switch "{prepend}{names.join(', ')} {spaces}{desc}"
503
- h.bool "{prepend}{names.join(', ')} {spaces}{desc}"
504
- h.flag "{prepend}{names.join(', ')} {args} {spaces}{desc} {options.bold}"
505
- h.command "{prepend}{names.join(', ')} {spaces}{desc}"
506
- end
507
- }
508
-
397
+
398
+ # @return [String]
399
+ # Returns the last description to be set with {#description}, it then clears the
400
+ # stored description so that it is not returned twice.
401
+ def current_desc
402
+ r = @_last_desc
403
+ @_last_desc = ""
404
+ r
405
+ end
406
+
509
407
  end
510
- end
408
+ end