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