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,100 +1,210 @@
1
- module Clive
2
-
3
- # @abstract Subclass and override {#initialize} and {#run} to create a
4
- # new Option class. {#to_h} can also be overriden to provide information
5
- # when building the help.
1
+ class Clive
2
+
3
+ # An option saves a value to the state or triggers a block if given when used.
4
+ # They can have a long, +--opt+, and/or short, +-o+ and can take arguments
5
+ # which can be constricted by passing various parameters.
6
6
  #
7
- # Option is the base class for switches, flags, commands, etc. It should be
8
- # used as a template for the way options (or whatever) are initialized, and
9
- # the other methods that may need implementing.
7
+ # class CLI < Clive
8
+ # VERSION = '0.0.1'
9
+ #
10
+ # desc 'Name of the person'
11
+ # opt :name, arg: '<first> [<middle>] <last>'
12
+ #
13
+ # opt :v, :version do
14
+ # puts CLI::VERSION
15
+ # end
16
+ # end
17
+ #
18
+ # You can also have boolean options, created with the +bool+ or +boolean+
19
+ # method which are simply Options with :boolean set to true. You can pass the
20
+ # option name as normal to set them to true or prepend +--no-+ to the name to
21
+ # set them to false.
22
+ #
23
+ # class CLI
24
+ # bool :auto, 'Auto regenerate person on change'
25
+ # end
10
26
  #
11
27
  class Option
12
- attr_accessor :names, :desc, :block
13
-
14
- # Create a new Option instance.
28
+
29
+ class InvalidNamesError < Error
30
+ reason '#1'
31
+ end
32
+
33
+ extend Type::Lookup
34
+
35
+ # @return [Array<Symbol>] List of names this Option can be called
36
+ attr_reader :names
37
+ # @return [Hash{Symbol=>Object}] Config options passed to {#initialize}
38
+ # using defaults when not given
39
+ attr_reader :config
40
+ # @return [Arguments] List of arguments this Option can take when ran
41
+ attr_reader :args
42
+ # @return [String] Description of the Option
43
+ attr_reader :description
44
+
45
+ # Default values to use for +config+. These are also the config options that
46
+ # an Option takes, see {#initialize} for details.
47
+ DEFAULTS = {
48
+ :boolean => false,
49
+ :group => nil,
50
+ :head => false,
51
+ :tail => false,
52
+ :runner => Clive::Option::Runner
53
+ }
54
+
55
+ # @param names [Array<Symbol>]
56
+ # Names for this option
15
57
  #
16
- # For subclasses the method call should take the form:
17
- # +initialize(names, desc, [special args], &block)+.
58
+ # @param description [String]
59
+ # Description of the option.
18
60
  #
19
- # @param names [Array[Symbol]]
20
- # An array of names the option can be invoked by.
61
+ # @param config [Hash]
62
+ # @option config [true, false] :head
63
+ # If option should be at top of help list
64
+ # @option config [true, false] :tail
65
+ # If option should be at bottom of help list
66
+ # @option config [String] :args
67
+ # Arguments that the option takes. See {Argument}.
68
+ # @option config [Type, Array[Type]] :as
69
+ # The class the argument(s) should be cast to. See {Type}.
70
+ # @option config [#match, Array[#match]] :match
71
+ # Regular expression that the argument(s) must match
72
+ # @option config [#include?, Array[#include?]] :in
73
+ # Collection that argument(s) must be in
74
+ # @option config :default
75
+ # Default value that is used if argument is not given
76
+ # @option config :group
77
+ # Name of the group this option belongs to
21
78
  #
22
- # @param desc [String]
23
- # A description of what the option does.
79
+ # @example
24
80
  #
25
- # @yield A block to run if the switch is triggered
81
+ # Option.new(
82
+ # [:N, :new],
83
+ # "Add a new thing",
84
+ # {:args => "<dir> [<size>]", :matches => [/^\//], :types => [nil, Integer]}
85
+ # )
26
86
  #
27
- def initialize(names, desc, &block)
28
- @names = names.map(&:to_s)
29
- @desc = desc
87
+ def initialize(names=[], description="", config={}, &block)
88
+ @names = names.sort_by {|i| i.to_s.size }
89
+
90
+ # @return [Symbol, nil] Short name from the names (ie. +:a+)
91
+ def @names.short
92
+ find {|i| i.to_s.size == 1 }
93
+ end
94
+
95
+ # @return [Symbol, nil] Long name from the names (ie. +:abc+)
96
+ def @names.long
97
+ find {|i| i.to_s.size > 1 }
98
+ end
99
+
100
+ @description = description
30
101
  @block = block
102
+
103
+ @args = Arguments.create( get_subhash(config, Arguments::Parser::KEYS.keys) )
104
+ @config = DEFAULTS.merge( get_subhash(config, DEFAULTS.keys) || {} )
31
105
  end
32
-
33
- # Calls the block.
34
- def run
35
- @block.call
106
+
107
+ # @return [Symbol] The longest name given
108
+ def name
109
+ @names.long || @names.short
36
110
  end
37
-
38
- # Convert the names to strings, if name is single character appends
39
- # +-+, else appends +--+.
40
- #
41
- # @param bool [Boolean] whether to add [no-] to long
42
- #
43
- # @example
44
- #
45
- # @names = ['v', 'verbose']
46
- # names_to_strings
47
- # #=> ['-v', '--verbose']
48
- #
49
- def names_to_strings(bool=false)
50
- r = []
51
- @names.each do |i|
52
- next if i.nil?
53
- if i.length == 1 # short
54
- r << "-#{i}"
55
- else # long
56
- if bool
57
- r << "--[no-]#{i}"
58
- else
59
- r << "--#{i}"
60
- end
61
- end
111
+
112
+ # @return [String] String representaion of the Option
113
+ def to_s
114
+ r = ""
115
+ r << "-#{@names.short}" if @names.short
116
+ if @names.long
117
+ r << ", " if @names.short
118
+ r << "--"
119
+ r << "[no-]" if @config[:boolean] == true
120
+ r << @names.long.to_s.gsub('_', '-')
62
121
  end
122
+
63
123
  r
64
124
  end
65
-
66
- # Tries to get the short name, if not chooses lowest alphabetically.
67
- #
68
- # @return [String] name to sort by
125
+
126
+ # @return [String]
127
+ def inspect
128
+ "#<#{self.class} #{to_s}>"
129
+ end
130
+
131
+ # @return Whether a block was given.
132
+ def block?
133
+ @block != nil
134
+ end
135
+
136
+ # Runs the Option's block with the current state and arguments passed.
69
137
  #
70
- def sort_name
71
- r = @names.sort[0]
72
- @names.each do |i|
73
- if i.length == 1
74
- r = i
138
+ # @param state [Hash] Local state for parser, this may be modified!
139
+ # @param args [Array] Arguments for the block which is run
140
+ # @param scope [Command] Scope of the state to use
141
+ # @return [Hash] the state which may have been modified!
142
+ def run(state, args=[], scope=nil)
143
+ mapped_args = if @config[:boolean] == true
144
+ [[:truth, args.first]]
145
+ else
146
+ @args.zip(args).map {|k,v| [k.name, v] }
147
+ end
148
+
149
+ if block?
150
+ if scope
151
+ state = @config[:runner]._run(mapped_args, state[scope.name], @block)
152
+ else
153
+ state = @config[:runner]._run(mapped_args, state, @block)
75
154
  end
155
+ else
156
+ state = set_state(state, args, scope)
76
157
  end
77
- r
158
+
159
+ state
78
160
  end
79
-
80
- # Compare options based on Option#sort_name
161
+
162
+ include Comparable
163
+
164
+ # Compare based on the size of {#name}, makes sure tails go to the bottom
165
+ # and heads go to the top. If both are head or tail then sorts based on the
166
+ # names.
167
+ #
168
+ # @param other [Option] Option to compare with
169
+ # @return [Integer] Either -1, 0 or 1
81
170
  def <=>(other)
82
- self.sort_name <=> other.sort_name
171
+ if (config[:tail] && !other.config[:tail]) ||
172
+ (other.config[:head] && !config[:head])
173
+ 1
174
+ elsif (other.config[:tail] && !config[:tail]) ||
175
+ (config[:head] && !other.config[:head])
176
+ -1
177
+ else
178
+ self.name.to_s <=> other.name.to_s
179
+ end
83
180
  end
84
-
85
- # @return [Hash{String=>Object}]
86
- # Returns a hash which can be passed to the help formatter.
181
+
182
+
183
+ private
184
+
185
+ # Sets a value in the state.
87
186
  #
88
- def to_h
89
- {
90
- "names" => names_to_strings,
91
- "desc" => @desc
92
- }
187
+ # @param state [#store, #[]]
188
+ # @param args [Array]
189
+ # @param scope [Symbol, nil]
190
+ def set_state(state, args, scope=nil)
191
+ args = (@args.max <= 1 ? args[0] : args)
192
+
193
+ if scope
194
+ state[scope.name].store [@names.long, @names.short].compact, args
195
+ else
196
+ state.store [@names.long, @names.short].compact, args
197
+ end
198
+
199
+ state
93
200
  end
94
-
95
- def inspect
96
- "#<#{self.class.name} [#{@names.join(', ')}]>"
201
+
202
+ # @param hash [Hash]
203
+ # @param keys [Array]
204
+ def get_subhash(hash, keys)
205
+ Hash[ hash.find_all {|k,v| keys.include?(k) } ]
97
206
  end
98
-
207
+
99
208
  end
209
+
100
210
  end
@@ -0,0 +1,163 @@
1
+ class Clive
2
+
3
+ # Methods for modifying a state object. Requires that the instance variable
4
+ # +@state+ exists and that it responds to +#fetch+, +#store+ and +#key?+.
5
+ module StateActions
6
+
7
+ # @param key [Symbol]
8
+ #
9
+ # @example
10
+ # set :some_key, 1
11
+ # opt :get_some_key do
12
+ # puts get(:some_key) #=> 1
13
+ # end
14
+ #
15
+ def get(key)
16
+ @state.fetch key
17
+ end
18
+
19
+ # @param key [Symbol]
20
+ # @param value [Object]
21
+ #
22
+ # @example
23
+ # opt :set_some_key do
24
+ # set :some_key, 1
25
+ # end
26
+ #
27
+ def set(key, value)
28
+ @state.store key, value
29
+ end
30
+
31
+ # @overload update(key, method, *args)
32
+ # Update the value for +key+ using the +method+ which is passed +args+
33
+ # @param key [Symbol]
34
+ # @param method [Symbol]
35
+ # @param args [Object]
36
+ #
37
+ # @example
38
+ # set :list, []
39
+ # opt :add, arg: '<item>' do
40
+ # update :list, :<<, item
41
+ # end
42
+ #
43
+ # @overload update(key, &block)
44
+ # Update the value for +key+ with a block
45
+ # @param key [Symbol]
46
+ #
47
+ # @example
48
+ # set :list, []
49
+ # opt :add, arg: '<item>' do
50
+ # update(:list) {|l| l << item }
51
+ # end
52
+ #
53
+ def update(*args)
54
+ if block_given?
55
+ key = args.first
56
+ set key, yield(get(key))
57
+ elsif args.size > 1
58
+ key, method = args.shift, args.shift
59
+ set key, get(key).send(method, *args)
60
+ else
61
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 2)"
62
+ end
63
+ end
64
+
65
+ # @param key [Symbol]
66
+ # @return State has +key+?
67
+ #
68
+ # @example
69
+ # # test.rb
70
+ # set :some_key, 1
71
+ # opt(:has_some_key) { puts has?(:some_key) }
72
+ # opt(:has_other_key) { puts has?(:other_key) }
73
+ #
74
+ # # ./test.rb --has-some-key #=> true
75
+ # # ./test.rb --has-other-key #=> false
76
+ #
77
+ def has?(key)
78
+ @state.key? key
79
+ end
80
+
81
+ end
82
+
83
+ class Option
84
+
85
+ # Runner is a class which is used for executing blocks given to Options and
86
+ # Commands. It allows you to inside blocks;
87
+ # - reference arguments by name (instead of using block params)
88
+ # - get values from the state hash
89
+ # - set value to the state hash
90
+ # - update values in the state hash
91
+ #
92
+ # @example Referencing Arguments by Name
93
+ #
94
+ # opt :size, args: '<height> <width>', as: [Float, Float] do # no params!
95
+ # puts "Area = #{height} * #{width} = #{height * width}"
96
+ # end
97
+ #
98
+ # @example Getting Values from State
99
+ #
100
+ # command :new, arg: '<dir>' do
101
+ #
102
+ # opt :type, in: %w(post page blog)
103
+ #
104
+ # action do
105
+ # type = has?(:type) ? get(:type) : 'page'
106
+ # puts "Creating #{type} in #{dir}!"
107
+ # end
108
+ #
109
+ # end
110
+ #
111
+ # @example Setting Values to State
112
+ #
113
+ # opt :set, arg: '<key> <value>', as: [Symbol, Object] do
114
+ # set key, value
115
+ # end
116
+ #
117
+ # @example Updating Values in State
118
+ #
119
+ # opt :modify, arg: '<key> <sym> [<args>]', as: [Symbol, Symbol, Array] do
120
+ # update key, sym, *args
121
+ # end
122
+ #
123
+ #
124
+ class Runner
125
+ class << self
126
+
127
+ # @param args [Array[Symbol,Object]]
128
+ # An array is used because with 1.8.7 a hash has unpredictable
129
+ # ordering of keys, this means an array is the only way I can be
130
+ # sure that the arguments are in order.
131
+ # @param state [Hash{Symbol=>Object}]
132
+ # @param fn [Proc]
133
+ def _run(args, state, fn)
134
+ return unless fn
135
+ # order of this doesn't matter as it will just be accessed by key
136
+ @args = Hash[args]
137
+ @state = state
138
+
139
+ if fn.arity > 0
140
+ # Remember to use the ordered array version
141
+ instance_exec(*args.map {|i| i.last }, &fn)
142
+ else
143
+ instance_exec(&fn)
144
+ end
145
+
146
+ @state
147
+ end
148
+
149
+ include Clive::StateActions
150
+
151
+ # Allows arguments passed in to be referenced directly by name.
152
+ def method_missing(sym, *args)
153
+ if @args.has_key?(sym)
154
+ @args[sym]
155
+ else
156
+ super
157
+ end
158
+ end
159
+
160
+ end
161
+ end
162
+ end
163
+ end
@@ -1,18 +1,101 @@
1
- # This should control the character output to the command line.
2
- # It will allow you to set different colours, using:
3
- #
4
- # "I'm red".red
5
- # "I'm red and bold".red.bold
6
- #
7
- #
1
+ class Clive
2
+ module Output extend self
3
+
4
+ # If the terminal width can't be determined use a sensible default value
5
+ TERMINAL_WIDTH = 80
6
+
7
+ # @param str [String] String to pad.
8
+ # @param len [Integer] Length to pad string to.
9
+ # @param with [String] String to add to +str+.
10
+ def pad(str, len, with=" ")
11
+ diff = len - str.clear_colours.size
12
+ str += with * diff unless diff < 0
13
+ str
14
+ end
15
+
16
+ # Wraps text. Each line is split by word and then a newline is inserted
17
+ # when the words exceed the allowed width. Lines after the first will
18
+ # have +left_margin+ spaces inserted.
19
+ #
20
+ # @example
21
+ #
22
+ # Clive::Output.wrap_text("Lorem ipsum dolor sit amet, consectetur
23
+ # adipisicing elit, sed do eiusmod tempor incididunt ut labore et
24
+ # dolore magna aliqua.", 4, 24)
25
+ # #=> "Lorem ipsum dolor
26
+ # # sit amet,
27
+ # # consectetur
28
+ # # adipisicing elit,
29
+ # # sed do eiusmod
30
+ # # tempor incididunt ut
31
+ # # labore et dolore
32
+ # # magna aliqua."
33
+ #
34
+ # @param str [String] Text to be wrapped
35
+ # @param left_margin [Integer] Width of space at left
36
+ # @param width [Integer] Total width of text, ie. from the left
37
+ # of the screen
38
+ def wrap_text(str, left_margin, width)
39
+ text_width = width - left_margin
40
+
41
+ words = str.split(" ")
42
+ r = [""]
43
+ i = 0
44
+
45
+ words.each do |word|
46
+ if (r[i] + word).clear_colours.size < text_width
47
+ r[i] << " " << word
48
+ else
49
+ i += 1
50
+ r[i] = word
51
+ end
52
+ end
53
+
54
+ # Clean up strings
55
+ r.map! {|i| i.strip }
56
+
57
+ ([r[0]] + r[1..-1].map {|i| l_pad(i, left_margin) }).join("\n")
58
+ end
8
59
 
9
- module Clive
10
- module Output
60
+ # @return [Integer,nil] Width of terminal window, or +nil+ if it cannot be
61
+ # determined.
62
+ # @see https://github.com/cldwalker/hirb/blob/v0.5.0/lib/hirb/util.rb#L61
63
+ def terminal_width
64
+ if (ENV['COLUMNS'] =~ /^\d+$/)
65
+ ENV['COLUMNS'].to_i
66
+ elsif (RUBY_PLATFORM =~ /java/ || (!STDIN.tty? && ENV['TERM'])) && command_exists?('tput')
67
+ `tput cols`.to_i
68
+ elsif STDIN.tty? && command_exists?('stty')
69
+ # returns 'height width'
70
+ `stty size`.scan(/\d+/).last.to_i
71
+ else
72
+ TERMINAL_WIDTH
73
+ end
74
+ rescue
75
+ TERMINAL_WIDTH
76
+ end
77
+
78
+
79
+ private
80
+
81
+ # Determines if a shell command exists by searching for it in ENV['PATH'].
82
+ # @see https://github.com/cldwalker/hirb/blob/v0.5.0/lib/hirb/util.rb#L55
83
+ def command_exists?(command)
84
+ ENV['PATH'].split(File::PATH_SEPARATOR).any? {|d| File.exists? File.join(d, command) }
85
+ end
86
+
87
+ # Same as #pad but adds to the left of +str+.
88
+ #
89
+ # @param str [String] String to pad.
90
+ # @param margin [Integer] Amount of +with+s to add to the left of +str+.
91
+ # @param with [String] String to pad with.
92
+ def l_pad(str, margin, with=" ")
93
+ (with * margin) + str
94
+ end
11
95
 
12
96
  end
13
97
  end
14
98
 
15
- # Monkey patches for colour
16
99
  class String
17
100
 
18
101
  # @example
@@ -31,8 +114,28 @@ class String
31
114
  #
32
115
  # puts "combo".blue.bold.underline.blink
33
116
  #
117
+ # @param code
118
+ # Colour code, see http://en.wikipedia.org/wiki/ANSI_escape_code#Colors
119
+ #
34
120
  def colour(code)
35
- "#{code}#{self}\e[0m"
121
+ r = "\e[#{code}m#{self}"
122
+ r << "\e[0m" unless self[-4..-1] == "\e[0m"
123
+ r
124
+ end
125
+
126
+ # Like #colour but modifies the string object.
127
+ def colour!(code)
128
+ replace self.colour(code)
129
+ end
130
+
131
+ # Remove any colour codes from a string.
132
+ def clear_colours
133
+ gsub /\e\[?\d\d{0,2}m/, ''
134
+ end
135
+
136
+ # Same as #clear_colours, but modifies string.
137
+ def clear_colours!
138
+ gsub! /\e\[?\d\d{0,2}m/, ''
36
139
  end
37
140
 
38
141
  COLOURS = {
@@ -55,19 +158,32 @@ class String
55
158
 
56
159
  ATTRIBUTES.each do |name, code|
57
160
  define_method name do
58
- colour("\e[#{code}m")
161
+ colour code
162
+ end
163
+
164
+ define_method "#{name}!" do
165
+ colour! code
59
166
  end
60
167
  end
61
168
 
62
169
  COLOURS.each do |name, code|
63
170
  define_method name do
64
- colour("\e[3#{code}m")
171
+ colour "3#{code}"
172
+ end
173
+
174
+ define_method "#{name}!" do
175
+ colour! "3#{code}"
65
176
  end
66
177
 
67
178
  define_method "#{name}_bg" do
68
- colour("\e[4#{code}m")
179
+ colour "4#{code}"
69
180
  end
70
181
 
182
+ define_method "#{name}_bg!" do
183
+ colour! "4#{code}"
184
+ end
185
+
186
+
71
187
  # Change name to grey instead of l_black
72
188
  l_name = "l_#{name}"
73
189
  if name == "black"
@@ -75,12 +191,21 @@ class String
75
191
  end
76
192
 
77
193
  define_method "#{l_name}" do
78
- colour("\e[9#{code}m")
194
+ colour "9#{code}"
195
+ end
196
+
197
+ define_method "#{l_name}!" do
198
+ colour! "9#{code}"
79
199
  end
80
200
 
81
201
  define_method "#{l_name}_bg" do
82
- colour("\e[10#{code}m")
202
+ colour "10#{code}"
203
+ end
204
+
205
+ define_method "#{l_name}_bg!" do
206
+ colour! "10#{code}"
83
207
  end
84
208
 
85
209
  end
210
+
86
211
  end