options_by_example 1.3.0 → 3.0.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: 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: