options_by_example 1.3.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: 26769b40ccdbee8bcb810e85755f7e3774145dda2ea8860edda1c99a65e168a5
4
- data.tar.gz: 5d11ede203262af12bb65613ea5d153bb699188443453d82f8fb5bccb13068a1
3
+ metadata.gz: 98c16609314706667d2b776fa71de92ca874b2fcc803ea8f80387fe4f0245d9a
4
+ data.tar.gz: 8cc7023f98016edaa5521d6184985fb4133135e902f0380f81e233b6f52bfb97
5
5
  SHA512:
6
- metadata.gz: 1cb7a3d1732b19543557ed113094204451f9d24e2fae91029aa19d3f2985482e5536cc8c185e1dbef15fd77d330dd7ea76e7d0a6c596f7e198278f4afdf3bc74
7
- data.tar.gz: 61a4d2a408fbe795040f7963e6d9cdb82076872bf7118c7fbb633b940684bd5eed9884f2d4a3f73cc4f321eb4e976d9ff5b252a17694a6316c4849314a5aa586
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,25 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'date'
4
+ require 'time'
5
+
3
6
 
4
7
  class OptionsByExample
8
+
9
+ class PrintUsageMessage < StandardError
10
+ end
11
+
5
12
  class Parser
6
13
 
7
14
  attr_reader :options
8
15
  attr_reader :arguments
9
16
 
10
- def initialize(argument_names_required, argument_names_optional, option_names)
17
+ def initialize(argument_names_required, argument_names_optional, default_values, option_names)
11
18
  @argument_names_required = argument_names_required
12
19
  @argument_names_optional = argument_names_optional
20
+ @default_values = default_values
13
21
  @option_names = option_names
14
22
 
15
- @arguments = {}
23
+ @arguments = @default_values.dup
16
24
  @options = {}
17
25
  end
18
26
 
19
27
  def parse(array)
20
28
 
21
29
  # Separate command-line options and their respective arguments into
22
- # chunks plus handling any remaining arguments. This organization
30
+ # chunks, plus tracking leading excess arguments. This organization
23
31
  # facilitates further processing and validation of the input.
24
32
 
25
33
  @chunks = []
@@ -29,8 +37,9 @@ class OptionsByExample
29
37
  current << each
30
38
  end
31
39
 
32
- find_help_option
33
- find_unknown_options
40
+ detect_help_option
41
+ flatten_stacked_shorthand_options
42
+ detect_unknown_options
34
43
  parse_options
35
44
 
36
45
  validate_number_of_arguments
@@ -42,16 +51,45 @@ class OptionsByExample
42
51
 
43
52
  private
44
53
 
45
- def find_help_option
54
+ def detect_help_option
46
55
  @chunks.each do |option, *args|
47
56
  case option
48
57
  when '-h', '--help'
49
- raise "puts @usage_message"
58
+ raise PrintUsageMessage
50
59
  end
51
60
  end
52
61
  end
53
62
 
54
- 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
55
93
  @chunks.each do |option, *args|
56
94
  raise "Found unknown option '#{option}'" unless @option_names.include?(option)
57
95
  end
@@ -68,7 +106,28 @@ class OptionsByExample
68
106
 
69
107
  if argument_name
70
108
  raise "Expected argument for option '#{option}', got none" if args.empty?
71
- @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
72
131
  end
73
132
 
74
133
  @remainder = args
@@ -78,10 +137,17 @@ class OptionsByExample
78
137
  def validate_number_of_arguments
79
138
  min_length = @argument_names_required.size
80
139
  max_length = @argument_names_optional.size + min_length
140
+
81
141
  if @remainder.size > max_length
82
142
  range = [min_length, max_length].uniq.join(?-)
83
143
  raise "Expected #{range} arguments, but received too many"
84
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
85
151
  end
86
152
 
87
153
  def parse_required_arguments
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OptionsByExample
4
- VERSION = '1.3.0'
4
+ VERSION = '3.0.0'
5
5
  end
6
6
 
7
7
 
@@ -11,9 +11,21 @@ __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
+
21
+ 2.0.0
22
+
23
+ - Replaced dynamic methods with explicit methods for options and arguments
24
+ - Removed ability to call dynamic methods with undeclared names
25
+
14
26
  1.3.0
15
27
 
16
- - Extract parsing functionality into class
28
+ - Extracted parsing functionality into class
17
29
  - Better error messages
18
30
 
19
31
  1.2.0
@@ -6,20 +6,14 @@ require 'options_by_example/parser'
6
6
 
7
7
  class OptionsByExample
8
8
 
9
- attr_reader :argument_names_optional
10
- attr_reader :argument_names_required
11
- attr_reader :option_names
12
-
13
9
  attr_reader :arguments
14
10
  attr_reader :options
15
11
 
16
-
17
12
  def self.read(data)
18
13
  return new data.read
19
14
  end
20
15
 
21
16
  def initialize(text)
22
- @settings = {exit_on_error: true}
23
17
  @usage_message = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
24
18
 
25
19
  # --- 1) Parse argument names -------------------------------------
@@ -29,10 +23,10 @@ class OptionsByExample
29
23
  #
30
24
  # Usage: connect [options] [mode] host port
31
25
 
32
- text =~ /Usage: (\w+|\$0)( \[options\])?(( \[\w+\])*)(( \w+)*)/
26
+ text =~ /Usage: (\$0|\w+)(?: \[options\])?((?: \[\w+\])*)((?: \w+)*)/
33
27
  raise RuntimeError, "Expected usage string, got none" unless $1
34
- @argument_names_optional = $3.to_s.split.map { |match| match.tr('[]', '').downcase }
35
- @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)
36
30
 
37
31
  # --- 2) Parse option names ---------------------------------------
38
32
  #
@@ -43,56 +37,69 @@ class OptionsByExample
43
37
  # -s, --secure Use secure connection
44
38
  # -v, --verbose Enable verbose output
45
39
  # -r, --retries NUM Number of connection retries (default 3)
46
- # -t, --timeout NUM Connection timeout in seconds (default 10)
40
+ # -t, --timeout NUM Set connection timeout in seconds
47
41
 
48
42
  @option_names = {}
49
- text.scan(/((--?\w+)(, --?\w+)*) ?(\w+)?/) do
50
- opts = $1.split(", ")
51
- 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
52
48
  end
53
- end
54
49
 
55
- def use(settings)
56
- @settings.update settings
57
-
58
- return self
50
+ initialize_argument_accessors
51
+ initialize_option_accessors
59
52
  end
60
53
 
61
54
  def parse(argv)
55
+ parse_without_exit argv
56
+ rescue PrintUsageMessage
57
+ puts @usage_message
58
+ exit 0
59
+ rescue RuntimeError => err
60
+ puts "ERROR: #{err.message}"
61
+ exit 1
62
+ end
63
+
64
+ def parse_without_exit(argv)
62
65
  parser = Parser.new(
63
66
  @argument_names_required,
64
67
  @argument_names_optional,
68
+ @default_values,
65
69
  @option_names,
66
70
  )
67
71
 
68
72
  parser.parse argv
69
-
70
- @options = parser.options
71
73
  @arguments = parser.arguments
74
+ @options = parser.options
72
75
 
73
76
  return self
74
- rescue RuntimeError => err
75
- raise unless @settings[:exit_on_error]
77
+ end
76
78
 
77
- if err.message == "puts @usage_message"
78
- puts @usage_message
79
- else
80
- puts "ERROR: #{err.message}"
79
+ private
80
+
81
+ def initialize_argument_accessors
82
+ [
83
+ *@argument_names_required,
84
+ *@argument_names_optional,
85
+ *@option_names.values.select(&:last).map(&:first),
86
+ ].each do |argument_name|
87
+ instance_eval %{
88
+ def argument_#{argument_name}
89
+ val = @arguments["#{argument_name}"]
90
+ val && block_given? ? (yield val) : val
91
+ end
92
+ }
81
93
  end
82
-
83
- exit 1
84
94
  end
85
95
 
86
-
87
- def method_missing(sym, *args, &block)
88
- case sym
89
- when /^argument_(\w+)$/
90
- val = @arguments[$1]
91
- block && val ? block.call(val) : val
92
- when /^include_(\w+)\?$/
93
- @options[$1]
94
- else
95
- super
96
+ def initialize_option_accessors
97
+ @option_names.each_value do |option_name, _|
98
+ instance_eval %{
99
+ def include_#{option_name}?
100
+ @options.include? "#{option_name}"
101
+ end
102
+ }
96
103
  end
97
104
  end
98
105
  end
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: 1.3.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-29 00:00:00.000000000 Z
11
+ date: 2023-05-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -27,6 +27,7 @@ licenses:
27
27
  metadata:
28
28
  homepage_uri: https://github.com/akuhn/options_by_example
29
29
  source_code_uri: https://github.com/akuhn/options_by_example
30
+ changelog_uri: https://github.com/akuhn/options_by_example/blob/master/lib/options_by_example/version.rb
30
31
  post_install_message:
31
32
  rdoc_options: []
32
33
  require_paths: