options_by_example 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb87e6b0c01a7f99d70d600c6adc142d96cae3fc18b122c2259c926504fb198b
4
- data.tar.gz: e21e7dd4f0a4a77fb7e2a84b4edc77293023730388e0fcbf4abe2d0e5344d5c5
3
+ metadata.gz: 98c16609314706667d2b776fa71de92ca874b2fcc803ea8f80387fe4f0245d9a
4
+ data.tar.gz: 8cc7023f98016edaa5521d6184985fb4133135e902f0380f81e233b6f52bfb97
5
5
  SHA512:
6
- metadata.gz: 7e5b59dc4a4b9ad41edeaadaf98fa3ee23abe4f262cfbb9ba93752dba66ff5868b704024a3d534b43386977caefe6a4eecea4a5e80c22f5c351f679ec2488bc3
7
- data.tar.gz: 7ecce96f859a83e3dc702740c3d5e72c957d44a42387dff4a70db249bc54fc1df45484bf5ff099e9bbd66d9dae06a548d179f107aa5f6cb87395a36a54551bc0
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,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'date'
4
+ require 'time'
5
+
3
6
 
4
7
  class OptionsByExample
5
8
 
@@ -11,19 +14,20 @@ class OptionsByExample
11
14
  attr_reader :options
12
15
  attr_reader :arguments
13
16
 
14
- def initialize(argument_names_required, argument_names_optional, option_names)
17
+ def initialize(argument_names_required, argument_names_optional, default_values, option_names)
15
18
  @argument_names_required = argument_names_required
16
19
  @argument_names_optional = argument_names_optional
20
+ @default_values = default_values
17
21
  @option_names = option_names
18
22
 
19
- @arguments = {}
23
+ @arguments = @default_values.dup
20
24
  @options = {}
21
25
  end
22
26
 
23
27
  def parse(array)
24
28
 
25
29
  # Separate command-line options and their respective arguments into
26
- # chunks plus handling any remaining arguments. This organization
30
+ # chunks, plus tracking leading excess arguments. This organization
27
31
  # facilitates further processing and validation of the input.
28
32
 
29
33
  @chunks = []
@@ -33,8 +37,9 @@ class OptionsByExample
33
37
  current << each
34
38
  end
35
39
 
36
- find_help_option
37
- find_unknown_options
40
+ detect_help_option
41
+ flatten_stacked_shorthand_options
42
+ detect_unknown_options
38
43
  parse_options
39
44
 
40
45
  validate_number_of_arguments
@@ -46,7 +51,7 @@ class OptionsByExample
46
51
 
47
52
  private
48
53
 
49
- def find_help_option
54
+ def detect_help_option
50
55
  @chunks.each do |option, *args|
51
56
  case option
52
57
  when '-h', '--help'
@@ -55,7 +60,36 @@ class OptionsByExample
55
60
  end
56
61
  end
57
62
 
58
- def find_unknown_options
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
59
93
  @chunks.each do |option, *args|
60
94
  raise "Found unknown option '#{option}'" unless @option_names.include?(option)
61
95
  end
@@ -72,7 +106,28 @@ class OptionsByExample
72
106
 
73
107
  if argument_name
74
108
  raise "Expected argument for option '#{option}', got none" if args.empty?
75
- @arguments[option_name] = args.shift
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
76
131
  end
77
132
 
78
133
  @remainder = args
@@ -82,10 +137,17 @@ class OptionsByExample
82
137
  def validate_number_of_arguments
83
138
  min_length = @argument_names_required.size
84
139
  max_length = @argument_names_optional.size + min_length
140
+
85
141
  if @remainder.size > max_length
86
142
  range = [min_length, max_length].uniq.join(?-)
87
143
  raise "Expected #{range} arguments, but received too many"
88
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
89
151
  end
90
152
 
91
153
  def parse_required_arguments
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OptionsByExample
4
- VERSION = '2.0.0'
4
+ VERSION = '3.0.0'
5
5
  end
6
6
 
7
7
 
@@ -11,6 +11,13 @@ __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
+
14
21
  2.0.0
15
22
 
16
23
  - Replaced dynamic methods with explicit methods for options and arguments
@@ -23,10 +23,10 @@ class OptionsByExample
23
23
  #
24
24
  # Usage: connect [options] [mode] host port
25
25
 
26
- text =~ /Usage: (\w+|\$0)( \[options\])?(( \[\w+\])*)(( \w+)*)/
26
+ text =~ /Usage: (\$0|\w+)(?: \[options\])?((?: \[\w+\])*)((?: \w+)*)/
27
27
  raise RuntimeError, "Expected usage string, got none" unless $1
28
- @argument_names_optional = $3.to_s.split.map { |match| match.tr('[]', '').downcase }
29
- @argument_names_required = $5.to_s.split.map(&:downcase)
28
+ @argument_names_optional = $2.to_s.split.map { |match| match.tr('[]', '').downcase }
29
+ @argument_names_required = $3.to_s.split.map(&:downcase)
30
30
 
31
31
  # --- 2) Parse option names ---------------------------------------
32
32
  #
@@ -37,12 +37,14 @@ class OptionsByExample
37
37
  # -s, --secure Use secure connection
38
38
  # -v, --verbose Enable verbose output
39
39
  # -r, --retries NUM Number of connection retries (default 3)
40
- # -t, --timeout NUM Connection timeout in seconds (default 10)
40
+ # -t, --timeout NUM Set connection timeout in seconds
41
41
 
42
42
  @option_names = {}
43
- text.scan(/((--?\w+)(, --?\w+)*) ?(\w+)?/) do
44
- opts = $1.split(", ")
45
- opts.each { |each| @option_names[each] = [opts.last.tr('-', ''), $4] }
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
46
48
  end
47
49
 
48
50
  initialize_argument_accessors
@@ -63,6 +65,7 @@ class OptionsByExample
63
65
  parser = Parser.new(
64
66
  @argument_names_required,
65
67
  @argument_names_optional,
68
+ @default_values,
66
69
  @option_names,
67
70
  )
68
71
 
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: 2.0.0
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-04-30 00:00:00.000000000 Z
11
+ date: 2023-05-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: