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
@@ -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