options_by_example 1.2.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/options_by_example/parser.rb +106 -0
- data/lib/options_by_example/version.rb +11 -1
- data/lib/options_by_example.rb +50 -86
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb87e6b0c01a7f99d70d600c6adc142d96cae3fc18b122c2259c926504fb198b
|
4
|
+
data.tar.gz: e21e7dd4f0a4a77fb7e2a84b4edc77293023730388e0fcbf4abe2d0e5344d5c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7e5b59dc4a4b9ad41edeaadaf98fa3ee23abe4f262cfbb9ba93752dba66ff5868b704024a3d534b43386977caefe6a4eecea4a5e80c22f5c351f679ec2488bc3
|
7
|
+
data.tar.gz: 7ecce96f859a83e3dc702740c3d5e72c957d44a42387dff4a70db249bc54fc1df45484bf5ff099e9bbd66d9dae06a548d179f107aa5f6cb87395a36a54551bc0
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
class OptionsByExample
|
5
|
+
|
6
|
+
class PrintUsageMessage < StandardError
|
7
|
+
end
|
8
|
+
|
9
|
+
class Parser
|
10
|
+
|
11
|
+
attr_reader :options
|
12
|
+
attr_reader :arguments
|
13
|
+
|
14
|
+
def initialize(argument_names_required, argument_names_optional, option_names)
|
15
|
+
@argument_names_required = argument_names_required
|
16
|
+
@argument_names_optional = argument_names_optional
|
17
|
+
@option_names = option_names
|
18
|
+
|
19
|
+
@arguments = {}
|
20
|
+
@options = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse(array)
|
24
|
+
|
25
|
+
# Separate command-line options and their respective arguments into
|
26
|
+
# chunks plus handling any remaining arguments. This organization
|
27
|
+
# facilitates further processing and validation of the input.
|
28
|
+
|
29
|
+
@chunks = []
|
30
|
+
@remainder = current = []
|
31
|
+
array.each do |each|
|
32
|
+
@chunks << current = [] if each.start_with?(?-)
|
33
|
+
current << each
|
34
|
+
end
|
35
|
+
|
36
|
+
find_help_option
|
37
|
+
find_unknown_options
|
38
|
+
parse_options
|
39
|
+
|
40
|
+
validate_number_of_arguments
|
41
|
+
parse_required_arguments
|
42
|
+
parse_optional_arguments
|
43
|
+
|
44
|
+
raise "Internal error: unreachable state" unless @remainder.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def find_help_option
|
50
|
+
@chunks.each do |option, *args|
|
51
|
+
case option
|
52
|
+
when '-h', '--help'
|
53
|
+
raise PrintUsageMessage
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_unknown_options
|
59
|
+
@chunks.each do |option, *args|
|
60
|
+
raise "Found unknown option '#{option}'" unless @option_names.include?(option)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_options
|
65
|
+
@chunks.each do |option, *args|
|
66
|
+
if @remainder.any?
|
67
|
+
raise "Unexpected arguments found before option '#{option}', please provide all options before arguments"
|
68
|
+
end
|
69
|
+
|
70
|
+
option_name, argument_name = @option_names[option]
|
71
|
+
@options[option_name] = true
|
72
|
+
|
73
|
+
if argument_name
|
74
|
+
raise "Expected argument for option '#{option}', got none" if args.empty?
|
75
|
+
@arguments[option_name] = args.shift
|
76
|
+
end
|
77
|
+
|
78
|
+
@remainder = args
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def validate_number_of_arguments
|
83
|
+
min_length = @argument_names_required.size
|
84
|
+
max_length = @argument_names_optional.size + min_length
|
85
|
+
if @remainder.size > max_length
|
86
|
+
range = [min_length, max_length].uniq.join(?-)
|
87
|
+
raise "Expected #{range} arguments, but received too many"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse_required_arguments
|
92
|
+
stash = @remainder.pop(@argument_names_required.length)
|
93
|
+
@argument_names_required.each do |argument_name|
|
94
|
+
raise "Missing required argument '#{argument_name}'" if stash.empty?
|
95
|
+
@arguments[argument_name] = stash.shift
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_optional_arguments
|
100
|
+
@argument_names_optional.each do |argument_name|
|
101
|
+
break if @remainder.empty?
|
102
|
+
@arguments[argument_name] = @remainder.shift
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class OptionsByExample
|
4
|
-
VERSION = '
|
4
|
+
VERSION = '2.0.0'
|
5
5
|
end
|
6
6
|
|
7
7
|
|
@@ -11,6 +11,16 @@ __END__
|
|
11
11
|
# Minor version bump when backward-compatible changes or enhancements
|
12
12
|
# Patch version bump when backward-compatible bug fixes, security updates etc
|
13
13
|
|
14
|
+
2.0.0
|
15
|
+
|
16
|
+
- Replaced dynamic methods with explicit methods for options and arguments
|
17
|
+
- Removed ability to call dynamic methods with undeclared names
|
18
|
+
|
19
|
+
1.3.0
|
20
|
+
|
21
|
+
- Extracted parsing functionality into class
|
22
|
+
- Better error messages
|
23
|
+
|
14
24
|
1.2.0
|
15
25
|
|
16
26
|
- Ensure compatibility with Ruby versions 1.9.3 and newer
|
data/lib/options_by_example.rb
CHANGED
@@ -1,38 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'options_by_example/version'
|
4
|
+
require 'options_by_example/parser'
|
4
5
|
|
5
6
|
|
6
7
|
class OptionsByExample
|
7
8
|
|
8
|
-
attr_reader :argument_names_optional
|
9
|
-
attr_reader :argument_names_required
|
10
|
-
attr_reader :option_names
|
11
|
-
|
12
9
|
attr_reader :arguments
|
13
10
|
attr_reader :options
|
14
11
|
|
15
|
-
|
16
12
|
def self.read(data)
|
17
13
|
return new data.read
|
18
14
|
end
|
19
15
|
|
20
16
|
def initialize(text)
|
21
|
-
@
|
17
|
+
@usage_message = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
|
22
18
|
|
23
|
-
#
|
19
|
+
# --- 1) Parse argument names -------------------------------------
|
24
20
|
#
|
25
21
|
# Parse the usage string and extract both optional argument names
|
26
22
|
# and required argument names, for example:
|
27
23
|
#
|
28
24
|
# Usage: connect [options] [mode] host port
|
29
25
|
|
30
|
-
text =~ /Usage: (\w+|\$0) \[options\](( \[\w+\])*)(( \w+)*)/
|
26
|
+
text =~ /Usage: (\w+|\$0)( \[options\])?(( \[\w+\])*)(( \w+)*)/
|
31
27
|
raise RuntimeError, "Expected usage string, got none" unless $1
|
32
|
-
@argument_names_optional = $
|
33
|
-
@argument_names_required = $
|
28
|
+
@argument_names_optional = $3.to_s.split.map { |match| match.tr('[]', '').downcase }
|
29
|
+
@argument_names_required = $5.to_s.split.map(&:downcase)
|
34
30
|
|
35
|
-
#
|
31
|
+
# --- 2) Parse option names ---------------------------------------
|
36
32
|
#
|
37
33
|
# Parse the usage message and extract option names, their short and
|
38
34
|
# long forms, and the associated argument name (if any), eg:
|
@@ -46,93 +42,61 @@ class OptionsByExample
|
|
46
42
|
@option_names = {}
|
47
43
|
text.scan(/((--?\w+)(, --?\w+)*) ?(\w+)?/) do
|
48
44
|
opts = $1.split(", ")
|
49
|
-
opts.each { |each| @option_names[each] = [opts.last.tr('-', ''),
|
45
|
+
opts.each { |each| @option_names[each] = [opts.last.tr('-', ''), $4] }
|
50
46
|
end
|
51
47
|
|
52
|
-
|
53
|
-
|
54
|
-
@option_names.update("-h" => :help, "--help" => :help)
|
48
|
+
initialize_argument_accessors
|
49
|
+
initialize_option_accessors
|
55
50
|
end
|
56
51
|
|
57
|
-
def parse(argv
|
58
|
-
|
59
|
-
|
60
|
-
@
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
break unless array.first.start_with?(?-)
|
67
|
-
most_recent_option = option = array.shift
|
68
|
-
option_name, argument_name = @option_names[option]
|
69
|
-
raise "Got unknown option #{option}" if option_name.nil?
|
70
|
-
raise if option_name == :help # Show usage without error message
|
71
|
-
@options[option_name] = true
|
72
|
-
|
73
|
-
# Consume argument, if expected by most recent option
|
74
|
-
if argument_name
|
75
|
-
argument = array.shift
|
76
|
-
raise "Expected argument for option #{option}" unless /^[^-]/ === argument
|
77
|
-
@arguments[option_name] = argument
|
78
|
-
most_recent_option = nil
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
# --- 2) Parse optional arguments ---------------------------------
|
83
|
-
|
84
|
-
# Check any start with --, ie excess options
|
85
|
-
# Check min_length - max_length here
|
86
|
-
|
87
|
-
stash = array.pop(@argument_names_required.length)
|
88
|
-
@argument_names_optional.each do |argument_name|
|
89
|
-
break if array.empty?
|
90
|
-
argument = array.shift
|
91
|
-
raise "Expected more arguments, got option #{option}" unless /^[^-]/ === argument
|
92
|
-
@arguments[argument_name] = argument
|
93
|
-
end
|
94
|
-
|
95
|
-
# --- 3) Parse required arguments ---------------------------------
|
96
|
-
|
97
|
-
@argument_names_required.each do |argument_name|
|
98
|
-
raise "Expected required argument #{argument_name.upcase}, got none" if stash.empty?
|
99
|
-
argument = stash.shift
|
100
|
-
raise "Expected more arguments, got option #{option}" unless /^[^-]/ === argument
|
101
|
-
@arguments[argument_name] = argument
|
102
|
-
end
|
52
|
+
def parse(argv)
|
53
|
+
parse_without_exit argv
|
54
|
+
rescue PrintUsageMessage
|
55
|
+
puts @usage_message
|
56
|
+
exit 0
|
57
|
+
rescue RuntimeError => err
|
58
|
+
puts "ERROR: #{err.message}"
|
59
|
+
exit 1
|
60
|
+
end
|
103
61
|
|
104
|
-
|
62
|
+
def parse_without_exit(argv)
|
63
|
+
parser = Parser.new(
|
64
|
+
@argument_names_required,
|
65
|
+
@argument_names_optional,
|
66
|
+
@option_names,
|
67
|
+
)
|
105
68
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
min_length = @argument_names_required.size
|
110
|
-
max_length = @argument_names_optional.size + min_length
|
111
|
-
raise "Expected #{min_length}#{"-#{max_length}" if max_length > min_length} arguments, got more"
|
112
|
-
end
|
69
|
+
parser.parse argv
|
70
|
+
@arguments = parser.arguments
|
71
|
+
@options = parser.options
|
113
72
|
|
114
73
|
return self
|
74
|
+
end
|
115
75
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
76
|
+
private
|
77
|
+
|
78
|
+
def initialize_argument_accessors
|
79
|
+
[
|
80
|
+
*@argument_names_required,
|
81
|
+
*@argument_names_optional,
|
82
|
+
*@option_names.values.select(&:last).map(&:first),
|
83
|
+
].each do |argument_name|
|
84
|
+
instance_eval %{
|
85
|
+
def argument_#{argument_name}
|
86
|
+
val = @arguments["#{argument_name}"]
|
87
|
+
val && block_given? ? (yield val) : val
|
88
|
+
end
|
89
|
+
}
|
124
90
|
end
|
125
91
|
end
|
126
92
|
|
127
|
-
def
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
else
|
135
|
-
super
|
93
|
+
def initialize_option_accessors
|
94
|
+
@option_names.each_value do |option_name, _|
|
95
|
+
instance_eval %{
|
96
|
+
def include_#{option_name}?
|
97
|
+
@options.include? "#{option_name}"
|
98
|
+
end
|
99
|
+
}
|
136
100
|
end
|
137
101
|
end
|
138
102
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: options_by_example
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adrian Kuhn
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-04-
|
11
|
+
date: 2023-04-30 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -19,6 +19,7 @@ extra_rdoc_files: []
|
|
19
19
|
files:
|
20
20
|
- README.md
|
21
21
|
- lib/options_by_example.rb
|
22
|
+
- lib/options_by_example/parser.rb
|
22
23
|
- lib/options_by_example/version.rb
|
23
24
|
homepage: https://github.com/akuhn/options_by_example
|
24
25
|
licenses:
|
@@ -26,6 +27,7 @@ licenses:
|
|
26
27
|
metadata:
|
27
28
|
homepage_uri: https://github.com/akuhn/options_by_example
|
28
29
|
source_code_uri: https://github.com/akuhn/options_by_example
|
30
|
+
changelog_uri: https://github.com/akuhn/options_by_example/blob/master/lib/options_by_example/version.rb
|
29
31
|
post_install_message:
|
30
32
|
rdoc_options: []
|
31
33
|
require_paths:
|