options_by_example 1.2.0 → 2.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: 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: