nugrant 2.0.0.dev2 → 2.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +6 -14
- data/.gitignore +2 -1
- data/.travis.yml +2 -2
- data/CHANGELOG.md +148 -3
- data/Gemfile +8 -20
- data/README.md +266 -72
- data/Rakefile +1 -0
- data/lib/nugrant.rb +14 -6
- data/lib/nugrant/bag.rb +116 -62
- data/lib/nugrant/helper/bag.rb +19 -19
- data/lib/nugrant/helper/env/exporter.rb +208 -0
- data/lib/nugrant/helper/env/namer.rb +47 -0
- data/lib/nugrant/helper/parameters.rb +12 -0
- data/lib/nugrant/helper/stack.rb +86 -0
- data/lib/nugrant/mixin/parameters.rb +98 -0
- data/lib/nugrant/parameters.rb +14 -68
- data/lib/nugrant/vagrant/errors.rb +27 -0
- data/lib/nugrant/vagrant/v2/command/env.rb +101 -0
- data/lib/nugrant/vagrant/v2/command/helper.rb +30 -0
- data/lib/nugrant/vagrant/v2/command/parameters.rb +16 -4
- data/lib/nugrant/vagrant/v2/command/restricted_keys.rb +60 -0
- data/lib/nugrant/vagrant/v2/command/root.rb +12 -2
- data/lib/nugrant/vagrant/v2/config/user.rb +9 -21
- data/lib/nugrant/vagrant/v2/plugin.rb +0 -1
- data/lib/nugrant/version.rb +1 -1
- data/locales/en.yml +13 -0
- data/nugrant.gemspec +3 -7
- data/test/lib/nugrant/helper/env/test_exporter.rb +238 -0
- data/test/lib/nugrant/helper/test_bag.rb +16 -0
- data/test/lib/nugrant/helper/test_parameters.rb +17 -0
- data/test/lib/nugrant/helper/test_stack.rb +152 -0
- data/test/lib/nugrant/test_bag.rb +132 -22
- data/test/lib/nugrant/test_config.rb +95 -92
- data/test/lib/nugrant/test_parameters.rb +232 -177
- data/test/lib/test_helper.rb +3 -0
- data/test/resources/json/params_user_nil_values.json +9 -0
- data/test/resources/vagrantfiles/v2.defaults_mixed_string_symbols +18 -0
- data/test/resources/vagrantfiles/v2.defaults_null_values_in_vagrantuser +23 -0
- data/test/resources/vagrantfiles/v2.defaults_using_string +18 -0
- data/test/resources/vagrantfiles/v2.defaults_using_symbol +18 -0
- data/test/resources/{Vagrantfile.v2.empty → vagrantfiles/v2.empty} +0 -2
- data/test/resources/{Vagrantfile.v2.fake → vagrantfiles/v2.fake} +4 -3
- data/test/resources/vagrantfiles/v2.missing_parameter +3 -0
- data/test/resources/{Vagrantfile.v2.real → vagrantfiles/v2.real} +0 -2
- data/test/resources/yaml/params_user_nil_values.yml +5 -0
- metadata +55 -88
- data/lib/nugrant/vagrant/v1/command/parameters.rb +0 -134
- data/lib/nugrant/vagrant/v1/command/root.rb +0 -81
- data/lib/nugrant/vagrant/v1/config/user.rb +0 -37
- data/lib/nugrant/vagrant/v1/plugin.rb +0 -6
- data/lib/vagrant_init.rb +0 -2
- data/test/resources/Vagrantfile.v1.empty +0 -2
- data/test/resources/Vagrantfile.v1.fake +0 -10
- data/test/resources/Vagrantfile.v1.real +0 -19
data/Rakefile
CHANGED
data/lib/nugrant.rb
CHANGED
@@ -8,16 +8,24 @@ unless defined?(KeyError)
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
+
module Nugrant
|
12
|
+
def self.setup_i18n()
|
13
|
+
I18n.load_path << File.expand_path("locales/en.yml", Nugrant.source_root)
|
14
|
+
I18n.reload!
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.source_root
|
18
|
+
@source_root ||= Pathname.new(File.expand_path("../../", __FILE__))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
11
22
|
if defined?(Vagrant)
|
23
|
+
Nugrant.setup_i18n()
|
24
|
+
|
12
25
|
case
|
13
26
|
when defined?(Vagrant::Plugin::V2)
|
14
27
|
require 'nugrant/vagrant/v2/plugin'
|
15
|
-
when Vagrant::VERSION =~ /1\.0\..*/
|
16
|
-
# Nothing to do, v1 plugins are picked by the vagrant_init.rb file
|
17
28
|
else
|
18
|
-
|
29
|
+
raise RuntimeError, "Vagrant [#{Vagrant::VERSION}] is not supported by Nugrant."
|
19
30
|
end
|
20
31
|
end
|
21
|
-
|
22
|
-
module Nugrant
|
23
|
-
end
|
data/lib/nugrant/bag.rb
CHANGED
@@ -1,96 +1,150 @@
|
|
1
1
|
module Nugrant
|
2
|
-
class Bag
|
3
|
-
attr_reader :__elements
|
2
|
+
class Bag < Hash
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
##
|
5
|
+
# Create a new Bag object which holds key/value pairs.
|
6
|
+
# The Bag object inherits from the Hash object, the main
|
7
|
+
# differences with a normal Hash are indifferent access
|
8
|
+
# (symbol or string) and method access (via method call).
|
9
|
+
#
|
10
|
+
# =| Arguments
|
11
|
+
# * `elements`
|
12
|
+
# The initial elements the bag should be built with it.'
|
13
|
+
# Must be an object responding to `each` and accepting
|
14
|
+
# a block with two arguments: `key, value`.]. Defaults to
|
15
|
+
# the empty hash.
|
16
|
+
#
|
17
|
+
# * `options`
|
18
|
+
# An options hash where some customization option can be passed.
|
19
|
+
# Defaults to an empty hash, see options for specific option default
|
20
|
+
# values.
|
21
|
+
#
|
22
|
+
# =| Options
|
23
|
+
# * `:key_error`
|
24
|
+
# A callable object receiving a single parameter `key` that is
|
25
|
+
# called when a key cannot be found in the Bag. The received key
|
26
|
+
# is already converted to a symbol. If the callable does not
|
27
|
+
# raise an exception, the result of it's execution is returned.
|
28
|
+
# The default value is a callable that throws a KeyError exception.
|
29
|
+
#
|
30
|
+
def initialize(elements = {}, options = {})
|
31
|
+
super()
|
32
|
+
|
33
|
+
@__key_error = options[:key_error] || Proc.new do |key|
|
34
|
+
raise KeyError, "Undefined parameter '#{key}'" if not key?(key)
|
8
35
|
end
|
9
36
|
|
10
|
-
|
37
|
+
(elements || {}).each do |key, value|
|
38
|
+
self[key] = value.kind_of?(Hash) ? Bag.new(value, options) : value
|
39
|
+
end
|
11
40
|
end
|
12
41
|
|
13
|
-
def
|
14
|
-
return
|
42
|
+
def method_missing(method, *args, &block)
|
43
|
+
return self[method]
|
15
44
|
end
|
16
45
|
|
17
|
-
|
18
|
-
|
46
|
+
##
|
47
|
+
### Hash Overriden Methods (for string & symbol indifferent access)
|
48
|
+
##
|
49
|
+
|
50
|
+
def [](input)
|
51
|
+
key = __convert_key(input)
|
52
|
+
return @__key_error.call(key) if not key?(key)
|
53
|
+
|
54
|
+
super(key)
|
19
55
|
end
|
20
56
|
|
21
|
-
def
|
22
|
-
|
57
|
+
def []=(input, value)
|
58
|
+
super(__convert_key(input), value)
|
23
59
|
end
|
24
60
|
|
25
|
-
def
|
26
|
-
|
61
|
+
def key?(key)
|
62
|
+
super(__convert_key(key))
|
27
63
|
end
|
28
64
|
|
29
65
|
##
|
30
|
-
# This method
|
31
|
-
#
|
32
|
-
#
|
66
|
+
# This method first start by converting the `input` parameter
|
67
|
+
# into a bag. It will then *deep* merge current values with
|
68
|
+
# the new ones coming from the `input`.
|
33
69
|
#
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
@__elements[key] = value
|
47
|
-
end
|
48
|
-
|
49
|
-
next
|
50
|
-
end
|
70
|
+
# The array merge strategy is by default to replace current
|
71
|
+
# values with new ones. You can use option `:array_strategy`
|
72
|
+
# to change this default behavior.
|
73
|
+
#
|
74
|
+
# +Options+
|
75
|
+
# * :array_strategy
|
76
|
+
# * :replace (Default) => Replace current values by new ones
|
77
|
+
# * :extend => Merge current values with new ones
|
78
|
+
# * :concat => Append new values to current ones
|
79
|
+
#
|
80
|
+
def merge!(input, options = {})
|
81
|
+
options = {:array_strategy => :replace}.merge(options)
|
51
82
|
|
52
|
-
|
53
|
-
|
54
|
-
|
83
|
+
array_strategy = options[:array_strategy]
|
84
|
+
input.each do |key, value|
|
85
|
+
current = __get(key)
|
86
|
+
case
|
87
|
+
when current == nil
|
88
|
+
self[key] = value
|
89
|
+
|
90
|
+
when current.kind_of?(Hash) && value.kind_of?(Hash)
|
91
|
+
current.merge!(value, options)
|
55
92
|
|
56
|
-
|
57
|
-
|
58
|
-
|
93
|
+
when current.kind_of?(Array) && value.kind_of?(Array)
|
94
|
+
self[key] = send("__#{array_strategy}_array_merge", current, value)
|
95
|
+
|
96
|
+
when value != nil
|
97
|
+
self[key] = value
|
98
|
+
end
|
59
99
|
end
|
60
100
|
end
|
61
101
|
|
62
|
-
def
|
102
|
+
def to_hash(options = {})
|
63
103
|
return {} if empty?()
|
64
104
|
|
65
|
-
|
66
|
-
each do |key, value|
|
67
|
-
hash[key.to_sym()] = value.kind_of?(Bag) ? value.__to_hash() : value
|
68
|
-
end
|
105
|
+
use_string_key = options[:use_string_key]
|
69
106
|
|
70
|
-
|
107
|
+
Hash[map do |key, value|
|
108
|
+
key = use_string_key ? key.to_s() : key
|
109
|
+
value = value.kind_of?(Bag) ? value.to_hash(options) : value
|
110
|
+
|
111
|
+
[key, value]
|
112
|
+
end]
|
71
113
|
end
|
72
114
|
|
73
|
-
|
74
|
-
|
75
|
-
|
115
|
+
##
|
116
|
+
### Aliases
|
117
|
+
##
|
76
118
|
|
77
|
-
|
78
|
-
if not value.kind_of?(Hash)
|
79
|
-
@__elements[key.to_sym()] = value
|
80
|
-
next
|
81
|
-
end
|
119
|
+
alias_method :to_ary, :to_a
|
82
120
|
|
83
|
-
|
84
|
-
|
85
|
-
|
121
|
+
##
|
122
|
+
### Private Methods
|
123
|
+
##
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def __convert_key(key)
|
128
|
+
return key.to_sym() if key.respond_to?(:to_sym)
|
129
|
+
|
130
|
+
raise ArgumentError, "Key cannot be converted to symbol, current value [#{key}] (#{key.class.name})"
|
86
131
|
end
|
87
132
|
|
88
|
-
def
|
89
|
-
|
90
|
-
|
91
|
-
|
133
|
+
def __get(key)
|
134
|
+
# Calls Hash method [__convert_key(key)], used internally to retrieve value without raising Undefined parameter
|
135
|
+
self.class.superclass.instance_method(:[]).bind(self).call(__convert_key(key))
|
136
|
+
end
|
137
|
+
|
138
|
+
def __concat_array_merge(current_array, new_array)
|
139
|
+
current_array + new_array
|
140
|
+
end
|
141
|
+
|
142
|
+
def __extend_array_merge(current_array, new_array)
|
143
|
+
current_array | new_array
|
144
|
+
end
|
92
145
|
|
93
|
-
|
146
|
+
def __replace_array_merge(current_array, new_array)
|
147
|
+
new_array
|
94
148
|
end
|
95
149
|
end
|
96
150
|
end
|
data/lib/nugrant/helper/bag.rb
CHANGED
@@ -6,36 +6,36 @@ require 'nugrant/bag'
|
|
6
6
|
module Nugrant
|
7
7
|
module Helper
|
8
8
|
module Bag
|
9
|
-
def self.read(filepath,
|
10
|
-
data = parse_data(filepath,
|
9
|
+
def self.read(filepath, filetype, options = {})
|
10
|
+
data = parse_data(filepath, filetype, options)
|
11
11
|
|
12
|
-
return Nugrant::Bag.new(data)
|
12
|
+
return Nugrant::Bag.new(data, options)
|
13
13
|
end
|
14
14
|
|
15
|
-
def self.
|
15
|
+
def self.restricted_keys()
|
16
|
+
Nugrant::Bag.instance_methods()
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.parse_data(filepath, filetype, options = {})
|
16
20
|
return if not File.exists?(filepath)
|
17
21
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
if error_handler
|
25
|
-
# TODO: Implements error handler logic
|
26
|
-
error_handler.handle("Could not parse the user #{format.to_s} parameters file '#{filepath}': #{error}")
|
27
|
-
end
|
22
|
+
File.open(filepath, "rb") do |file|
|
23
|
+
return send("parse_#{filetype}", file)
|
24
|
+
end
|
25
|
+
rescue => error
|
26
|
+
if options[:error_handler]
|
27
|
+
options[:error_handler].handle("Could not parse the user #{filetype} parameters file '#{filepath}': #{error}")
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
-
def self.parse_json(
|
32
|
-
|
31
|
+
def self.parse_json(io)
|
32
|
+
MultiJson.load(io.read())
|
33
33
|
end
|
34
34
|
|
35
|
-
def self.parse_yaml(
|
36
|
-
YAML::ENGINE.yamler= 'syck' if defined?(YAML::ENGINE)
|
35
|
+
def self.parse_yaml(io)
|
36
|
+
YAML::ENGINE.yamler = 'syck' if (defined?(Syck) || defined?(YAML::Syck)) && defined?(YAML::ENGINE)
|
37
37
|
|
38
|
-
YAML.load(
|
38
|
+
YAML.load(io.read())
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
require 'nugrant/bag'
|
4
|
+
require 'nugrant/helper/env/namer'
|
5
|
+
|
6
|
+
module Nugrant
|
7
|
+
module Helper
|
8
|
+
module Env
|
9
|
+
module Exporter
|
10
|
+
@@DEFAULT_AUTOENV_PATH = "./.env"
|
11
|
+
@@DEFAULT_SCRIPT_PATH = "./nugrant2env.sh"
|
12
|
+
|
13
|
+
@@VALID_EXPORTERS = [:autoenv, :script, :terminal]
|
14
|
+
|
15
|
+
##
|
16
|
+
# Returns true if the exporter name received is a valid
|
17
|
+
# valid export, false otherwise.
|
18
|
+
#
|
19
|
+
# @param exporter The exporter name to check validity
|
20
|
+
#
|
21
|
+
# @return true if exporter is valid, false otherwise.
|
22
|
+
def self.valid?(exporter)
|
23
|
+
@@VALID_EXPORTERS.include?(exporter)
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Creates an autoenv script containing the commands that are required
|
28
|
+
# to export or unset a bunch of environment variables taken from the
|
29
|
+
# bag.
|
30
|
+
#
|
31
|
+
# @param bag The bag to create the script for.
|
32
|
+
#
|
33
|
+
# @return (side-effect) Creates a script file containing commands
|
34
|
+
# to export or unset environment variables for
|
35
|
+
# bag.
|
36
|
+
#
|
37
|
+
# Options:
|
38
|
+
# * :autoenv_path => The path where to write the script, defaults to `./.env`.
|
39
|
+
# * :escape_value => If true, escape the value to export (or unset), default to true.
|
40
|
+
# * :io => The io where the command should be written, default to nil which create the autoenv on disk.
|
41
|
+
# * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
|
42
|
+
# * :override => If true, variable a exported even when the override an existing env key, default to true.
|
43
|
+
# * :type => The type of command, default to :export.
|
44
|
+
#
|
45
|
+
def self.autoenv_exporter(bag, options = {})
|
46
|
+
io = options[:io] || (File.open(File.expand_path(options[:autoenv_path] || @@DEFAULT_AUTOENV_PATH), "w"))
|
47
|
+
|
48
|
+
terminal_exporter(bag, options.merge({:io => io}))
|
49
|
+
ensure
|
50
|
+
io.close() if io
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Creates a bash script containing the commands that are required
|
55
|
+
# to export or unset a bunch of environment variables taken from the
|
56
|
+
# bag.
|
57
|
+
#
|
58
|
+
# @param bag The bag to create the script for.
|
59
|
+
#
|
60
|
+
# @return (side-effect) Creates a script file containing commands
|
61
|
+
# to export or unset environment variables for
|
62
|
+
# bag.
|
63
|
+
#
|
64
|
+
# Options:
|
65
|
+
# * :escape_value => If true, escape the value to export (or unset), default to true.
|
66
|
+
# * :io => The io where the command should be written, default to nil which create the script on disk.
|
67
|
+
# * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
|
68
|
+
# * :override => If true, variable a exported even when the override an existing env key, default to true.
|
69
|
+
# * :script_path => The path where to write the script, defaults to `./nugrant2env.sh`.
|
70
|
+
# * :type => The type of command, default to :export.
|
71
|
+
#
|
72
|
+
def self.script_exporter(bag, options = {})
|
73
|
+
io = options[:io] || (File.open(File.expand_path(options[:script_path] || @@DEFAULT_SCRIPT_PATH), "w"))
|
74
|
+
|
75
|
+
io.puts("#!/bin/env sh")
|
76
|
+
io.puts()
|
77
|
+
|
78
|
+
terminal_exporter(bag, options.merge({:io => io}))
|
79
|
+
ensure
|
80
|
+
io.close() if io
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Export to terminal the commands that are required
|
85
|
+
# to export or unset a bunch of environment variables taken from the
|
86
|
+
# bag.
|
87
|
+
#
|
88
|
+
# @param bag The bag to create the script for.
|
89
|
+
#
|
90
|
+
# @return (side-effect) Outputs to io the commands generated.
|
91
|
+
#
|
92
|
+
# Options:
|
93
|
+
# * :escape_value => If true, escape the value to export (or unset), default to true.
|
94
|
+
# * :io => The io where the command should be displayed, default to $stdout.
|
95
|
+
# * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
|
96
|
+
# * :override => If true, variable a exported even when the override an existing env key, default to true.
|
97
|
+
# * :type => The type of command, default to :export.
|
98
|
+
#
|
99
|
+
def self.terminal_exporter(bag, options = {})
|
100
|
+
io = options[:io] || $stdout
|
101
|
+
type = options[:type] || :export
|
102
|
+
|
103
|
+
export(bag, options) do |key, value|
|
104
|
+
io.puts(command(type, key, value, options))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Generic function to export a bag. This walk the bag,
|
110
|
+
# for each element, it creates the key using the namer
|
111
|
+
# and then forward the key and value to the block if
|
112
|
+
# the variable does not override an existing environment
|
113
|
+
# variable or if options :override is set to true.
|
114
|
+
#
|
115
|
+
# @param bag The bag to export.
|
116
|
+
#
|
117
|
+
# @return (side-effect) Yields each key and value to a block
|
118
|
+
#
|
119
|
+
# Options:
|
120
|
+
# * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
|
121
|
+
# * :override => If true, variable a exported even when the override an existing env key, default to true.
|
122
|
+
#
|
123
|
+
def self.export(bag, options = {})
|
124
|
+
namer = options[:namer] || Env::Namer.default()
|
125
|
+
override = options.fetch(:override, true)
|
126
|
+
|
127
|
+
variables = {}
|
128
|
+
walk_bag(bag) do |segments, key, value|
|
129
|
+
key = namer.call(segments)
|
130
|
+
|
131
|
+
variables[key] = value if override or not ENV[key]
|
132
|
+
end
|
133
|
+
|
134
|
+
variables.sort().each do |key, value|
|
135
|
+
yield key, value
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Given a key and a value, return a string representation
|
141
|
+
# of the command type requested. Available types:
|
142
|
+
#
|
143
|
+
# * :export => A bash compatible export command
|
144
|
+
# * :unset => A bash compatible export command
|
145
|
+
#
|
146
|
+
def self.command(type, key, value, options = {})
|
147
|
+
# TODO: Replace by a map type => function name
|
148
|
+
case
|
149
|
+
when type == :export
|
150
|
+
export_command(key, value, options)
|
151
|
+
when type == :unset
|
152
|
+
unset_command(key, value, options)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# Returns a string representation of the command
|
158
|
+
# that needs to be used on the current platform
|
159
|
+
# to export an environment variable.
|
160
|
+
#
|
161
|
+
# @param key The key of the environment variable to export.
|
162
|
+
# It cannot be nil.
|
163
|
+
# @param value The value of the environment variable to export
|
164
|
+
#
|
165
|
+
# @return The export command, as a string
|
166
|
+
#
|
167
|
+
# Options:
|
168
|
+
# * :escape_value (true) => If true, escape the value to export.
|
169
|
+
#
|
170
|
+
def self.export_command(key, value, options = {})
|
171
|
+
value = value.to_s()
|
172
|
+
value = Shellwords.escape(value) if options[:escape_value] == nil || options[:escape_value]
|
173
|
+
|
174
|
+
# TODO: Handle platform differently
|
175
|
+
"export #{key}=#{value}"
|
176
|
+
end
|
177
|
+
|
178
|
+
##
|
179
|
+
# Returns a string representation of the command
|
180
|
+
# that needs to be used on the current platform
|
181
|
+
# to unset an environment variable.
|
182
|
+
#
|
183
|
+
# @param key The key of the environment variable to export.
|
184
|
+
# It cannot be nil.
|
185
|
+
#
|
186
|
+
# @return The unset command, as a string
|
187
|
+
#
|
188
|
+
def self.unset_command(key, value, options = {})
|
189
|
+
# TODO: Handle platform differently
|
190
|
+
"unset #{key}"
|
191
|
+
end
|
192
|
+
|
193
|
+
# FIXME: Move this directly into bag class
|
194
|
+
def self.walk_bag(bag, parents = [], &block)
|
195
|
+
commands = []
|
196
|
+
|
197
|
+
bag.each do |key, value|
|
198
|
+
segments = parents + [key]
|
199
|
+
nested_bag = value.kind_of?(Nugrant::Bag)
|
200
|
+
|
201
|
+
walk_bag(value, segments, &block) if nested_bag
|
202
|
+
yield segments, key, value if not nested_bag
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|