clive 0.8.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|