options_by_example 3.2.0 → 3.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5533bd626a90e56af73f7fe32cbd2c0a79630da399242c98abce184ac084dd9
4
- data.tar.gz: 4583a92414c1e10544ffb42b4e45da0d1c0eac1adc1d38748bd31e24e791337e
3
+ metadata.gz: 8ced46483d8abae62122bb45cd0d4753408c4d93e08405a3409583ec9fcfb477
4
+ data.tar.gz: e8484baa8976c72154fb2da2509482d985501c08960e7e67fdf04731e307787b
5
5
  SHA512:
6
- metadata.gz: cd7df1507d066462d1031d71dc054fa886764dddca9fe17b4d6f7310f9e17f75673a6a6709e6ef1636d863dd35a537918d9e6838a04981cd947174d402569480
7
- data.tar.gz: 9f11d4fdae24faefc1069435683e65c648635568d54d19e3e8197040b721db3630be4aa779781403d26b5c2e04dd23b3b6bcc0e3bef11d405855ac07e89a0630
6
+ metadata.gz: 22f691bf1fc36e14868996b32b15de17a1760add666aa046e6dc57fb0e09f8dad75c7c18260255930c88ea3e56a1c994285dd320a659e6c9ee92fc4ae45d3f10
7
+ data.tar.gz: 1d54dca36e9829f2018af7493586e18ca7d590819d112e28482c93684212adc4ccd53e5c5183d49885c927f4ddb9e23c0654874ddbceb0e617f95c6fc6b96258
@@ -9,19 +9,18 @@ class OptionsByExample
9
9
  class PrintUsageMessage < StandardError
10
10
  end
11
11
 
12
- class Parser
12
+ class CommandlineParser
13
13
 
14
- attr_reader :options
15
- attr_reader :arguments
14
+ attr_reader :option_values
15
+ attr_reader :argument_values
16
16
 
17
- def initialize(argument_names_required, argument_names_optional, default_values, option_names)
18
- @argument_names_required = argument_names_required
19
- @argument_names_optional = argument_names_optional
20
- @default_values = default_values
21
- @option_names = option_names
17
+ def initialize(usage)
18
+ @argument_names = usage.argument_names
19
+ @default_values = usage.default_values
20
+ @option_names = usage.option_names
22
21
 
23
- @arguments = @default_values.dup
24
- @options = {}
22
+ @argument_values = @default_values.dup
23
+ @option_values = {}
25
24
  end
26
25
 
27
26
  def parse(array)
@@ -30,16 +29,17 @@ class OptionsByExample
30
29
  # chunks, plus tracking leading excess arguments. This organization
31
30
  # facilitates further processing and validation of the input.
32
31
 
33
- @chunks = []
32
+ @slices = []
34
33
  @remainder = current = []
35
34
  array.each do |each|
36
- @chunks << current = [] if each.start_with?(?-)
35
+ @slices << current = [] if each.start_with?(?-)
37
36
  current << each
38
37
  end
39
38
 
40
- detect_help_option
41
- flatten_stacked_shorthand_options
42
- detect_unknown_options
39
+ raise_if_help_option
40
+ unpack_combined_shorthand_options
41
+ expand_dash_number_to_dash_n_option
42
+ raise_if_unknown_options
43
43
  parse_options
44
44
 
45
45
  validate_number_of_arguments
@@ -51,8 +51,8 @@ class OptionsByExample
51
51
 
52
52
  private
53
53
 
54
- def detect_help_option
55
- @chunks.each do |option, *args|
54
+ def raise_if_help_option
55
+ @slices.each do |option, *args|
56
56
  case option
57
57
  when '-h', '--help'
58
58
  raise PrintUsageMessage
@@ -60,7 +60,7 @@ class OptionsByExample
60
60
  end
61
61
  end
62
62
 
63
- def flatten_stacked_shorthand_options
63
+ def unpack_combined_shorthand_options
64
64
 
65
65
  # Expand any combined shorthand options like -svt into their
66
66
  # separate components (-s, -v, and -t) and assigns any arguments
@@ -68,7 +68,7 @@ class OptionsByExample
68
68
  # a helpful error message with suggestion if possible.
69
69
 
70
70
  list = []
71
- @chunks.each do |option, *args|
71
+ @slices.each do |option, *args|
72
72
  if option =~ /^-([a-zA-Z]{2,})$/
73
73
  shorthands = $1.each_char.map { |char| "-#{char}" }
74
74
 
@@ -86,23 +86,31 @@ class OptionsByExample
86
86
  end
87
87
  end
88
88
 
89
- @chunks = list
89
+ @slices = list
90
90
  end
91
91
 
92
- def detect_unknown_options
93
- @chunks.each do |option, *args|
92
+ def expand_dash_number_to_dash_n_option
93
+ @slices.each do |each|
94
+ if each.first =~ /^-(\d+)$/
95
+ each[0..0] = ['-n', $1]
96
+ end
97
+ end
98
+ end
99
+
100
+ def raise_if_unknown_options
101
+ @slices.each do |option, *args|
94
102
  raise "Found unknown option '#{option}'" unless @option_names.include?(option)
95
103
  end
96
104
  end
97
105
 
98
106
  def parse_options
99
- @chunks.each do |option, *args|
107
+ @slices.each do |option, *args|
100
108
  if @remainder.any?
101
109
  raise "Unexpected arguments found before option '#{option}', please provide all options before arguments"
102
110
  end
103
111
 
104
112
  option_name, argument_name = @option_names[option]
105
- @options[option_name] = true
113
+ @option_values[option_name] = true
106
114
 
107
115
  if argument_name
108
116
  raise "Expected argument for option '#{option}', got none" if args.empty?
@@ -124,7 +132,7 @@ class OptionsByExample
124
132
  raise "Invalid argument \"#{value}\" for option '#{option}', please provide #{expected_type}"
125
133
  end
126
134
 
127
- @arguments[option_name] = value
135
+ @argument_values[option_name] = value
128
136
  @option_took_argument = option
129
137
  else
130
138
  @option_took_argument = nil
@@ -135,10 +143,14 @@ class OptionsByExample
135
143
  end
136
144
 
137
145
  def validate_number_of_arguments
138
- min_length = @argument_names_required.size
139
- max_length = @argument_names_optional.size + min_length
146
+ count_optional_arguments = @argument_names.values.count(:optional)
147
+ count_required_arguments = @argument_names.values.count(:required)
148
+ count_vararg_arguments = @argument_names.values.count(:vararg)
149
+
150
+ min_length = count_required_arguments + count_vararg_arguments
151
+ max_length = count_required_arguments + count_optional_arguments
140
152
 
141
- if @remainder.size > max_length
153
+ if @remainder.size > max_length && count_vararg_arguments == 0
142
154
  range = [min_length, max_length].uniq.join(?-)
143
155
  raise "Expected #{range} arguments, but received too many"
144
156
  end
@@ -151,17 +163,35 @@ class OptionsByExample
151
163
  end
152
164
 
153
165
  def parse_required_arguments
154
- stash = @remainder.pop(@argument_names_required.length)
155
- @argument_names_required.each do |argument_name|
156
- raise "Missing required argument '#{argument_name}'" if stash.empty?
157
- @arguments[argument_name] = stash.shift
166
+ if @argument_names.values.include?(:vararg)
167
+ remaining_arguments = @argument_names.length
168
+ @argument_names.each do |argument_name, arity|
169
+ raise "unreachable" if @remainder.empty?
170
+ remaining_arguments -= 1
171
+ case arity
172
+ when :required
173
+ @argument_values[argument_name] = @remainder.shift
174
+ when :vararg
175
+ @argument_values[argument_name] = @remainder.shift(@remainder.length - remaining_arguments)
176
+ else
177
+ raise "unreachable"
178
+ end
179
+ end
180
+ return
181
+ end
182
+
183
+ @argument_names.reverse_each do |argument_name, arity|
184
+ break if arity == :optional
185
+ raise "unreachable" if @remainder.empty?
186
+ @argument_values[argument_name] = @remainder.pop
158
187
  end
159
188
  end
160
189
 
161
190
  def parse_optional_arguments
162
- @argument_names_optional.each do |argument_name|
191
+ @argument_names.each do |argument_name, arity|
192
+ break unless arity == :optional
163
193
  break if @remainder.empty?
164
- @arguments[argument_name] = @remainder.shift
194
+ @argument_values[argument_name] = @remainder.shift
165
195
  end
166
196
  end
167
197
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OptionsByExample
4
+
5
+ class UsageSpecification
6
+
7
+ attr_reader :message
8
+ attr_reader :argument_names
9
+ attr_reader :default_values
10
+ attr_reader :option_names
11
+
12
+ def initialize(text)
13
+ @message = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
14
+
15
+ # --- 1) Parse argument names -------------------------------------
16
+ #
17
+ # Parse the usage string and extract both optional argument names
18
+ # and required argument names, for example:
19
+ #
20
+ # Usage: connect [options] [mode] host port
21
+
22
+ @argument_names = {}
23
+ inline_options = []
24
+
25
+ usage_line = text.lines.grep(/Usage:/).first
26
+ raise RuntimeError, "Expected usage string, got none" unless usage_line
27
+ tokens = usage_line.scan(/\[.*?\]|\w+ \.\.\.|\S+/)
28
+ raise unless tokens.shift == 'Usage:'
29
+ raise unless tokens.shift
30
+ tokens.shift if tokens.first == '[options]'
31
+
32
+ while /^\[(--?\w.*)\]$/ === tokens.first
33
+ inline_options << $1
34
+ tokens.shift
35
+ end
36
+
37
+ while /^\[(\w+)\]$/ === tokens.first
38
+ @argument_names[sanitize $1] = :optional
39
+ tokens.shift
40
+ end
41
+
42
+ while /^(\w+)( ?\.\.\.)?$/ === tokens.first
43
+ vararg_if_dotted = $2 ? :vararg : :required
44
+ @argument_names[sanitize $1] = vararg_if_dotted
45
+ tokens.shift
46
+ end
47
+
48
+ raise "Found invalid usage token '#{tokens.first}'" unless tokens.empty?
49
+
50
+ count_optional_arguments = @argument_names.values.count(:optional)
51
+ count_vararg_arguments = @argument_names.values.count(:vararg)
52
+
53
+ raise "Cannot combine dotted and optional arguments" if count_optional_arguments > 0 && count_vararg_arguments > 0
54
+ raise "Found more than one dotted arguments" if count_vararg_arguments > 1
55
+
56
+ # --- 2) Parse option names ---------------------------------------
57
+ #
58
+ # Parse the usage message and extract option names, their short and
59
+ # long forms, and the associated argument name (if any), eg:
60
+ #
61
+ # Options:
62
+ # -s, --secure Use secure connection
63
+ # -v, --verbose Enable verbose output
64
+ # -r, --retries NUM Number of connection retries (default 3)
65
+ # -t, --timeout NUM Set connection timeout in seconds
66
+
67
+ @option_names = {}
68
+ @default_values = {}
69
+
70
+ options = inline_options + text.lines.grep(/^\s*--?\w/)
71
+ options.each do |string|
72
+ tokens = string.scan(/--?\w[\w-]*(?: \w+)?|,|\(default \S+\)|\S+/)
73
+
74
+ short_form = nil
75
+ long_form = nil
76
+ option_name = nil
77
+ argument_name = nil
78
+ default_value = nil
79
+
80
+ if /^-(\w)( \w+)?$/ === tokens.first
81
+ short_form, argument_name = tokens.shift.split
82
+ option_name = sanitize $1
83
+ tokens.shift if ',' === tokens.first
84
+ end
85
+
86
+ if /^--([\w-]+)( \w+)?$/ === tokens.first
87
+ long_form, argument_name = tokens.shift.split
88
+ option_name = sanitize $1
89
+ end
90
+
91
+ if /^\(default (\S+)\)$/ === tokens.last
92
+ default_value = $1
93
+ end
94
+
95
+ [short_form, long_form].compact.each do |each|
96
+ @option_names[each] = [option_name, argument_name]
97
+ end
98
+
99
+ @default_values[option_name] = default_value if default_value
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def sanitize(string)
106
+ string.tr('^a-zA-Z0-9', '_').downcase.to_sym
107
+ end
108
+ end
109
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OptionsByExample
4
- VERSION = '3.2.0'
4
+ VERSION = '3.3.0'
5
5
  end
6
6
 
7
7
 
@@ -11,6 +11,11 @@ __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.3.0
15
+ - Expand dash-number to dash-n option
16
+ - Complete support for inline specification of options
17
+ - Support repeated arguments using dot-dot-dot
18
+
14
19
  3.2.0
15
20
 
16
21
  - New method #get returns argument value or nil
@@ -1,52 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'options_by_example/version'
4
- require 'options_by_example/parser'
4
+ require 'options_by_example/commandline_parser'
5
+ require 'options_by_example/usage_specification'
5
6
 
6
7
 
7
8
  class OptionsByExample
8
9
 
9
10
  attr_reader :arguments
10
11
  attr_reader :options
11
- attr_reader :usage_message
12
+ attr_reader :usage_spec
12
13
 
13
14
  def self.read(data)
14
15
  return new data.read
15
16
  end
16
17
 
17
18
  def initialize(text)
18
- @usage_message = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
19
-
20
- # --- 1) Parse argument names -------------------------------------
21
- #
22
- # Parse the usage string and extract both optional argument names
23
- # and required argument names, for example:
24
- #
25
- # Usage: connect [options] [mode] host port
26
-
27
- text =~ /Usage: (\$0|\w+)(?: \[options\])?((?: \[\w+\])*)((?: \w+)*)/
28
- raise RuntimeError, "Expected usage string, got none" unless $1
29
- @argument_names_optional = $2.to_s.split.map { |match| sanitize match.tr('[]', '') }
30
- @argument_names_required = $3.to_s.split.map { |match| sanitize match }
31
-
32
- # --- 2) Parse option names ---------------------------------------
33
- #
34
- # Parse the usage message and extract option names, their short and
35
- # long forms, and the associated argument name (if any), eg:
36
- #
37
- # Options:
38
- # -s, --secure Use secure connection
39
- # -v, --verbose Enable verbose output
40
- # -r, --retries NUM Number of connection retries (default 3)
41
- # -t, --timeout NUM Set connection timeout in seconds
42
-
43
- @option_names = {}
44
- @default_values = {}
45
- text.scan(/(?:(-\w), ?)?(--([\w-]+))(?: (\w+))?(?:.*\(default:? (\w+)\))?/) do
46
- flags = [$1, $2].compact
47
- flags.each { |each| @option_names[each] = [(sanitize $3), $4] }
48
- @default_values[sanitize $3] = $5 if $5
49
- end
19
+ @usage_spec = UsageSpecification.new(text)
50
20
 
51
21
  initialize_argument_accessors
52
22
  initialize_option_accessors
@@ -55,7 +25,7 @@ class OptionsByExample
55
25
  def parse(argv)
56
26
  parse_without_exit argv
57
27
  rescue PrintUsageMessage
58
- puts @usage_message
28
+ puts @usage_spec.message
59
29
  exit 0
60
30
  rescue RuntimeError => err
61
31
  puts "ERROR: #{err.message}"
@@ -84,25 +54,19 @@ class OptionsByExample
84
54
  private
85
55
 
86
56
  def parse_without_exit(argv)
87
- parser = Parser.new(
88
- @argument_names_required,
89
- @argument_names_optional,
90
- @default_values,
91
- @option_names,
92
- )
57
+ parser = CommandlineParser.new(@usage_spec)
58
+ parser.parse(argv)
93
59
 
94
- parser.parse argv
95
- @arguments = parser.arguments
96
- @options = parser.options
60
+ @arguments = parser.argument_values
61
+ @options = parser.option_values
97
62
 
98
63
  return self
99
64
  end
100
65
 
101
66
  def initialize_argument_accessors
102
67
  [
103
- *@argument_names_required,
104
- *@argument_names_optional,
105
- *@option_names.values.select(&:last).map(&:first),
68
+ *@usage_spec.argument_names.keys,
69
+ *@usage_spec.option_names.values.select(&:last).map(&:first),
106
70
  ].each do |argument_name|
107
71
  instance_eval %{
108
72
  def argument_#{argument_name}
@@ -114,7 +78,7 @@ class OptionsByExample
114
78
  end
115
79
 
116
80
  def initialize_option_accessors
117
- @option_names.each_value do |option_name, _|
81
+ @usage_spec.option_names.each_value do |option_name, _|
118
82
  instance_eval %{
119
83
  def include_#{option_name}?
120
84
  @options.include? :#{option_name}
@@ -122,9 +86,5 @@ class OptionsByExample
122
86
  }
123
87
  end
124
88
  end
125
-
126
- def sanitize(string)
127
- string.tr('^a-zA-Z0-9', '_').downcase.to_sym
128
- end
129
89
  end
130
90
 
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: options_by_example
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Kuhn
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-17 00:00:00.000000000 Z
11
+ date: 2026-02-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description:
13
+ description:
14
14
  email:
15
15
  - akuhn@iam.unibe.ch
16
16
  executables: []
@@ -19,7 +19,8 @@ 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
+ - lib/options_by_example/commandline_parser.rb
23
+ - lib/options_by_example/usage_specification.rb
23
24
  - lib/options_by_example/version.rb
24
25
  homepage: https://github.com/akuhn/options_by_example
25
26
  licenses:
@@ -28,7 +29,7 @@ metadata:
28
29
  homepage_uri: https://github.com/akuhn/options_by_example
29
30
  source_code_uri: https://github.com/akuhn/options_by_example
30
31
  changelog_uri: https://github.com/akuhn/options_by_example/blob/master/lib/options_by_example/version.rb
31
- post_install_message:
32
+ post_install_message:
32
33
  rdoc_options: []
33
34
  require_paths:
34
35
  - lib
@@ -43,8 +44,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
43
44
  - !ruby/object:Gem::Version
44
45
  version: '0'
45
46
  requirements: []
46
- rubygems_version: 3.3.7
47
- signing_key:
47
+ rubygems_version: 3.0.3.1
48
+ signing_key:
48
49
  specification_version: 4
49
50
  summary: No-code options parser that extracts arguments directly from usage text.
50
51
  test_files: []