slop 2.4.4 → 3.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.md +0 -15
- data/README.md +46 -206
- data/lib/slop.rb +399 -888
- data/lib/slop/commands.rb +154 -0
- data/lib/slop/option.rb +149 -0
- data/slop.gemspec +1 -1
- data/test/commands_test.rb +58 -119
- data/test/option_test.rb +65 -159
- data/test/slop_test.rb +174 -497
- metadata +7 -5
@@ -0,0 +1,154 @@
|
|
1
|
+
class Slop
|
2
|
+
class Commands
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :config, :commands
|
6
|
+
attr_writer :banner
|
7
|
+
|
8
|
+
# Create a new instance of Slop::Commands and optionally build
|
9
|
+
# Slop instances via a block. Any configuration options used in
|
10
|
+
# this method will be the default configuration options sent to
|
11
|
+
# each Slop object created.
|
12
|
+
#
|
13
|
+
# config - An optional configuration Hash.
|
14
|
+
# block - Optional block used to define commands.
|
15
|
+
def initialize(config = {}, &block)
|
16
|
+
@config = config
|
17
|
+
@commands = {}
|
18
|
+
@banner = nil
|
19
|
+
|
20
|
+
if block_given?
|
21
|
+
block.arity == 1 ? yield(self) : instance_eval(&block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Optionally set the banner for this command help output.
|
26
|
+
#
|
27
|
+
# banner - The String text to set the banner.
|
28
|
+
#
|
29
|
+
# Returns the String banner if one is set.
|
30
|
+
def banner(banner = nil)
|
31
|
+
@banner = banner if banner
|
32
|
+
@banner
|
33
|
+
end
|
34
|
+
|
35
|
+
# Add a Slop instance for a specific command.
|
36
|
+
#
|
37
|
+
# command - A String or Symbol key used to identify this command.
|
38
|
+
# config - A Hash of configuration options to pass to Slop.
|
39
|
+
# block - An optional block used to pass options to Slop.
|
40
|
+
#
|
41
|
+
# Returns the newly created Slop instance mapped to command.
|
42
|
+
def on(command, config = {}, &block)
|
43
|
+
commands[command] = Slop.new(@config.merge(config), &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add a Slop instance used when no other commands exist.
|
47
|
+
#
|
48
|
+
# config - A Hash of configuration options to pass to Slop.
|
49
|
+
# block - An optional block used to pass options to Slop.
|
50
|
+
#
|
51
|
+
# Returns the newly created Slop instance mapped to default.
|
52
|
+
def default(config = {}, &block)
|
53
|
+
commands['default'] = Slop.new(@config.merge(config), &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Add a global Slop instance.
|
57
|
+
#
|
58
|
+
# config - A Hash of configuration options to pass to Slop.
|
59
|
+
# block - An optional block used to pass options to Slop.
|
60
|
+
#
|
61
|
+
# Returns the newly created Slop instance mapped to global.
|
62
|
+
def global(config = {}, &block)
|
63
|
+
commands['global'] = Slop.new(@config.merge(config), &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Fetch the instance of Slop tied to a command.
|
67
|
+
#
|
68
|
+
# key - The String or Symbol key used to locate this command.
|
69
|
+
#
|
70
|
+
# Returns the Slop instance if this key is found, nil otherwise.
|
71
|
+
def [](key)
|
72
|
+
commands[key.to_s]
|
73
|
+
end
|
74
|
+
alias get []
|
75
|
+
|
76
|
+
# Parse a list of items.
|
77
|
+
#
|
78
|
+
# items - The Array of items to parse.
|
79
|
+
#
|
80
|
+
# Returns the original Array of items.
|
81
|
+
def parse(items = ARGV)
|
82
|
+
parse_items(items)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Enumerable interface.
|
86
|
+
def each(&block)
|
87
|
+
@commands.each(&block)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Parse a list of items, removing any options or option arguments found.
|
91
|
+
#
|
92
|
+
# items - The Array of items to parse.
|
93
|
+
#
|
94
|
+
# Returns the original Array of items with options removed.
|
95
|
+
def parse!(items = ARGV)
|
96
|
+
parse_items(items, true)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns a nested Hash with Slop options and values. See Slop#to_hash.
|
100
|
+
def to_hash
|
101
|
+
Hash[commands.map { |k, v| [k.to_sym, v.to_hash] }]
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns the help String.
|
105
|
+
def to_s
|
106
|
+
defaults = commands.delete('default')
|
107
|
+
globals = commands.delete('global')
|
108
|
+
helps = commands.reject { |_, v| v.options.none? }
|
109
|
+
helps.merge!('Global options' => globals.to_s) if globals
|
110
|
+
helps.merge!('Other options' => defaults.to_s) if defaults
|
111
|
+
banner = @banner ? "#{@banner}\n" : ""
|
112
|
+
banner + helps.map { |key, opts| " #{key}\n#{opts}" }.join("\n\n")
|
113
|
+
end
|
114
|
+
alias help to_s
|
115
|
+
|
116
|
+
# Returns the inspection String.
|
117
|
+
def inspect
|
118
|
+
"#<Slop::Commands #{config.inspect} #{commands.values.map(&:inspect)}>"
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# Parse a list of items.
|
124
|
+
#
|
125
|
+
# items - The Array of items to parse.
|
126
|
+
# bang - When true, #parse! will be called instead of #parse.
|
127
|
+
#
|
128
|
+
# Returns the Array of items (with options removed if bang == true).
|
129
|
+
def parse_items(items, bang = false)
|
130
|
+
if opts = commands[items[0].to_s]
|
131
|
+
items.shift
|
132
|
+
bang ? opts.parse!(items) : opts.parse(items)
|
133
|
+
execute_global_opts(items, bang)
|
134
|
+
else
|
135
|
+
if opts = commands['default']
|
136
|
+
bang ? opts.parse!(items) : opts.parse(items)
|
137
|
+
else
|
138
|
+
if config[:strict] && items[0]
|
139
|
+
raise InvalidCommandError, "Unknown command `#{items[0]}`"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
execute_global_opts(items, bang)
|
143
|
+
end
|
144
|
+
items
|
145
|
+
end
|
146
|
+
|
147
|
+
def execute_global_opts(items, bang)
|
148
|
+
if global_opts = commands['global']
|
149
|
+
bang ? global_opts.parse!(items) : global_opts.parse(items)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|
data/lib/slop/option.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
class Slop
|
2
|
+
class Option
|
3
|
+
|
4
|
+
# The default Hash of configuration options this class uses.
|
5
|
+
DEFAULT_OPTIONS = {
|
6
|
+
:argument => false,
|
7
|
+
:optional_argument => false,
|
8
|
+
:tail => false,
|
9
|
+
:default => nil,
|
10
|
+
:callback => nil,
|
11
|
+
:delimiter => ',',
|
12
|
+
:limit => 0,
|
13
|
+
:match => nil,
|
14
|
+
:optional => true,
|
15
|
+
:required => false,
|
16
|
+
:as => String,
|
17
|
+
:autocreated => false
|
18
|
+
}
|
19
|
+
|
20
|
+
attr_reader :short, :long, :description, :config, :types
|
21
|
+
attr_accessor :count
|
22
|
+
attr_writer :value
|
23
|
+
|
24
|
+
# Incapsulate internal option information, mainly used to store
|
25
|
+
# option specific configuration data, most of the meat of this
|
26
|
+
# class is found in the #value method.
|
27
|
+
#
|
28
|
+
# slop - The instance of Slop tied to this Option.
|
29
|
+
# short - The String or Symbol short flag.
|
30
|
+
# long - The String or Symbol long flag.
|
31
|
+
# description - The String description text.
|
32
|
+
# config - A Hash of configuration options.
|
33
|
+
# block - An optional block used as a callback.
|
34
|
+
def initialize(slop, short, long, description, config = {}, &block)
|
35
|
+
@slop = slop
|
36
|
+
@short = short
|
37
|
+
@long = long
|
38
|
+
@description = description
|
39
|
+
@config = DEFAULT_OPTIONS.merge(config)
|
40
|
+
@count = 0
|
41
|
+
@callback = block_given? ? block : config[:callback]
|
42
|
+
@value = nil
|
43
|
+
|
44
|
+
@types = {
|
45
|
+
:string => proc { |v| v.to_s },
|
46
|
+
:symbol => proc { |v| v.to_sym },
|
47
|
+
:integer => proc { |v| v.to_s.to_i },
|
48
|
+
:float => proc { |v| v.to_f },
|
49
|
+
:array => proc { |v| v.split(@config[:delimiter], @config[:limit]) },
|
50
|
+
:range => proc { |v| value_to_range(v) }
|
51
|
+
}
|
52
|
+
|
53
|
+
if long && long.size > @slop.config[:longest_flag]
|
54
|
+
@slop.config[:longest_flag] = long.size
|
55
|
+
end
|
56
|
+
|
57
|
+
@config.each_key do |key|
|
58
|
+
self.class.send(:define_method, "#{key}?") { !!@config[key] }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns true if this option expects an argument.
|
63
|
+
def expects_argument?
|
64
|
+
config[:argument] && config[:argument] != :optional
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns true if this option accepts an optional argument.
|
68
|
+
def accepts_optional_argument?
|
69
|
+
config[:optional_argument] || config[:argument] == :optional
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the String flag of this option. Preferring the long flag.
|
73
|
+
def key
|
74
|
+
long || short
|
75
|
+
end
|
76
|
+
|
77
|
+
# Call this options callback if one exists, and it responds to call().
|
78
|
+
#
|
79
|
+
# Returns nothing.
|
80
|
+
def call(*objects)
|
81
|
+
@callback.call(*objects) if @callback.respond_to?(:call)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Fetch the argument value for this option.
|
85
|
+
#
|
86
|
+
# Returns the Object once any type conversions have taken place.
|
87
|
+
def value
|
88
|
+
value = @value || config[:default]
|
89
|
+
return if value.nil?
|
90
|
+
|
91
|
+
type = config[:as]
|
92
|
+
if type.respond_to?(:call)
|
93
|
+
type.call(value)
|
94
|
+
else
|
95
|
+
if callable = types[type.to_s.downcase.to_sym]
|
96
|
+
callable.call(value)
|
97
|
+
else
|
98
|
+
value
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns the help String for this option.
|
104
|
+
def to_s
|
105
|
+
return config[:help] if config[:help].respond_to?(:to_str)
|
106
|
+
|
107
|
+
out = " "
|
108
|
+
out += short ? "-#{short}, " : ' ' * 4
|
109
|
+
|
110
|
+
if long
|
111
|
+
out += "--#{long}"
|
112
|
+
size = long.size
|
113
|
+
diff = @slop.config[:longest_flag] - size
|
114
|
+
out += " " * (diff + 6)
|
115
|
+
else
|
116
|
+
out += " " * (@slop.config[:longest_flag] + 8)
|
117
|
+
end
|
118
|
+
|
119
|
+
"#{out}#{description}"
|
120
|
+
end
|
121
|
+
alias help to_s
|
122
|
+
|
123
|
+
# Returns the String inspection text.
|
124
|
+
def inspect
|
125
|
+
"#<Slop::Option [-#{short} | --#{long}" +
|
126
|
+
"#{'=' if expects_argument?}#{'=?' if accepts_optional_argument?}]" +
|
127
|
+
" (#{description}) #{config.inspect}"
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
# Convert an object to a Range if possible. If this method is passed
|
133
|
+
# what does *not* look like a Range, but looks like an Integer of some
|
134
|
+
# sort, it will call #to_i on the Object and return the Integer
|
135
|
+
# representation.
|
136
|
+
#
|
137
|
+
# value - The Object we want to convert to a range.
|
138
|
+
#
|
139
|
+
# Returns the Range value if one could be found, else the original object.
|
140
|
+
def value_to_range(value)
|
141
|
+
if value.to_s =~ /\A(?:\-?\d+|(-?\d+?)(\.\.\.?|-|,)(-?\d+))\z/
|
142
|
+
$1 ? Range.new($1.to_i, $3.to_i, $2 == '...') : value.to_i
|
143
|
+
else
|
144
|
+
value
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
data/slop.gemspec
CHANGED
data/test/commands_test.rb
CHANGED
@@ -1,151 +1,90 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
class CommandsTest < TestCase
|
4
|
-
test 'creating commands' do
|
5
|
-
slop = Slop.new do
|
6
|
-
command :foo do on :f, :foo, 'foo option' end
|
7
|
-
command :bar do on :f, :foo; on :b, :bar, true end
|
8
|
-
end
|
9
|
-
|
10
|
-
slop.commands.each_value do |command|
|
11
|
-
assert_kind_of Slop, command
|
12
|
-
end
|
13
4
|
|
14
|
-
|
5
|
+
def setup
|
6
|
+
@commands = Slop::Commands.new do
|
7
|
+
on 'new' do
|
8
|
+
on '--force', 'Force creation'
|
9
|
+
on '--outdir=', 'Output directory'
|
10
|
+
end
|
15
11
|
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
on 'version' do
|
13
|
+
add_callback(:empty) { 'version 1' }
|
14
|
+
end
|
15
|
+
end
|
19
16
|
end
|
20
17
|
|
21
|
-
test
|
22
|
-
|
23
|
-
|
24
|
-
assert_raises(ArgumentError) { slop.command :foo }
|
18
|
+
test "it nests instances of Slop" do
|
19
|
+
assert_empty Slop::Commands.new.commands
|
20
|
+
@commands.commands.each_value { |k| assert_kind_of Slop, k }
|
25
21
|
end
|
26
22
|
|
27
|
-
test
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
23
|
+
test "accessing Slop instances via get/[]" do
|
24
|
+
assert_kind_of Slop, @commands['new']
|
25
|
+
assert_kind_of Slop, @commands[:new]
|
26
|
+
assert_nil @commands[:unknown]
|
27
|
+
assert_equal 'Force creation', @commands[:new].fetch_option(:force).description
|
32
28
|
end
|
33
29
|
|
34
|
-
test
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
30
|
+
test "to_hash" do
|
31
|
+
assert_equal({
|
32
|
+
:new => { :force => nil, :outdir => nil },
|
33
|
+
:version => {}
|
34
|
+
}, @commands.to_hash)
|
39
35
|
end
|
40
36
|
|
41
|
-
test
|
42
|
-
|
43
|
-
|
44
|
-
end
|
45
|
-
desc = slop.commands[:foo].commands[:bar].commands[:baz].options[:f].description
|
46
|
-
assert_equal 'D:', desc
|
37
|
+
test "raising on unknown commands with :strict => true" do
|
38
|
+
cmds = Slop::Commands.new(:strict => true)
|
39
|
+
assert_raises(Slop::InvalidCommandError) { cmds.parse %w( abc ) }
|
47
40
|
end
|
48
41
|
|
49
|
-
test
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
end
|
54
|
-
assert_equal 'bar', slop.commands[:foo].banner
|
55
|
-
assert_equal 'bar', slop.commands[:bar].banner
|
42
|
+
test "adding global options" do
|
43
|
+
cmds = Slop::Commands.new { global { on '--verbose' } }
|
44
|
+
cmds.parse %w( --verbose )
|
45
|
+
assert cmds[:global].verbose?
|
56
46
|
end
|
57
47
|
|
58
|
-
test
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
on_empty { inslop = true }
|
63
|
-
end
|
64
|
-
slop.parse %w//
|
65
|
-
assert inslop
|
66
|
-
refute incmd
|
67
|
-
inslop = false
|
68
|
-
slop.parse %w/foo/
|
69
|
-
assert incmd
|
70
|
-
refute inslop
|
48
|
+
test "global options are always executed" do
|
49
|
+
@commands.global { on 'foo=' }
|
50
|
+
@commands.parse %w( new --force --foo bar )
|
51
|
+
assert_equal 'bar', @commands[:global][:foo]
|
71
52
|
end
|
72
53
|
|
73
|
-
test
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
on :v, :verbose
|
78
|
-
execute { |o, a| foo = o.verbose? }
|
79
|
-
end
|
80
|
-
slop.command :bar do
|
81
|
-
on :v, :verbose
|
82
|
-
execute { |o, a| bar = o.verbose? }
|
83
|
-
end
|
84
|
-
slop.parse %w[ foo --verbose ]
|
85
|
-
|
86
|
-
assert foo
|
87
|
-
refute bar
|
54
|
+
test "default options are only executed when there's nothing else" do
|
55
|
+
@commands.default { on 'foo=' }
|
56
|
+
@commands.parse %w( new --force --foo bar )
|
57
|
+
assert_nil @commands[:default][:foo]
|
88
58
|
end
|
89
59
|
|
90
|
-
test
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
execute do |o, a|
|
95
|
-
opts = o
|
96
|
-
args = a
|
97
|
-
end
|
98
|
-
end
|
99
|
-
slop.parse %w[ foo bar baz ]
|
100
|
-
|
101
|
-
assert_equal %w[ bar baz ], args
|
102
|
-
assert_kind_of Slop, opts
|
60
|
+
test "adding default options" do
|
61
|
+
cmds = Slop::Commands.new { default { on '--verbose' } }
|
62
|
+
cmds.parse %w( --verbose )
|
63
|
+
assert cmds[:default].verbose?
|
103
64
|
end
|
104
65
|
|
105
|
-
test
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
command :bar do
|
110
|
-
execute { |o, a| args = a }
|
111
|
-
end
|
112
|
-
end
|
113
|
-
slop.parse %w[ foo bar baz ]
|
114
|
-
|
115
|
-
assert_equal %w[ baz ], args
|
66
|
+
test "on/global and default all return newly created slop instances" do
|
67
|
+
assert_kind_of Slop, @commands.on('foo')
|
68
|
+
assert_kind_of Slop, @commands.default
|
69
|
+
assert_kind_of Slop, @commands.global
|
116
70
|
end
|
117
71
|
|
118
|
-
test '
|
119
|
-
|
120
|
-
|
121
|
-
slop.command :baz, :aliases => [:lorem, :ipsum]
|
72
|
+
test "parse does nothing when there's nothing to parse" do
|
73
|
+
assert @commands.parse []
|
74
|
+
end
|
122
75
|
|
123
|
-
|
124
|
-
|
76
|
+
test "parse returns the original array of items" do
|
77
|
+
items = %w( foo bar baz )
|
78
|
+
assert_equal items, @commands.parse(items)
|
125
79
|
|
126
|
-
|
80
|
+
items = %w( new --force )
|
81
|
+
assert_equal items, @commands.parse(items)
|
127
82
|
end
|
128
83
|
|
129
|
-
test
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
slop.parse %w[ fo ]
|
134
|
-
assert foo
|
135
|
-
|
136
|
-
slop.command(:bar) { execute { bar = true } }
|
137
|
-
slop.command(:bara) { execute { bara = true } }
|
138
|
-
slop.parse %w[ bar ]
|
139
|
-
assert bar
|
140
|
-
refute bara
|
84
|
+
test "parse! removes options/arguments" do
|
85
|
+
items = %w( new --outdir foo )
|
86
|
+
@commands.parse!(items)
|
87
|
+
assert_equal [], items
|
141
88
|
end
|
142
89
|
|
143
|
-
test 'ambiguous command completion' do
|
144
|
-
io = StringIO.new
|
145
|
-
slop = Slop.new(:io => io)
|
146
|
-
slop.command :bar
|
147
|
-
slop.command :baz
|
148
|
-
slop.parse %w[ ba ]
|
149
|
-
assert_equal "Command 'ba' is ambiguous:\n bar, baz\n", io.string
|
150
|
-
end
|
151
90
|
end
|