options_by_example 1.2.0 → 1.3.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: 26769b40ccdbee8bcb810e85755f7e3774145dda2ea8860edda1c99a65e168a5
4
+ data.tar.gz: 5d11ede203262af12bb65613ea5d153bb699188443453d82f8fb5bccb13068a1
5
5
  SHA512:
6
- metadata.gz: 1670b512ab6a4517bc38141a403d88798f5ca250b23dd7c7d3958a542dc9c1aeeca42b1d7ecfc76219c553d14c3a96e4662806712e1abc410b0f5a675254d3e2
7
- data.tar.gz: b82aaa8cca78d5c92166c64268fc49edb20507b504bd87bec50446d1de9877b497169a4f657457e9fc6469fe6a38a7baabe488aeccf95c6a6ca10a584eafe843
6
+ metadata.gz: 1cb7a3d1732b19543557ed113094204451f9d24e2fae91029aa19d3f2985482e5536cc8c185e1dbef15fd77d330dd7ea76e7d0a6c596f7e198278f4afdf3bc74
7
+ data.tar.gz: 61a4d2a408fbe795040f7963e6d9cdb82076872bf7118c7fbb633b940684bd5eed9884f2d4a3f73cc4f321eb4e976d9ff5b252a17694a6316c4849314a5aa586
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ class OptionsByExample
5
+ class Parser
6
+
7
+ attr_reader :options
8
+ attr_reader :arguments
9
+
10
+ def initialize(argument_names_required, argument_names_optional, option_names)
11
+ @argument_names_required = argument_names_required
12
+ @argument_names_optional = argument_names_optional
13
+ @option_names = option_names
14
+
15
+ @arguments = {}
16
+ @options = {}
17
+ end
18
+
19
+ def parse(array)
20
+
21
+ # Separate command-line options and their respective arguments into
22
+ # chunks plus handling any remaining arguments. This organization
23
+ # facilitates further processing and validation of the input.
24
+
25
+ @chunks = []
26
+ @remainder = current = []
27
+ array.each do |each|
28
+ @chunks << current = [] if each.start_with?(?-)
29
+ current << each
30
+ end
31
+
32
+ find_help_option
33
+ find_unknown_options
34
+ parse_options
35
+
36
+ validate_number_of_arguments
37
+ parse_required_arguments
38
+ parse_optional_arguments
39
+
40
+ raise "Internal error: unreachable state" unless @remainder.empty?
41
+ end
42
+
43
+ private
44
+
45
+ def find_help_option
46
+ @chunks.each do |option, *args|
47
+ case option
48
+ when '-h', '--help'
49
+ raise "puts @usage_message"
50
+ end
51
+ end
52
+ end
53
+
54
+ def find_unknown_options
55
+ @chunks.each do |option, *args|
56
+ raise "Found unknown option '#{option}'" unless @option_names.include?(option)
57
+ end
58
+ end
59
+
60
+ def parse_options
61
+ @chunks.each do |option, *args|
62
+ if @remainder.any?
63
+ raise "Unexpected arguments found before option '#{option}', please provide all options before arguments"
64
+ end
65
+
66
+ option_name, argument_name = @option_names[option]
67
+ @options[option_name] = true
68
+
69
+ if argument_name
70
+ raise "Expected argument for option '#{option}', got none" if args.empty?
71
+ @arguments[option_name] = args.shift
72
+ end
73
+
74
+ @remainder = args
75
+ end
76
+ end
77
+
78
+ def validate_number_of_arguments
79
+ min_length = @argument_names_required.size
80
+ max_length = @argument_names_optional.size + min_length
81
+ if @remainder.size > max_length
82
+ range = [min_length, max_length].uniq.join(?-)
83
+ raise "Expected #{range} arguments, but received too many"
84
+ end
85
+ end
86
+
87
+ def parse_required_arguments
88
+ stash = @remainder.pop(@argument_names_required.length)
89
+ @argument_names_required.each do |argument_name|
90
+ raise "Missing required argument '#{argument_name}'" if stash.empty?
91
+ @arguments[argument_name] = stash.shift
92
+ end
93
+ end
94
+
95
+ def parse_optional_arguments
96
+ @argument_names_optional.each do |argument_name|
97
+ break if @remainder.empty?
98
+ @arguments[argument_name] = @remainder.shift
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OptionsByExample
4
- VERSION = '1.2.0'
4
+ VERSION = '1.3.0'
5
5
  end
6
6
 
7
7
 
@@ -11,6 +11,11 @@ __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
+ 1.3.0
15
+
16
+ - Extract parsing functionality into class
17
+ - Better error messages
18
+
14
19
  1.2.0
15
20
 
16
21
  - Ensure compatibility with Ruby versions 1.9.3 and newer
@@ -1,6 +1,7 @@
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
@@ -18,21 +19,22 @@ class OptionsByExample
18
19
  end
19
20
 
20
21
  def initialize(text)
21
- @usage = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
22
+ @settings = {exit_on_error: true}
23
+ @usage_message = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
22
24
 
23
- # ---- 1) Parse argument names ------------------------------------
25
+ # --- 1) Parse argument names -------------------------------------
24
26
  #
25
27
  # Parse the usage string and extract both optional argument names
26
28
  # and required argument names, for example:
27
29
  #
28
30
  # Usage: connect [options] [mode] host port
29
31
 
30
- text =~ /Usage: (\w+|\$0) \[options\](( \[\w+\])*)(( \w+)*)/
32
+ text =~ /Usage: (\w+|\$0)( \[options\])?(( \[\w+\])*)(( \w+)*)/
31
33
  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)
34
+ @argument_names_optional = $3.to_s.split.map { |match| match.tr('[]', '').downcase }
35
+ @argument_names_required = $5.to_s.split.map(&:downcase)
34
36
 
35
- # ---- 2) Parse option names --------------------------------------
37
+ # --- 2) Parse option names ---------------------------------------
36
38
  #
37
39
  # Parse the usage message and extract option names, their short and
38
40
  # long forms, and the associated argument name (if any), eg:
@@ -46,84 +48,42 @@ class OptionsByExample
46
48
  @option_names = {}
47
49
  text.scan(/((--?\w+)(, --?\w+)*) ?(\w+)?/) do
48
50
  opts = $1.split(", ")
49
- opts.each { |each| @option_names[each] = [opts.last.tr('-', ''), ($4.downcase if $4)] }
51
+ opts.each { |each| @option_names[each] = [opts.last.tr('-', ''), $4] }
50
52
  end
51
-
52
- # ---- 3) Include help option by default --------------------------
53
-
54
- @option_names.update("-h" => :help, "--help" => :help)
55
53
  end
56
54
 
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 ---------------------------------
55
+ def use(settings)
56
+ @settings.update settings
83
57
 
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 ---------------------------------
58
+ return self
59
+ end
96
60
 
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
61
+ def parse(argv)
62
+ parser = Parser.new(
63
+ @argument_names_required,
64
+ @argument_names_optional,
65
+ @option_names,
66
+ )
103
67
 
104
- # --- 4) Expect to be done ----------------------------------------
68
+ parser.parse argv
105
69
 
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
70
+ @options = parser.options
71
+ @arguments = parser.arguments
113
72
 
114
73
  return self
115
-
116
74
  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
75
+ raise unless @settings[:exit_on_error]
76
+
77
+ if err.message == "puts @usage_message"
78
+ puts @usage_message
122
79
  else
123
- raise # Reraise the same exception
80
+ puts "ERROR: #{err.message}"
124
81
  end
82
+
83
+ exit 1
125
84
  end
126
85
 
86
+
127
87
  def method_missing(sym, *args, &block)
128
88
  case sym
129
89
  when /^argument_(\w+)$/
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: 1.3.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-29 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: