options_by_example 1.3.0 → 3.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/README.md +13 -0
- data/lib/options_by_example/parser.rb +75 -9
- data/lib/options_by_example/version.rb +14 -2
- data/lib/options_by_example.rb +45 -38
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98c16609314706667d2b776fa71de92ca874b2fcc803ea8f80387fe4f0245d9a
|
4
|
+
data.tar.gz: 8cc7023f98016edaa5521d6184985fb4133135e902f0380f81e233b6f52bfb97
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4e54eeb9f5beef88733ac4c70055570eeff930ccacd287f88b80905bd1760b6b1ec3513c5ea144d1c7cdc259af7d4991f81c5be82249cecb2f939d291d29032
|
7
|
+
data.tar.gz: 9126c11cba93c504fd41939dfa24c62578889b8546b9f472091a9463eed4cc6e3f409ff7e40a8219ce9a70913ae63872486745629431290e8f87dec6edf351be
|
data/README.md
CHANGED
@@ -9,6 +9,19 @@ Features
|
|
9
9
|
- Parses those arguments and options from the command line (ARGV)
|
10
10
|
- Raises errors for unknown options or missing required arguments
|
11
11
|
|
12
|
+
Installation
|
13
|
+
|
14
|
+
To use options_by_example, first install the gem by running:
|
15
|
+
|
16
|
+
```
|
17
|
+
gem install options_by_example
|
18
|
+
```
|
19
|
+
|
20
|
+
Alternatively, add this line to your Gemfile and run bundle install:
|
21
|
+
|
22
|
+
```
|
23
|
+
gem 'options_by_example'
|
24
|
+
```
|
12
25
|
|
13
26
|
Example
|
14
27
|
|
@@ -1,25 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'date'
|
4
|
+
require 'time'
|
5
|
+
|
3
6
|
|
4
7
|
class OptionsByExample
|
8
|
+
|
9
|
+
class PrintUsageMessage < StandardError
|
10
|
+
end
|
11
|
+
|
5
12
|
class Parser
|
6
13
|
|
7
14
|
attr_reader :options
|
8
15
|
attr_reader :arguments
|
9
16
|
|
10
|
-
def initialize(argument_names_required, argument_names_optional, option_names)
|
17
|
+
def initialize(argument_names_required, argument_names_optional, default_values, option_names)
|
11
18
|
@argument_names_required = argument_names_required
|
12
19
|
@argument_names_optional = argument_names_optional
|
20
|
+
@default_values = default_values
|
13
21
|
@option_names = option_names
|
14
22
|
|
15
|
-
@arguments =
|
23
|
+
@arguments = @default_values.dup
|
16
24
|
@options = {}
|
17
25
|
end
|
18
26
|
|
19
27
|
def parse(array)
|
20
28
|
|
21
29
|
# Separate command-line options and their respective arguments into
|
22
|
-
# chunks plus
|
30
|
+
# chunks, plus tracking leading excess arguments. This organization
|
23
31
|
# facilitates further processing and validation of the input.
|
24
32
|
|
25
33
|
@chunks = []
|
@@ -29,8 +37,9 @@ class OptionsByExample
|
|
29
37
|
current << each
|
30
38
|
end
|
31
39
|
|
32
|
-
|
33
|
-
|
40
|
+
detect_help_option
|
41
|
+
flatten_stacked_shorthand_options
|
42
|
+
detect_unknown_options
|
34
43
|
parse_options
|
35
44
|
|
36
45
|
validate_number_of_arguments
|
@@ -42,16 +51,45 @@ class OptionsByExample
|
|
42
51
|
|
43
52
|
private
|
44
53
|
|
45
|
-
def
|
54
|
+
def detect_help_option
|
46
55
|
@chunks.each do |option, *args|
|
47
56
|
case option
|
48
57
|
when '-h', '--help'
|
49
|
-
raise
|
58
|
+
raise PrintUsageMessage
|
50
59
|
end
|
51
60
|
end
|
52
61
|
end
|
53
62
|
|
54
|
-
def
|
63
|
+
def flatten_stacked_shorthand_options
|
64
|
+
|
65
|
+
# Expand any combined shorthand options like -svt into their
|
66
|
+
# separate components (-s, -v, and -t) and assigns any arguments
|
67
|
+
# to the last component. If an unknown shorthand is found, raise
|
68
|
+
# a helpful error message with suggestion if possible.
|
69
|
+
|
70
|
+
list = []
|
71
|
+
@chunks.each do |option, *args|
|
72
|
+
if option =~ /^-([a-zA-Z]{2,})$/
|
73
|
+
shorthands = $1.each_char.map { |char| "-#{char}" }
|
74
|
+
|
75
|
+
shorthands.each do |each|
|
76
|
+
if not @option_names.include?(each)
|
77
|
+
did_you_mean = ", did you mean '-#{option}'?" if @option_names.include?("-#{option}")
|
78
|
+
raise "Found unknown option #{each} inside '#{option}'#{did_you_mean}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
list.concat shorthands.map { |each| [each] }
|
83
|
+
list.last.concat args
|
84
|
+
else
|
85
|
+
list << [option, *args]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
@chunks = list
|
90
|
+
end
|
91
|
+
|
92
|
+
def detect_unknown_options
|
55
93
|
@chunks.each do |option, *args|
|
56
94
|
raise "Found unknown option '#{option}'" unless @option_names.include?(option)
|
57
95
|
end
|
@@ -68,7 +106,28 @@ class OptionsByExample
|
|
68
106
|
|
69
107
|
if argument_name
|
70
108
|
raise "Expected argument for option '#{option}', got none" if args.empty?
|
71
|
-
|
109
|
+
value = args.shift
|
110
|
+
|
111
|
+
begin
|
112
|
+
case argument_name
|
113
|
+
when 'NUM'
|
114
|
+
expected_type = 'an integer value'
|
115
|
+
value = Integer value
|
116
|
+
when 'DATE'
|
117
|
+
expected_type = 'a date (e.g. YYYY-MM-DD)'
|
118
|
+
value = Date.parse value
|
119
|
+
when 'TIME'
|
120
|
+
expected_type = 'a timestamp (e.g. HH:MM:SS)'
|
121
|
+
value = Time.parse value
|
122
|
+
end
|
123
|
+
rescue ArgumentError
|
124
|
+
raise "Invalid argument \"#{value}\" for option '#{option}', please provide #{expected_type}"
|
125
|
+
end
|
126
|
+
|
127
|
+
@arguments[option_name] = value
|
128
|
+
@option_took_argument = option
|
129
|
+
else
|
130
|
+
@option_took_argument = nil
|
72
131
|
end
|
73
132
|
|
74
133
|
@remainder = args
|
@@ -78,10 +137,17 @@ class OptionsByExample
|
|
78
137
|
def validate_number_of_arguments
|
79
138
|
min_length = @argument_names_required.size
|
80
139
|
max_length = @argument_names_optional.size + min_length
|
140
|
+
|
81
141
|
if @remainder.size > max_length
|
82
142
|
range = [min_length, max_length].uniq.join(?-)
|
83
143
|
raise "Expected #{range} arguments, but received too many"
|
84
144
|
end
|
145
|
+
|
146
|
+
if @remainder.size < min_length
|
147
|
+
too_few = @remainder.empty? ? 'none' : (@remainder.size == 1 ? 'only one' : 'too few')
|
148
|
+
remark = " (considering #{@option_took_argument} takes an argument)" if @option_took_argument
|
149
|
+
raise "Expected #{min_length} required arguments, but received #{too_few}#{remark}"
|
150
|
+
end
|
85
151
|
end
|
86
152
|
|
87
153
|
def parse_required_arguments
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class OptionsByExample
|
4
|
-
VERSION = '
|
4
|
+
VERSION = '3.0.0'
|
5
5
|
end
|
6
6
|
|
7
7
|
|
@@ -11,9 +11,21 @@ __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
|
+
3.0.0
|
15
|
+
|
16
|
+
- Support options with default values
|
17
|
+
- Improved support for one-line usage messages
|
18
|
+
- Expand combined shorthand options into their separate components
|
19
|
+
- Shorthand options must be single letter only
|
20
|
+
|
21
|
+
2.0.0
|
22
|
+
|
23
|
+
- Replaced dynamic methods with explicit methods for options and arguments
|
24
|
+
- Removed ability to call dynamic methods with undeclared names
|
25
|
+
|
14
26
|
1.3.0
|
15
27
|
|
16
|
-
-
|
28
|
+
- Extracted parsing functionality into class
|
17
29
|
- Better error messages
|
18
30
|
|
19
31
|
1.2.0
|
data/lib/options_by_example.rb
CHANGED
@@ -6,20 +6,14 @@ require 'options_by_example/parser'
|
|
6
6
|
|
7
7
|
class OptionsByExample
|
8
8
|
|
9
|
-
attr_reader :argument_names_optional
|
10
|
-
attr_reader :argument_names_required
|
11
|
-
attr_reader :option_names
|
12
|
-
|
13
9
|
attr_reader :arguments
|
14
10
|
attr_reader :options
|
15
11
|
|
16
|
-
|
17
12
|
def self.read(data)
|
18
13
|
return new data.read
|
19
14
|
end
|
20
15
|
|
21
16
|
def initialize(text)
|
22
|
-
@settings = {exit_on_error: true}
|
23
17
|
@usage_message = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
|
24
18
|
|
25
19
|
# --- 1) Parse argument names -------------------------------------
|
@@ -29,10 +23,10 @@ class OptionsByExample
|
|
29
23
|
#
|
30
24
|
# Usage: connect [options] [mode] host port
|
31
25
|
|
32
|
-
text =~ /Usage: (
|
26
|
+
text =~ /Usage: (\$0|\w+)(?: \[options\])?((?: \[\w+\])*)((?: \w+)*)/
|
33
27
|
raise RuntimeError, "Expected usage string, got none" unless $1
|
34
|
-
@argument_names_optional = $
|
35
|
-
@argument_names_required = $
|
28
|
+
@argument_names_optional = $2.to_s.split.map { |match| match.tr('[]', '').downcase }
|
29
|
+
@argument_names_required = $3.to_s.split.map(&:downcase)
|
36
30
|
|
37
31
|
# --- 2) Parse option names ---------------------------------------
|
38
32
|
#
|
@@ -43,56 +37,69 @@ class OptionsByExample
|
|
43
37
|
# -s, --secure Use secure connection
|
44
38
|
# -v, --verbose Enable verbose output
|
45
39
|
# -r, --retries NUM Number of connection retries (default 3)
|
46
|
-
# -t, --timeout NUM
|
40
|
+
# -t, --timeout NUM Set connection timeout in seconds
|
47
41
|
|
48
42
|
@option_names = {}
|
49
|
-
|
50
|
-
|
51
|
-
|
43
|
+
@default_values = {}
|
44
|
+
text.scan(/(?:(-\w), ?)?(--(\w+))(?: (\w+))?(?:.*\(default:? (\w+)\))?/) do
|
45
|
+
flags = [$1, $2].compact
|
46
|
+
flags.each { |each| @option_names[each] = [$3, $4] }
|
47
|
+
@default_values[$3] = $5 if $5
|
52
48
|
end
|
53
|
-
end
|
54
49
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
return self
|
50
|
+
initialize_argument_accessors
|
51
|
+
initialize_option_accessors
|
59
52
|
end
|
60
53
|
|
61
54
|
def parse(argv)
|
55
|
+
parse_without_exit argv
|
56
|
+
rescue PrintUsageMessage
|
57
|
+
puts @usage_message
|
58
|
+
exit 0
|
59
|
+
rescue RuntimeError => err
|
60
|
+
puts "ERROR: #{err.message}"
|
61
|
+
exit 1
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_without_exit(argv)
|
62
65
|
parser = Parser.new(
|
63
66
|
@argument_names_required,
|
64
67
|
@argument_names_optional,
|
68
|
+
@default_values,
|
65
69
|
@option_names,
|
66
70
|
)
|
67
71
|
|
68
72
|
parser.parse argv
|
69
|
-
|
70
|
-
@options = parser.options
|
71
73
|
@arguments = parser.arguments
|
74
|
+
@options = parser.options
|
72
75
|
|
73
76
|
return self
|
74
|
-
|
75
|
-
raise unless @settings[:exit_on_error]
|
77
|
+
end
|
76
78
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
79
|
+
private
|
80
|
+
|
81
|
+
def initialize_argument_accessors
|
82
|
+
[
|
83
|
+
*@argument_names_required,
|
84
|
+
*@argument_names_optional,
|
85
|
+
*@option_names.values.select(&:last).map(&:first),
|
86
|
+
].each do |argument_name|
|
87
|
+
instance_eval %{
|
88
|
+
def argument_#{argument_name}
|
89
|
+
val = @arguments["#{argument_name}"]
|
90
|
+
val && block_given? ? (yield val) : val
|
91
|
+
end
|
92
|
+
}
|
81
93
|
end
|
82
|
-
|
83
|
-
exit 1
|
84
94
|
end
|
85
95
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
@options[$1]
|
94
|
-
else
|
95
|
-
super
|
96
|
+
def initialize_option_accessors
|
97
|
+
@option_names.each_value do |option_name, _|
|
98
|
+
instance_eval %{
|
99
|
+
def include_#{option_name}?
|
100
|
+
@options.include? "#{option_name}"
|
101
|
+
end
|
102
|
+
}
|
96
103
|
end
|
97
104
|
end
|
98
105
|
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: 3.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-
|
11
|
+
date: 2023-05-17 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -27,6 +27,7 @@ licenses:
|
|
27
27
|
metadata:
|
28
28
|
homepage_uri: https://github.com/akuhn/options_by_example
|
29
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
|
30
31
|
post_install_message:
|
31
32
|
rdoc_options: []
|
32
33
|
require_paths:
|