clive 0.6.2 → 0.7.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 +129 -126
- data/lib/clive.rb +30 -119
- data/lib/clive/bool.rb +32 -34
- data/lib/clive/command.rb +171 -54
- data/lib/clive/exceptions.rb +2 -2
- data/lib/clive/ext.rb +21 -3
- data/lib/clive/flag.rb +80 -67
- data/lib/clive/formatter.rb +180 -0
- data/lib/clive/option.rb +41 -25
- data/lib/clive/output.rb +14 -18
- data/lib/clive/parser.rb +79 -16
- data/lib/clive/switch.rb +8 -17
- data/lib/clive/tokens.rb +1 -1
- data/lib/clive/version.rb +2 -2
- data/spec/clive/bool_spec.rb +54 -0
- data/spec/clive/command_spec.rb +260 -0
- data/spec/clive/exceptions_spec.rb +1 -0
- data/spec/clive/ext_spec.rb +1 -0
- data/spec/clive/flag_spec.rb +84 -0
- data/spec/clive/formatter_spec.rb +108 -0
- data/spec/clive/option_spec.rb +34 -0
- data/spec/clive/output_spec.rb +5 -0
- data/spec/clive/parser_spec.rb +106 -0
- data/spec/clive/switch_spec.rb +14 -0
- data/spec/clive/tokens_spec.rb +38 -0
- data/spec/shared_specs.rb +16 -0
- data/spec/spec_helper.rb +12 -0
- metadata +34 -8
data/lib/clive/flag.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
module Clive
|
2
2
|
|
3
3
|
# A switch that takes one or more arguments.
|
4
4
|
# eg. wget --tries=10
|
@@ -10,68 +10,58 @@ class Clive
|
|
10
10
|
# Creates a new Flag instance. A flag is a switch that can take one or more
|
11
11
|
# arguments.
|
12
12
|
#
|
13
|
-
#
|
14
|
-
#
|
13
|
+
# @param names [Array[Symbol]]
|
14
|
+
# An array of names the flag can be invoked by. Can contain a long name
|
15
|
+
# and/or a short name.
|
15
16
|
#
|
16
|
-
# @
|
17
|
-
#
|
18
|
-
# @param [Symbol] long longer switch to be used, eg. +:tries+ => +--tries=10+
|
19
|
-
# @param [String, Array] args
|
20
|
-
# either a string showing the arguments to be given, eg.
|
21
|
-
#
|
22
|
-
# "FROM" # single arg, or
|
23
|
-
# "FROM TO" # two args, or
|
24
|
-
# "[VIA]" # optional arg surrounded by square brackets
|
17
|
+
# @param desc [String]
|
18
|
+
# A description of the flag.
|
25
19
|
#
|
26
|
-
#
|
20
|
+
# @param args [String, Array, Range]
|
21
|
+
# Either, a string showing the arguments to be given, eg.
|
22
|
+
#
|
23
|
+
# "FROM" # single argument required, or
|
24
|
+
# "[FROM]" # single optional argument, or
|
25
|
+
# "FROM TO" # multiple arguments required, or
|
26
|
+
# "FROM [VIA] TO" # multiple arguments with optional argument
|
27
27
|
#
|
28
|
-
#
|
28
|
+
# OR an array of acceptable inputs, eg.
|
29
29
|
#
|
30
|
-
#
|
30
|
+
# %w(large small medium) # will only accept these arguments
|
31
|
+
#
|
32
|
+
# OR a range, showing the acceptable inputs, eg.
|
33
|
+
# 1..10 #=> means 1, 2, 3, ..., 8, 9, 10
|
31
34
|
#
|
32
|
-
# @yield [String]
|
35
|
+
# @yield [String]
|
36
|
+
# A block to be run if switch is triggered, will always be passed a string
|
33
37
|
#
|
34
|
-
def initialize(
|
35
|
-
@names =
|
36
|
-
@args =
|
38
|
+
def initialize(names, desc, args, &block)
|
39
|
+
@names = Clive::Array.new(names.map(&:to_s))
|
40
|
+
@args = Clive::Array.new
|
37
41
|
|
38
42
|
# Need to be able to make each arg_name optional or not
|
39
43
|
# and allow for type in future
|
40
44
|
args.each do |i|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
:optional => true,
|
49
|
-
:type => type
|
50
|
-
}
|
51
|
-
else
|
52
|
-
@args << {
|
53
|
-
:name => arg,
|
54
|
-
:optional => false,
|
55
|
-
:type => type
|
56
|
-
}
|
57
|
-
end
|
45
|
+
case i
|
46
|
+
when String
|
47
|
+
i.split(' ').each do |arg|
|
48
|
+
optional = false
|
49
|
+
if arg[0] == "["
|
50
|
+
optional = true
|
51
|
+
arg = arg[1..-2]
|
58
52
|
end
|
59
|
-
|
60
|
-
@desc = i
|
53
|
+
@args << {:name => arg, :optional => optional}
|
61
54
|
end
|
62
55
|
else
|
63
|
-
|
64
|
-
@names << i.to_s
|
65
|
-
else
|
66
|
-
@args = i
|
67
|
-
end
|
56
|
+
@args = i
|
68
57
|
end
|
69
58
|
end
|
70
|
-
|
71
|
-
if @args == []
|
72
|
-
@args = [{:name => "ARG", :optional => false, :type => String}]
|
73
|
-
end
|
74
59
|
|
60
|
+
if @args.empty?
|
61
|
+
@args = [{:name => "ARG", :optional => false}]
|
62
|
+
end
|
63
|
+
|
64
|
+
@desc = desc
|
75
65
|
@block = block
|
76
66
|
end
|
77
67
|
|
@@ -81,10 +71,10 @@ class Clive
|
|
81
71
|
# @raise [InvalidArgument] only if +args+ is an array of acceptable inputs
|
82
72
|
# and a match is not found.
|
83
73
|
def run(args)
|
84
|
-
if @args[0].is_a?
|
74
|
+
if @args.is_a?(Array) && @args[0].is_a?(Hash)
|
85
75
|
args = Clive::Array.new(@args.collect {|i| !i[:optional]}).optimise_fill(args)
|
86
76
|
else # list
|
87
|
-
unless @args.include? args[0]
|
77
|
+
unless @args.to_a.map(&:to_s).include? args[0]
|
88
78
|
raise InvalidArgument.new(args)
|
89
79
|
end
|
90
80
|
end
|
@@ -94,33 +84,56 @@ class Clive
|
|
94
84
|
# @param [Boolean] optional whether to include optional arguments
|
95
85
|
# @return [Integer] number of arguments this takes
|
96
86
|
def arg_num(optional)
|
97
|
-
if @args[0].is_a?
|
87
|
+
if @args.is_a?(Array) && @args[0].is_a?(Hash)
|
98
88
|
@args.find_all {|i| i[:optional] == optional }.size
|
99
89
|
else
|
100
90
|
1
|
101
91
|
end
|
102
92
|
end
|
103
93
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
94
|
+
def args_to_strings
|
95
|
+
if @args.is_a? Range
|
96
|
+
[""]
|
97
|
+
elsif @args[0].is_a? Hash
|
98
|
+
r = []
|
99
|
+
@args.each do |arg|
|
100
|
+
if arg[:optional]
|
101
|
+
r << "[" + arg[:name] + "]"
|
102
|
+
else
|
103
|
+
r << arg[:name]
|
104
|
+
end
|
114
105
|
end
|
106
|
+
r
|
115
107
|
else
|
116
|
-
|
108
|
+
[""]
|
117
109
|
end
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
110
|
+
end
|
111
|
+
|
112
|
+
def arg_size
|
113
|
+
if @args.is_a?(Range) || @args.is_a?(Array)
|
114
|
+
1
|
115
|
+
else
|
116
|
+
@args.size
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def options_to_strings
|
121
|
+
if @args.is_a? Range
|
122
|
+
[@args.to_s]
|
123
|
+
elsif @args[0].is_a? Hash
|
124
|
+
['']
|
125
|
+
else
|
126
|
+
@args
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_h
|
131
|
+
{
|
132
|
+
'names' => Clive::Array.new(names_to_strings),
|
133
|
+
'desc' => @desc,
|
134
|
+
'args' => Clive::Array.new(args_to_strings),
|
135
|
+
'options' => Clive::Array.new(options_to_strings)
|
136
|
+
}
|
124
137
|
end
|
125
138
|
|
126
139
|
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Clive
|
4
|
+
|
5
|
+
# The formatter controls formatting of help. It can be configured
|
6
|
+
# using Clive::Command#help_formatter.
|
7
|
+
class Formatter
|
8
|
+
|
9
|
+
class Obj
|
10
|
+
def initialize(args)
|
11
|
+
args.each do |k, v|
|
12
|
+
self.class.send(:define_method, k) { v }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Evaluate the code given within the Obj created.
|
17
|
+
def evaluate(code)
|
18
|
+
eval(code)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sizes
|
23
|
+
attr_accessor :width, :prepend
|
24
|
+
|
25
|
+
def initialize(width, prepend, &block)
|
26
|
+
@width = width
|
27
|
+
@prepend = prepend
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def format(header, footer, commands, options)
|
32
|
+
result = ""
|
33
|
+
|
34
|
+
switches = options.find_all {|i| i.class == Clive::Switch }.map(&:to_h)
|
35
|
+
bools = options.find_all {|i| i.class == Clive::Bool }.map(&:to_h).compact
|
36
|
+
flags = options.find_all {|i| i.class == Clive::Flag }.map(&:to_h)
|
37
|
+
commands = commands.map(&:to_h)
|
38
|
+
|
39
|
+
result << header << "\n" if header
|
40
|
+
|
41
|
+
unless commands.empty?
|
42
|
+
result << "\n Commands: \n"
|
43
|
+
|
44
|
+
commands.each do |hash|
|
45
|
+
hash['prepend'] = " " * @prepend
|
46
|
+
result << parse(@command, hash) << "\n"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
unless options.empty?
|
52
|
+
result << "\n Options: \n"
|
53
|
+
|
54
|
+
switches.each do |hash|
|
55
|
+
hash['prepend'] = " " * @prepend
|
56
|
+
result << parse(@switch, hash) << "\n"
|
57
|
+
end
|
58
|
+
|
59
|
+
bools.each do |hash|
|
60
|
+
hash['prepend'] = " " * @prepend
|
61
|
+
result << parse(@bool, hash) << "\n"
|
62
|
+
end
|
63
|
+
|
64
|
+
flags.each do |hash|
|
65
|
+
hash['prepend'] = " " * @prepend
|
66
|
+
result << parse(@flag, hash) << "\n"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
result << "\n" << footer << "\n" if footer
|
71
|
+
|
72
|
+
result
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def switch(format)
|
77
|
+
@switch = format
|
78
|
+
end
|
79
|
+
|
80
|
+
def bool(format)
|
81
|
+
@bool = format
|
82
|
+
end
|
83
|
+
|
84
|
+
def flag(format)
|
85
|
+
@flag = format
|
86
|
+
end
|
87
|
+
|
88
|
+
def command(format)
|
89
|
+
@command = format
|
90
|
+
end
|
91
|
+
|
92
|
+
def help(format)
|
93
|
+
@help = format
|
94
|
+
end
|
95
|
+
|
96
|
+
def summary(format)
|
97
|
+
@summary = format
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
def parse(format, args)
|
102
|
+
front, back = format.split('{spaces}')
|
103
|
+
|
104
|
+
front_p = parse_format(front, args)
|
105
|
+
back_p = parse_format(back, args)
|
106
|
+
|
107
|
+
s = @width - front_p.length
|
108
|
+
s = 0 if s < 0 # can't have negative spaces!
|
109
|
+
spaces = " " * s
|
110
|
+
|
111
|
+
front_p << spaces << back_p
|
112
|
+
end
|
113
|
+
|
114
|
+
def parse_format(format, args)
|
115
|
+
if format
|
116
|
+
@scanner = StringScanner.new(format)
|
117
|
+
result = []
|
118
|
+
|
119
|
+
# Create object to eval in
|
120
|
+
obj = Obj.new(args)
|
121
|
+
|
122
|
+
until @scanner.eos?
|
123
|
+
a = scan_block || a = scan_text
|
124
|
+
result << a
|
125
|
+
end
|
126
|
+
|
127
|
+
r = ""
|
128
|
+
result.each do |(t, v)|
|
129
|
+
case t
|
130
|
+
when :block # contains ruby to eval
|
131
|
+
r << obj.evaluate(v)
|
132
|
+
when :text # add this with no adjustment
|
133
|
+
r << v
|
134
|
+
end
|
135
|
+
end
|
136
|
+
r
|
137
|
+
else
|
138
|
+
""
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
# @group Scanning
|
144
|
+
def scan_block
|
145
|
+
return unless @scanner.scan /\{/
|
146
|
+
|
147
|
+
pos = @scanner.pos
|
148
|
+
if @scanner.scan_until /\}/
|
149
|
+
@scanner.pos -= @scanner.matched.size
|
150
|
+
[:block, @scanner.pre_match[pos..-1]]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def scan_text
|
155
|
+
text = nil
|
156
|
+
|
157
|
+
pos = @scanner.pos
|
158
|
+
if @scanner.scan_until /(?<=[^\\])\{/
|
159
|
+
@scanner.pos -= @scanner.matched.size
|
160
|
+
text = @scanner.pre_match[pos..-1]
|
161
|
+
end
|
162
|
+
|
163
|
+
if text.nil?
|
164
|
+
text = @scanner.rest
|
165
|
+
@scanner.clear
|
166
|
+
end
|
167
|
+
|
168
|
+
# Remove }s from text
|
169
|
+
if text[0] == "}"
|
170
|
+
text = text[1..-1]
|
171
|
+
end
|
172
|
+
|
173
|
+
text.gsub!(/\\(.)/) {|m| m[1] }
|
174
|
+
|
175
|
+
[:text, text]
|
176
|
+
end
|
177
|
+
# @endgroup
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|
data/lib/clive/option.rb
CHANGED
@@ -1,32 +1,44 @@
|
|
1
|
-
|
1
|
+
module Clive
|
2
2
|
|
3
|
-
# @abstract Subclass and override {#initialize} and {#run} to create a
|
3
|
+
# @abstract Subclass and override {#initialize} and {#run} to create a
|
4
|
+
# new Option class. {#to_h} can also be overriden to provide information
|
5
|
+
# when building the help.
|
6
|
+
#
|
7
|
+
# Option is the base class for switches, flags, commands, etc. It should be
|
8
|
+
# used as a template for the way options (or whatever) are initialized, and
|
9
|
+
# the other methods that may need implementing.
|
10
|
+
#
|
4
11
|
class Option
|
5
12
|
attr_accessor :names, :desc, :block
|
6
13
|
|
7
|
-
|
8
|
-
|
9
|
-
|
14
|
+
# Create a new Option instance.
|
15
|
+
#
|
16
|
+
# For subclasses the method call should take the form:
|
17
|
+
# +initialize(names, desc, [special args], &block)+.
|
18
|
+
#
|
19
|
+
# @param names [Array[Symbol]]
|
20
|
+
# An array of names the option can be invoked by.
|
21
|
+
#
|
22
|
+
# @param desc [String]
|
23
|
+
# A description of what the option does.
|
24
|
+
#
|
25
|
+
# @yield A block to run if the switch is triggered
|
26
|
+
#
|
27
|
+
def initialize(names, desc, &block)
|
28
|
+
@names = names.map(&:to_s)
|
29
|
+
@desc = desc
|
30
|
+
@block = block
|
10
31
|
end
|
11
32
|
|
33
|
+
# Calls the block.
|
12
34
|
def run
|
13
|
-
|
14
|
-
# @block.call
|
15
|
-
end
|
16
|
-
|
17
|
-
def summary(width=30, prepend=5)
|
18
|
-
n = names_to_strings.join(', ')
|
19
|
-
spaces = width-n.length
|
20
|
-
spaces = 1 if spaces < 1
|
21
|
-
s = spaces(spaces)
|
22
|
-
p = spaces(prepend)
|
23
|
-
"#{p}#{n}#{s}#{@desc}"
|
35
|
+
@block.call
|
24
36
|
end
|
25
37
|
|
26
38
|
# Convert the names to strings, if name is single character appends
|
27
39
|
# +-+, else appends +--+.
|
28
40
|
#
|
29
|
-
# @param [Boolean]
|
41
|
+
# @param bool [Boolean] whether to add [no-] to long
|
30
42
|
#
|
31
43
|
# @example
|
32
44
|
#
|
@@ -51,16 +63,10 @@ class Clive
|
|
51
63
|
r
|
52
64
|
end
|
53
65
|
|
54
|
-
#
|
55
|
-
def spaces(n)
|
56
|
-
s = ''
|
57
|
-
(0...n).each {s << ' '}
|
58
|
-
s
|
59
|
-
end
|
60
|
-
|
61
|
-
# Tries to get the short name, if not choose lowest alphabetically
|
66
|
+
# Tries to get the short name, if not chooses lowest alphabetically.
|
62
67
|
#
|
63
68
|
# @return [String] name to sort by
|
69
|
+
#
|
64
70
|
def sort_name
|
65
71
|
r = @names.sort[0]
|
66
72
|
@names.each do |i|
|
@@ -75,6 +81,16 @@ class Clive
|
|
75
81
|
def <=>(other)
|
76
82
|
self.sort_name <=> other.sort_name
|
77
83
|
end
|
84
|
+
|
85
|
+
# @return [Hash{String=>Object}]
|
86
|
+
# Returns a hash which can be passed to the help formatter.
|
87
|
+
#
|
88
|
+
def to_h
|
89
|
+
{
|
90
|
+
"names" => names_to_strings,
|
91
|
+
"desc" => @desc
|
92
|
+
}
|
93
|
+
end
|
78
94
|
|
79
95
|
end
|
80
96
|
end
|