options_by_example 2.0.0 → 3.1.0

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