bee 0.3.1 → 0.4.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.
- data/README +1 -1
- data/bin/bee +16 -0
- data/bin/bee.bat +32 -0
- data/lib/bee.rb +165 -170
- data/lib/bee_console.rb +90 -87
- data/lib/bee_task.rb +149 -0
- data/lib/bee_task_default.rb +525 -0
- data/lib/bee_util.rb +230 -0
- data/test/tc_bee_build.rb +216 -0
- data/test/tc_bee_console.rb +106 -0
- data/test/tc_bee_console_formatter.rb +38 -3
- data/test/tc_bee_context.rb +84 -0
- data/test/tc_bee_task_default.rb +467 -0
- data/test/tc_bee_util.rb +85 -0
- data/test/test_build.rb +26 -0
- data/test/test_build_listener.rb +60 -0
- data/test/tmp_test_case.rb +58 -0
- data/test/ts_bee.rb +1 -1
- metadata +35 -8
- data/lib/bee_default_context.rb +0 -475
- data/test/tc_bee_default_tasks.rb +0 -298
data/lib/bee_console.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2006 Michel Casabianca <
|
1
|
+
# Copyright 2006-2007 Michel Casabianca <michel.casabianca@gmail.com>
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -12,23 +12,28 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
+
require 'bee'
|
16
|
+
require 'bee_task'
|
17
|
+
require 'bee_util'
|
18
|
+
require 'getoptlong'
|
19
|
+
|
15
20
|
# Module for Bee stuff.
|
16
21
|
module Bee
|
17
22
|
|
18
23
|
module Console
|
19
24
|
|
20
|
-
require 'bee'
|
21
|
-
require 'getoptlong'
|
22
|
-
|
23
25
|
# Command line help.
|
24
26
|
HELP = 'Usage: bee [options] [targets]
|
25
27
|
-h Print help about usage and exit.
|
26
28
|
-b Print help about build and exit.
|
27
|
-
-k
|
28
|
-
|
29
|
+
-k task Print help about tasks in a package (writing "foo.?") or a
|
30
|
+
given one (writing "foo.bar") and exit.
|
31
|
+
-t Write template build file on disk (use -f to write build file
|
32
|
+
in another one than default "build.yml").
|
29
33
|
-v Enable verbose mode.
|
30
34
|
-s style Define style for output (see documentation).
|
31
35
|
-f file Build file to run (defaults to "build.yml").
|
36
|
+
-r Look for build file recursively up in file system.
|
32
37
|
targets Targets to run (default target if omitted).'
|
33
38
|
# Name for default build file.
|
34
39
|
DEFAULT_BUILD_FILE = 'build.yml'
|
@@ -38,6 +43,10 @@ targets Targets to run (default target if omitted).'
|
|
38
43
|
EXIT_BUILD_ERROR = 2
|
39
44
|
# Exit value on unknown error
|
40
45
|
EXIT_UNKNOWN_ERROR = 3
|
46
|
+
# Bee options environment variable.
|
47
|
+
BEE_OPT_ENV = 'BEEOPT'
|
48
|
+
# README file where copyright is extracted.
|
49
|
+
README_FILE = 'README'
|
41
50
|
|
42
51
|
# Parse command line and return parsed arguments.
|
43
52
|
def self.parse_command_line
|
@@ -49,19 +58,21 @@ targets Targets to run (default target if omitted).'
|
|
49
58
|
verbose = false
|
50
59
|
style = nil
|
51
60
|
file = DEFAULT_BUILD_FILE
|
61
|
+
recursive = false
|
52
62
|
targets = []
|
53
63
|
# read options in BEEOPT environment variable
|
54
|
-
options = ENV[
|
64
|
+
options = ENV[BEE_OPT_ENV]
|
55
65
|
options.split(' ').reverse.each { |option| ARGV.unshift(option) } if
|
56
66
|
options
|
57
67
|
# parse command line arguments
|
58
68
|
opts = GetoptLong.new(['--help', '-h', GetoptLong::NO_ARGUMENT],
|
59
69
|
['--help-build', '-b', GetoptLong::NO_ARGUMENT],
|
60
|
-
['--help-task','-k', GetoptLong::
|
70
|
+
['--help-task','-k', GetoptLong::REQUIRED_ARGUMENT],
|
61
71
|
['--template', '-t', GetoptLong::NO_ARGUMENT ],
|
62
72
|
['--verbose', '-v', GetoptLong::NO_ARGUMENT],
|
63
73
|
['--style', '-s', GetoptLong::REQUIRED_ARGUMENT],
|
64
|
-
['--file', '-f', GetoptLong::REQUIRED_ARGUMENT]
|
74
|
+
['--file', '-f', GetoptLong::REQUIRED_ARGUMENT],
|
75
|
+
['--recursive', '-r', GetoptLong::NO_ARGUMENT])
|
65
76
|
opts.each do |opt, arg|
|
66
77
|
case opt
|
67
78
|
when '--help'
|
@@ -79,11 +90,13 @@ targets Targets to run (default target if omitted).'
|
|
79
90
|
style = arg
|
80
91
|
when '--file'
|
81
92
|
file = arg
|
93
|
+
when '--recursive'
|
94
|
+
recursive = true
|
82
95
|
end
|
83
96
|
end
|
84
97
|
targets = ARGV
|
85
98
|
return [help, help_build, help_task, task, template, verbose, style,
|
86
|
-
file, targets]
|
99
|
+
file, recursive, targets]
|
87
100
|
end
|
88
101
|
|
89
102
|
# Start build from command line.
|
@@ -91,7 +104,7 @@ targets Targets to run (default target if omitted).'
|
|
91
104
|
STDOUT.sync = true
|
92
105
|
begin
|
93
106
|
help, help_build, help_task, task, template, verbose, style, file,
|
94
|
-
|
107
|
+
recursive, targets = parse_command_line
|
95
108
|
rescue
|
96
109
|
puts "ERROR: parsing command line (type 'bee -h' for help)"
|
97
110
|
exit(EXIT_PARSING_CMDLINE)
|
@@ -99,34 +112,33 @@ targets Targets to run (default target if omitted).'
|
|
99
112
|
formatter = Formatter.new(style)
|
100
113
|
begin
|
101
114
|
if help
|
102
|
-
readme = File.join(File.dirname(__FILE__), '..',
|
103
|
-
if File.exists?(readme)
|
115
|
+
readme = File.join(File.dirname(__FILE__), '..', README_FILE)
|
116
|
+
if File.exists?(readme) and File.file?(readme) and
|
117
|
+
File.readable?(readme)
|
104
118
|
copyright = File.read(readme).strip!.split("\n")[-1]
|
105
119
|
puts copyright
|
106
120
|
end
|
107
121
|
puts HELP
|
108
122
|
elsif help_build
|
109
|
-
build = Build.
|
123
|
+
build = Build.load(file, recursive)
|
110
124
|
puts formatter.help_build(build)
|
111
125
|
elsif help_task
|
112
|
-
|
113
|
-
puts formatter.help_task(build, task)
|
126
|
+
puts formatter.help_task(task)
|
114
127
|
elsif template
|
115
128
|
puts "Writing build template in file '#{file}'..."
|
116
|
-
raise BuildError.new("Build file '#{file}' already exists") if
|
129
|
+
raise Bee::Util::BuildError.new("Build file '#{file}' already exists") if
|
117
130
|
File.exists?(file)
|
118
131
|
File.open(file, 'w') { |file| file.write(BUILD_TEMPLATE) }
|
119
132
|
puts formatter.format_success("OK")
|
120
133
|
else
|
121
|
-
listener =
|
122
|
-
build = Build.
|
134
|
+
listener = Listener.new(formatter, verbose)
|
135
|
+
build = Build.load(file, recursive)
|
123
136
|
build.run(targets, listener)
|
124
137
|
end
|
125
|
-
rescue BuildError
|
138
|
+
rescue Bee::Util::BuildError => e
|
126
139
|
puts "#{formatter.format_error('ERROR')}: #{$!}"
|
140
|
+
puts e.backtrace.join("\n") if verbose
|
127
141
|
exit(EXIT_BUILD_ERROR)
|
128
|
-
rescue SystemExit
|
129
|
-
# do nothing, exit in code
|
130
142
|
rescue Exception => e
|
131
143
|
puts "#{formatter.format_error('ERROR')}: #{$!}"
|
132
144
|
puts e.backtrace.join("\n")
|
@@ -134,16 +146,10 @@ targets Targets to run (default target if omitted).'
|
|
134
146
|
end
|
135
147
|
end
|
136
148
|
|
137
|
-
#########################################################################
|
138
|
-
# FORMATTER CLASS #
|
139
|
-
#########################################################################
|
140
|
-
|
141
149
|
# Class to format build output on console.
|
142
150
|
class Formatter
|
143
151
|
|
144
|
-
include BuildErrorMixin
|
145
|
-
|
146
|
-
###################### ANSI COLORS AND STYLES #########################
|
152
|
+
include Bee::Util::BuildErrorMixin
|
147
153
|
|
148
154
|
# List of colors.
|
149
155
|
COLORS = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
|
@@ -182,8 +188,6 @@ targets Targets to run (default target if omitted).'
|
|
182
188
|
:hidden => 8
|
183
189
|
}
|
184
190
|
|
185
|
-
############################ DEFAULT STYLE ############################
|
186
|
-
|
187
191
|
# Default style (supposed to work on any configuration).
|
188
192
|
DEFAULT_STYLE = {
|
189
193
|
:line_character => '-'
|
@@ -205,11 +209,9 @@ targets Targets to run (default target if omitted).'
|
|
205
209
|
'sb' => 'success_background',
|
206
210
|
'es' => 'error_style',
|
207
211
|
'ef' => 'error_foreground',
|
208
|
-
'eb' => 'error_background'
|
212
|
+
'eb' => 'error_background'
|
209
213
|
}
|
210
214
|
|
211
|
-
############################## METHODS ################################
|
212
|
-
|
213
215
|
# Constructor.
|
214
216
|
# - style: style as a Hash or a String.
|
215
217
|
def initialize(style)
|
@@ -227,17 +229,7 @@ targets Targets to run (default target if omitted).'
|
|
227
229
|
# - target: target to format.
|
228
230
|
def format_target(target)
|
229
231
|
name = target.name
|
230
|
-
|
231
|
-
length = @style[:line_length] || line_length || DEFAULT_LINE_LENGTH
|
232
|
-
right = ' ' + @style[:line_character]*2
|
233
|
-
left = @style[:line_character]*(length - (name.length + 4)) + ' '
|
234
|
-
line = left + name + right
|
235
|
-
# apply style
|
236
|
-
formatted = style(line,
|
237
|
-
@style[:target_style],
|
238
|
-
@style[:target_foreground],
|
239
|
-
@style[:target_background])
|
240
|
-
return formatted
|
232
|
+
return format_title(name)
|
241
233
|
end
|
242
234
|
|
243
235
|
# Format a task.
|
@@ -247,7 +239,7 @@ targets Targets to run (default target if omitted).'
|
|
247
239
|
source = task
|
248
240
|
elsif task.kind_of?(Hash)
|
249
241
|
if task.key?('rb')
|
250
|
-
source = task['rb']
|
242
|
+
source = "rb: #{task['rb']}"
|
251
243
|
else
|
252
244
|
source = YAML::dump(task)
|
253
245
|
source = source.sub(/---/, '')
|
@@ -285,43 +277,45 @@ targets Targets to run (default target if omitted).'
|
|
285
277
|
# - build: running build.
|
286
278
|
def help_build(build)
|
287
279
|
help = ''
|
280
|
+
# print build name and description
|
288
281
|
if build.name
|
289
282
|
help << "- Build: #{build.name.inspect}\n"
|
290
283
|
end
|
291
284
|
if build.description
|
292
285
|
help << format_description('Description', build.description, 2, false)
|
293
286
|
end
|
294
|
-
|
287
|
+
# print build properties
|
288
|
+
if build.context.properties.length > 0
|
295
289
|
help << "- Properties:\n"
|
296
|
-
for property in build.properties.
|
297
|
-
help << " - #{property}:
|
290
|
+
for property in build.context.properties.sort
|
291
|
+
help << " - #{property}: " +
|
292
|
+
"#{build.context.get_property(property, true).inspect}\n"
|
298
293
|
end
|
299
294
|
end
|
295
|
+
# print build targets
|
300
296
|
if build.targets.length > 0
|
301
297
|
help << "- Targets:\n"
|
302
298
|
for target in build.targets.values.sort { |a, b| a.name <=> b.name }
|
303
299
|
help << format_description(target.name, target.description, 2)
|
304
300
|
end
|
305
301
|
end
|
302
|
+
# print default target
|
306
303
|
help << "- Default: #{build.default}"
|
307
304
|
return help.strip
|
308
305
|
end
|
309
306
|
|
310
307
|
# Return help about task(s).
|
311
|
-
# - build: running build.
|
312
308
|
# - task: task to print help about (all tasks if nil).
|
313
|
-
def help_task(
|
314
|
-
task =
|
309
|
+
def help_task(task)
|
310
|
+
task = '*' if task == nil or task.length == 0
|
311
|
+
package_manager = Bee::Task::PackageManager.new(nil)
|
312
|
+
methods = package_manager.help_task(task)
|
315
313
|
help = ''
|
316
|
-
|
317
|
-
|
318
|
-
help <<
|
319
|
-
|
320
|
-
help << "
|
321
|
-
for task in build.tasks.keys.sort
|
322
|
-
help << "#{task}, "
|
323
|
-
end
|
324
|
-
help = help[0..-3]
|
314
|
+
for method in methods.keys
|
315
|
+
help << format_title(method)
|
316
|
+
help << "\n"
|
317
|
+
help << methods[method].strip
|
318
|
+
help << "\n"
|
325
319
|
end
|
326
320
|
return help
|
327
321
|
end
|
@@ -353,20 +347,6 @@ targets Targets to run (default target if omitted).'
|
|
353
347
|
return colorized
|
354
348
|
end
|
355
349
|
|
356
|
-
# Get line length calling IOCTL. Return nil if call failed.
|
357
|
-
def line_length
|
358
|
-
begin
|
359
|
-
tiocgwinsz = 0x5413
|
360
|
-
string = [0, 0, 0, 0].pack('SSSS')
|
361
|
-
if $stdin.ioctl(tiocgwinsz, string) >= 0 then
|
362
|
-
rows, cols, xpixels, ypixels = string.unpack('SSSS')
|
363
|
-
return cols
|
364
|
-
end
|
365
|
-
rescue
|
366
|
-
return nil
|
367
|
-
end
|
368
|
-
end
|
369
|
-
|
370
350
|
# Parse style from command line. If error occurs parsing style, return
|
371
351
|
# nil (which means default style).
|
372
352
|
# - string: style to parse.
|
@@ -405,7 +385,7 @@ targets Targets to run (default target if omitted).'
|
|
405
385
|
string = ' '*indent
|
406
386
|
string << '- ' if bullet
|
407
387
|
string << title
|
408
|
-
if text
|
388
|
+
if text and !text.empty?
|
409
389
|
string << ": "
|
410
390
|
if text.split("\n").length > 1
|
411
391
|
string << "\n"
|
@@ -415,19 +395,34 @@ targets Targets to run (default target if omitted).'
|
|
415
395
|
else
|
416
396
|
string << text.strip + "\n"
|
417
397
|
end
|
398
|
+
else
|
399
|
+
string << "\n"
|
418
400
|
end
|
419
401
|
return string
|
420
402
|
end
|
403
|
+
|
404
|
+
# Format a title.
|
405
|
+
# - title: title to format.
|
406
|
+
def format_title(title)
|
407
|
+
length = @style[:line_length] ||
|
408
|
+
Bee::Util::term_width ||
|
409
|
+
DEFAULT_LINE_LENGTH
|
410
|
+
right = ' ' + @style[:line_character]*2
|
411
|
+
left = @style[:line_character]*(length - (title.length + 4)) + ' '
|
412
|
+
line = left + title + right
|
413
|
+
# apply style
|
414
|
+
formatted = style(line,
|
415
|
+
@style[:target_style],
|
416
|
+
@style[:target_foreground],
|
417
|
+
@style[:target_background])
|
418
|
+
return formatted
|
419
|
+
end
|
421
420
|
|
422
421
|
end
|
423
422
|
|
424
|
-
#########################################################################
|
425
|
-
# CONSOLE LISTENER #
|
426
|
-
#########################################################################
|
427
|
-
|
428
423
|
# Listener when running in a console. Prints messages on the console using
|
429
424
|
# a given formatter.
|
430
|
-
class
|
425
|
+
class Listener
|
431
426
|
|
432
427
|
# Formatter used by listener.
|
433
428
|
attr_reader :formatter
|
@@ -506,13 +501,22 @@ targets Targets to run (default target if omitted).'
|
|
506
501
|
message << exception.to_s
|
507
502
|
puts "#{@formatter.format_error('ERROR')}: #{message}"
|
508
503
|
end
|
504
|
+
|
505
|
+
# Print text on the console.
|
506
|
+
# - text: text to print.
|
507
|
+
def print(text)
|
508
|
+
Kernel.print(text)
|
509
|
+
end
|
510
|
+
|
511
|
+
# Puts text on the console.
|
512
|
+
# - text: text to puts.
|
513
|
+
def puts(text)
|
514
|
+
Kernel.puts(text)
|
515
|
+
end
|
509
516
|
|
510
517
|
end
|
511
518
|
|
512
|
-
|
513
|
-
# BUILD TEMPLATE #
|
514
|
-
#########################################################################
|
515
|
-
|
519
|
+
# Build template.
|
516
520
|
BUILD_TEMPLATE =
|
517
521
|
'# Template build file
|
518
522
|
- build: template
|
@@ -533,8 +537,7 @@ targets Targets to run (default target if omitted).'
|
|
533
537
|
depends: capitalize
|
534
538
|
description: Print greatings.
|
535
539
|
script:
|
536
|
-
-
|
537
|
-
'
|
540
|
+
- print: "Hello #{who}!"'
|
538
541
|
|
539
542
|
end
|
540
543
|
|
data/lib/bee_task.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
# Copyright 2006-2007 Michel Casabianca <michel.casabianca@gmail.com>
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'bee'
|
16
|
+
require 'bee_util'
|
17
|
+
|
18
|
+
module Bee
|
19
|
+
|
20
|
+
# Module for bee tasks.
|
21
|
+
module Task
|
22
|
+
|
23
|
+
# Base class for task package. Provides methods to access build context
|
24
|
+
# (to set and get properties and evaluate an object in this context).
|
25
|
+
class Package < Bee::Util::MethodInfoBase
|
26
|
+
|
27
|
+
include Bee::Util::BuildErrorMixin
|
28
|
+
|
29
|
+
# Constructor.
|
30
|
+
# - build: the build we are running.
|
31
|
+
def initialize(build)
|
32
|
+
@build = build
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
# Check a task parameters. Raise a RuntimeError with explanation message
|
38
|
+
# if a mandatory parameter is missing or an unknown parameter was found.
|
39
|
+
# - params: task parameters as a Hash.
|
40
|
+
# - description: parameters description as a Hash associating a parameter
|
41
|
+
# name with symbol :mandatory or :optional.
|
42
|
+
def check_task_parameters(params, description)
|
43
|
+
error "Parameters must be a Hash" unless params.kind_of?(Hash)
|
44
|
+
for param in description.keys
|
45
|
+
error "Missing mandatory parameter '#{param}'" unless
|
46
|
+
params[param] or description[param] == :optional
|
47
|
+
end
|
48
|
+
for param in params.keys
|
49
|
+
error "Unknown parameter '#{param}'" if not description.key?(param)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Print text on the console.
|
54
|
+
# - text: text to print.
|
55
|
+
def print(text)
|
56
|
+
if @build.listener
|
57
|
+
@build.listener.print(text)
|
58
|
+
else
|
59
|
+
Kernel.print(text)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Puts text on the console.
|
64
|
+
# - text: text to puts.
|
65
|
+
def puts(text)
|
66
|
+
if @build.listener
|
67
|
+
@build.listener.puts(text)
|
68
|
+
else
|
69
|
+
Kernel.puts(text)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
# Package manager is responsible for loading packages and calling tasks.
|
76
|
+
class PackageManager
|
77
|
+
|
78
|
+
include Bee::Util::BuildErrorMixin
|
79
|
+
|
80
|
+
# Constructor.
|
81
|
+
# - build: the build we are running.
|
82
|
+
def initialize(build)
|
83
|
+
@build = build
|
84
|
+
@packages = {}
|
85
|
+
end
|
86
|
+
|
87
|
+
# Run a given task.
|
88
|
+
# - task: YAML object for the task to run.
|
89
|
+
def run_task(task)
|
90
|
+
packaged = task.keys[0]
|
91
|
+
package, name = Bee::Util::get_package_name(packaged)
|
92
|
+
parameters = @build.context.evaluate_object(task[packaged])
|
93
|
+
if not @packages[package]
|
94
|
+
load_package(package)
|
95
|
+
end
|
96
|
+
error "Task '#{name}' not found in package '#{package}'" if
|
97
|
+
not @packages[package].respond_to?(name)
|
98
|
+
@packages[package].send(name, parameters)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Get help for a given task.
|
102
|
+
# - task: YAML object for the task to run.
|
103
|
+
def help_task(task)
|
104
|
+
package, name = Bee::Util::get_package_name(task)
|
105
|
+
if not @packages[package]
|
106
|
+
load_package(package)
|
107
|
+
end
|
108
|
+
help = {}
|
109
|
+
if name == '?'
|
110
|
+
methods = @packages[package].class.public_instance_methods(false)
|
111
|
+
for method in methods
|
112
|
+
help[method] = @packages[package].class.method_info(method).comment
|
113
|
+
end
|
114
|
+
return help
|
115
|
+
else
|
116
|
+
error "Task '#{name}' not found in package '#{package}'" if
|
117
|
+
not @packages[package].respond_to?(name)
|
118
|
+
help[task] = @packages[package].class.method_info(name).comment
|
119
|
+
end
|
120
|
+
return help
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
# Load a given package using introspection: we try to instantiate class
|
126
|
+
# named after the package capitalized, in module Bee::Task.
|
127
|
+
# - package: the package name.
|
128
|
+
def load_package(package)
|
129
|
+
if package
|
130
|
+
package.downcase!
|
131
|
+
script = "bee_task_#{package.downcase}"
|
132
|
+
clazz = package.capitalize
|
133
|
+
else
|
134
|
+
script = 'bee_task'
|
135
|
+
clazz = 'Default'
|
136
|
+
end
|
137
|
+
begin
|
138
|
+
require script
|
139
|
+
@packages[package] = Bee::Task.const_get(clazz).new(@build)
|
140
|
+
rescue Exception
|
141
|
+
error "Task package '#{package}' not found"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|