clive 0.8.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/LICENSE +1 -1
  2. data/README.md +328 -227
  3. data/lib/clive.rb +130 -50
  4. data/lib/clive/argument.rb +170 -0
  5. data/lib/clive/arguments.rb +139 -0
  6. data/lib/clive/arguments/parser.rb +210 -0
  7. data/lib/clive/base.rb +189 -0
  8. data/lib/clive/command.rb +342 -444
  9. data/lib/clive/error.rb +66 -0
  10. data/lib/clive/formatter.rb +57 -141
  11. data/lib/clive/formatter/colour.rb +37 -0
  12. data/lib/clive/formatter/plain.rb +172 -0
  13. data/lib/clive/option.rb +185 -75
  14. data/lib/clive/option/runner.rb +163 -0
  15. data/lib/clive/output.rb +141 -16
  16. data/lib/clive/parser.rb +180 -87
  17. data/lib/clive/struct_hash.rb +109 -0
  18. data/lib/clive/type.rb +117 -0
  19. data/lib/clive/type/definitions.rb +170 -0
  20. data/lib/clive/type/lookup.rb +23 -0
  21. data/lib/clive/version.rb +3 -3
  22. data/spec/clive/a_cli_spec.rb +245 -0
  23. data/spec/clive/argument_spec.rb +148 -0
  24. data/spec/clive/arguments/parser_spec.rb +35 -0
  25. data/spec/clive/arguments_spec.rb +191 -0
  26. data/spec/clive/command_spec.rb +276 -209
  27. data/spec/clive/formatter/colour_spec.rb +129 -0
  28. data/spec/clive/formatter/plain_spec.rb +129 -0
  29. data/spec/clive/option/runner_spec.rb +92 -0
  30. data/spec/clive/option_spec.rb +149 -23
  31. data/spec/clive/output_spec.rb +86 -2
  32. data/spec/clive/parser_spec.rb +201 -81
  33. data/spec/clive/struct_hash_spec.rb +82 -0
  34. data/spec/clive/type/definitions_spec.rb +312 -0
  35. data/spec/clive/type_spec.rb +107 -0
  36. data/spec/clive_spec.rb +60 -0
  37. data/spec/extras/expectations.rb +86 -0
  38. data/spec/extras/focus.rb +22 -0
  39. data/spec/helper.rb +35 -0
  40. metadata +56 -36
  41. data/lib/clive/bool.rb +0 -67
  42. data/lib/clive/exceptions.rb +0 -54
  43. data/lib/clive/flag.rb +0 -199
  44. data/lib/clive/switch.rb +0 -31
  45. data/lib/clive/tokens.rb +0 -141
  46. data/spec/clive/bool_spec.rb +0 -54
  47. data/spec/clive/flag_spec.rb +0 -117
  48. data/spec/clive/formatter_spec.rb +0 -108
  49. data/spec/clive/switch_spec.rb +0 -14
  50. data/spec/clive/tokens_spec.rb +0 -38
  51. data/spec/shared_specs.rb +0 -16
  52. data/spec/spec_helper.rb +0 -12
@@ -1,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