planter-cli 3.0.4 → 3.0.5

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