options_by_example 2.0.0 → 3.1.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: f90dee5b19415bddb809efe80e4fc2c6f39daf7b82259a0c247caa789d21ff23
4
+ data.tar.gz: b74ece7699aee6f785e9a2fa5149f2a2ee6f2c7f5a4365a429e958d676e9aaf1
5
5
  SHA512:
6
- metadata.gz: 7e5b59dc4a4b9ad41edeaadaf98fa3ee23abe4f262cfbb9ba93752dba66ff5868b704024a3d534b43386977caefe6a4eecea4a5e80c22f5c351f679ec2488bc3
7
- data.tar.gz: 7ecce96f859a83e3dc702740c3d5e72c957d44a42387dff4a70db249bc54fc1df45484bf5ff099e9bbd66d9dae06a548d179f107aa5f6cb87395a36a54551bc0
6
+ metadata.gz: c4e235ad68624c57e3efe1915a31c006128351f6c870fa5aefa8f6b2856ec0bab33c24107c1d99b308d9b5fa54cabdff901d3669413e4021c839d7778ecb9cb7
7
+ data.tar.gz: a5c7213462bc16e1b88411b92c28c9be5b71cf9838bcad217316586b28318398441e1cc69acf56e223812c7c0fb51dcf381403158a35283f58ea2b0ce6f777bc
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.1.0'
5
5
  end
6
6
 
7
7
 
@@ -11,6 +11,20 @@ __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.1.0
15
+
16
+ - Support dash in argument and option names
17
+ - Method #if_present passes argument to block if present
18
+ - Method #include? return true if option is present
19
+
20
+ 3.0.0
21
+
22
+ - Support options with default values
23
+ - Improved support for one-line usage messages
24
+ - Expand combined shorthand options into their separate components
25
+ - Shorthand options must be single letter only
26
+ - Support options with typed arguments
27
+
14
28
  2.0.0
15
29
 
16
30
  - 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| sanitize match.tr('[]', '') }
29
+ @argument_names_required = $3.to_s.split.map { |match| sanitize match }
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] = [(sanitize $3), $4] }
47
+ @default_values[sanitize $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
 
@@ -73,6 +76,17 @@ class OptionsByExample
73
76
  return self
74
77
  end
75
78
 
79
+ def if_present(name)
80
+ raise ArgumentError, 'block missing' unless block_given?
81
+
82
+ value = @arguments[name]
83
+ value.nil? ? value : (yield value)
84
+ end
85
+
86
+ def include?(name)
87
+ @options.include?(name)
88
+ end
89
+
76
90
  private
77
91
 
78
92
  def initialize_argument_accessors
@@ -83,7 +97,7 @@ class OptionsByExample
83
97
  ].each do |argument_name|
84
98
  instance_eval %{
85
99
  def argument_#{argument_name}
86
- val = @arguments["#{argument_name}"]
100
+ val = @arguments[:#{argument_name}]
87
101
  val && block_given? ? (yield val) : val
88
102
  end
89
103
  }
@@ -94,10 +108,14 @@ class OptionsByExample
94
108
  @option_names.each_value do |option_name, _|
95
109
  instance_eval %{
96
110
  def include_#{option_name}?
97
- @options.include? "#{option_name}"
111
+ @options.include? :#{option_name}
98
112
  end
99
113
  }
100
114
  end
101
115
  end
116
+
117
+ def sanitize(string)
118
+ string.tr('^a-zA-Z0-9', '_').downcase.to_sym
119
+ end
102
120
  end
103
121
 
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.1.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-25 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: