planter-cli 3.0.4 → 3.0.5

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/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
@@ -73,7 +74,7 @@ module Planter
73
74
  ## @return [Number] numeric response
74
75
  ##
75
76
  def read_number(integer: false)
76
- default = @default ? " {bw}[#{@default}]" : ''
77
+ default = @default ? " {xw}[{xbw}#{@default}{xw}]" : ''
77
78
  Planter.notify("{by}#{@prompt} {xc}({bw}#{@min}{xc}-{bw}#{@max}{xc})#{default}")
78
79
 
79
80
  res = @gum ? read_number_gum : read_line_tty
@@ -95,15 +96,20 @@ module Planter
95
96
  default = @value || @default
96
97
  return nil unless default
97
98
 
99
+ if default =~ /'.*?'/
100
+ @date_format = default.match(/'(.*?)'/)[1].strip
101
+ default = default.gsub(/'.*?'/, '').strip
102
+ end
103
+
98
104
  case default
99
105
  when /^(no|ti)/
100
- Time.now.strftime('%Y-%m-%d %H:%M')
106
+ Time.now.strftime(@date_format || '%Y-%m-%d %H:%M')
101
107
  when /^(to|da)/
102
- Time.now.strftime('%Y-%m-%d')
108
+ Time.now.strftime(@date_format || '%Y-%m-%d')
103
109
  when /^%/
104
- Time.now.strftime(@default)
110
+ Time.now.strftime(default)
105
111
  else
106
- Chronic.parse(default).strftime('%Y-%m-%d')
112
+ Chronic.parse(default).strftime(@date_format || '%Y-%m-%d')
107
113
  end
108
114
  end
109
115
 
@@ -123,7 +129,7 @@ module Planter
123
129
  line = @gum ? read_line_gum : read_line_tty
124
130
  return default unless line
125
131
 
126
- Chronic.parse(line).strftime('%Y-%m-%d')
132
+ Chronic.parse(line).strftime(@date_format || '%Y-%m-%d')
127
133
  end
128
134
 
129
135
  ##
@@ -136,6 +142,8 @@ module Planter
136
142
  ## @return [String] the single-line response
137
143
  ##
138
144
  def read_line(prompt: nil)
145
+ return @default if Planter.accept_defaults || ENV['PLANTER_DEBUG']
146
+
139
147
  prompt ||= @prompt
140
148
  default = @default ? " {bw}[#{@default}]" : ''
141
149
  Planter.notify("{by}#{prompt}#{default}", newline: false)
@@ -234,7 +242,7 @@ module Planter
234
242
  ##
235
243
  ## Choose from an array of multiple choices. Letter surrounded in
236
244
  ## parenthesis becomes character for response. Only one letter should be
237
- ## specified and must be unique.
245
+ ## specified per choice and must be unique.
238
246
  ##
239
247
  ## @param choices [Array] The choices
240
248
  ## @param prompt [String] The prompt
@@ -260,6 +268,8 @@ module Planter
260
268
  values = choices.to_values.map(&:clean_value)
261
269
  end
262
270
 
271
+ die('Choice(s) without selector, please edit config') unless keys.all?(&:has_selector?)
272
+
263
273
  default = case default_response.to_s
264
274
  when /^\d+$/
265
275
  values[default.to_i]
@@ -307,6 +317,8 @@ module Planter
307
317
 
308
318
  res = res.empty? ? default : res
309
319
 
320
+ return choice(choices, prompt, default_response: default_response) if res.nil? || res.empty?
321
+
310
322
  if res.to_i.positive?
311
323
  values[res.to_i - 1]
312
324
  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
@@ -138,11 +138,14 @@ module Planter
138
138
  MOD_RX = '(?<mod>
139
139
  (?::
140
140
  (
141
- l(?:ow(?:er)?)?)?|
142
- u(?:p(?:per)?)?|
141
+ l(?:ow(?:er(case)?)?)?)?|
142
+ d(?:own(?:case)?)?|
143
+ u(?:p(?:per(case)?)?)?|upcase|
143
144
  c(?:ap(?:ital(?:ize)?)?)?|
144
145
  t(?:itle)?|
145
146
  snake|camel|slug|
147
+ fl|first_letter|
148
+ fw|first_word|
146
149
  f(?:ile(?:name)?
147
150
  )?
148
151
  )*
@@ -172,7 +175,7 @@ module Planter
172
175
  m['default'].apply_var_names
173
176
  else
174
177
  # Retrieve the default value for the variable from the configuration
175
- vars = Planter.config[:variables].filter { |v| v[:key] == m['varname'] }
178
+ vars = Planter.config.variables.filter { |v| v[:key] == m['varname'] }
176
179
  default = vars.first[:default] if vars.count.positive?
177
180
  if default.nil?
178
181
  m[0]
@@ -204,8 +207,7 @@ module Planter
204
207
  def apply_logic(variables = nil)
205
208
  variables = variables.nil? ? Planter.variables : variables
206
209
 
207
- gsub(/%%if .*?%%.*?%%end(if)?%%/mi) do |construct|
208
- res = false
210
+ gsub(/%%if .*?%%.*?%%end( ?if)?%%/mi) do |construct|
209
211
  # Get the condition and the content
210
212
  output = construct.match(/%%else%%(.*?)%%end/m) ? Regexp.last_match(1) : ''
211
213
 
@@ -213,72 +215,8 @@ module Planter
213
215
  /%%(?<statement>(?:els(?:e )?)?if) (?<condition>.*?)%%(?<content>.*?)(?=%%)/mi).map do
214
216
  Regexp.last_match
215
217
  end
216
- conditions.each do |condition|
217
- variable, operator, value = condition['condition'].split(/ +/, 3)
218
- value.strip_quotes!
219
- variable = variable.to_var
220
- negate = false
221
- if operator =~ /^!/
222
- operator = operator[1..-1]
223
- negate = true
224
- end
225
- operator = case operator
226
- when /^={1,2}/
227
- :equal
228
- when /^=~/
229
- :matches_regex
230
- when /\*=/
231
- :contains
232
- when /\^=/
233
- :starts_with
234
- when /\$=/
235
- :ends_with
236
- when />/
237
- :greater_than
238
- when /</
239
- :less_than
240
- when />=/
241
- :greater_than_or_equal
242
- when /<=/
243
- :less_than_or_equal
244
- else
245
- :equal
246
- end
247
-
248
- comp = variables[variable.to_var].to_s
249
-
250
- res = case operator
251
- when :equal
252
- comp =~ /^#{value}$/i
253
- when :matches_regex
254
- comp =~ Regexp.new(value.gsub(%r{^/|/$}, ''))
255
- when :contains
256
- comp =~ /#{value}/i
257
- when :starts_with
258
- comp =~ /^#{value}/i
259
- when :ends_with
260
- comp =~ /#{value}$/i
261
- when :greater_than
262
- comp > value.to_f
263
- when :less_than
264
- comp < value.to_f
265
- when :greater_than_or_equal
266
- comp >= value.to_f
267
- when :less_than_or_equal
268
- comp <= value.to_f
269
- else
270
- false
271
- end
272
- res = !res if negate
273
-
274
- next unless res
275
-
276
- Planter.notify("Condition matched: #{comp} #{negate ? 'not ' : ''}#{operator} #{value}", :debug)
277
- output = condition['content']
278
- break
279
- end
280
218
 
281
- output
219
+ apply_conditions(conditions, variables, output)
282
220
  end
283
221
  end
284
222
 
@@ -287,6 +225,118 @@ module Planter
287
225
  replace apply_logic(variables)
288
226
  end
289
227
 
228
+ ##
229
+ ## Apply operator logic to a string. Operators are defined as
230
+ ## :copy, :overwrite, :ignore, or :merge. Logic can be if/else
231
+ ## constructs or inline operators.
232
+ ##
233
+ ## @example "var = 1; if var == 1:copy; else: ignore" #=> :copy
234
+ ## @example "var = 2; copy if var == 1 else ignore" #=> :ignore
235
+ ##
236
+ ## @param variables [Hash] Hash of variables (default: Planter.variables)
237
+ ##
238
+ def apply_operator_logic(variables = nil)
239
+ variables = variables.nil? ? Planter.variables : variables
240
+ op_rx = ' *(?<content>c(?:opy)?|o(?:ver(?:write)?)?|i(?:gnore)?|m(?:erge)?)? *'
241
+
242
+ strip.gsub(/^if .*?(?:end(?: ?if)?|$)/mi) do |construct|
243
+ # Get the condition and the content
244
+ output = construct.match(/else:#{op_rx}/m) ? Regexp.last_match(1) : ''
245
+
246
+ conditions = construct.to_enum(:scan,
247
+ /(?<statement>(?:els(?:e )?)?if) +(?<condition>.*?):#{op_rx}(?=;|$)/mi).map do
248
+ Regexp.last_match
249
+ end
250
+
251
+ apply_conditions(conditions, variables, output)
252
+ end.gsub(/^#{op_rx} +if .*?(end( ?if)?|$)/mi) do |construct|
253
+ # Get the condition and the content
254
+ output = construct.match(/else[; ]+(#{op_rx})/m) ? Regexp.last_match(1) : :ignore
255
+ condition = construct.match(/^#{op_rx}(?<statement>if) +(?<condition>.*?)(?=;|$)/mi)
256
+
257
+ apply_conditions([condition], variables, output)
258
+ end.normalize_operator
259
+ end
260
+
261
+ ##
262
+ ## Apply conditions
263
+ ##
264
+ ## @param conditions [Array<MatchData>] Array of conditions ['statement', 'condition', 'content']
265
+ ## @param variables [Hash] Hash of variables
266
+ ## @param output [String] Output string
267
+ ##
268
+ ## @return [String] Output string
269
+ ##
270
+ def apply_conditions(conditions, variables, output)
271
+ res = false
272
+ conditions.each do |condition|
273
+ variable, operator, value = condition['condition'].split(/ +/, 3)
274
+ value.strip_quotes!
275
+ variable = variable.to_var
276
+ negate = false
277
+ if operator =~ /^!/
278
+ operator = operator[1..-1]
279
+ negate = true
280
+ end
281
+ operator = case operator
282
+ when /^={1,2}/
283
+ :equal
284
+ when /^=~/
285
+ :matches_regex
286
+ when /\*=/
287
+ :contains
288
+ when /\^=/
289
+ :starts_with
290
+ when /\$=/
291
+ :ends_with
292
+ when />/
293
+ :greater_than
294
+ when /</
295
+ :less_than
296
+ when />=/
297
+ :greater_than_or_equal
298
+ when /<=/
299
+ :less_than_or_equal
300
+ else
301
+ :equal
302
+ end
303
+
304
+ comp = variables[variable.to_var].to_s
305
+
306
+ res = case operator
307
+ when :equal
308
+ comp =~ /^#{value}$/i
309
+ when :matches_regex
310
+ comp =~ Regexp.new(value.gsub(%r{^/|/$}, ''))
311
+ when :contains
312
+ comp =~ /#{value}/i
313
+ when :starts_with
314
+ comp =~ /^#{value}/i
315
+ when :ends_with
316
+ comp =~ /#{value}$/i
317
+ when :greater_than
318
+ comp > value.to_f
319
+ when :less_than
320
+ comp < value.to_f
321
+ when :greater_than_or_equal
322
+ comp >= value.to_f
323
+ when :less_than_or_equal
324
+ comp <= value.to_f
325
+ else
326
+ false
327
+ end
328
+ res = res ? true : false
329
+ res = !res if negate
330
+
331
+ next unless res
332
+
333
+ Planter.notify("Condition matched: #{comp} #{negate ? 'not ' : ''}#{operator} #{value}", :debug)
334
+ output = condition['content']
335
+ break
336
+ end
337
+ output
338
+ end
339
+
290
340
  ##
291
341
  ## Apply key/value substitutions to a string. Variables are represented as
292
342
  ## %%key%%, and the hash passed to the function is { key: value }
@@ -326,6 +376,8 @@ module Planter
326
376
  if m['mod']
327
377
  mods = m['mod']&.split(/:/)
328
378
  mods&.each do |mod|
379
+ next if mod.nil? || mod.empty?
380
+
329
381
  v = v.apply_mod(mod.normalize_mod)
330
382
  end
331
383
  end
@@ -369,13 +421,20 @@ module Planter
369
421
  end
370
422
 
371
423
  ##
372
- ## Apply regex replacements from @config[:replacements]
424
+ ## Apply all logic, variables, and regexes to a string
425
+ ##
426
+ def apply_all
427
+ apply_logic.apply_variables.apply_regexes
428
+ end
429
+
430
+ ##
431
+ ## Apply regex replacements from Planter.config[:replacements]
373
432
  ##
374
433
  ## @return [String] string with regexes applied
375
434
  ##
376
435
  def apply_regexes(regexes = nil)
377
436
  content = dup.clean_encode
378
- regexes = regexes.nil? && Planter.config.key?(:replacements) ? Planter.config[:replacements] : regexes
437
+ regexes = regexes.nil? && Planter.config.key?(:replacements) ? Planter.config.replacements : regexes
379
438
 
380
439
  return self unless regexes
381
440
 
@@ -455,6 +514,10 @@ module Planter
455
514
  snake_case
456
515
  when :camel_case
457
516
  camel_case
517
+ when :first_letter
518
+ split('')[0]
519
+ when :first_word
520
+ split(/[ !,?;:]+/)[0]
458
521
  else
459
522
  self
460
523
  end
@@ -481,7 +544,7 @@ module Planter
481
544
  ##
482
545
  def normalize_mod
483
546
  case self
484
- when /^(f|slug)/
547
+ when /^(file|slug)/
485
548
  :slug
486
549
  when /^cam/
487
550
  :camel_case
@@ -489,10 +552,14 @@ module Planter
489
552
  :snake_case
490
553
  when /^u/
491
554
  :uppercase
492
- when /^l/
555
+ when /^[ld]/
493
556
  :lowercase
494
557
  when /^[ct]/
495
558
  :title_case
559
+ when /^(fl|first_letter)/
560
+ :first_letter
561
+ when /^(fw|first_word)/
562
+ :first_word
496
563
  end
497
564
  end
498
565
 
@@ -622,5 +689,14 @@ module Planter
622
689
  gsub(/\((.)\)/, '{dw}({xbw}\1{dw}){xw}')
623
690
  end
624
691
  end
692
+
693
+ #
694
+ # Test if a string has a parenthetical selector
695
+ #
696
+ # @return [Boolean] has selector
697
+ #
698
+ def has_selector?
699
+ self =~ /\(.\)/ ? true : false
700
+ end
625
701
  end
626
702
  end
data/lib/planter/tag.rb CHANGED
@@ -1,8 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Planter
4
+ #
5
+ # File tagging module
6
+ #
7
+ # @author Brett Terpstra <me@brettterpstra.com>
8
+ #
4
9
  module Tag
10
+ # File tagging class
5
11
  class << self
12
+ #
13
+ # Set tags on target file.
14
+ #
15
+ # @param target [String] path to target file
16
+ # @param tags [Array] Array of tags to set
17
+ #
18
+ # @return [Boolean] success
19
+ #
6
20
  def set(target, tags)
7
21
  return false unless TTY::Which.exist?('xattr')
8
22
 
@@ -36,6 +50,13 @@ module Planter
36
50
  res
37
51
  end
38
52
 
53
+ #
54
+ # Get tags on target file.
55
+ #
56
+ # @param target [String] target file path
57
+ #
58
+ # @return [Array] Array of tags
59
+ #
39
60
  def get(target)
40
61
  return false unless TTY::Which.exist?('xattr')
41
62
 
@@ -49,6 +70,14 @@ module Planter
49
70
  tags
50
71
  end
51
72
 
73
+ #
74
+ # Copy tags from one file to another.
75
+ #
76
+ # @param source [String] path to source file
77
+ # @param target [String] path to target file
78
+ #
79
+ # @return [Boolean] success
80
+ #
52
81
  def copy(source, target)
53
82
  return false unless TTY::Which.exist?('xattr')
54
83
 
@@ -67,6 +96,15 @@ module Planter
67
96
 
68
97
  private
69
98
 
99
+ #
100
+ # Set tags on target file.
101
+ #
102
+ # @param target [String] file path
103
+ # @param tags [Array] Array of tags
104
+ #
105
+ # @return [Boolean] success
106
+ #
107
+ # @api private
70
108
  def set_tags(target, tags)
71
109
  return false unless TTY::Which.exist?('xattr')
72
110
 
@@ -3,5 +3,5 @@
3
3
  # Primary module for this gem.
4
4
  module Planter
5
5
  # Current Planter version.
6
- VERSION = '3.0.4'
6
+ VERSION = '3.0.5'
7
7
  end