options_by_example 1.2.0 → 2.0.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.
- 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:
|