options_by_example 1.3.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|