consoler 1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a3db645fb1a61eee615bf593a5a078320c098081
4
+ data.tar.gz: 675713207b19a2b6685be32447a79783738bb78d
5
+ SHA512:
6
+ metadata.gz: 9e021691c53d383d0a9b778116269d58b3b1357abd7dc51bb9e23150498f1046a62b4e0da21cd64a4e2466cc383d86f3ee5db3b3de571b8b2e94b3aca16cc69b
7
+ data.tar.gz: 410a8cc86d20818a59908a0de6f7e2561e7f8716820b1a198599c2304abae5fe99206d979af336f4527db6141035eef1f9f9b35c5d74d187aa9a81859d987c59
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options'
4
+ require_relative 'arguments'
5
+
6
+ module Consoler
7
+
8
+ # Consoler application
9
+ #
10
+ # @example A simple application
11
+ # # create a application
12
+ # app = Consoler::Application.new description: 'A simple app'
13
+ #
14
+ # # define a command
15
+ # app.build 'target [--clean]' do |target, clean|
16
+ # # clean contains a boolean
17
+ # clean_up if clean
18
+ #
19
+ # # target contains a string
20
+ # build_project target
21
+ # end
22
+ # app.run(['build', 'production', '--clean'])
23
+ #
24
+ # # this does not match, nothing is executed and the usage message is printed
25
+ # app.run(['deploy', 'production'])
26
+ class Application
27
+
28
+ # Create a consoler application
29
+ #
30
+ # @param options [Hash] Options for the application
31
+ # @option options [String] :description The description for the application (optional)
32
+ def initialize(options={})
33
+ @description = options[:description]
34
+ @commands = []
35
+ end
36
+
37
+ # Register a command for this app
38
+ #
39
+ # @param command_name [Symbol] Name of the command
40
+ # @param input [String, Consoler::Application] Options definition or a complete subapp
41
+ # @yield [...] Executed when the action is matched with parameters based on your options
42
+ # @return [nil]
43
+ def method_missing(command_name, input = nil, &block)
44
+ action = nil
45
+ options_def = ''
46
+
47
+ unless block.nil? then
48
+ action = block
49
+ options_def = input
50
+
51
+ if not options_def.nil? and not options_def.instance_of? String then
52
+ raise 'Invalid options'
53
+ end
54
+ end
55
+
56
+ if input.instance_of? Consoler::Application then
57
+ action = input
58
+ options_def = ''
59
+ end
60
+
61
+ if action.nil? then
62
+ raise 'Invalid subapp/block'
63
+ end
64
+
65
+ command = command_name.to_s
66
+
67
+ _add_command(command, options_def, action)
68
+
69
+ return nil
70
+ end
71
+
72
+ # Run the application with a list of arguments
73
+ #
74
+ # @param args [Array] Arguments
75
+ # @param disable_usage_message [Boolean] Disable the usage message when nothing it matched
76
+ # @return [mixed] Result of your matched command, <tt>nil</tt> otherwise
77
+ def run(args = ARGV, disable_usage_message = false)
78
+ # TODO signal handling of some kind?
79
+
80
+ result, matched = _run(args)
81
+
82
+ if not matched and not disable_usage_message
83
+ usage
84
+ end
85
+
86
+ return result
87
+ end
88
+
89
+ # Show the usage message
90
+ #
91
+ # Contains all commands and options, including subapps
92
+ def usage
93
+ puts "#{@description}\n\n" unless @description.nil?
94
+ puts 'Usage:'
95
+
96
+ _commands_usage $0
97
+ end
98
+
99
+ protected
100
+
101
+ def _run(args)
102
+ arg = args.shift
103
+ arguments = Consoler::Arguments.new args
104
+
105
+ @commands.each do |command|
106
+ if command.command == arg then
107
+ if command.action.instance_of? Consoler::Application then
108
+ result, matched = command.action._run(args)
109
+
110
+ if matched then
111
+ return result, true
112
+ end
113
+ else
114
+ match = arguments.match command.options
115
+
116
+ next if match.nil?
117
+
118
+ return _dispatch(command.action, match), true
119
+ end
120
+ end
121
+ end
122
+
123
+ return nil, false
124
+ end
125
+
126
+ def _commands_usage(prefix='')
127
+ @commands.each do |command|
128
+ if command.action.instance_of? Consoler::Application then
129
+ command.action._commands_usage "#{prefix} #{command.command}"
130
+ else
131
+ print " #{prefix} #{command.command}"
132
+
133
+ if command.options.size then
134
+ print " #{command.options.to_definition}"
135
+ end
136
+
137
+ unless command.options.description.nil? then
138
+ print " -- #{command.options.description}"
139
+ end
140
+
141
+ print "\n"
142
+ end
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def _add_command(command, options_def, action)
149
+ @commands.push(Consoler::Command.new(
150
+ command: command,
151
+ options: Consoler::Options.new(options_def),
152
+ action: action,
153
+ ))
154
+ end
155
+
156
+ def _dispatch(action, match)
157
+ arguments = action.parameters.map do |parameter|
158
+ match[parameter[1].to_s]
159
+ end
160
+
161
+ action.call(*arguments)
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'matcher'
4
+
5
+ module Consoler
6
+
7
+ # Arguments
8
+ #
9
+ # @attr_reader [Array<String>] args Raw arguments
10
+ class Arguments
11
+ attr_reader :args
12
+
13
+ def initialize(args)
14
+ @args = args
15
+ end
16
+
17
+ # Match arguments against options
18
+ #
19
+ # @see Consoler::Matcher#match
20
+ # @return [Hash, nil] Matched information, or <tt>nil</tt> is returned when there was no match
21
+ def match(options)
22
+ matcher = Consoler::Matcher.new self, options
23
+ matcher.match
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consoler
4
+
5
+ # Consoler command
6
+ #
7
+ # Basically a named hash
8
+ #
9
+ # @attr_reader [String] command Name of the command
10
+ # @attr_reader [Consoler::Options] options List of all options
11
+ # @attr_reader [Proc] action Action for this command
12
+ class Command
13
+ attr_reader :command
14
+ attr_reader :options
15
+ attr_reader :action
16
+
17
+ # Create a command
18
+ #
19
+ # @param [Hash] options
20
+ # @option options [String] :command Name of the command
21
+ # @option options [Consoler::Options] :options List of all options
22
+ # @option options [Proc] :action Action for this command
23
+ def initialize(options)
24
+ @command = options[:command]
25
+ @options = options[:options]
26
+ @action = options[:action]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consoler
4
+
5
+ # Argument/Options matcher
6
+ #
7
+ # Given a list of arguments and a list option try to match them
8
+ class Matcher
9
+
10
+ # Create a matcher
11
+ #
12
+ # @param [Consoler::Arguments] arguments List of arguments
13
+ # @param [Consoler::Options] options List of options
14
+ def initialize(arguments, options)
15
+ @arguments = arguments
16
+ @options = options
17
+
18
+ @index = 0
19
+ @matched_options = {}
20
+ @argument_values = []
21
+ end
22
+
23
+ # Match arguments against options
24
+ #
25
+ # @return [Hash, nil] Matched information, or <tt>nil</tt> is returned when there was no match
26
+ def match
27
+ parse_options = true
28
+
29
+ loop_args do |arg|
30
+ unless parse_options then
31
+ @argument_values.push arg
32
+ next
33
+ end
34
+
35
+ if arg == '--' then
36
+ parse_options = false
37
+ next
38
+ end
39
+
40
+ analyzed = _analyze arg
41
+
42
+ if analyzed.nil?
43
+ return nil
44
+ end
45
+ end
46
+
47
+ remaining = _match_arguments
48
+ _fill_defaults
49
+
50
+ if @matched_options.size == @options.size then
51
+ @matched_options['remaining'] = remaining
52
+ return @matched_options
53
+ end
54
+
55
+ return nil
56
+ end
57
+
58
+ private
59
+
60
+ def _analyze(arg)
61
+ is_long = false
62
+ is_short = false
63
+ name = nil
64
+
65
+ if arg[0..1] == '--' then
66
+ is_long = true
67
+ name = arg[2..-1]
68
+ elsif arg[0] == '-' then
69
+ is_short = true
70
+ name = arg[1..-1]
71
+ end
72
+
73
+ if name.nil?
74
+ @argument_values.push arg
75
+ return true
76
+ end
77
+
78
+ unless name.nil? then
79
+ option_name = if is_short then
80
+ name[0]
81
+ else
82
+ name
83
+ end
84
+
85
+ option = @options.get option_name
86
+
87
+ return nil if option.nil?
88
+
89
+ needs_short = option.is_short
90
+ needs_long = option.is_long
91
+
92
+ if needs_long and not is_long then
93
+ return nil
94
+ elsif needs_short and not is_short then
95
+ return nil
96
+ end
97
+
98
+ if is_long then
99
+ if option.is_value then
100
+ return nil if peek_next.nil?
101
+ @matched_options[name] = peek_next
102
+ skip
103
+ else
104
+ @matched_options[name] = true
105
+ end
106
+ end
107
+
108
+ if is_short then
109
+ if name.size == 1 and option.is_value then
110
+ return nil if peek_next.nil?
111
+ @matched_options[name] = peek_next
112
+ skip
113
+ else
114
+ name.split('').each do |n|
115
+ if @matched_options[n].nil? then
116
+ @matched_options[n] = 0
117
+ end
118
+
119
+ @matched_options[n] += 1
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ return true
126
+ end
127
+
128
+ def current
129
+ @arguments.args[@index]
130
+ end
131
+
132
+ def peek_next
133
+ @arguments.args[@index + 1]
134
+ end
135
+
136
+ def loop_args
137
+ @index = 0
138
+ size = @arguments.args.size
139
+
140
+ while @index < size do
141
+ yield current
142
+
143
+ skip
144
+ end
145
+ end
146
+
147
+ def skip
148
+ @index += 1
149
+ end
150
+
151
+ def _match_arguments
152
+ @optionals_before = {}
153
+ @optionals_before_has_remaining = false
154
+
155
+ argument_values_index = 0
156
+
157
+ _match_arguments_optionals_before
158
+
159
+ @optionals_before.each do |mandatory_arg_name, optionals|
160
+ optionals.each do |_, optional|
161
+ optional.each do |before|
162
+ if before[:included] then
163
+ @matched_options[before[:name]] = @argument_values[argument_values_index]
164
+ argument_values_index += 1
165
+ end
166
+ end
167
+ end
168
+
169
+ if mandatory_arg_name != :REMAINING then
170
+ @matched_options[mandatory_arg_name] = @argument_values[argument_values_index]
171
+ argument_values_index += 1
172
+ end
173
+ end
174
+
175
+ remaining = []
176
+
177
+ while argument_values_index < @argument_values.size do
178
+ remaining.push @argument_values[argument_values_index]
179
+ argument_values_index += 1
180
+ end
181
+
182
+ remaining
183
+ end
184
+
185
+ def _match_arguments_optionals_before
186
+ @optionals_before = {}
187
+ tracker = {}
188
+
189
+ @options.each do |option, key|
190
+ next unless option.is_argument
191
+
192
+ if option.is_optional then
193
+ tracker[option.is_optional] = [] if tracker[option.is_optional].nil?
194
+
195
+ tracker[option.is_optional].push({
196
+ included: false,
197
+ name: option.name,
198
+ })
199
+ else
200
+ @optionals_before[option.name] = tracker
201
+ tracker = {}
202
+ end
203
+ end
204
+
205
+ if tracker != {} then
206
+ @optionals_before[:REMAINING] = tracker
207
+ @optionals_before_has_remaining = true
208
+ end
209
+
210
+ _match_arguments_optoins_before_matcher
211
+ end
212
+
213
+ def _match_arguments_optoins_before_matcher
214
+ mandatories_matched = @optionals_before.size
215
+
216
+ if @optionals_before_has_remaining then
217
+ mandatories_matched -= 1
218
+ end
219
+
220
+ total = 0
221
+
222
+ _each_optional_before_sorted do |before|
223
+ if (total + before.size + mandatories_matched) <= @argument_values.size then
224
+ total += before.size
225
+
226
+ before.each do |val|
227
+ val[:included] = true;
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ def _fill_defaults
234
+ @options.each do |option|
235
+ if option.is_optional then
236
+ unless @matched_options.has_key? option.name then
237
+ @matched_options[option.name] = option.default_value
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ def _each_optional_before_sorted
244
+ @optionals_before.each do |_, optionals|
245
+ tmp = []
246
+ optionals.each do |optional_index, before|
247
+ tmp.push({
248
+ count: before.size,
249
+ index: optional_index,
250
+ })
251
+ end
252
+
253
+ tmp.sort! { |a, b| b[:count] - a[:count] }.each do |item|
254
+ yield optionals[item[:index]]
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consoler
4
+
5
+ # Represents an option
6
+ #
7
+ # @attr_reader [String] name Name of the options
8
+ # @attr_reader [Boolean] is_long Is the option long (<tt>--option</tt>)
9
+ # @attr_reader [Boolean] is_short Is the option short (<tt>-o</tt>)
10
+ # @attr_reader [Boolean] is_argument Is the option an argument
11
+ # @attr_reader [Boolean] is_value Does the option need a value (<tt>--option=</tt>)
12
+ # @attr_reader [Integer] is_optional Is the option optional (> 0) (<tt>[option]</tt>)
13
+ class Option
14
+ attr_reader :name
15
+ attr_reader :is_long
16
+ attr_reader :is_short
17
+ attr_reader :is_argument
18
+ attr_reader :is_value
19
+ attr_reader :is_optional
20
+
21
+ # Create a option
22
+ #
23
+ # Yields an option for every option detected
24
+ #
25
+ # @param option_def [String] Definition of the option
26
+ # @param tracker [Consoler::OptionalsTracker] optionals tracker
27
+ def self.create(option_def, tracker)
28
+ option = Option.new option_def, tracker
29
+
30
+ if option.is_short and option.name.size > 1 then
31
+ old_tracking = tracker.is_tracking
32
+ old_is_value = option.is_value
33
+
34
+ if option.is_optional then
35
+ tracker.is_tracking = true
36
+ end
37
+
38
+ names = option.name.split('')
39
+
40
+ names.each_with_index do |name, i|
41
+ new_name = "-#{name}"
42
+
43
+ if old_is_value and i == names.count - 1 then
44
+ new_name = "#{new_name}="
45
+ end
46
+
47
+ yield Option.new new_name, tracker
48
+ end
49
+
50
+ tracker.is_tracking = old_tracking
51
+ else
52
+ yield option
53
+ end
54
+ end
55
+
56
+ # Get the definition of the option
57
+ #
58
+ # Does not include the optional information, as that is linked to other
59
+ # options
60
+ #
61
+ # @return [String]
62
+ def to_definition
63
+ definition = name
64
+
65
+ if is_long then
66
+ definition = "--#{definition}"
67
+ elsif is_short then
68
+ definition = "-#{definition}"
69
+ end
70
+
71
+ if is_value then
72
+ definition = "#{definition}="
73
+ end
74
+
75
+ definition
76
+ end
77
+
78
+ # Get the default value of this option
79
+ #
80
+ # @return [nil | 0 | false]
81
+ def default_value
82
+ return nil if is_value
83
+ return 0 if is_short
84
+ return false if is_long
85
+
86
+ return nil
87
+ end
88
+
89
+ protected
90
+
91
+ # Create a option
92
+ #
93
+ # @param [String] option_def Definition of the option
94
+ # @param [Consoler::Tracker] tracker tracker
95
+ def initialize(option_def, tracker)
96
+ option, @is_optional = _is_optional option_def, tracker
97
+ option, @is_long = _is_long option
98
+ option, @is_short = _is_short option
99
+ @is_argument = (not @is_long and not @is_short)
100
+ option, @is_value = _value option, @is_argument
101
+
102
+ @name = option
103
+
104
+ if @name.empty? then
105
+ raise 'Option must have a name'
106
+ end
107
+
108
+ if @is_long and @is_short
109
+ raise 'Option can not be a long and a short option'
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def _is_optional(option, tracker)
116
+ if option[0] == '[' then
117
+ if !tracker.is_tracking then
118
+ tracker.is_tracking = true
119
+ tracker.index += 1
120
+ option = option[1..-1]
121
+ else
122
+ raise 'Nested optionals are not allowed'
123
+ end
124
+ end
125
+
126
+ optional = if tracker.is_tracking then
127
+ tracker.index
128
+ else
129
+ nil
130
+ end
131
+
132
+ if option[-1] == ']' then
133
+ if tracker.is_tracking then
134
+ tracker.is_tracking = false
135
+ option = option[0..-2]
136
+ else
137
+ raise 'Unopened optional'
138
+ end
139
+ end
140
+
141
+ return option, optional
142
+ end
143
+
144
+ def _is_long(option)
145
+ if option[0..1] == '--' then
146
+ long = true
147
+ option = option[2..-1]
148
+ else
149
+ long = false
150
+ end
151
+
152
+ return option, long
153
+ end
154
+
155
+ def _is_short(option)
156
+ if option[0] == '-' then
157
+ short = true
158
+ option = option[1..-1]
159
+ else
160
+ short = false
161
+ end
162
+
163
+ return option, short
164
+ end
165
+
166
+ def _value(option, argument)
167
+ if option[-1] == '=' then
168
+ if argument then
169
+ raise 'Arguments can\'t have a value'
170
+ end
171
+
172
+ value = true
173
+ option = option[0..-2]
174
+ else
175
+ value = false
176
+ end
177
+
178
+ return option, value
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'option'
4
+
5
+ module Consoler
6
+
7
+ # List of options
8
+ #
9
+ # @attr_reader [String] description Description of the options
10
+ class Options
11
+ attr_reader :description
12
+
13
+ # Create a list of option based on a string definition
14
+ #
15
+ # @param options_def [String] A string definition of the desired options
16
+ def initialize(options_def)
17
+ @options = []
18
+ @description = nil
19
+
20
+ return if options_def.nil?
21
+
22
+ if match = /(^|\s+)-- (?<description>.*)$/.match(options_def) then
23
+ @description = match[:description]
24
+ options_def = options_def[0...-match[0].size]
25
+ end
26
+
27
+ options = options_def.split ' '
28
+ tracker = Consoler::OptionalsTracker.new
29
+
30
+ option_names = []
31
+
32
+ while option_def = options.shift do
33
+ Consoler::Option.create option_def, tracker do |option|
34
+ raise "Duplicate option name: #{option.name}" if option_names.include? option.name
35
+
36
+ @options.push option
37
+ option_names.push option.name
38
+ end
39
+ end
40
+ end
41
+
42
+ # Get a options by its name
43
+ #
44
+ # @param name [String] Name of the option
45
+ # @return [Consoler::Option, nil]
46
+ def get(name)
47
+ each do |option|
48
+ if option.name == name then
49
+ return option
50
+ end
51
+ end
52
+
53
+ return nil
54
+ end
55
+
56
+ # Loop through all options
57
+ #
58
+ # @yield [Consoler::Option, Integer] An option
59
+ # @return [Consoler::Options]
60
+ def each
61
+ @options.each_with_index do |option, i|
62
+ yield option, i
63
+ end
64
+
65
+ self
66
+ end
67
+
68
+ # Get the number of options
69
+ #
70
+ # @return [Integer]
71
+ def size
72
+ @options.size
73
+ end
74
+
75
+ def to_definition
76
+ definition = ''
77
+ optional = nil
78
+
79
+ each do |option, i|
80
+ definition += ' '
81
+
82
+ if optional.nil? and option.is_optional then
83
+ definition += '['
84
+ optional = option.is_optional
85
+ end
86
+
87
+ definition += option.to_definition
88
+
89
+ if option.is_optional then
90
+ if @options[i + 1].nil? or optional != @options[i + 1].is_optional then
91
+ definition += ']'
92
+ optional = nil
93
+ end
94
+ end
95
+ end
96
+
97
+ definition.strip
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ # Optionals tracker
104
+ #
105
+ # @attr [Boolean] is_tracking Is inside optional options
106
+ # @attr [Integer] index Optional group
107
+ class OptionalsTracker
108
+ attr_accessor :is_tracking
109
+ attr_accessor :index
110
+
111
+ # Create an optionals tracker
112
+ def initialize
113
+ @is_tracking = nil
114
+ @index = 0
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consoler
4
+
5
+ # Current version number
6
+ VERSION = '1.0.0'
7
+ end
data/lib/consoler.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'consoler/application'
4
+ require_relative 'consoler/command'
5
+
6
+ # Consoler
7
+ #
8
+ # Sinatra-like application builder for the console
9
+ #
10
+ # See {Consoler::Application} for usage information
11
+ module Consoler
12
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: consoler
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Tim
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-03-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: yard
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.9.12
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.9.12
27
+ - !ruby/object:Gem::Dependency
28
+ name: simplecov
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.15.1
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.15.1
41
+ description: Sinatra-like application builder for the console
42
+ email: me@justim.net
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/consoler.rb
48
+ - lib/consoler/application.rb
49
+ - lib/consoler/arguments.rb
50
+ - lib/consoler/command.rb
51
+ - lib/consoler/matcher.rb
52
+ - lib/consoler/option.rb
53
+ - lib/consoler/options.rb
54
+ - lib/consoler/version.rb
55
+ homepage: https://github.com/justim/consoler-rb
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ yard.run: yri
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 2.6.11
77
+ signing_key:
78
+ specification_version: 4
79
+ summary: Consoler
80
+ test_files: []