options_by_example 1.2.0 → 2.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: b9973ed4bcfac2c5bdd856347b1956fe52fc134c0c120e288548d164179e3578
4
- data.tar.gz: a9f55ef81262189094c8718713a55a93c05b5d64d5655ef6fd1ce5db1ddddb29
3
+ metadata.gz: eb87e6b0c01a7f99d70d600c6adc142d96cae3fc18b122c2259c926504fb198b
4
+ data.tar.gz: e21e7dd4f0a4a77fb7e2a84b4edc77293023730388e0fcbf4abe2d0e5344d5c5
5
5
  SHA512:
6
- metadata.gz: 1670b512ab6a4517bc38141a403d88798f5ca250b23dd7c7d3958a542dc9c1aeeca42b1d7ecfc76219c553d14c3a96e4662806712e1abc410b0f5a675254d3e2
7
- data.tar.gz: b82aaa8cca78d5c92166c64268fc49edb20507b504bd87bec50446d1de9877b497169a4f657457e9fc6469fe6a38a7baabe488aeccf95c6a6ca10a584eafe843
6
+ metadata.gz: 7e5b59dc4a4b9ad41edeaadaf98fa3ee23abe4f262cfbb9ba93752dba66ff5868b704024a3d534b43386977caefe6a4eecea4a5e80c22f5c351f679ec2488bc3
7
+ data.tar.gz: 7ecce96f859a83e3dc702740c3d5e72c957d44a42387dff4a70db249bc54fc1df45484bf5ff099e9bbd66d9dae06a548d179f107aa5f6cb87395a36a54551bc0
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ class OptionsByExample
5
+
6
+ class PrintUsageMessage < StandardError
7
+ end
8
+
9
+ class Parser
10
+
11
+ attr_reader :options
12
+ attr_reader :arguments
13
+
14
+ def initialize(argument_names_required, argument_names_optional, option_names)
15
+ @argument_names_required = argument_names_required
16
+ @argument_names_optional = argument_names_optional
17
+ @option_names = option_names
18
+
19
+ @arguments = {}
20
+ @options = {}
21
+ end
22
+
23
+ def parse(array)
24
+
25
+ # Separate command-line options and their respective arguments into
26
+ # chunks plus handling any remaining arguments. This organization
27
+ # facilitates further processing and validation of the input.
28
+
29
+ @chunks = []
30
+ @remainder = current = []
31
+ array.each do |each|
32
+ @chunks << current = [] if each.start_with?(?-)
33
+ current << each
34
+ end
35
+
36
+ find_help_option
37
+ find_unknown_options
38
+ parse_options
39
+
40
+ validate_number_of_arguments
41
+ parse_required_arguments
42
+ parse_optional_arguments
43
+
44
+ raise "Internal error: unreachable state" unless @remainder.empty?
45
+ end
46
+
47
+ private
48
+
49
+ def find_help_option
50
+ @chunks.each do |option, *args|
51
+ case option
52
+ when '-h', '--help'
53
+ raise PrintUsageMessage
54
+ end
55
+ end
56
+ end
57
+
58
+ def find_unknown_options
59
+ @chunks.each do |option, *args|
60
+ raise "Found unknown option '#{option}'" unless @option_names.include?(option)
61
+ end
62
+ end
63
+
64
+ def parse_options
65
+ @chunks.each do |option, *args|
66
+ if @remainder.any?
67
+ raise "Unexpected arguments found before option '#{option}', please provide all options before arguments"
68
+ end
69
+
70
+ option_name, argument_name = @option_names[option]
71
+ @options[option_name] = true
72
+
73
+ if argument_name
74
+ raise "Expected argument for option '#{option}', got none" if args.empty?
75
+ @arguments[option_name] = args.shift
76
+ end
77
+
78
+ @remainder = args
79
+ end
80
+ end
81
+
82
+ def validate_number_of_arguments
83
+ min_length = @argument_names_required.size
84
+ max_length = @argument_names_optional.size + min_length
85
+ if @remainder.size > max_length
86
+ range = [min_length, max_length].uniq.join(?-)
87
+ raise "Expected #{range} arguments, but received too many"
88
+ end
89
+ end
90
+
91
+ def parse_required_arguments
92
+ stash = @remainder.pop(@argument_names_required.length)
93
+ @argument_names_required.each do |argument_name|
94
+ raise "Missing required argument '#{argument_name}'" if stash.empty?
95
+ @arguments[argument_name] = stash.shift
96
+ end
97
+ end
98
+
99
+ def parse_optional_arguments
100
+ @argument_names_optional.each do |argument_name|
101
+ break if @remainder.empty?
102
+ @arguments[argument_name] = @remainder.shift
103
+ end
104
+ end
105
+ end
106
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OptionsByExample
4
- VERSION = '1.2.0'
4
+ VERSION = '2.0.0'
5
5
  end
6
6
 
7
7
 
@@ -11,6 +11,16 @@ __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
+ 2.0.0
15
+
16
+ - Replaced dynamic methods with explicit methods for options and arguments
17
+ - Removed ability to call dynamic methods with undeclared names
18
+
19
+ 1.3.0
20
+
21
+ - Extracted parsing functionality into class
22
+ - Better error messages
23
+
14
24
  1.2.0
15
25
 
16
26
  - Ensure compatibility with Ruby versions 1.9.3 and newer
@@ -1,38 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'options_by_example/version'
4
+ require 'options_by_example/parser'
4
5
 
5
6
 
6
7
  class OptionsByExample
7
8
 
8
- attr_reader :argument_names_optional
9
- attr_reader :argument_names_required
10
- attr_reader :option_names
11
-
12
9
  attr_reader :arguments
13
10
  attr_reader :options
14
11
 
15
-
16
12
  def self.read(data)
17
13
  return new data.read
18
14
  end
19
15
 
20
16
  def initialize(text)
21
- @usage = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
17
+ @usage_message = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
22
18
 
23
- # ---- 1) Parse argument names ------------------------------------
19
+ # --- 1) Parse argument names -------------------------------------
24
20
  #
25
21
  # Parse the usage string and extract both optional argument names
26
22
  # and required argument names, for example:
27
23
  #
28
24
  # Usage: connect [options] [mode] host port
29
25
 
30
- text =~ /Usage: (\w+|\$0) \[options\](( \[\w+\])*)(( \w+)*)/
26
+ text =~ /Usage: (\w+|\$0)( \[options\])?(( \[\w+\])*)(( \w+)*)/
31
27
  raise RuntimeError, "Expected usage string, got none" unless $1
32
- @argument_names_optional = $2.to_s.split.map { |match| match.tr('[]', '').downcase }
33
- @argument_names_required = $4.to_s.split.map(&:downcase)
28
+ @argument_names_optional = $3.to_s.split.map { |match| match.tr('[]', '').downcase }
29
+ @argument_names_required = $5.to_s.split.map(&:downcase)
34
30
 
35
- # ---- 2) Parse option names --------------------------------------
31
+ # --- 2) Parse option names ---------------------------------------
36
32
  #
37
33
  # Parse the usage message and extract option names, their short and
38
34
  # long forms, and the associated argument name (if any), eg:
@@ -46,93 +42,61 @@ class OptionsByExample
46
42
  @option_names = {}
47
43
  text.scan(/((--?\w+)(, --?\w+)*) ?(\w+)?/) do
48
44
  opts = $1.split(", ")
49
- opts.each { |each| @option_names[each] = [opts.last.tr('-', ''), ($4.downcase if $4)] }
45
+ opts.each { |each| @option_names[each] = [opts.last.tr('-', ''), $4] }
50
46
  end
51
47
 
52
- # ---- 3) Include help option by default --------------------------
53
-
54
- @option_names.update("-h" => :help, "--help" => :help)
48
+ initialize_argument_accessors
49
+ initialize_option_accessors
55
50
  end
56
51
 
57
- def parse(argv, options = nil)
58
- array = argv.dup
59
- @arguments = {}
60
- @options = {}
61
-
62
- # --- 1) Parse options --------------------------------------------
63
-
64
- most_recent_option = nil
65
- until array.empty? do
66
- break unless array.first.start_with?(?-)
67
- most_recent_option = option = array.shift
68
- option_name, argument_name = @option_names[option]
69
- raise "Got unknown option #{option}" if option_name.nil?
70
- raise if option_name == :help # Show usage without error message
71
- @options[option_name] = true
72
-
73
- # Consume argument, if expected by most recent option
74
- if argument_name
75
- argument = array.shift
76
- raise "Expected argument for option #{option}" unless /^[^-]/ === argument
77
- @arguments[option_name] = argument
78
- most_recent_option = nil
79
- end
80
- end
81
-
82
- # --- 2) Parse optional arguments ---------------------------------
83
-
84
- # Check any start with --, ie excess options
85
- # Check min_length - max_length here
86
-
87
- stash = array.pop(@argument_names_required.length)
88
- @argument_names_optional.each do |argument_name|
89
- break if array.empty?
90
- argument = array.shift
91
- raise "Expected more arguments, got option #{option}" unless /^[^-]/ === argument
92
- @arguments[argument_name] = argument
93
- end
94
-
95
- # --- 3) Parse required arguments ---------------------------------
96
-
97
- @argument_names_required.each do |argument_name|
98
- raise "Expected required argument #{argument_name.upcase}, got none" if stash.empty?
99
- argument = stash.shift
100
- raise "Expected more arguments, got option #{option}" unless /^[^-]/ === argument
101
- @arguments[argument_name] = argument
102
- end
52
+ def parse(argv)
53
+ parse_without_exit argv
54
+ rescue PrintUsageMessage
55
+ puts @usage_message
56
+ exit 0
57
+ rescue RuntimeError => err
58
+ puts "ERROR: #{err.message}"
59
+ exit 1
60
+ end
103
61
 
104
- # --- 4) Expect to be done ----------------------------------------
62
+ def parse_without_exit(argv)
63
+ parser = Parser.new(
64
+ @argument_names_required,
65
+ @argument_names_optional,
66
+ @option_names,
67
+ )
105
68
 
106
- if not array.empty?
107
- # Custom error message if most recent option did not require argument
108
- raise "Got unexpected argument for option #{most_recent_option}" if most_recent_option
109
- min_length = @argument_names_required.size
110
- max_length = @argument_names_optional.size + min_length
111
- raise "Expected #{min_length}#{"-#{max_length}" if max_length > min_length} arguments, got more"
112
- end
69
+ parser.parse argv
70
+ @arguments = parser.arguments
71
+ @options = parser.options
113
72
 
114
73
  return self
74
+ end
115
75
 
116
- rescue RuntimeError => err
117
- exit_on_error = options ? options[:exit_on_error] : true
118
- if exit_on_error
119
- puts "ERROR: #{err.message}\n\n" unless err.message.empty?
120
- puts @usage
121
- exit
122
- else
123
- raise # Reraise the same exception
76
+ private
77
+
78
+ def initialize_argument_accessors
79
+ [
80
+ *@argument_names_required,
81
+ *@argument_names_optional,
82
+ *@option_names.values.select(&:last).map(&:first),
83
+ ].each do |argument_name|
84
+ instance_eval %{
85
+ def argument_#{argument_name}
86
+ val = @arguments["#{argument_name}"]
87
+ val && block_given? ? (yield val) : val
88
+ end
89
+ }
124
90
  end
125
91
  end
126
92
 
127
- def method_missing(sym, *args, &block)
128
- case sym
129
- when /^argument_(\w+)$/
130
- val = @arguments[$1]
131
- block && val ? block.call(val) : val
132
- when /^include_(\w+)\?$/
133
- @options[$1]
134
- else
135
- super
93
+ def initialize_option_accessors
94
+ @option_names.each_value do |option_name, _|
95
+ instance_eval %{
96
+ def include_#{option_name}?
97
+ @options.include? "#{option_name}"
98
+ end
99
+ }
136
100
  end
137
101
  end
138
102
  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.2.0
4
+ version: 2.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-28 00:00:00.000000000 Z
11
+ date: 2023-04-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -19,6 +19,7 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - README.md
21
21
  - lib/options_by_example.rb
22
+ - lib/options_by_example/parser.rb
22
23
  - lib/options_by_example/version.rb
23
24
  homepage: https://github.com/akuhn/options_by_example
24
25
  licenses:
@@ -26,6 +27,7 @@ licenses:
26
27
  metadata:
27
28
  homepage_uri: https://github.com/akuhn/options_by_example
28
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
29
31
  post_install_message:
30
32
  rdoc_options: []
31
33
  require_paths: