consoler 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []