choice 0.1.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/CHANGELOG +2 -0
- data/LICENSE +18 -0
- data/README +347 -0
- data/examples/ftpd.rb +78 -0
- data/lib/choice.rb +132 -0
- data/lib/choice/lazyhash.rb +67 -0
- data/lib/choice/option.rb +104 -0
- data/lib/choice/parser.rb +145 -0
- data/lib/choice/version.rb +8 -0
- data/lib/choice/writer.rb +186 -0
- data/test/test_choice.rb +119 -0
- data/test/test_lazyhash.rb +76 -0
- data/test/test_option.rb +144 -0
- data/test/test_parser.rb +182 -0
- data/test/test_writer.rb +103 -0
- metadata +60 -0
@@ -0,0 +1,186 @@
|
|
1
|
+
module Choice
|
2
|
+
# This module writes to the screen. As of now, its only real use is writing
|
3
|
+
# the help screen.
|
4
|
+
module Writer #:nodoc: all
|
5
|
+
|
6
|
+
# Some constants used for printing and line widths
|
7
|
+
SHORT_LENGTH = 6
|
8
|
+
SHORT_BREAK_LENGTH = 2
|
9
|
+
LONG_LENGTH = 29
|
10
|
+
PRE_DESC_LENGTH = SHORT_LENGTH + SHORT_BREAK_LENGTH + LONG_LENGTH
|
11
|
+
|
12
|
+
|
13
|
+
# The main method. Takes a hash of arguments with the following possible
|
14
|
+
# keys, running them through the appropriate method:
|
15
|
+
# banner, header, options, footer
|
16
|
+
#
|
17
|
+
# Can also be told where to print (default STDOUT) and not to exit after
|
18
|
+
# printing the help screen, which it does by default.
|
19
|
+
def self.help(args, target = STDOUT, dont_exit = false)
|
20
|
+
# Set our printing target.
|
21
|
+
self.target = target
|
22
|
+
|
23
|
+
# The banner method needs to know about the passed options if it's going
|
24
|
+
# to do its magic. Only really needs :options if :banner is nil.
|
25
|
+
banner(args[:banner], args[:options])
|
26
|
+
|
27
|
+
# Run these three methods, passing in the appropriate hash element.
|
28
|
+
%w[header options footer].each do |meth|
|
29
|
+
send(meth, args[meth.to_sym])
|
30
|
+
end
|
31
|
+
|
32
|
+
# Exit. Unless you don't want to.
|
33
|
+
exit unless dont_exit
|
34
|
+
end
|
35
|
+
|
36
|
+
class <<self
|
37
|
+
private
|
38
|
+
|
39
|
+
# Print a passed banner or assemble the default banner, which is usage.
|
40
|
+
def banner(banner, options)
|
41
|
+
if banner
|
42
|
+
puts banner
|
43
|
+
else
|
44
|
+
# Usage needs to know about the defined options.
|
45
|
+
usage(options)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Print our header, which is just lines after the banner and before the
|
50
|
+
# options block. Needs an array, prints each element as a line.
|
51
|
+
def header(header)
|
52
|
+
if header.is_a?(Array) and header.size > 0
|
53
|
+
header.each { |line| puts line }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Print out the options block by going through each option and printing
|
58
|
+
# it as a line (or more). Expects an array.
|
59
|
+
def options(options)
|
60
|
+
# Do nothing if there's nothing to do.
|
61
|
+
return if options.nil? || !options.size
|
62
|
+
|
63
|
+
# If the option is a hash, run it through option_line. Otherwise
|
64
|
+
# just print it out as is.
|
65
|
+
options.each do |name, option|
|
66
|
+
if option.respond_to?(:to_h)
|
67
|
+
option_line(option.to_h)
|
68
|
+
else
|
69
|
+
puts name
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# The heavy lifting: print a line for an option. Has intimate knowledge
|
75
|
+
# of what keys are expected.
|
76
|
+
def option_line(option)
|
77
|
+
# Expect a hash
|
78
|
+
return unless option.is_a?(Hash)
|
79
|
+
|
80
|
+
# Make this easier on us
|
81
|
+
short = option['short']
|
82
|
+
long = option['long']
|
83
|
+
line = ''
|
84
|
+
|
85
|
+
# Get the short part.
|
86
|
+
line << sprintf("%#{SHORT_LENGTH}s", short)
|
87
|
+
line << sprintf("%-#{SHORT_BREAK_LENGTH}s", (',' if short && long))
|
88
|
+
|
89
|
+
# Get the long part.
|
90
|
+
line << sprintf("%-#{LONG_LENGTH}s", long)
|
91
|
+
|
92
|
+
# Print what we have so far
|
93
|
+
print line
|
94
|
+
|
95
|
+
# If there's a desc, print it.
|
96
|
+
if option['desc']
|
97
|
+
# If the line is too long, spill over to the next line
|
98
|
+
if line.length > PRE_DESC_LENGTH
|
99
|
+
puts
|
100
|
+
print " " * PRE_DESC_LENGTH
|
101
|
+
end
|
102
|
+
|
103
|
+
puts option['desc'].shift
|
104
|
+
|
105
|
+
# If there is more than one desc line, print each one in succession
|
106
|
+
# as separate lines.
|
107
|
+
option['desc'].each do |desc|
|
108
|
+
puts ' '*37 + desc
|
109
|
+
end
|
110
|
+
|
111
|
+
else
|
112
|
+
# No desc, just print a newline.
|
113
|
+
puts
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Expects an array, prints each element as a line.
|
119
|
+
def footer(footer)
|
120
|
+
footer.each { |line| puts line } unless footer.nil?
|
121
|
+
end
|
122
|
+
|
123
|
+
# Prints the usage statement, e.g. Usage prog.rb [-abc]
|
124
|
+
# Expects an array.
|
125
|
+
def usage(options)
|
126
|
+
# Really we just need an enumerable.
|
127
|
+
return unless options.respond_to?(:each)
|
128
|
+
|
129
|
+
# Start off the options with a dash.
|
130
|
+
opts = '-'
|
131
|
+
|
132
|
+
# Figure out the option shorts.
|
133
|
+
options.dup.each do |option|
|
134
|
+
# We really need an array here.
|
135
|
+
next unless option.is_a?(Array)
|
136
|
+
|
137
|
+
# Grab the hash of the last element, which should be the second
|
138
|
+
# element.
|
139
|
+
option = option.last.to_h
|
140
|
+
|
141
|
+
# Add the short to the options string.
|
142
|
+
opts << option['short'].sub('-','') if option['short']
|
143
|
+
end
|
144
|
+
|
145
|
+
# Figure out if we actually got any options.
|
146
|
+
opts = if opts =~ /^-(.+)/
|
147
|
+
" [#{opts}]"
|
148
|
+
end.to_s
|
149
|
+
|
150
|
+
# Print it out, with our newly aquired options string.
|
151
|
+
puts "Usage: #{program}" << opts
|
152
|
+
end
|
153
|
+
|
154
|
+
# Figure out the name of this program based on what was run.
|
155
|
+
def program
|
156
|
+
if (/(\/|\\)/ =~ $0) then File.basename($0) else $0 end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Set where we print.
|
160
|
+
def target=(target)
|
161
|
+
@@target = target
|
162
|
+
end
|
163
|
+
|
164
|
+
# Where do we print?
|
165
|
+
def target
|
166
|
+
@@target
|
167
|
+
end
|
168
|
+
|
169
|
+
# Fake puts
|
170
|
+
def puts(str = nil)
|
171
|
+
str = '' if str.nil?
|
172
|
+
print(str + "\n")
|
173
|
+
end
|
174
|
+
|
175
|
+
# Fake printf
|
176
|
+
def printf(format, *args)
|
177
|
+
print(sprintf(format, *args))
|
178
|
+
end
|
179
|
+
|
180
|
+
# Fake print -- just add to target, which may not be STDOUT.
|
181
|
+
def print(str)
|
182
|
+
target << str
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
data/test/test_choice.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
$:.unshift "../lib:lib"
|
2
|
+
require 'test/unit'
|
3
|
+
require 'choice'
|
4
|
+
|
5
|
+
class TestChoice < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def setup
|
8
|
+
Choice.reset
|
9
|
+
Choice.dont_exit_on_help = true
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_choices
|
13
|
+
Choice.options do
|
14
|
+
header "Tell me about yourself?"
|
15
|
+
header ""
|
16
|
+
option :band do
|
17
|
+
short "-b"
|
18
|
+
long "--band=BAND"
|
19
|
+
cast String
|
20
|
+
desc "Your favorite band."
|
21
|
+
validate /\w+/
|
22
|
+
end
|
23
|
+
option :animal do
|
24
|
+
short "-a"
|
25
|
+
long "--animal=ANIMAL"
|
26
|
+
cast String
|
27
|
+
desc "Your favorite animal."
|
28
|
+
end
|
29
|
+
footer ""
|
30
|
+
footer "--help This message"
|
31
|
+
end
|
32
|
+
|
33
|
+
band = 'LedZeppelin'
|
34
|
+
animal = 'Reindeer'
|
35
|
+
|
36
|
+
args = ['-b', band, "--animal=#{animal}"]
|
37
|
+
Choice.args = args
|
38
|
+
|
39
|
+
assert_equal band, Choice.choices['band']
|
40
|
+
assert_equal animal, Choice.choices[:animal]
|
41
|
+
assert_equal ["Tell me about yourself?", ""], Choice.header
|
42
|
+
assert_equal ["", "--help This message"], Choice.footer
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_failed_parse
|
46
|
+
assert Hash.new, Choice.parse
|
47
|
+
end
|
48
|
+
|
49
|
+
HELP_STRING = ''
|
50
|
+
def test_help
|
51
|
+
Choice.output_to(HELP_STRING)
|
52
|
+
Choice.args = ['-m', 'lunch', '--help']
|
53
|
+
|
54
|
+
Choice.options do
|
55
|
+
banner "Usage: choice [-mu]"
|
56
|
+
header ""
|
57
|
+
option :meal do
|
58
|
+
short '-m'
|
59
|
+
desc 'Your favorite meal.'
|
60
|
+
end
|
61
|
+
|
62
|
+
separator ""
|
63
|
+
separator "And you eat it with..."
|
64
|
+
|
65
|
+
option :utencil do
|
66
|
+
short "-u"
|
67
|
+
long "--utencil=[UTENCIL]"
|
68
|
+
desc "Your favorite eating utencil."
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
help_string = <<-HELP
|
73
|
+
Usage: choice [-mu]
|
74
|
+
|
75
|
+
-m Your favorite meal.
|
76
|
+
|
77
|
+
And you eat it with...
|
78
|
+
-u, --utencil=[UTENCIL] Your favorite eating utencil.
|
79
|
+
HELP
|
80
|
+
|
81
|
+
assert_equal help_string, HELP_STRING
|
82
|
+
end
|
83
|
+
|
84
|
+
UNKNOWN_STRING = ''
|
85
|
+
def test_unknown_argument
|
86
|
+
Choice.output_to(UNKNOWN_STRING)
|
87
|
+
Choice.args = ['-m', 'lunch', '--motorcycles']
|
88
|
+
|
89
|
+
Choice.options do
|
90
|
+
banner "Usage: choice [-mu]"
|
91
|
+
header ""
|
92
|
+
option :meal do
|
93
|
+
short '-m'
|
94
|
+
desc 'Your favorite meal.'
|
95
|
+
end
|
96
|
+
|
97
|
+
separator ""
|
98
|
+
separator "And you eat it with..."
|
99
|
+
|
100
|
+
option :utencil do
|
101
|
+
short "-u"
|
102
|
+
long "--utencil=[UTENCIL]"
|
103
|
+
desc "Your favorite eating utencil."
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
help_string = <<-HELP
|
108
|
+
Usage: choice [-mu]
|
109
|
+
|
110
|
+
-m Your favorite meal.
|
111
|
+
|
112
|
+
And you eat it with...
|
113
|
+
-u, --utencil=[UTENCIL] Your favorite eating utencil.
|
114
|
+
HELP
|
115
|
+
|
116
|
+
assert_equal help_string, UNKNOWN_STRING
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
$:.unshift "../lib:lib"
|
2
|
+
require 'test/unit'
|
3
|
+
require 'choice/lazyhash'
|
4
|
+
|
5
|
+
class TestLazyHash < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def test_symbol
|
8
|
+
name = 'Igor'
|
9
|
+
age = 41
|
10
|
+
|
11
|
+
hash = Choice::LazyHash.new
|
12
|
+
hash['name'] = name
|
13
|
+
hash[:age] = age
|
14
|
+
|
15
|
+
assert_equal name, hash[:name]
|
16
|
+
assert_equal age, hash[:age]
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_string
|
20
|
+
name = "Frank Stein"
|
21
|
+
age = 30
|
22
|
+
|
23
|
+
hash = Choice::LazyHash.new
|
24
|
+
hash[:name] = name
|
25
|
+
hash['age'] = age
|
26
|
+
|
27
|
+
assert_equal name, hash['name']
|
28
|
+
assert_equal age, hash['age']
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_store_and_fetch
|
32
|
+
name = 'Jimini Jeremiah'
|
33
|
+
job = 'Interior Decorator'
|
34
|
+
|
35
|
+
hash = Choice::LazyHash.new
|
36
|
+
hash.store('name', name)
|
37
|
+
hash.store(:job, job)
|
38
|
+
|
39
|
+
assert_equal name, hash.fetch(:name)
|
40
|
+
assert_equal job, hash.fetch('job')
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_messages
|
44
|
+
star = 'Sol'
|
45
|
+
planet = 'Mars'
|
46
|
+
|
47
|
+
hash = Choice::LazyHash.new
|
48
|
+
hash.star = star
|
49
|
+
hash.planet = planet
|
50
|
+
|
51
|
+
assert_equal star, hash.star
|
52
|
+
assert_equal planet, hash.planet
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_from_hash
|
56
|
+
state = 'Nebraska'
|
57
|
+
country = 'Mexico'
|
58
|
+
|
59
|
+
hash = { :state => state, :country => country }
|
60
|
+
lazy = Choice::LazyHash.new(hash)
|
61
|
+
|
62
|
+
assert_equal state, lazy['state']
|
63
|
+
assert_equal country, lazy[:country]
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_to_lazyhash
|
67
|
+
hash = { :name => 'Jimmy', :age => 25 }
|
68
|
+
lazy = hash.to_lazyhash
|
69
|
+
|
70
|
+
assert_equal hash[:name], lazy.name
|
71
|
+
assert_equal hash[:name], lazy[:name]
|
72
|
+
assert_equal hash[:age], lazy.age
|
73
|
+
assert_equal hash[:age], lazy[:age]
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
data/test/test_option.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
$:.unshift "../lib:lib"
|
2
|
+
require 'test/unit'
|
3
|
+
require 'choice/option'
|
4
|
+
|
5
|
+
class TestOption < Test::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
Choice.reset
|
8
|
+
@option = Choice::Option.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_desc
|
12
|
+
line_one = "This is a description."
|
13
|
+
line_two = "I can add many lines."
|
14
|
+
line_three = "There is no limit."
|
15
|
+
|
16
|
+
assert_equal false, @option.desc?
|
17
|
+
|
18
|
+
@option.desc line_one
|
19
|
+
@option.desc line_two
|
20
|
+
|
21
|
+
assert @option.desc?
|
22
|
+
assert_equal [line_one, line_two], @option.desc
|
23
|
+
|
24
|
+
@option.desc line_three
|
25
|
+
assert @option.desc?
|
26
|
+
assert_equal [line_one, line_two, line_three], @option.desc
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_choice
|
30
|
+
short = "-p"
|
31
|
+
|
32
|
+
assert_equal false, @option.short?
|
33
|
+
|
34
|
+
@option.short short
|
35
|
+
|
36
|
+
assert @option.short?
|
37
|
+
assert_equal short, @option.short
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_no_choice
|
41
|
+
default = 42
|
42
|
+
|
43
|
+
assert_raise(Choice::Option::ParseError) do
|
44
|
+
@option.defaut?
|
45
|
+
end
|
46
|
+
|
47
|
+
assert_raise(Choice::Option::ParseError) do
|
48
|
+
@option.defaut default
|
49
|
+
end
|
50
|
+
|
51
|
+
assert_raise(Choice::Option::ParseError) do
|
52
|
+
@option.defaut
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_block_choice
|
58
|
+
assert_equal false, @option.action?
|
59
|
+
|
60
|
+
@option.action do
|
61
|
+
1 + 1
|
62
|
+
end
|
63
|
+
|
64
|
+
assert @option.action?
|
65
|
+
assert_block(&@option.action)
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_default_option
|
69
|
+
option = Choice::Option.new('port')
|
70
|
+
|
71
|
+
assert_equal '-p', option.short
|
72
|
+
assert_equal '--port=PORT', option.long
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_format
|
76
|
+
@option = Choice::Option.new do
|
77
|
+
validate /^\w+$/
|
78
|
+
end
|
79
|
+
|
80
|
+
assert_equal /^\w+$/, @option.validate
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_dsl
|
84
|
+
@option = Choice::Option.new do
|
85
|
+
short "-h"
|
86
|
+
long "--host=HOST"
|
87
|
+
cast String
|
88
|
+
desc "The hostname."
|
89
|
+
desc "(Alphanumeric only)"
|
90
|
+
filter do
|
91
|
+
5 * 10
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
assert_equal "-h", @option.short
|
96
|
+
assert_equal "--host=HOST", @option.long
|
97
|
+
assert_equal String, @option.cast
|
98
|
+
assert_equal ["The hostname.", "(Alphanumeric only)"], @option.desc
|
99
|
+
assert_equal proc { 5 * 10 }.call, @option.filter.call
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_to_a
|
103
|
+
desc = "This is your description."
|
104
|
+
short = "-t"
|
105
|
+
long = "--test=METHOD"
|
106
|
+
default = :to_a
|
107
|
+
|
108
|
+
@option.desc desc
|
109
|
+
@option.short short
|
110
|
+
@option.long long
|
111
|
+
@option.default default
|
112
|
+
@option.action { 1 + 1 }
|
113
|
+
array = @option.to_a
|
114
|
+
|
115
|
+
assert_equal Choice::Option, @option.class
|
116
|
+
assert_equal Array, array.class
|
117
|
+
assert array.include?([desc])
|
118
|
+
assert array.include?(short)
|
119
|
+
assert array.include?(long)
|
120
|
+
assert array.include?(default)
|
121
|
+
assert_equal proc { 1 + 1 }.call, array.select { |a| a.is_a? Proc }.first.call
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_to_h
|
125
|
+
desc = "This is your description."
|
126
|
+
short = "-t"
|
127
|
+
long = "--test=METHOD"
|
128
|
+
cast = Integer
|
129
|
+
|
130
|
+
@option.desc desc
|
131
|
+
@option.short short
|
132
|
+
@option.long long
|
133
|
+
@option.cast cast
|
134
|
+
@option.filter { 2 + 2 }
|
135
|
+
hash = @option.to_h
|
136
|
+
|
137
|
+
assert_equal Choice::Option, @option.class
|
138
|
+
assert_equal Hash, hash.class
|
139
|
+
assert_equal [desc], hash['desc']
|
140
|
+
assert_equal short, hash['short']
|
141
|
+
assert_equal cast, hash['cast']
|
142
|
+
assert_equal proc { 2 + 2 }.call, hash['filter'].call
|
143
|
+
end
|
144
|
+
end
|