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