planter-cli 3.0.4 → 3.0.7

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.
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Planter
4
+ ## Configuration class
5
+ class Config < Hash
6
+ attr_reader :template
7
+
8
+ ##
9
+ ## Initialize a new Config object for a template
10
+ ##
11
+ def initialize
12
+ super()
13
+
14
+ @config = initial_config
15
+ @template = Planter.template
16
+
17
+ load_template
18
+
19
+ die('No configuration found', :config) unless @config
20
+
21
+ generate_accessors
22
+ end
23
+
24
+ ## String representation of the configuration
25
+ def to_s
26
+ @config.to_s
27
+ end
28
+
29
+ ## Get a config key
30
+ ##
31
+ ## @param key [String,Symbol] key
32
+ ##
33
+ ## @return [String] value
34
+ ##
35
+ def [](key)
36
+ @config[key]
37
+ end
38
+
39
+ ##
40
+ ## Set a config option
41
+ ##
42
+ ## @param key [String,Symbol] key
43
+ ## @param value [String] value
44
+ ##
45
+ def []=(key, value)
46
+ @config[key.to_sym] = value
47
+ generate_accessors
48
+ end
49
+
50
+ private
51
+
52
+ ## Default configuration
53
+ ##
54
+ ## @return [Hash] default configuration
55
+ ##
56
+ ## @api private
57
+ ##
58
+ def initial_config
59
+ {
60
+ defaults: false,
61
+ git_init: false,
62
+ files: { '_planter.yml' => 'ignore' },
63
+ color: true,
64
+ preserve_tags: nil,
65
+ variables: nil,
66
+ replacements: nil,
67
+ repo: false,
68
+ patterns: nil,
69
+ debug: false,
70
+ script: nil
71
+ }
72
+ end
73
+
74
+ ## Generate accessors for configuration
75
+ ##
76
+ ## @api private
77
+ ##
78
+ def generate_accessors
79
+ @config.each do |k, v|
80
+ define_singleton_method(k) { v } unless respond_to?(k)
81
+ end
82
+ end
83
+
84
+ ##
85
+ ## Build a configuration from template name
86
+ ##
87
+ ## @return [Hash] Configuration object
88
+ ##
89
+ ## @api private
90
+ ##
91
+ def load_template
92
+ Planter.variables ||= {}
93
+ FileUtils.mkdir_p(Planter.base_dir) unless File.directory?(Planter.base_dir)
94
+ base_config = File.join(Planter.base_dir, 'planter.yml')
95
+
96
+ if File.exist?(base_config)
97
+ @config = @config.deep_merge(YAML.load(IO.read(base_config)).symbolize_keys)
98
+ else
99
+ default_base_config = {
100
+ defaults: false,
101
+ git_init: false,
102
+ files: { '_planter.yml' => 'ignore' },
103
+ color: true,
104
+ preserve_tags: true
105
+ }
106
+ begin
107
+ File.open(base_config, 'w') { |f| f.puts(YAML.dump(default_base_config.stringify_keys)) }
108
+ rescue Errno::ENOENT
109
+ Planter.notify("Unable to create #{base_config}", :error)
110
+ end
111
+ @config = @config.deep_merge(default_base_config).symbolize_keys
112
+ Planter.notify("New configuration written to #{base_config}, edit as needed.", :warn)
113
+ end
114
+
115
+ base_dir = File.join(Planter.base_dir, 'templates', @template)
116
+ unless File.directory?(base_dir)
117
+ notify("Template #{@template} does not exist", :error)
118
+ res = Prompt.yn('Create template directory', default_response: false)
119
+
120
+ die('Canceled') unless res
121
+
122
+ FileUtils.mkdir_p(base_dir)
123
+ end
124
+
125
+ load_template_config
126
+
127
+ config_array_to_hash(:files) if @config[:files].is_a?(Array)
128
+ config_array_to_hash(:replacements) if @config[:replacements].is_a?(Array)
129
+ rescue Psych::SyntaxError => e
130
+ die("Parse error in configuration file:\n#{e.message}", :config)
131
+ end
132
+
133
+ ##
134
+ ## Load a template-specific configuration
135
+ ##
136
+ ## @return [Hash] updated config object
137
+ ##
138
+ ## @api private
139
+ ##
140
+ def load_template_config
141
+ base_dir = File.join(Planter.base_dir, 'templates', @template)
142
+ config = File.join(base_dir, '_planter.yml')
143
+
144
+ unless File.exist?(config)
145
+ default_config = {
146
+ variables: [
147
+ key: 'var_key',
148
+ prompt: 'CLI Prompt',
149
+ type: '[string, float, integer, number, date]',
150
+ value: '(optional, force value, can include variables. Empty to prompt. For date type: today, now, etc.)',
151
+ default: '(optional default value, leave empty or remove key for no default)',
152
+ min: '(optional, for number type set a minimum value)',
153
+ max: '(optional, for number type set a maximum value)'
154
+ ],
155
+ git_init: false,
156
+ files: {
157
+ '*.tmp' => 'ignore',
158
+ '*.bak' => 'ignore',
159
+ '.DS_Store' => 'ignore'
160
+ }
161
+ }
162
+ FileUtils.mkdir_p(base_dir)
163
+ File.open(config, 'w') { |f| f.puts(YAML.dump(default_config.stringify_keys)) }
164
+ Planter.notify("New configuration written to #{config}, please edit.", :warn)
165
+ Process.exit 0
166
+ end
167
+ @config = @config.deep_merge(YAML.load(IO.read(config)).symbolize_keys)
168
+ end
169
+
170
+ ##
171
+ ## Convert an errant array to a hash
172
+ ##
173
+ ## @param key [Symbol] The key in @config to convert
174
+ ##
175
+ ## @api private
176
+ ##
177
+ def config_array_to_hash(key)
178
+ files = {}
179
+ @config[key].each do |k|
180
+ files[k.keys.first] = k.values.first
181
+ end
182
+ @config[key] = files
183
+ end
184
+ end
185
+ end
@@ -6,8 +6,14 @@ module Planter
6
6
  # Operation to execute on the file
7
7
  attr_accessor :operation
8
8
 
9
- # File path and target path
10
- attr_reader :file, :target, :tags
9
+ # File path
10
+ attr_reader :file
11
+
12
+ # Target path
13
+ attr_reader :target
14
+
15
+ # Tags
16
+ attr_reader :tags
11
17
 
12
18
  ##
13
19
  ## Initialize a FileEntry object
@@ -19,7 +25,7 @@ module Planter
19
25
  ## @return [FileEntry] a Hash of parameters
20
26
  ##
21
27
  def initialize(file, target, operation)
22
- return nil unless File.exist?(file)
28
+ return unless File.exist?(file)
23
29
 
24
30
  @file = file
25
31
  @target = target
@@ -50,6 +50,10 @@ module Planter
50
50
  ##
51
51
  ## @param entry [FileEntry] The file entry
52
52
  ##
53
+ ## @return [Boolean] success
54
+ ##
55
+ ## @api private
56
+ ##
53
57
  def handle_operator(entry)
54
58
  case entry.operation
55
59
  when :ignore
@@ -65,8 +69,17 @@ module Planter
65
69
  apply_tags(entry)
66
70
  end
67
71
 
72
+ #
73
+ # Apply tags to the target file from the source file
74
+ #
75
+ # @param [FileEntry] entry
76
+ #
77
+ # @return [Boolean] success
78
+ #
79
+ # @api private
80
+ #
68
81
  def apply_tags(entry)
69
- return unless Planter.config[:preserve_tags]
82
+ return unless Planter.config.preserve_tags
70
83
 
71
84
  Tag.copy(entry.file, entry.target) if File.exist?(entry.target)
72
85
  end
@@ -76,6 +89,8 @@ module Planter
76
89
  ##
77
90
  ## @return [Boolean] success
78
91
  ##
92
+ ## @api private
93
+ ##
79
94
  def prepare_copy
80
95
  @files.each do |entry|
81
96
  if entry.matches_pattern?
@@ -90,6 +105,10 @@ module Planter
90
105
  ##
91
106
  ## @param entry [FileEntry] The file entry
92
107
  ##
108
+ ## @return [Boolean] success
109
+ ##
110
+ ## @api private
111
+ ##
93
112
  def propogate_operation(entry)
94
113
  @files.each do |file|
95
114
  file.operation = entry.operation if file.file =~ /^#{entry.file}/
@@ -104,6 +123,8 @@ module Planter
104
123
  ##
105
124
  ## @return [Boolean] success
106
125
  ##
126
+ ## @api private
127
+ ##
107
128
  def merge(entry)
108
129
  return copy_file(entry) if File.directory?(entry.file)
109
130
 
@@ -157,7 +178,12 @@ module Planter
157
178
  ##
158
179
  ## @return [Boolean] success
159
180
  ##
181
+ ## @api private
182
+ ##
160
183
  def copy_file(file, overwrite: false)
184
+ # If the target file already exists and overwrite is true,
185
+ # or Planter.overwrite is true, then delete the target file
186
+ FileUtils.rm_rf(file.target) if (overwrite || Planter.overwrite) && File.exist?(file.target)
161
187
  # Check if the target file already exists
162
188
  # If it does and overwrite is true, or Planter.overwrite is true,
163
189
  # or if the file doesn't exist, then copy the file
@@ -165,7 +191,7 @@ module Planter
165
191
  # Make sure the target directory exists
166
192
  FileUtils.mkdir_p(File.dirname(file.target))
167
193
  # Copy the file if it isn't a directory
168
- FileUtils.cp(file.file, file.target) unless File.directory?(file.file)
194
+ FileUtils.cp_r(file.file, file.target) unless File.directory?(file.file)
169
195
  # Log a message to the console
170
196
  Planter.notify("[Copied] #{file.file} => #{file.target}", :debug, above_spinner: true)
171
197
  # Return true to indicate success
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Planter
4
+ # Integer extensions
5
+ class ::Integer
6
+ # Clean value (dummy method)
7
+ def clean_value
8
+ self
9
+ end
10
+
11
+ # Has selector (dummy method)
12
+ def selector?
13
+ true
14
+ end
15
+
16
+ # Highlight character
17
+ def highlight_character(default: nil)
18
+ "(#{self})".highlight_character(default: default)
19
+ end
20
+ end
21
+
22
+ # Float extensions
23
+ class ::Float
24
+ # Clean value (dummy method)
25
+ def clean_value
26
+ self
27
+ end
28
+
29
+ # Has selector (dummy method)
30
+ def selector?
31
+ true
32
+ end
33
+
34
+ # Highlight character
35
+ def highlight_character(default: nil)
36
+ "(#{self})".highlight_character(default: default)
37
+ end
38
+ end
39
+ end
data/lib/planter/plant.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  module Planter
4
4
  # Primary class
5
5
  class Plant
6
+ attr_reader :config
7
+
6
8
  ##
7
9
  ## Initialize a new Plant object
8
10
  ##
@@ -11,20 +13,24 @@ module Planter
11
13
  ##
12
14
  def initialize(template = nil, variables = nil)
13
15
  Planter.variables = variables if variables.is_a?(Hash)
14
- Planter.config = template if template
16
+ # Planter.config = template if template
17
+ template ||= Planter.template
18
+ die('No template specified', :config) unless template
19
+
20
+ @config = Planter::Config.new
15
21
 
16
- @basedir = File.join(Planter.base_dir, 'templates', Planter.template)
22
+ @basedir = File.join(Planter.base_dir, 'templates', @config.template)
17
23
  @target = Planter.target || Dir.pwd
18
24
 
19
- @git = Planter.config[:git_init] || false
25
+ @git = @config.git_init || false
20
26
  @debug = Planter.debug
21
- @repo = Planter.config[:repo] || false
27
+ @repo = @config.repo || false
22
28
 
23
29
  # Coerce any existing variables (like from the command line) to the types
24
30
  # defined in configuration
25
31
  coerced = {}
26
32
  Planter.variables.each do |k, v|
27
- cfg_var = Planter.config[:variables].select { |var| k = var[:key] }
33
+ cfg_var = @config.variables.select { |var| k = var[:key] }
28
34
  next unless cfg_var.count.positive?
29
35
 
30
36
  var = cfg_var.first
@@ -34,7 +40,7 @@ module Planter
34
40
  coerced.each { |k, v| Planter.variables[k] = v }
35
41
 
36
42
  # Ask user for any variables not already defined
37
- Planter.config[:variables].each do |var|
43
+ @config.variables.each do |var|
38
44
  key = var[:key].to_var
39
45
  next if Planter.variables.keys.include?(key)
40
46
 
@@ -46,7 +52,8 @@ module Planter
46
52
  value: var[:value],
47
53
  min: var[:min],
48
54
  max: var[:max],
49
- choices: var[:choices] || nil
55
+ choices: var[:choices] || nil,
56
+ date_format: var[:date_format] || nil
50
57
  )
51
58
  answer = q.ask
52
59
  if answer.nil?
@@ -55,7 +62,7 @@ module Planter
55
62
  answer = var[:default]
56
63
  end
57
64
 
58
- Planter.variables[key] = answer
65
+ Planter.variables[key] = answer.apply_all
59
66
  end
60
67
 
61
68
  git_pull if @repo
@@ -94,26 +101,26 @@ module Planter
94
101
  def git_pull
95
102
  Planter.spinner.update(title: 'Pulling git repo')
96
103
 
97
- raise Errors::GitError.new('`git` executable not found') unless TTY::Which.exist?('git')
104
+ die('`git` executable not found', :git) unless TTY::Which.exist?('git')
98
105
 
99
106
  pwd = Dir.pwd
100
107
  @repo = expand_repo(@repo)
101
108
 
102
109
  if File.exist?(repo_dir)
103
110
  Dir.chdir(repo_dir)
104
- raise Errors::GitError.new("Directory #{repo_dir} exists but is not git repo") unless File.exist?('.git')
111
+ die("Directory #{repo_dir} exists but is not git repo", :git) unless File.exist?('.git')
105
112
 
106
113
  res = `git pull`
107
- raise Errors::GitError.new("Error pulling #{@repo}:\n#{res}") unless $?.success?
114
+ die("Error pulling #{@repo}:\n#{res}", :git) unless $?.success?
108
115
  else
109
116
  Dir.chdir(@basedir)
110
117
  res = `git clone "#{@repo}" "#{repo_dir}"`
111
- raise Errors::GitError.new("Error cloning #{@repo}:\n#{res}") unless $?.success?
118
+ die("Error cloning #{@repo}:\n#{res}", :git) unless $?.success?
112
119
  end
113
120
  Dir.chdir(pwd)
114
121
  @basedir = repo_dir
115
122
  rescue StandardError => e
116
- raise Errors::GitError.new("Error pulling #{@repo}:\n#{e.message}")
123
+ die("Error pulling #{@repo}:\n#{e.message}", :git)
117
124
  end
118
125
 
119
126
  ##
@@ -139,7 +146,7 @@ module Planter
139
146
  end
140
147
 
141
148
  if @git
142
- raise Errors::GitError.new('`git` executable not found') unless TTY::Which.exist?('git')
149
+ die('`git` executable not found', :git) unless TTY::Which.exist?('git')
143
150
 
144
151
  Planter.spinner.update(title: 'Initializing git repo')
145
152
  res = add_git
@@ -149,10 +156,10 @@ module Planter
149
156
  end
150
157
  end
151
158
 
152
- if Planter.config[:script]
159
+ if @config.script
153
160
  Planter.spinner.update(title: 'Running script')
154
161
 
155
- scripts = Planter.config[:script]
162
+ scripts = @config.script
156
163
  scripts = [scripts] if scripts.is_a?(String)
157
164
  scripts.each do |script|
158
165
  s = Planter::Script.new(@basedir, Dir.pwd, script)
@@ -184,7 +191,7 @@ module Planter
184
191
 
185
192
  content = IO.read(file)
186
193
 
187
- new_content = content.apply_logic.apply_variables.apply_regexes
194
+ new_content = content.apply_all
188
195
 
189
196
  new_content.gsub!(%r{^.{.4}/?merge *.{,4}\n}, '') if new_content =~ /^.{.4}merge *\n/
190
197
 
@@ -21,9 +21,10 @@ module Planter
21
21
  @min = question[:min]&.to_f || 1.0
22
22
  @max = question[:max]&.to_f || 10.0
23
23
  @prompt = question[:prompt] || nil
24
- @default = question[:default]
24
+ @default = question[:default]&.to_s&.apply_all || nil
25
25
  @value = question[:value]
26
26
  @choices = question[:choices] || []
27
+ @date_format = question[:date_format] || nil
27
28
  @gum = false # TTY::Which.exist?('gum')
28
29
  end
29
30
 
@@ -35,7 +36,7 @@ module Planter
35
36
  def ask
36
37
  return nil if @prompt.nil?
37
38
 
38
- return @value.to_s.apply_variables.apply_regexes.coerce(@type) if @value && @type != :date
39
+ return @value.to_s.apply_all.coerce(@type) if @value && @type != :date
39
40
 
40
41
  res = case @type
41
42
  when :choice
@@ -58,9 +59,9 @@ module Planter
58
59
  read_line
59
60
  end
60
61
  Planter.notify("{dw}#{prompt} => {dy}#{res}{x}", :debug, newline: false)
61
- res
62
+ res.to_s.apply_all
62
63
  rescue TTY::Reader::InputInterrupt
63
- raise Errors::InputError.new('Canceled')
64
+ die('Canceled')
64
65
  end
65
66
 
66
67
  private
@@ -72,8 +73,10 @@ module Planter
72
73
  ##
73
74
  ## @return [Number] numeric response
74
75
  ##
76
+ ## @api private
77
+ ##
75
78
  def read_number(integer: false)
76
- default = @default ? " {bw}[#{@default}]" : ''
79
+ default = @default ? " {xw}[{xbw}#{@default}{xw}]" : ''
77
80
  Planter.notify("{by}#{@prompt} {xc}({bw}#{@min}{xc}-{bw}#{@max}{xc})#{default}")
78
81
 
79
82
  res = @gum ? read_number_gum : read_line_tty
@@ -91,19 +94,26 @@ module Planter
91
94
  ##
92
95
  ## @return [String] date string
93
96
  ##
97
+ ## @api private
98
+ ##
94
99
  def date_default
95
100
  default = @value || @default
96
101
  return nil unless default
97
102
 
103
+ if default =~ /'.*?'/
104
+ @date_format = default.match(/'(.*?)'/)[1].strip
105
+ default = default.gsub(/'.*?'/, '').strip
106
+ end
107
+
98
108
  case default
99
109
  when /^(no|ti)/
100
- Time.now.strftime('%Y-%m-%d %H:%M')
110
+ Time.now.strftime(@date_format || '%Y-%m-%d %H:%M')
101
111
  when /^(to|da)/
102
- Time.now.strftime('%Y-%m-%d')
112
+ Time.now.strftime(@date_format || '%Y-%m-%d')
103
113
  when /^%/
104
- Time.now.strftime(@default)
114
+ Time.now.strftime(default)
105
115
  else
106
- Chronic.parse(default).strftime('%Y-%m-%d')
116
+ Chronic.parse(default).strftime(@date_format || '%Y-%m-%d')
107
117
  end
108
118
  end
109
119
 
@@ -114,6 +124,8 @@ module Planter
114
124
  ##
115
125
  ## @return [Date] Parsed Date object
116
126
  ##
127
+ ## @api private
128
+ ##
117
129
  def read_date(prompt: nil)
118
130
  prompt ||= @prompt
119
131
  default = date_default
@@ -123,7 +135,7 @@ module Planter
123
135
  line = @gum ? read_line_gum : read_line_tty
124
136
  return default unless line
125
137
 
126
- Chronic.parse(line).strftime('%Y-%m-%d')
138
+ Chronic.parse(line).strftime(@date_format || '%Y-%m-%d')
127
139
  end
128
140
 
129
141
  ##
@@ -135,7 +147,11 @@ module Planter
135
147
  ##
136
148
  ## @return [String] the single-line response
137
149
  ##
150
+ ## @api private
151
+ ##
138
152
  def read_line(prompt: nil)
153
+ return @default if Planter.accept_defaults || ENV['PLANTER_DEBUG']
154
+
139
155
  prompt ||= @prompt
140
156
  default = @default ? " {bw}[#{@default}]" : ''
141
157
  Planter.notify("{by}#{prompt}#{default}", newline: false)
@@ -156,6 +172,8 @@ module Planter
156
172
  ##
157
173
  ## @return [String] the multi-line response
158
174
  ##
175
+ ## @api private
176
+ ##
159
177
  def read_lines(prompt: nil)
160
178
  prompt ||= @prompt
161
179
  save = @gum ? 'Ctrl-J for newline, Enter' : 'Ctrl-D'
@@ -172,6 +190,8 @@ module Planter
172
190
  ##
173
191
  ## @return [String] String response
174
192
  ##
193
+ ## @api private
194
+ ##
175
195
  def read_number_gum
176
196
  trap('SIGINT') { exit! }
177
197
  res = `gum input --placeholder "#{@min}-#{@max}"`.strip
@@ -185,6 +205,8 @@ module Planter
185
205
  ##
186
206
  ## @return [String] String response
187
207
  ##
208
+ ## @api private
209
+ ##
188
210
  def read_line_tty
189
211
  trap('SIGINT') { exit! }
190
212
  reader = TTY::Reader.new
@@ -199,6 +221,8 @@ module Planter
199
221
  ##
200
222
  ## @return [String] String response
201
223
  ##
224
+ ## @api private
225
+ ##
202
226
  def read_line_gum
203
227
  trap('SIGINT') { exit! }
204
228
  res = `gum input --placeholder "(blank to use default)"`.strip
@@ -212,6 +236,8 @@ module Planter
212
236
  ##
213
237
  ## @return [string] multiline input
214
238
  ##
239
+ ## @api private
240
+ ##
215
241
  def read_mutliline_tty
216
242
  trap('SIGINT') { exit! }
217
243
  reader = TTY::Reader.new
@@ -224,6 +250,8 @@ module Planter
224
250
  ##
225
251
  ## @return [string] multiline input
226
252
  ##
253
+ ## @api private
254
+ ##
227
255
  def read_multiline_gum(prompt)
228
256
  trap('SIGINT') { exit! }
229
257
  width = TTY::Screen.cols > 80 ? 80 : TTY::Screen.cols
@@ -234,7 +262,7 @@ module Planter
234
262
  ##
235
263
  ## Choose from an array of multiple choices. Letter surrounded in
236
264
  ## parenthesis becomes character for response. Only one letter should be
237
- ## specified and must be unique.
265
+ ## specified per choice and must be unique.
238
266
  ##
239
267
  ## @param choices [Array] The choices
240
268
  ## @param prompt [String] The prompt
@@ -260,6 +288,8 @@ module Planter
260
288
  values = choices.to_values.map(&:clean_value)
261
289
  end
262
290
 
291
+ die('Choice(s) without selector, please edit config') unless keys.all?(&:selector?)
292
+
263
293
  default = case default_response.to_s
264
294
  when /^\d+$/
265
295
  values[default.to_i]
@@ -307,6 +337,8 @@ module Planter
307
337
 
308
338
  res = res.empty? ? default : res
309
339
 
340
+ return choice(choices, prompt, default_response: default_response) if res.nil? || res.empty?
341
+
310
342
  if res.to_i.positive?
311
343
  values[res.to_i - 1]
312
344
  elsif res =~ /^[a-z]/ && keys&.option_index(res)
@@ -14,12 +14,12 @@ module Planter
14
14
  ##
15
15
  def initialize(template_dir, output_dir, script)
16
16
  found = find_script(template_dir, script)
17
- raise ScriptError.new("Script #{script} not found") unless found
17
+ die("Script #{script} not found", :script) unless found
18
18
 
19
19
  @script = found
20
20
  make_executable
21
21
 
22
- raise ScriptError.new("Output directory #{output_dir} not found") unless File.directory?(output_dir)
22
+ die("Output directory #{output_dir} not found", :script) unless File.directory?(output_dir)
23
23
 
24
24
  @template_directory = template_dir
25
25
  @directory = output_dir
@@ -63,7 +63,7 @@ module Planter
63
63
  stdout, stderr, status = Open3.capture3(@script, @template_directory, @directory)
64
64
  Planter.notify("STDOUT:\n#{stdout}", :debug) unless stdout.empty?
65
65
  Planter.notify("STDERR:\n#{stderr}", :debug) unless stderr.empty?
66
- raise ScriptError.new("Error running #{@script}") unless status.success?
66
+ die("Error running #{@script}", :script) unless status.success?
67
67
 
68
68
  true
69
69
  end