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.
- data/LICENSE +1 -1
- data/README.md +328 -227
- data/lib/clive.rb +130 -50
- data/lib/clive/argument.rb +170 -0
- data/lib/clive/arguments.rb +139 -0
- data/lib/clive/arguments/parser.rb +210 -0
- data/lib/clive/base.rb +189 -0
- data/lib/clive/command.rb +342 -444
- data/lib/clive/error.rb +66 -0
- data/lib/clive/formatter.rb +57 -141
- data/lib/clive/formatter/colour.rb +37 -0
- data/lib/clive/formatter/plain.rb +172 -0
- data/lib/clive/option.rb +185 -75
- data/lib/clive/option/runner.rb +163 -0
- data/lib/clive/output.rb +141 -16
- data/lib/clive/parser.rb +180 -87
- data/lib/clive/struct_hash.rb +109 -0
- data/lib/clive/type.rb +117 -0
- data/lib/clive/type/definitions.rb +170 -0
- data/lib/clive/type/lookup.rb +23 -0
- data/lib/clive/version.rb +3 -3
- data/spec/clive/a_cli_spec.rb +245 -0
- data/spec/clive/argument_spec.rb +148 -0
- data/spec/clive/arguments/parser_spec.rb +35 -0
- data/spec/clive/arguments_spec.rb +191 -0
- data/spec/clive/command_spec.rb +276 -209
- data/spec/clive/formatter/colour_spec.rb +129 -0
- data/spec/clive/formatter/plain_spec.rb +129 -0
- data/spec/clive/option/runner_spec.rb +92 -0
- data/spec/clive/option_spec.rb +149 -23
- data/spec/clive/output_spec.rb +86 -2
- data/spec/clive/parser_spec.rb +201 -81
- data/spec/clive/struct_hash_spec.rb +82 -0
- data/spec/clive/type/definitions_spec.rb +312 -0
- data/spec/clive/type_spec.rb +107 -0
- data/spec/clive_spec.rb +60 -0
- data/spec/extras/expectations.rb +86 -0
- data/spec/extras/focus.rb +22 -0
- data/spec/helper.rb +35 -0
- metadata +56 -36
- data/lib/clive/bool.rb +0 -67
- data/lib/clive/exceptions.rb +0 -54
- data/lib/clive/flag.rb +0 -199
- data/lib/clive/switch.rb +0 -31
- data/lib/clive/tokens.rb +0 -141
- data/spec/clive/bool_spec.rb +0 -54
- data/spec/clive/flag_spec.rb +0 -117
- data/spec/clive/formatter_spec.rb +0 -108
- data/spec/clive/switch_spec.rb +0 -14
- data/spec/clive/tokens_spec.rb +0 -38
- data/spec/shared_specs.rb +0 -16
- data/spec/spec_helper.rb +0 -12
data/lib/clive/parser.rb
CHANGED
@@ -1,104 +1,197 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
# include Clive::Parser
|
12
|
-
# option_hash :opts
|
13
|
-
#
|
14
|
-
# switch :v, :verbose, "Run verbosely" do
|
15
|
-
# opts[:verbose] = true
|
16
|
-
# end
|
17
|
-
# end
|
18
|
-
#
|
19
|
-
# CLI.parse ARGV
|
20
|
-
# p CLI.opts
|
21
|
-
#
|
22
|
-
module Parser
|
23
|
-
|
24
|
-
# When the module is included we need to keep track of the new class it
|
25
|
-
# is now in and we need to create a new base command. So here instance
|
26
|
-
# variables are set directly in the new class, and the class is made to
|
27
|
-
# extend the methods in Parser so they are available as class methods.
|
28
|
-
#
|
29
|
-
def self.included(klass)
|
30
|
-
klass.instance_variable_set("@klass", klass)
|
31
|
-
klass.extend(self)
|
32
|
-
klass.instance_variable_set "@base", Clive::Command.setup(klass)
|
1
|
+
class Clive
|
2
|
+
|
3
|
+
class Parser
|
4
|
+
|
5
|
+
class MissingArgumentError < Error
|
6
|
+
reason 'missing argument for #0, found #1, needed #2'
|
7
|
+
end
|
8
|
+
|
9
|
+
class MissingOptionError < Error
|
10
|
+
reason 'option could not be found: #0'
|
33
11
|
end
|
34
|
-
|
35
|
-
|
36
|
-
|
12
|
+
|
13
|
+
DEFAULTS = {
|
14
|
+
:state => ::Clive::StructHash
|
15
|
+
}
|
16
|
+
|
17
|
+
# @param base [Command]
|
37
18
|
#
|
38
|
-
|
39
|
-
|
40
|
-
#
|
41
|
-
def
|
42
|
-
base
|
19
|
+
# @param config [Hash]
|
20
|
+
# @option config [.new, #[], #[]=, #alias] :state
|
21
|
+
# What class the state should be
|
22
|
+
def initialize(base, config)
|
23
|
+
@base = base
|
24
|
+
@config = DEFAULTS.merge(config)
|
43
25
|
end
|
44
|
-
|
45
|
-
#
|
46
|
-
|
47
|
-
|
26
|
+
|
27
|
+
# The parser should work how you expect. It allows you to put global options before and after
|
28
|
+
# a command section (if it exists, which it doesn't), so you have something like.
|
29
|
+
#
|
30
|
+
# my_app.rb [global options] ([command] [options] [args]) [g. options] [g. args] [g. options] etc.
|
31
|
+
# | global section | command section | global section
|
32
|
+
#
|
33
|
+
# Only one command can be run, if you attempt to use two the other will be caught as an argument.
|
34
|
+
#
|
35
|
+
# @param argv [Array]
|
36
|
+
# The input to parse from the command line, usually ARGV.
|
37
|
+
#
|
38
|
+
# @param pre_state [Hash]
|
39
|
+
# A pre-populated state to be used.
|
40
|
+
#
|
41
|
+
def parse(argv, pre_state)
|
42
|
+
@argv = argv
|
43
|
+
@i = 0
|
44
|
+
|
45
|
+
@state = @config[:state].new(pre_state)
|
46
|
+
@state.store :args, []
|
47
|
+
|
48
|
+
# Pull out 'help' command immediately if found
|
49
|
+
if @argv[0] == 'help'
|
50
|
+
if @argv[1]
|
51
|
+
if @base.has?(@argv[1])
|
52
|
+
command = @base.find(@argv[1])
|
53
|
+
command.run_block({})
|
54
|
+
puts command.help
|
55
|
+
else
|
56
|
+
puts "Error: command #{@argv[1]} could not be found. Try `help` to see the available commands."
|
57
|
+
end
|
58
|
+
else
|
59
|
+
puts @base.help
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
until ended?
|
64
|
+
# does +curr+ exist? (and also check that if it is a command a command hasn't been run yet
|
65
|
+
if @base.has?(curr) && ((@base.find(curr).kind_of?(Command) && !command_ran?) || @base.find(curr).kind_of?(Option))
|
66
|
+
|
67
|
+
found = @base.find(curr)
|
68
|
+
|
69
|
+
# is it a command?
|
70
|
+
if found.kind_of?(Command)
|
71
|
+
@command_ran = true
|
72
|
+
@state.store found.names, found.run_block(@config[:state].new)
|
73
|
+
|
74
|
+
inc
|
75
|
+
args = []
|
76
|
+
|
77
|
+
until ended?
|
78
|
+
if found.has?(curr)
|
79
|
+
run_option found.find(curr), found
|
80
|
+
else
|
81
|
+
break unless found.args.possible?(args + [curr])
|
82
|
+
args << curr
|
83
|
+
end
|
84
|
+
inc
|
85
|
+
end
|
86
|
+
dec
|
87
|
+
|
88
|
+
found.run @state, validate_arguments(found, args), found
|
89
|
+
|
90
|
+
# otherwise it is an option
|
91
|
+
else
|
92
|
+
run_option found
|
93
|
+
end
|
94
|
+
|
95
|
+
# it's a no- option
|
96
|
+
elsif curr[0..4] == '--no-' && @base.find("--#{curr[5..-1]}").config[:boolean] == true
|
97
|
+
@base.find("--#{curr[5..-1]}").run @state, [false]
|
98
|
+
|
99
|
+
# it's one (or more) short options
|
100
|
+
elsif curr[0..0] == '-' && curr.size > 2 && @base.has?("-#{curr[1..1]}")
|
101
|
+
currs = curr[1..-1].split('').map {|i| "-#{i}" }
|
102
|
+
|
103
|
+
currs.each do |c|
|
104
|
+
opt = @base.find(c)
|
105
|
+
raise MissingOptionError.new(c) unless opt
|
106
|
+
|
107
|
+
if c == currs.last
|
108
|
+
run_option opt
|
109
|
+
else
|
110
|
+
# can't take any arguments as an option is next to it
|
111
|
+
if opt.args.min > 0
|
112
|
+
raise MissingArgumentError.new(opt, [], opt.args)
|
113
|
+
else
|
114
|
+
opt.run @state, [true]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# otherwise it is an argument
|
120
|
+
else
|
121
|
+
@state.args << curr
|
122
|
+
end
|
123
|
+
|
124
|
+
inc
|
125
|
+
end
|
126
|
+
|
127
|
+
@state
|
48
128
|
end
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
129
|
+
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def run_option(opt, within=nil)
|
134
|
+
args = opt.args.max > 0 ? do_arguments_for(opt) : [true]
|
135
|
+
opt.run @state, args, within
|
53
136
|
end
|
54
|
-
|
55
|
-
#
|
56
|
-
def
|
57
|
-
|
137
|
+
|
138
|
+
# Increment the index
|
139
|
+
def inc
|
140
|
+
@i += 1
|
58
141
|
end
|
59
|
-
|
60
|
-
#
|
61
|
-
def
|
62
|
-
|
142
|
+
|
143
|
+
# Decrement the index
|
144
|
+
def dec
|
145
|
+
@i -= 1
|
63
146
|
end
|
64
|
-
|
65
|
-
# @
|
66
|
-
def
|
67
|
-
|
147
|
+
|
148
|
+
# @return [String] The current token
|
149
|
+
def curr
|
150
|
+
@argv[@i]
|
68
151
|
end
|
69
|
-
|
70
|
-
#
|
71
|
-
def
|
72
|
-
|
152
|
+
|
153
|
+
# Whether the index is at the end of the argv
|
154
|
+
def ended?
|
155
|
+
@i >= @argv.size
|
73
156
|
end
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
if value
|
78
|
-
@klass.class_attr_accessor name => value
|
79
|
-
else
|
80
|
-
@klass.class_attr_accessor name
|
81
|
-
end
|
157
|
+
|
158
|
+
def command_ran?
|
159
|
+
@command_ran || false
|
82
160
|
end
|
83
|
-
|
84
|
-
#
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
161
|
+
|
162
|
+
# Returns the finished argument list for +opt+ which can then be pushed to the state.
|
163
|
+
def do_arguments_for(opt)
|
164
|
+
arg_list = collect_arguments(opt)
|
165
|
+
arg_list = validate_arguments(opt, arg_list)
|
166
|
+
|
167
|
+
arg_list
|
168
|
+
end
|
169
|
+
|
170
|
+
# Collects the arguments for +opt+.
|
171
|
+
def collect_arguments(opt)
|
172
|
+
inc
|
173
|
+
arg_list = []
|
174
|
+
while !ended? && arg_list.size < opt.args.max
|
175
|
+
break unless opt.args.possible?(arg_list + [curr])
|
176
|
+
arg_list << curr
|
177
|
+
inc
|
93
178
|
end
|
179
|
+
dec
|
180
|
+
arg_list
|
94
181
|
end
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
182
|
+
|
183
|
+
# Makes sure the found list of arguments is valid, if not raises
|
184
|
+
# MissingArgumentError. Returns the valid argument list with the arguments
|
185
|
+
# as the correct type, in the correct positions and with default values
|
186
|
+
# inserted if necessary.
|
187
|
+
def validate_arguments(opt, arg_list)
|
188
|
+
# If we don't have enough args
|
189
|
+
unless opt.args.valid?(arg_list)
|
190
|
+
raise MissingArgumentError.new(opt, arg_list, opt.args.to_s)
|
99
191
|
end
|
192
|
+
|
193
|
+
opt.args.create_valid(arg_list)
|
100
194
|
end
|
101
|
-
alias_method :option_list, :option_array
|
102
195
|
|
103
196
|
end
|
104
197
|
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
class Clive
|
2
|
+
|
3
|
+
# A Struct-like Hash (or Hash-like Struct)
|
4
|
+
#
|
5
|
+
# sh = StructHash.new(:a => 1)
|
6
|
+
# sh.a #=> 1
|
7
|
+
# sh[:a] #=> 1
|
8
|
+
#
|
9
|
+
# sh.set 42, [:answer, :life]
|
10
|
+
# sh.answer #=> 42
|
11
|
+
# sh.life #=> 42
|
12
|
+
#
|
13
|
+
# sh.to_h
|
14
|
+
# #=> {:a => 1, :answer => 42}
|
15
|
+
# sh.to_struct('Thing')
|
16
|
+
# #=> #<struct Struct::Thing @a=1 @answer=42>
|
17
|
+
#
|
18
|
+
class StructHash
|
19
|
+
|
20
|
+
alias_method :__respond_to?, :respond_to?
|
21
|
+
|
22
|
+
skip_methods = %w(object_id respond_to_missing? inspect === to_s class)
|
23
|
+
instance_methods.each do |m|
|
24
|
+
undef_method m unless skip_methods.include?(m.to_s) || m =~ /^__/
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(kvs={})
|
28
|
+
@data = kvs
|
29
|
+
@aliases = Hash[ kvs.map {|k,v| [k, k] } ]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Sets a value in the StructHash, this can be set with multiple keys but the
|
33
|
+
# first will be set as the most important key, the others will not show up in
|
34
|
+
# #to_h or #to_struct.
|
35
|
+
#
|
36
|
+
# @param keys [#to_sym, Array<#to_sym>]
|
37
|
+
# @param val
|
38
|
+
def store(keys, val)
|
39
|
+
keys = Array(keys).map(&:to_sym)
|
40
|
+
|
41
|
+
keys.each do |key|
|
42
|
+
@aliases[key] = keys.first
|
43
|
+
end
|
44
|
+
|
45
|
+
@data[keys.first] = val
|
46
|
+
end
|
47
|
+
|
48
|
+
# Gets the value from the StructHash corresponding to the key given.
|
49
|
+
#
|
50
|
+
# @param key [Symbol]
|
51
|
+
def fetch(key)
|
52
|
+
@data.fetch @aliases[key]
|
53
|
+
end
|
54
|
+
alias_method :[], :fetch
|
55
|
+
|
56
|
+
# Checks whether the StructHash contains an entry for the key given.
|
57
|
+
def key?(key)
|
58
|
+
@aliases.key? key
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Hash] The data without the +:args+ key.
|
62
|
+
def data
|
63
|
+
@data.reject {|k,v| k == :args }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns a hash representation of the StructHash instance, using only the
|
67
|
+
# important keys. This acts recursively, so any contained StructHashes
|
68
|
+
# will have #to_h called on them.
|
69
|
+
#
|
70
|
+
# @return [Hash]
|
71
|
+
def to_h
|
72
|
+
Hash[ data.map {|k,v| v.is_a?(StructHash) ? [k, v.to_h] : [k, v] } ]
|
73
|
+
end
|
74
|
+
alias_method :to_hash, :to_h
|
75
|
+
|
76
|
+
# Returns a struct representation of the StructHash instance, using only the
|
77
|
+
# important keys. This does not modify any contained StructHash instances
|
78
|
+
# like #to_h, but leaves them as they are.
|
79
|
+
#
|
80
|
+
# @return [Struct]
|
81
|
+
def to_struct(name=nil)
|
82
|
+
Struct.new(name, *data.keys).new *data.values
|
83
|
+
end
|
84
|
+
|
85
|
+
# Checks whether the method corresponds to a key, if so gets the value.
|
86
|
+
# Checks whether the method ends with '?', then checks if the key exists.
|
87
|
+
# Otherwise calls super.
|
88
|
+
def method_missing(sym, *args, &block)
|
89
|
+
if key?(sym)
|
90
|
+
fetch sym
|
91
|
+
elsif sym.to_s[-1..-1] == "?"
|
92
|
+
key? sym.to_s[0..-2].to_sym
|
93
|
+
else
|
94
|
+
super sym, *args, &block
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def respond_to?(sym, include_private=false)
|
99
|
+
return true if key?(sym)
|
100
|
+
return true if sym.to_s[-1..-1] == "?" && key?(sym.to_s[0..-2].to_sym)
|
101
|
+
return __respond_to?(sym, include_private)
|
102
|
+
end
|
103
|
+
|
104
|
+
def ==(other)
|
105
|
+
to_h == other.respond_to?(:to_h) ? other.to_h : other
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
data/lib/clive/type.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
class Clive
|
2
|
+
class Type
|
3
|
+
|
4
|
+
# @param arg [::String]
|
5
|
+
def valid?(arg)
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
# @param arg [::String]
|
10
|
+
def typecast(arg)
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
# Find the class for +name+.
|
17
|
+
# @param name [::String]
|
18
|
+
def find_class(name)
|
19
|
+
name = name.split('::').last
|
20
|
+
Clive::Type.const_get(name) if Clive::Type.const_defined?(name)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Shorthand to define #valid? for subclasses of {Type}, pass a
|
24
|
+
# regular expression that should be matched or a symbol for a
|
25
|
+
# method which will be called on the argument that returns either
|
26
|
+
# +true+ (valid) or +false+ (invalid).
|
27
|
+
#
|
28
|
+
# @param other [#to_proc, ::Regexp]
|
29
|
+
#
|
30
|
+
# @example With a regular expression
|
31
|
+
#
|
32
|
+
# class YesNo < Type
|
33
|
+
# match /yes|no/
|
34
|
+
# # ...
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @example With a method symbol
|
38
|
+
#
|
39
|
+
# class String
|
40
|
+
# def five?
|
41
|
+
# size == 5
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# class FiveChars < Type
|
46
|
+
# match :five?
|
47
|
+
# # ...
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
def match(other)
|
51
|
+
if other.respond_to?(:to_proc)
|
52
|
+
@valid = other.to_proc
|
53
|
+
else
|
54
|
+
@valid = proc {|arg| other =~ arg.to_s }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Similar to {.match} but opposite, so where {.match} would be valid
|
59
|
+
# refute is invalid.
|
60
|
+
#
|
61
|
+
# @param other [#to_proc, ::Regexp]
|
62
|
+
def refute(other)
|
63
|
+
if other.respond_to?(:to_proc)
|
64
|
+
@valid = proc {|arg| !arg.send(other) }
|
65
|
+
else
|
66
|
+
@valid = proc {|arg| other !~ arg.to_s }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Shorthand to define a method which is called on the string argument
|
71
|
+
# to return the correct type.
|
72
|
+
#
|
73
|
+
# @param sym [::Symbol]
|
74
|
+
#
|
75
|
+
# @example
|
76
|
+
#
|
77
|
+
# class Symbol < Type
|
78
|
+
# # ...
|
79
|
+
# cast :to_sym
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
def cast(sym, *args)
|
83
|
+
@cast = [sym, args]
|
84
|
+
end
|
85
|
+
|
86
|
+
# Checks whether the +arg+ passed is valid, if {.match} or {.refute}
|
87
|
+
# have been called it uses the Proc created by them otherwise calls
|
88
|
+
# {#valid?}.
|
89
|
+
#
|
90
|
+
# @param arg [::String]
|
91
|
+
def valid?(arg)
|
92
|
+
if @valid
|
93
|
+
@valid.call arg
|
94
|
+
else
|
95
|
+
new.valid? arg
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Casts the +arg+ to the correct type, if {.cast} has been called it
|
100
|
+
# uses the proc created otherwise it calls {#typecast}.
|
101
|
+
#
|
102
|
+
# @param arg [::String]
|
103
|
+
def typecast(arg)
|
104
|
+
if @cast
|
105
|
+
arg.send @cast[0], *@cast[1]
|
106
|
+
else
|
107
|
+
new.typecast arg
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
require 'clive/type/definitions'
|
117
|
+
require 'clive/type/lookup'
|