slop 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -37,8 +37,28 @@ You can also return your options as a Hash
37
37
 
38
38
  opts.to_hash #=> {'name' => 'Lee Jarvis', 'verbose' => true, 'age' => nil, 'sex' => 'male'}
39
39
 
40
+ # Symbols
41
+ opts.to_hash(true) #=> {:name => 'Lee Jarvis', :verbose => true, :age => nil, :sex => 'male'}
42
+
43
+ If you pass a block to `Slop#parse`, Slop will yield non-options as
44
+ they're found, just like
45
+ [OptionParser](http://rubydoc.info/stdlib/optparse/1.9.2/OptionParser:order)
46
+ does it.
47
+
48
+ opts = Slop.new do
49
+ on :n, :name, :optional => false
50
+ end
51
+
52
+ opts.parse do |arg|
53
+ puts arg
54
+ end
55
+
56
+ # if ARGV is `foo --name Lee bar`
57
+ foo
58
+ bar
59
+
40
60
  If you don't like the method `on` (because it sounds like the option **expects**
41
- a callback), you can use the `opt` or `option` alternatives.
61
+ a block), you can use the `opt` or `option` alternatives.
42
62
 
43
63
  on :v, :verbose
44
64
  opt :v, :verbose
@@ -5,6 +5,8 @@ class Slop
5
5
  include Enumerable
6
6
 
7
7
  class MissingArgumentError < ArgumentError; end
8
+ class InvalidArgumentError < ArgumentError; end
9
+ class InvalidOptionError < ArgumentError; end
8
10
 
9
11
  # Parses the items from a CLI format into a friendly object.
10
12
  #
@@ -21,25 +23,42 @@ class Slop
21
23
  # program.rb --verbose -n 'Emily' -a 25
22
24
  # @see Slop#banner
23
25
  # @see Slop#option
24
- def self.parse(items=ARGV, &block)
25
- slop = new(&block)
26
- slop.parse items
27
- slop
26
+ def self.parse(items=ARGV, options={}, &block)
27
+ initialize_and_parse(items, false, options, &block)
28
+ end
29
+
30
+ # Identical to {Slop.parse}, but removes parsed options from the original Array.
31
+ #
32
+ # @yield Specify available CLI arguments using Slop# methods such as Slop#banner and Slop#option
33
+ # @return [Slop] Returns an instance of Slop.
34
+ def self.parse!(items=ARGV, options={}, &block)
35
+ initialize_and_parse(items, true, options, &block)
28
36
  end
29
37
 
30
38
  attr_reader :options
31
39
  attr_writer :banner
32
40
  attr_accessor :longest_flag
33
41
 
34
- def initialize(&block)
42
+ # @param [Hash] options
43
+ # @option options [Boolean] :help Automatically add the `help` option
44
+ # @option options [Boolean] :strict Strict mode raises when a non listed
45
+ # option is found, false by default
46
+ def initialize(options={}, &block)
35
47
  @options = Options.new
36
48
  @banner = nil
37
49
  @longest_flag = 0
38
- @items = []
50
+ @strict = options[:strict]
39
51
 
40
52
  if block_given?
41
53
  block.arity == 1 ? yield(self) : instance_eval(&block)
42
54
  end
55
+
56
+ if options[:help]
57
+ on :h, :help, 'Print this help message', :tail => true do
58
+ puts help
59
+ exit
60
+ end
61
+ end
43
62
  end
44
63
 
45
64
  # Set or return banner text.
@@ -59,21 +78,15 @@ class Slop
59
78
  # Parse a list of options, leaving the original Array unchanged.
60
79
  #
61
80
  # @param items
62
- def parse(items=ARGV)
63
- @items = parse_items items, block_given?
64
-
65
- if block_given?
66
- @items.each { |item| yield item }
67
- end
68
-
69
- @items
81
+ def parse(items=ARGV, &block)
82
+ parse_items items, &block
70
83
  end
71
84
 
72
85
  # Parse a list of options, removing parsed options from the original Array.
73
86
  #
74
- # @parse items
75
- def parse!(items=ARGV)
76
- parse_items items, true
87
+ # @param items
88
+ def parse!(items=ARGV, &block)
89
+ parse_items items, true, &block
77
90
  end
78
91
 
79
92
  # Enumerable interface
@@ -169,46 +182,85 @@ class Slop
169
182
  # @see Slop#banner
170
183
  def to_s
171
184
  banner = "#{@banner}\n" if @banner
172
- (banner || '') + options.map(&:to_s).join("\n")
185
+ (banner || '') + options.to_help
173
186
  end
174
187
  alias :help :to_s
175
188
 
176
189
  private
177
190
 
178
- def parse_items(items, delete=false)
191
+ def self.initialize_and_parse(items, delete, options, &block)
192
+ if items.is_a?(Hash) && options.empty?
193
+ options = items
194
+ items = ARGV
195
+ end
196
+
197
+ slop = new(options, &block)
198
+ delete ? slop.parse!(items) : slop.parse(items)
199
+ slop
200
+ end
201
+
202
+ def parse_items(items, delete=false, &block)
179
203
  trash = []
180
204
 
181
205
  items.each do |item|
182
- flag = item.to_s.sub(/^--?/, '')
206
+ item = item.to_s
207
+ flag = item.sub(/^--?/, '')
183
208
  option = @options[flag]
184
209
 
210
+ if !option && item =~ /^-[^-]/
211
+ flag, argument = flag.split('', 2)
212
+ option = @options[flag]
213
+ end
214
+
185
215
  if option
186
- trash << item if delete
216
+ trash << item
187
217
  option.argument_value = true
188
218
 
189
219
  if option.expects_argument? || option.accepts_optional_argument?
190
- argument = items.at(items.index(item) + 1)
191
- trash << argument if delete
220
+ argument ||= items.at(items.index(item) + 1)
221
+ trash << argument
192
222
 
193
223
  if argument
224
+ check_matching_argument(option, argument)
194
225
  option.argument_value = argument
195
226
  option.callback.call option.argument_value if option.callback
196
227
  else
197
228
  option.argument_value = nil
198
- if option.accepts_optional_argument?
199
- option.callback.call nil if option.callback
200
- else
201
- raise MissingArgumentError,
202
- "'#{flag}' expects an argument, none given"
203
- end
229
+ check_optional_argument(option, flag)
204
230
  end
205
231
  elsif option.callback
206
232
  option.callback.call nil
207
233
  end
234
+ else
235
+ check_invalid_option(item, flag)
236
+ block.call(item) if block_given? && !trash.include?(item)
208
237
  end
209
238
  end
210
239
 
211
- items.delete_if { |item| trash.include? item }
240
+ items.delete_if { |item| trash.include? item } if delete
241
+ items
242
+ end
243
+
244
+ def check_matching_argument(option, argument)
245
+ if option.match && !argument.match(option.match)
246
+ raise InvalidArgumentError,
247
+ "'#{argument}' does not match #{option.match.inspect}"
248
+ end
249
+ end
250
+
251
+ def check_optional_argument(option, flag)
252
+ if option.accepts_optional_argument?
253
+ option.callback.call nil if option.callback
254
+ else
255
+ raise MissingArgumentError,
256
+ "'#{flag}' expects an argument, none given"
257
+ end
258
+ end
259
+
260
+ def check_invalid_option(item, flag)
261
+ if item[/^--?/] && @strict
262
+ raise InvalidOptionError, "Unknown option -- '#{flag}'"
263
+ end
212
264
  end
213
265
 
214
266
  def clean_options(args)
@@ -1,5 +1,8 @@
1
1
  class Slop
2
2
  class Options < Array
3
+
4
+ # @param [Boolean] symbols true to cast hash keys to symbols
5
+ # @return [Hash]
3
6
  def to_hash(symbols)
4
7
  out = {}
5
8
  each do |option|
@@ -10,8 +13,10 @@ class Slop
10
13
  out
11
14
  end
12
15
 
13
- def [](item)
14
- item = item.to_s
16
+ # @param [Object] flag
17
+ # @return [Option] the option assoiated with this flag
18
+ def [](flag)
19
+ item = flag.to_s
15
20
  if item =~ /^\d+$/
16
21
  slice item.to_i
17
22
  else
@@ -20,16 +25,51 @@ class Slop
20
25
  end
21
26
  end
22
27
  end
28
+
29
+ # @return [String]
30
+ def to_help
31
+ heads = select {|x| !x.tail }
32
+ tails = select {|x| x.tail }
33
+ (heads + tails).map(&:to_s).join("\n")
34
+ end
23
35
  end
24
36
 
25
37
  class Option
26
38
 
39
+ # @return [String, #to_s]
27
40
  attr_reader :short_flag
41
+
42
+ # @return [String, #to_s]
28
43
  attr_reader :long_flag
44
+
45
+ # @return [String]
29
46
  attr_reader :description
47
+
48
+ # @return [Proc, #call]
30
49
  attr_reader :callback
50
+
51
+ # @return [Boolean]
52
+ attr_reader :tail
53
+
54
+ # @return [Regex]
55
+ attr_reader :match
56
+
31
57
  attr_writer :argument_value
32
58
 
59
+ # @param [Slop] slop
60
+ # @param [String, #to_s] short
61
+ # @param [String, #to_s] long
62
+ # @param [String] description
63
+ # @param [Boolean] argument
64
+ # @param [Hash] options
65
+ # @option options [Boolean] :optional
66
+ # @option options [Boolean] :argument
67
+ # @option options [Object] :default
68
+ # @option options [Proc, #call] :callback
69
+ # @option options [String, #to_s] :delimiter
70
+ # @option options [Integer] :limit
71
+ # @option options [Boolean] :tail
72
+ # @option options [Regexp] :match
33
73
  def initialize(slop, short, long, description, argument, options={}, &blk)
34
74
  @slop = slop
35
75
  @short_flag = short
@@ -38,6 +78,8 @@ class Slop
38
78
  @options = options
39
79
  @expects_argument = argument
40
80
  @expects_argument = true if options[:optional] == false
81
+ @tail = options[:tail]
82
+ @match = options[:match]
41
83
 
42
84
  if @long_flag && @long_flag.size > @slop.longest_flag
43
85
  @slop.longest_flag = @long_flag.size
@@ -48,22 +90,28 @@ class Slop
48
90
  @argument_value = nil
49
91
  end
50
92
 
93
+ # @return [Boolean] true if this option expects an argument
51
94
  def expects_argument?
52
95
  @expects_argument || @options[:argument]
53
96
  end
54
97
 
98
+ # @return [Boolean] true if this option expects an optional argument
55
99
  def accepts_optional_argument?
56
100
  @options[:optional]
57
101
  end
58
102
 
103
+ # @return [String] either the long or short flag for this option
59
104
  def key
60
105
  @long_flag || @short_flag
61
106
  end
62
107
 
108
+ # @return [Object]
63
109
  def default
64
110
  @options[:default]
65
111
  end
66
112
 
113
+ # @return [Object] the argument value after it's been case
114
+ # according to the `:as` option
67
115
  def argument_value
68
116
  value = @argument_value || default
69
117
  return if value.nil?
@@ -103,5 +151,4 @@ class Slop
103
151
  "description=#{@description.inspect}>"
104
152
  end
105
153
  end
106
-
107
- end
154
+ end
@@ -1,3 +1,3 @@
1
1
  class Slop
2
- VERSION = '1.1.0'
2
+ VERSION = '1.2.0'
3
3
  end
@@ -88,4 +88,19 @@ class OptionTest < TestCase
88
88
  assert_equal 'foo', option(:f, :foo).key
89
89
  assert_equal 'b', option(:b).key
90
90
  end
91
+
92
+ test 'tail to append items to the options list when printing help' do
93
+ slop = Slop.new
94
+ slop.on :f, :foo, :tail => true
95
+ slop.on :b, :bar
96
+ assert slop.to_s.strip =~ /foo$/
97
+ end
98
+
99
+ test 'argument matching' do
100
+ slop = Slop.new
101
+ slop.on :f, :foo, true, :match => /^h/
102
+
103
+ assert_raises(Slop::InvalidArgumentError, /world/) { slop.parse %w/--foo world/ }
104
+ assert slop.parse %w/--foo hello/
105
+ end
91
106
  end
@@ -33,6 +33,15 @@ class SlopTest < TestCase
33
33
  assert_kind_of Slop, slop
34
34
  end
35
35
 
36
+ test 'automatically adding the help option' do
37
+ slop = Slop.new
38
+ assert_empty slop.options
39
+
40
+ slop = Slop.new :help => true
41
+ refute_empty slop.options
42
+ assert_equal 'Print this help message', slop.options[:help].description
43
+ end
44
+
36
45
  test 'yielding non-options when a block is passed to "parse"' do
37
46
  opts = Slop.new do
38
47
  on :name, true
@@ -42,6 +51,15 @@ class SlopTest < TestCase
42
51
  end
43
52
  end
44
53
 
54
+ test 'preserving order when yielding non-options' do
55
+ items = []
56
+
57
+ slop = Slop.new { on(:name, true) { |name| items << name } }
58
+ slop.parse(%w/foo --name bar baz/) { |value| items << value }
59
+
60
+ assert_equal %w/foo bar baz/, items
61
+ end
62
+
45
63
  test 'setting the banner' do
46
64
  slop = Slop.new
47
65
  slop.banner = "foo bar"
@@ -62,6 +80,15 @@ class SlopTest < TestCase
62
80
  assert_equal 8, slop.longest_flag
63
81
  end
64
82
 
83
+ test 'parse returning the list of arguments left after parsing' do
84
+ opts = Slop.new do
85
+ on :name, true
86
+ end
87
+
88
+ assert_equal %w/a/, opts.parse!(%w/--name lee a/)
89
+ assert_equal %w/--name lee a/, opts.parse(%w/--name lee a/)
90
+ end
91
+
65
92
  test '#parse does not remove parsed items' do
66
93
  items = %w/--foo/
67
94
  Slop.new { |opt| opt.on :foo }.parse(items)
@@ -180,4 +207,22 @@ class SlopTest < TestCase
180
207
  opts.parse %w/--name lee/
181
208
  assert_equal 'lee', name
182
209
  end
210
+
211
+ test 'strict mode' do
212
+ strict = Slop.new :strict => true
213
+ totallynotstrict = Slop.new
214
+
215
+ assert_raises(Slop::InvalidOptionError, /--foo/) { strict.parse %w/--foo/ }
216
+ assert totallynotstrict.parse %w/--foo/
217
+ end
218
+
219
+ test 'short option flag with no space between flag and argument' do
220
+ slop = Slop.new
221
+ slop.opt :p, :password, true
222
+ slop.opt :s, :shortpass, true
223
+ slop.parse %w/-p foo -sbar/
224
+
225
+ assert_equal 'foo', slop[:password]
226
+ assert_equal 'bar', slop[:shortpass]
227
+ end
183
228
  end
metadata CHANGED
@@ -1,13 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slop
3
3
  version: !ruby/object:Gem::Version
4
- hash: 19
5
4
  prerelease:
6
- segments:
7
- - 1
8
- - 1
9
- - 0
10
- version: 1.1.0
5
+ version: 1.2.0
11
6
  platform: ruby
12
7
  authors:
13
8
  - Lee Jarvis
@@ -15,7 +10,7 @@ autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
12
 
18
- date: 2011-03-17 00:00:00 +00:00
13
+ date: 2011-03-21 00:00:00 +00:00
19
14
  default_executable:
20
15
  dependencies: []
21
16
 
@@ -55,18 +50,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
55
50
  requirements:
56
51
  - - ">="
57
52
  - !ruby/object:Gem::Version
58
- hash: 3
59
- segments:
60
- - 0
61
53
  version: "0"
62
54
  required_rubygems_version: !ruby/object:Gem::Requirement
63
55
  none: false
64
56
  requirements:
65
57
  - - ">="
66
58
  - !ruby/object:Gem::Version
67
- hash: 3
68
- segments:
69
- - 0
70
59
  version: "0"
71
60
  requirements: []
72
61