options_by_example 3.1.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: f90dee5b19415bddb809efe80e4fc2c6f39daf7b82259a0c247caa789d21ff23
4
- data.tar.gz: b74ece7699aee6f785e9a2fa5149f2a2ee6f2c7f5a4365a429e958d676e9aaf1
3
+ metadata.gz: 8ced46483d8abae62122bb45cd0d4753408c4d93e08405a3409583ec9fcfb477
4
+ data.tar.gz: e8484baa8976c72154fb2da2509482d985501c08960e7e67fdf04731e307787b
5
5
  SHA512:
6
- metadata.gz: c4e235ad68624c57e3efe1915a31c006128351f6c870fa5aefa8f6b2856ec0bab33c24107c1d99b308d9b5fa54cabdff901d3669413e4021c839d7778ecb9cb7
7
- data.tar.gz: a5c7213462bc16e1b88411b92c28c9be5b71cf9838bcad217316586b28318398441e1cc69acf56e223812c7c0fb51dcf381403158a35283f58ea2b0ce6f777bc
6
+ metadata.gz: 22f691bf1fc36e14868996b32b15de17a1760add666aa046e6dc57fb0e09f8dad75c7c18260255930c88ea3e56a1c994285dd320a659e6c9ee92fc4ae45d3f10
7
+ data.tar.gz: 1d54dca36e9829f2018af7493586e18ca7d590819d112e28482c93684212adc4ccd53e5c5183d49885c927f4ddb9e23c0654874ddbceb0e617f95c6fc6b96258
data/README.md CHANGED
@@ -30,15 +30,15 @@ require 'options_by_example'
30
30
 
31
31
  Options = OptionsByExample.read(DATA).parse(ARGV)
32
32
 
33
- puts Options.include_secure?
34
- puts Options.include_verbose?
35
- puts Options.include_retries?
36
- puts Options.include_timeout?
37
- puts Options.argument_retries
38
- puts Options.argument_timeout
39
- puts Options.argument_mode
40
- puts Options.argument_host
41
- puts Options.argument_port
33
+ puts Options.include? :secure
34
+ puts Options.include? :verbose
35
+ puts Options.include? :retries
36
+ puts Options.include? :timeout
37
+ puts Options.get :retries
38
+ puts Options.get :timeout
39
+ puts Options.get :mode
40
+ puts Options.get :host
41
+ puts Options.get :port
42
42
 
43
43
 
44
44
  __END__
@@ -50,8 +50,8 @@ Usage: connect [options] [mode] host port
50
50
  Options:
51
51
  -s, --secure Establish a secure connection (SSL/TSL)
52
52
  -v, --verbose Enable verbose output for detailed information
53
- -r, --retries NUM Specify the number of connection retries (default 3)
54
- -t, --timeout NUM Set the connection timeout in seconds (default 10)
53
+ -r, --retries NUM Number of connection retries (default 3)
54
+ -t, --timeout NUM Set connection timeout in seconds
55
55
 
56
56
  Arguments:
57
57
  [mode] Optional connection mode (active or passive)
@@ -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.1.0'
4
+ VERSION = '3.3.0'
5
5
  end
6
6
 
7
7
 
@@ -11,6 +11,17 @@ __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
+
19
+ 3.2.0
20
+
21
+ - New method #get returns argument value or nil
22
+ - New method #fetch returns argument value or raises error
23
+ - Changed internal method #parse_without_exit to private
24
+
14
25
  3.1.0
15
26
 
16
27
  - Support dash in argument and option names
@@ -1,51 +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
12
+ attr_reader :usage_spec
11
13
 
12
14
  def self.read(data)
13
15
  return new data.read
14
16
  end
15
17
 
16
18
  def initialize(text)
17
- @usage_message = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
18
-
19
- # --- 1) Parse argument names -------------------------------------
20
- #
21
- # Parse the usage string and extract both optional argument names
22
- # and required argument names, for example:
23
- #
24
- # Usage: connect [options] [mode] host port
25
-
26
- text =~ /Usage: (\$0|\w+)(?: \[options\])?((?: \[\w+\])*)((?: \w+)*)/
27
- raise RuntimeError, "Expected usage string, got none" unless $1
28
- @argument_names_optional = $2.to_s.split.map { |match| sanitize match.tr('[]', '') }
29
- @argument_names_required = $3.to_s.split.map { |match| sanitize match }
30
-
31
- # --- 2) Parse option names ---------------------------------------
32
- #
33
- # Parse the usage message and extract option names, their short and
34
- # long forms, and the associated argument name (if any), eg:
35
- #
36
- # Options:
37
- # -s, --secure Use secure connection
38
- # -v, --verbose Enable verbose output
39
- # -r, --retries NUM Number of connection retries (default 3)
40
- # -t, --timeout NUM Set connection timeout in seconds
41
-
42
- @option_names = {}
43
- @default_values = {}
44
- text.scan(/(?:(-\w), ?)?(--([\w-]+))(?: (\w+))?(?:.*\(default:? (\w+)\))?/) do
45
- flags = [$1, $2].compact
46
- flags.each { |each| @option_names[each] = [(sanitize $3), $4] }
47
- @default_values[sanitize $3] = $5 if $5
48
- end
19
+ @usage_spec = UsageSpecification.new(text)
49
20
 
50
21
  initialize_argument_accessors
51
22
  initialize_option_accessors
@@ -54,26 +25,19 @@ class OptionsByExample
54
25
  def parse(argv)
55
26
  parse_without_exit argv
56
27
  rescue PrintUsageMessage
57
- puts @usage_message
28
+ puts @usage_spec.message
58
29
  exit 0
59
30
  rescue RuntimeError => err
60
31
  puts "ERROR: #{err.message}"
61
32
  exit 1
62
33
  end
63
34
 
64
- def parse_without_exit(argv)
65
- parser = Parser.new(
66
- @argument_names_required,
67
- @argument_names_optional,
68
- @default_values,
69
- @option_names,
70
- )
71
-
72
- parser.parse argv
73
- @arguments = parser.arguments
74
- @options = parser.options
35
+ def fetch(*args, &block)
36
+ @arguments.fetch(*args, &block)
37
+ end
75
38
 
76
- return self
39
+ def get(name)
40
+ @arguments[name]
77
41
  end
78
42
 
79
43
  def if_present(name)
@@ -89,11 +53,20 @@ class OptionsByExample
89
53
 
90
54
  private
91
55
 
56
+ def parse_without_exit(argv)
57
+ parser = CommandlineParser.new(@usage_spec)
58
+ parser.parse(argv)
59
+
60
+ @arguments = parser.argument_values
61
+ @options = parser.option_values
62
+
63
+ return self
64
+ end
65
+
92
66
  def initialize_argument_accessors
93
67
  [
94
- *@argument_names_required,
95
- *@argument_names_optional,
96
- *@option_names.values.select(&:last).map(&:first),
68
+ *@usage_spec.argument_names.keys,
69
+ *@usage_spec.option_names.values.select(&:last).map(&:first),
97
70
  ].each do |argument_name|
98
71
  instance_eval %{
99
72
  def argument_#{argument_name}
@@ -105,7 +78,7 @@ class OptionsByExample
105
78
  end
106
79
 
107
80
  def initialize_option_accessors
108
- @option_names.each_value do |option_name, _|
81
+ @usage_spec.option_names.each_value do |option_name, _|
109
82
  instance_eval %{
110
83
  def include_#{option_name}?
111
84
  @options.include? :#{option_name}
@@ -113,9 +86,5 @@ class OptionsByExample
113
86
  }
114
87
  end
115
88
  end
116
-
117
- def sanitize(string)
118
- string.tr('^a-zA-Z0-9', '_').downcase.to_sym
119
- end
120
89
  end
121
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.1.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-05-25 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: []