help_parser 1.0.2
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.
- checksums.yaml +7 -0
- data/History.txt +1 -0
- data/Manifest.txt +23 -0
- data/README.rdoc +72 -0
- data/Rakefile +94 -0
- data/TODO.txt +0 -0
- data/examples/command_suite +130 -0
- data/examples/implementation_error_duplicate_key +30 -0
- data/examples/implementation_error_redefinition +29 -0
- data/examples/implementation_error_terminal +30 -0
- data/examples/simple +29 -0
- data/features/edge.feature +30 -0
- data/features/simple.feature +125 -0
- data/features/step_definitions/help_parser_steps.rb +80 -0
- data/features/suite.feature +131 -0
- data/help_parser.gemspec +52 -0
- data/lib/help_parser.rb +2 -0
- data/lib/help_parser/help_parser.rb +285 -0
- data/lib/help_parser/version.rb +3 -0
- data/tasks/gemspecker.rb +60 -0
- data/tasks/manifester.rb +13 -0
- data/tasks/syntaxer.rb +25 -0
- data/tasks/unit_tests.rb +18 -0
- data/test/help_parser.rb +250 -0
- metadata +70 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
@edge
|
2
|
+
Feature: Edge cases
|
3
|
+
|
4
|
+
Scenario: Redefinition of dictionary
|
5
|
+
For example, --help already maps to -h,
|
6
|
+
so if help text says different
|
7
|
+
we should get an ImplementationError.
|
8
|
+
* Given command "./examples/implementation_error_redefinition"
|
9
|
+
* Given option ""
|
10
|
+
* When we run command
|
11
|
+
* Then exception is "HELP_PARSER::ImplementationError"
|
12
|
+
* Then message matches "redefinition"
|
13
|
+
|
14
|
+
Scenario: Terminal
|
15
|
+
For example, -h defines the "user wants help" state.
|
16
|
+
If the help text tries to create another terminal state for -h,
|
17
|
+
we should get an ImplementationError.
|
18
|
+
* Given command "./examples/implementation_error_terminal"
|
19
|
+
* Given option ""
|
20
|
+
* When we run command
|
21
|
+
* Then exception is "HELP_PARSER::ImplementationError"
|
22
|
+
* Then message matches "terminal"
|
23
|
+
|
24
|
+
Scenario: Redifinition in help text
|
25
|
+
For example, it the help text says --zebra is -z and -a.
|
26
|
+
* Given command "./examples/implementation_error_duplicate_key"
|
27
|
+
* Given option ""
|
28
|
+
* When we run command
|
29
|
+
* Then exception is "HELP_PARSER::ImplementationError"
|
30
|
+
* Then message matches "redefinition"
|
@@ -0,0 +1,125 @@
|
|
1
|
+
@simple
|
2
|
+
Feature: Testing examples/simple
|
3
|
+
|
4
|
+
Background:
|
5
|
+
* Given command "./examples/simple"
|
6
|
+
|
7
|
+
Scenario: Version, short option: -v
|
8
|
+
* Given option "-v"
|
9
|
+
* When we run command
|
10
|
+
* Then exception is "HELP_PARSER::VersionException"
|
11
|
+
* Then message is "1.2.3"
|
12
|
+
* Then options[v] is "true"
|
13
|
+
* Then options[version] is "true"
|
14
|
+
|
15
|
+
Scenario: Version, long option: --version
|
16
|
+
* Given option "--version"
|
17
|
+
* When we run command
|
18
|
+
* Then exception is "HELP_PARSER::VersionException"
|
19
|
+
* Then message is "1.2.3"
|
20
|
+
* Then options[v] is "true"
|
21
|
+
* Then options[version] is "true"
|
22
|
+
|
23
|
+
Scenario: Help, short option: -h
|
24
|
+
* Given option "-h"
|
25
|
+
* When we run command
|
26
|
+
* Then exception is "HELP_PARSER::HelpException"
|
27
|
+
* Then message matches "^Usage:"
|
28
|
+
* Then options[h] is "true"
|
29
|
+
* Then options[help] is "true"
|
30
|
+
|
31
|
+
Scenario: Help, long option: --help
|
32
|
+
* Given option "--help"
|
33
|
+
* When we run command
|
34
|
+
* Then exception is "HELP_PARSER::HelpException"
|
35
|
+
* Then message matches "^Usage:"
|
36
|
+
* Then options[h] is "true"
|
37
|
+
* Then options[help] is "true"
|
38
|
+
|
39
|
+
Scenario: Duplicate with long option: -h --help
|
40
|
+
* Given option "-h --help"
|
41
|
+
* When we run command
|
42
|
+
* Then exception is "HELP_PARSER::UsageError"
|
43
|
+
* Then message matches "^duplicate"
|
44
|
+
|
45
|
+
Scenario: Duplicate with short option: --help -h
|
46
|
+
* Given option "-h --help"
|
47
|
+
* When we run command
|
48
|
+
* Then exception is "HELP_PARSER::UsageError"
|
49
|
+
* Then message matches "^duplicate"
|
50
|
+
|
51
|
+
Scenario: No options: ""
|
52
|
+
* Given option ""
|
53
|
+
* When we run command
|
54
|
+
* Then exception is ""
|
55
|
+
* Then message is ""
|
56
|
+
* Then options.empty?
|
57
|
+
|
58
|
+
Scenario: No comment: --nocomment
|
59
|
+
We don't need to comment a command line option in the help.
|
60
|
+
* Given option "--nocomment"
|
61
|
+
* When we run command
|
62
|
+
* Then exception is ""
|
63
|
+
* Then message is ""
|
64
|
+
* Then options[nocomment] is "true"
|
65
|
+
* Then options.length is 1
|
66
|
+
|
67
|
+
Scenario: Long option values, String: --long=aStringValue
|
68
|
+
* Given option "--long=aStringValue"
|
69
|
+
* When we run command
|
70
|
+
* Then options[long] is "aStringValue"
|
71
|
+
|
72
|
+
Scenario: Long option values, Integer: --long=42
|
73
|
+
* Given option "--long=42"
|
74
|
+
* When we run command
|
75
|
+
* Then options[long] is "42"
|
76
|
+
* Then options[long] kind of Integer
|
77
|
+
|
78
|
+
Scenario: Long option values, default true: --long
|
79
|
+
* Given option "--long"
|
80
|
+
* When we run command
|
81
|
+
* Then options[long] is "true"
|
82
|
+
|
83
|
+
Scenario: Long option values, negated: --no-long
|
84
|
+
* Given option "--no-long"
|
85
|
+
* When we run command
|
86
|
+
* Then options[long] is "false"
|
87
|
+
|
88
|
+
Scenario: Long option values, Array: long=1,2,3
|
89
|
+
* Given option "--long=1,2,3"
|
90
|
+
* When we run command
|
91
|
+
* Then options[long] kind of Array
|
92
|
+
|
93
|
+
Scenario: Three non synonim-ed keys: -k, -V, --nocomment
|
94
|
+
* Given options "--nocomment -k -V"
|
95
|
+
* When we run command
|
96
|
+
* Then options.length is 3
|
97
|
+
* Then options[k] is "true"
|
98
|
+
* Then options[V] is "true"
|
99
|
+
* Then options[nocomment] is "true"
|
100
|
+
|
101
|
+
Scenario: One synonim-ed key: -s
|
102
|
+
* Given options "-s"
|
103
|
+
* When we run command
|
104
|
+
* Then options.length is 2
|
105
|
+
* Then options[s] is "true"
|
106
|
+
* Then options[long] is "true"
|
107
|
+
|
108
|
+
Scenario: One synonim-ed key: --long=abc
|
109
|
+
* Given options "--long=abc"
|
110
|
+
* When we run command
|
111
|
+
* Then options.length is 2
|
112
|
+
* Then options[s] is "abc"
|
113
|
+
* Then options[long] is "abc"
|
114
|
+
|
115
|
+
Scenario: Attempting to set a negation: --no-long=NotAllowed
|
116
|
+
* Given options "--no-long=NotAllowed"
|
117
|
+
* When we run command
|
118
|
+
* Then exception is "HELP_PARSER::UsageError"
|
119
|
+
* Then message matches "no-long=NotAllowed"
|
120
|
+
|
121
|
+
Scenario: User passed undefined option: --cacahuates
|
122
|
+
* Given option "--cacahuates"
|
123
|
+
* When we run command
|
124
|
+
* Then exception is "HELP_PARSER::UsageError"
|
125
|
+
* Then message matches "not.*valid.*cacahuates"
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
Given /Given command "([^"]*)"/ do |command|
|
5
|
+
@command = command
|
6
|
+
end
|
7
|
+
|
8
|
+
Given /Given option(s?) "([^"]*)"/ do |p, options|
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
When /When we run command/ do
|
13
|
+
@stdout, @stderr, @status = Open3.capture3("#{@command} #{@options}")
|
14
|
+
begin
|
15
|
+
@json = JSON.parse @stdout
|
16
|
+
rescue StandardError
|
17
|
+
@json = {}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Then /Then exit status is "(\d+)"/ do |status|
|
22
|
+
unless @status.exit_status == status.to_i
|
23
|
+
raise "Got #{@status.exit_status} instead of #{status}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
Then /Then (\w+) is "([^"]*)"/ do |key, expected|
|
28
|
+
value = @json[key].to_s
|
29
|
+
unless value == expected
|
30
|
+
raise "Got '#{value}' instead of '#{expected}'"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Then /Then (\w+) matches "([^"]*)"/ do |key, expected|
|
35
|
+
value = @json[key]
|
36
|
+
unless value =~ /#{expected}/
|
37
|
+
raise "'#{expected}' did not match:\n#{value}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
Then /Then options\[(\w+)\] is "([^*]*)"/ do |key,expected|
|
42
|
+
options = @json['options']
|
43
|
+
value = (options)? options[key].to_s : ''
|
44
|
+
unless expected == value
|
45
|
+
raise "Got '#{value}' instead of '#{expected}'"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
Then /Then options\[(\w+)\] kind of (\w+)/ do |key, type|
|
50
|
+
options = @json['options']
|
51
|
+
value = options[key]
|
52
|
+
case type
|
53
|
+
when 'Integer'
|
54
|
+
raise "Not an integer" unless value.kind_of?(Integer)
|
55
|
+
when 'Array'
|
56
|
+
raise "Not an array" unless value.kind_of?(Array)
|
57
|
+
else
|
58
|
+
raise "Unrecognized type #{type}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
Then /Then options.empty\?/ do
|
63
|
+
options = @json['options']
|
64
|
+
raise "options not empty!" unless options.empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
Then /Then options.length is (\d+)/ do |expected|
|
68
|
+
expected = expected.to_i
|
69
|
+
options = @json['options']
|
70
|
+
actual = options.length
|
71
|
+
raise "Length was #{actual}, not #{expected}." unless actual == expected
|
72
|
+
end
|
73
|
+
|
74
|
+
=begin
|
75
|
+
Then // do ||
|
76
|
+
unless ==
|
77
|
+
raise "Got '#{@json['']}' instead of '#{}'"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
=end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
@suite
|
2
|
+
Feature: Testing examples/command_suite
|
3
|
+
|
4
|
+
Background:
|
5
|
+
* Given command "./examples/command_suite"
|
6
|
+
|
7
|
+
Scenario: Version: -v
|
8
|
+
* Given option "-v"
|
9
|
+
* When we run command
|
10
|
+
* Then message is "10.02.3"
|
11
|
+
* Then exception is "HELP_PARSER::VersionException"
|
12
|
+
* Then options[v] is "true"
|
13
|
+
* Then options[version] is "true"
|
14
|
+
* Then command is ""
|
15
|
+
|
16
|
+
Scenario: Help: --help
|
17
|
+
* Given option "--help"
|
18
|
+
* When we run command
|
19
|
+
* Then message matches "^This is a test script"
|
20
|
+
* Then exception is "HELP_PARSER::HelpException"
|
21
|
+
* Then options[h] is "true"
|
22
|
+
* Then options[help] is "true"
|
23
|
+
* Then command is ""
|
24
|
+
|
25
|
+
Scenario: First Version: first --version
|
26
|
+
* Given option "first --version"
|
27
|
+
* When we run command
|
28
|
+
* Then message is "10.02.3"
|
29
|
+
* Then exception is "HELP_PARSER::VersionException"
|
30
|
+
* Then options[v] is "true"
|
31
|
+
* Then options[version] is "true"
|
32
|
+
* Then command is "first"
|
33
|
+
|
34
|
+
Scenario: First Help: first -h
|
35
|
+
* Given option "first --h"
|
36
|
+
* When we run command
|
37
|
+
* Then message matches "^command_suite-first"
|
38
|
+
* Then exception is "HELP_PARSER::HelpException"
|
39
|
+
* Then options[h] is "true"
|
40
|
+
* Then options[help] is "true"
|
41
|
+
* Then command is "first"
|
42
|
+
|
43
|
+
Scenario: Second Version: second --version
|
44
|
+
* Given option "second --version"
|
45
|
+
* When we run command
|
46
|
+
* Then message is "10.02.3"
|
47
|
+
* Then exception is "HELP_PARSER::VersionException"
|
48
|
+
* Then options[v] is "true"
|
49
|
+
* Then options[version] is "true"
|
50
|
+
* Then command is "second"
|
51
|
+
|
52
|
+
Scenario: Second Help: second -h
|
53
|
+
* Given option "second --h"
|
54
|
+
* When we run command
|
55
|
+
* Then message matches "^command_suite-second"
|
56
|
+
* Then exception is "HELP_PARSER::HelpException"
|
57
|
+
* Then options[h] is "true"
|
58
|
+
* Then options[help] is "true"
|
59
|
+
* Then command is "second"
|
60
|
+
|
61
|
+
Scenario: ""
|
62
|
+
* Given option ""
|
63
|
+
* When we run command
|
64
|
+
* Then message is "Need command."
|
65
|
+
* Then exception is "HELP_PARSER::UsageError"
|
66
|
+
|
67
|
+
Scenario: "first"
|
68
|
+
* Given option "first"
|
69
|
+
* When we run command
|
70
|
+
#
|
71
|
+
* Then string is "string1"
|
72
|
+
* Then float is "1.0"
|
73
|
+
* Then integer is "1"
|
74
|
+
#
|
75
|
+
* Then cstring is "string1"
|
76
|
+
* Then cfloat is "1.0"
|
77
|
+
* Then cinteger is "1"
|
78
|
+
#
|
79
|
+
* Then command is "first"
|
80
|
+
|
81
|
+
Scenario: "first --string --float --integer"
|
82
|
+
* Given options "first --string --float --integer"
|
83
|
+
* When we run command
|
84
|
+
#
|
85
|
+
* Then string is "string1"
|
86
|
+
* Then float is "1.0"
|
87
|
+
* Then integer is "1"
|
88
|
+
#
|
89
|
+
* Then cstring is "string2"
|
90
|
+
* Then cfloat is "2.0"
|
91
|
+
* Then cinteger is "2"
|
92
|
+
#
|
93
|
+
* Then command is "first"
|
94
|
+
|
95
|
+
Scenario: "--string --float --integer second --string=string3 --float=3.0 --integer=3"
|
96
|
+
* Given options "--string --float --integer second --string=string3 --float=3.0 --integer=3"
|
97
|
+
* When we run command
|
98
|
+
#
|
99
|
+
* Then string is "string2"
|
100
|
+
* Then float is "2.0"
|
101
|
+
* Then integer is "2"
|
102
|
+
#
|
103
|
+
* Then cstring is "string3"
|
104
|
+
* Then cfloat is "3.0"
|
105
|
+
* Then cinteger is "3"
|
106
|
+
#
|
107
|
+
* Then command is "second"
|
108
|
+
|
109
|
+
Scenario: "--string=1 second"
|
110
|
+
* Given options "--string=1 second"
|
111
|
+
* When we run command
|
112
|
+
* Then exception is "HELP_PARSER::UsageError"
|
113
|
+
* Then message matches "^type error.*String"
|
114
|
+
|
115
|
+
Scenario: "second --integer=string"
|
116
|
+
* Given options "second --integer=string"
|
117
|
+
* When we run command
|
118
|
+
* Then exception is "HELP_PARSER::UsageError"
|
119
|
+
* Then message matches "^type error.*Fixnum"
|
120
|
+
|
121
|
+
Scenario: "first --float=1"
|
122
|
+
* Given options "first --float=1"
|
123
|
+
* When we run command
|
124
|
+
* Then exception is "HELP_PARSER::UsageError"
|
125
|
+
* Then message matches "^type error.*Float"
|
126
|
+
|
127
|
+
Scenario: "--array=seven first"
|
128
|
+
* Given options "--array=seven first"
|
129
|
+
* When we run command
|
130
|
+
* Then exception is "HELP_PARSER::UsageError"
|
131
|
+
* Then message matches "^type error.*Array"
|
data/help_parser.gemspec
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
|
3
|
+
s.name = 'help_parser'
|
4
|
+
s.version = '1.0.2'
|
5
|
+
|
6
|
+
s.homepage = 'https://github.com/carlosjhr64/help_parser'
|
7
|
+
|
8
|
+
s.author = 'CarlosJHR64'
|
9
|
+
s.email = 'carlosjhr64@gmail.com'
|
10
|
+
|
11
|
+
s.date = '2013-12-13'
|
12
|
+
s.licenses = ['MIT']
|
13
|
+
|
14
|
+
s.description = <<DESCRIPTION
|
15
|
+
_HelpParser_ - Parses your help text to define your command line options.
|
16
|
+
DESCRIPTION
|
17
|
+
|
18
|
+
s.summary = <<SUMMARY
|
19
|
+
_HelpParser_ - Parses your help text to define your command line options.
|
20
|
+
SUMMARY
|
21
|
+
|
22
|
+
s.extra_rdoc_files = ['README.rdoc']
|
23
|
+
s.rdoc_options = ["--main", "README.rdoc"]
|
24
|
+
|
25
|
+
s.require_paths = ["lib"]
|
26
|
+
s.files = %w(
|
27
|
+
History.txt
|
28
|
+
Manifest.txt
|
29
|
+
README.rdoc
|
30
|
+
Rakefile
|
31
|
+
TODO.txt
|
32
|
+
examples/command_suite
|
33
|
+
examples/implementation_error_duplicate_key
|
34
|
+
examples/implementation_error_redefinition
|
35
|
+
examples/implementation_error_terminal
|
36
|
+
examples/simple
|
37
|
+
features/edge.feature
|
38
|
+
features/simple.feature
|
39
|
+
features/step_definitions/help_parser_steps.rb
|
40
|
+
features/suite.feature
|
41
|
+
help_parser.gemspec
|
42
|
+
lib/help_parser.rb
|
43
|
+
lib/help_parser/help_parser.rb
|
44
|
+
lib/help_parser/version.rb
|
45
|
+
tasks/gemspecker.rb
|
46
|
+
tasks/manifester.rb
|
47
|
+
tasks/syntaxer.rb
|
48
|
+
tasks/unit_tests.rb
|
49
|
+
test/help_parser.rb
|
50
|
+
)
|
51
|
+
|
52
|
+
end
|
data/lib/help_parser.rb
ADDED
@@ -0,0 +1,285 @@
|
|
1
|
+
module HELP_PARSER
|
2
|
+
|
3
|
+
# **UsageError** is an error when the user passed command
|
4
|
+
# line options that are inconsistent with expectations.
|
5
|
+
# The raise in a class method is as follows:
|
6
|
+
# raise UsageError, [message, self]
|
7
|
+
# The object that raised the error will be available as:
|
8
|
+
# $!.obj
|
9
|
+
class UsageError < StandardError
|
10
|
+
attr_accessor :obj
|
11
|
+
def initialize(array)
|
12
|
+
self.obj = array.pop
|
13
|
+
msg = array.pop
|
14
|
+
super(msg)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
# **ImplementationError** is an error due to a
|
18
|
+
# inconsistent help text. The raise in a class method is
|
19
|
+
# as follows:
|
20
|
+
# raise ImplementationError, [message, self]
|
21
|
+
# The object that raised the error will be available as:
|
22
|
+
# $!.obj
|
23
|
+
class ImplementationError < StandardError
|
24
|
+
attr_accessor :obj
|
25
|
+
def initialize(array)
|
26
|
+
self.obj = array.pop
|
27
|
+
msg = array.pop
|
28
|
+
super(msg)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# UsageException is an actual exception to normal flow.
|
33
|
+
# For example, when a user asks for help or version
|
34
|
+
# number in the command line options. The raise in a
|
35
|
+
# class method is as follows:
|
36
|
+
# raise UsageException, [message, self]
|
37
|
+
# The object that raised the exception will be available
|
38
|
+
# as:
|
39
|
+
# $!.obj
|
40
|
+
class UsageException < Exception
|
41
|
+
attr_accessor :obj
|
42
|
+
def initialize(array)
|
43
|
+
self.obj = array.pop
|
44
|
+
msg = array.pop
|
45
|
+
super(msg)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# HelpException is a type of UsageException, where the
|
50
|
+
# user asks for help in the command line options. The
|
51
|
+
# raise in a class method is as follows:
|
52
|
+
# raise HelpException, self
|
53
|
+
# The object that raised the exception will be available
|
54
|
+
# as:
|
55
|
+
# $!.obj
|
56
|
+
# The message will be the help text:
|
57
|
+
# $!.message == $!.obj.help # => true
|
58
|
+
class HelpException < UsageException
|
59
|
+
def initialize(help_parser)
|
60
|
+
super([help_parser.help, help_parser])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# VersionException is a type of UsageException, where the
|
65
|
+
# user asks for version in the command line options. The
|
66
|
+
# raise in a class method is as follows:
|
67
|
+
# raise VersionException, self
|
68
|
+
# The object that raised the exception will be available
|
69
|
+
# as:
|
70
|
+
# $!.obj
|
71
|
+
# The message will be the version text:
|
72
|
+
# $!.message == $!.obj.version # => true
|
73
|
+
class VersionException < UsageException
|
74
|
+
def initialize(help_parser)
|
75
|
+
super([help_parser.version, help_parser])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
module Errors
|
80
|
+
# HelpParser#usage_error will have make the HelpParser
|
81
|
+
# object raise a UsageError error with the given message.
|
82
|
+
def usage_error(msg)
|
83
|
+
raise UsageError, [msg, self]
|
84
|
+
end
|
85
|
+
|
86
|
+
# HelpParser#implementation_error will have make the
|
87
|
+
# HelpParser object raise a UsageError error with the
|
88
|
+
# given message.
|
89
|
+
def implementation_error(msg)
|
90
|
+
raise ImplementationError, [msg, self]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class HelpParser < Hash
|
95
|
+
include Errors
|
96
|
+
|
97
|
+
# I'm wrapping class functions in a Function module.
|
98
|
+
module Function
|
99
|
+
|
100
|
+
# I'm wrapping Function's helper functions in a Private
|
101
|
+
# module.
|
102
|
+
module Private
|
103
|
+
# HelpParser::Function::Private.to_value
|
104
|
+
def to_value(string)
|
105
|
+
case string
|
106
|
+
when /^-?\d+$/
|
107
|
+
return string.to_i
|
108
|
+
when /^-?\d+\.\d+$/
|
109
|
+
return string.to_f
|
110
|
+
when /,/
|
111
|
+
return string.split(',').map{|s| to_value(s)}
|
112
|
+
end
|
113
|
+
return string
|
114
|
+
end
|
115
|
+
end
|
116
|
+
extend Private
|
117
|
+
|
118
|
+
# HelpParser::Function.str2sym converts String keys to
|
119
|
+
# Symbol. Symbol keys can have '-', but it's cumbersome
|
120
|
+
# notation:
|
121
|
+
# :"a-b"
|
122
|
+
# So I translate it to underscore:
|
123
|
+
# :a_b
|
124
|
+
def str2sym(str)
|
125
|
+
str.gsub('-','_').to_sym
|
126
|
+
end
|
127
|
+
|
128
|
+
# HelpParser#parse parses the @argv for command line
|
129
|
+
# options, shifting them out, and passes on the key-value
|
130
|
+
# pairs to the block. The keys are translated to their
|
131
|
+
# short version if available, and the values are converted
|
132
|
+
# to their best interpreted form as per Funtion.to_value.
|
133
|
+
# Furthermore, no-key values are negated.
|
134
|
+
def parse(argv, obj, &block)
|
135
|
+
while argv[0] and argv[0][0]=='-'
|
136
|
+
option = argv.shift[1..-1]
|
137
|
+
if option[0]=='-'
|
138
|
+
loption, value = option[1..-1].split('=', 2)
|
139
|
+
if loption=~/no-(\w.*)$/
|
140
|
+
loption = $1
|
141
|
+
if value
|
142
|
+
raise UsageError, ["no-key should not have value: no-#{loption}=#{value}", obj]
|
143
|
+
end
|
144
|
+
value = false
|
145
|
+
end
|
146
|
+
if value.nil?
|
147
|
+
value = true
|
148
|
+
block.call loption, value
|
149
|
+
else
|
150
|
+
block.call loption, Function.to_value(value)
|
151
|
+
end
|
152
|
+
else
|
153
|
+
# These are single character options...
|
154
|
+
option.each_char do |c|
|
155
|
+
block.call c, true
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
extend Function
|
162
|
+
|
163
|
+
def parse(&block)
|
164
|
+
HelpParser.parse(@argv, self, &block)
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
# HelpParser.new(version, help, argv=ARGV, dict={})
|
169
|
+
# Not necessarily in that order, see the code.
|
170
|
+
attr_reader :version, :help, :dict
|
171
|
+
def initialize(*parameters)
|
172
|
+
super()
|
173
|
+
|
174
|
+
# Figure out what parameters you were given...
|
175
|
+
@argv = parameters.detect{|parameter| parameter.class == Array} || ARGV
|
176
|
+
@dict = parameters.detect{|parameter| parameter.class == Hash } || {}
|
177
|
+
parameters = parameters.select{|parameter| parameter.class == String}
|
178
|
+
@version = parameters.shift
|
179
|
+
@help = parameters.pop
|
180
|
+
|
181
|
+
raise "Need help" unless @help
|
182
|
+
raise "Need version" unless @version
|
183
|
+
|
184
|
+
# We ensure the following two mappings...
|
185
|
+
@dict[:help] = :h
|
186
|
+
@dict[:version] = :v
|
187
|
+
|
188
|
+
# Using the help for the options definition
|
189
|
+
@help.split("\n").select{|l|
|
190
|
+
(l=~/^\s*-/)
|
191
|
+
}.map{|l|
|
192
|
+
l.strip.split(/[,\s]+/)[0..1].select{|o|
|
193
|
+
o[0]=='-'
|
194
|
+
}.map{|o|
|
195
|
+
HelpParser.str2sym o.sub(/--?/,'').sub(/=.*$/,'')
|
196
|
+
}.reverse
|
197
|
+
}.each do |k,v|
|
198
|
+
unless @dict[k].nil?
|
199
|
+
# This is an implementation error. The help text is inconsistent.
|
200
|
+
# Note that we may have been given @dict, and we're verifying the help.
|
201
|
+
implementation_error "#{k} redefinition from #{@dict[k]} to #{v}." unless @dict[k]==v
|
202
|
+
end
|
203
|
+
@dict[k]=v
|
204
|
+
end
|
205
|
+
|
206
|
+
# Values must be terminal states, that is for example:
|
207
|
+
# if :version => :v, then :v => nil.
|
208
|
+
terminals = []
|
209
|
+
@dict.each do |k,v|
|
210
|
+
next if v.nil?
|
211
|
+
if @dict.has_key?(v)
|
212
|
+
unless @dict[v].nil?
|
213
|
+
implementation_error "Expected terminal #{v}: #{k} => #{v} => #{@dict[v]}"
|
214
|
+
end
|
215
|
+
else
|
216
|
+
terminals.push(v)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
terminals.each{|v| @dict[v] = nil}
|
220
|
+
|
221
|
+
# Parsing command line options
|
222
|
+
parse do |opt, value|
|
223
|
+
key = HelpParser.str2sym opt
|
224
|
+
# Translate key to the terminal key.
|
225
|
+
key = @dict[key] unless @dict[key].nil?
|
226
|
+
# Ensure user only uses the defined keys.
|
227
|
+
usage_error "not a valid option: #{opt}" unless @dict.has_key?(key)
|
228
|
+
# As we parse ARGV, we should only be setting keys once.
|
229
|
+
usage_error "duplicate command line option: #{opt}" if self.has_key?(key)
|
230
|
+
self[key] = value
|
231
|
+
end
|
232
|
+
|
233
|
+
# The parse takes all options to its terminal value.
|
234
|
+
# Now set the synonims...
|
235
|
+
@dict.each do |synonim, terminal|
|
236
|
+
next if terminal.nil?
|
237
|
+
if self.has_key?(terminal)
|
238
|
+
# I did not mean for the synonyms to have been set yet, so if I do,
|
239
|
+
# I have a bug somewhere.
|
240
|
+
raise "unexpected key overwrite" if self.has_key?(synonim)
|
241
|
+
self[synonim] = self[terminal]
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Did the user ask for help or version?
|
246
|
+
raise HelpException, self if self[:h]==true
|
247
|
+
raise VersionException, self if self[:v]==true
|
248
|
+
end
|
249
|
+
|
250
|
+
# HelpParser#defaults is a helper method to []
|
251
|
+
def defaults(symbol, default1, default2=default1)
|
252
|
+
type = (default2 || default1).class
|
253
|
+
a = self[symbol]
|
254
|
+
value = (a.nil?)? default1 : a
|
255
|
+
value = default2 if value==true
|
256
|
+
# !value is true if value is false or nil.
|
257
|
+
usage_error "type error: #{symbol} needs to be #{type}" unless !value or value.class == type
|
258
|
+
return value
|
259
|
+
end
|
260
|
+
|
261
|
+
# HelpParser#defaults! is used to replace value with a
|
262
|
+
# default in the hash, self.
|
263
|
+
def defaults!(symbol, default1, default2)
|
264
|
+
self[symbol] = defaults(symbol, default1, default2)
|
265
|
+
end
|
266
|
+
|
267
|
+
# HelpParser#[k, default1=nil, default2=nil]
|
268
|
+
# Basically, for #[k, default1, default2]:
|
269
|
+
# value = help_parser[k]
|
270
|
+
# case value
|
271
|
+
# when nil then default1
|
272
|
+
# when true then default2
|
273
|
+
# else value
|
274
|
+
# end
|
275
|
+
# For #[k, default1]:
|
276
|
+
# value = help_parser[k] || default1
|
277
|
+
# For #[k]:
|
278
|
+
# help_parser[k]
|
279
|
+
# Raises a UsageError if the value is not of the expected
|
280
|
+
# type as given by the last default.
|
281
|
+
def [](k, *defaults12)
|
282
|
+
(defaults12.length==0)? super(k) : defaults(k, *defaults12)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|