slop 1.1.0 → 1.2.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.
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