planter-cli 3.0.2 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4b54e16d9836121fd1fa5831a1c7433056f45e9372b9985640a6052a2852aab
4
- data.tar.gz: d2a57bd628c33f9dcccdc605577a11e4aaacbeef3a0c261f96835771f8fc0223
3
+ metadata.gz: d811f48053b6544dc93bec282620300d673dc5524cc534e0b4fe59d194d09c19
4
+ data.tar.gz: 8da8c9f74029c96f7286f25c80be21a35d87237c2942db24a9864d881d04f509
5
5
  SHA512:
6
- metadata.gz: 0001cffb09df38704dbeadb6b68b42418a0b02741430fd3932fcdf845d53052945d1e51dd5bd6227c8f02cd70fbbd86f8d037bd1ef2da7547c5ae3f859320a93
7
- data.tar.gz: b612aa38f0aaa7d1746b8715e032e93550735510b9e0e5f90cbdaef35d0cb0eea3f9afbc8c1f001f03eb40c0de41f7773e5ef7f4799e1792a4543be36ff4074b
6
+ metadata.gz: 73daffea9046af25fb1c1d1839eb622ca87ec43ab2d5aab4bbf6f593cb4ea24176f9f443aadf4178610d181eb4cec5276ed92cd7ca880f1d5a72ab364e397ab9
7
+ data.tar.gz: 104ca18f851112b4142f5ef6e5b31eb564d8e83be9de87f0cb1107a42e35f180adf23858896a599f3aeee7eff2683b90b03fe4ffc6ae8dc3945bc237ffffaf14
data/.gitignore CHANGED
@@ -46,3 +46,4 @@ Gemfile.lock
46
46
  spec/test/
47
47
  spec/noop
48
48
  bundle
49
+ .vscode
data/.rubocop.yml CHANGED
@@ -11,7 +11,7 @@ AllCops:
11
11
  - Gemfile
12
12
  - Guardfile
13
13
  - Rakefile
14
- - bin/planter
14
+ - bin/plant
15
15
  - lib/**/*.rb
16
16
  Exclude:
17
17
  - pkg/**/*.rb
@@ -41,7 +41,6 @@ Metrics/BlockLength:
41
41
  Max: 45
42
42
  Exclude:
43
43
  - Rakefile
44
- - bin/untitled
45
44
  - lib/*.rb
46
45
 
47
46
  Metrics/ClassLength:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ### 3.0.3
2
+
3
+ 2024-09-02 08:02
4
+
5
+ #### CHANGED
6
+
7
+ - Multiline is now "paragraph"
8
+ - Module type now requires "mod" at minimum
9
+
10
+ #### NEW
11
+
12
+ - Multiple choice variable type (ch|mu)
13
+ - If/then logic in templates
14
+
1
15
  ### 3.0.2
2
16
 
3
17
  2024-09-01 09:46
data/README.md CHANGED
@@ -47,7 +47,7 @@ First, there's a `variables` section that defines variables used in the template
47
47
  variables:
48
48
  - key: var_key
49
49
  prompt: Prompt text
50
- type: string # [string,multiline,float,integer,number,date] defaults to string
50
+ type: string # [string,paragraph,float,integer,number,date,choice] defaults to string
51
51
  # value: (force value, string can include %%variables%% and regexes will be replaced. For date type can be today, time, now, etc.)
52
52
  default: Untitled
53
53
  min: 1
@@ -64,6 +64,85 @@ repo: # If a repository URL is provided, it will be pulled and duplicated instea
64
64
 
65
65
  In a template you can add a default value for a placholder by adding `%default value` to it. For example, `%%project%Default Project%%` will set the placeholder to `Default Project` if the variable value matches the default value in the configuration. This allows you to accept the default on the command line but have a different value inserted in the template. To use another variable in its place, use `$KEY` in the placeholder, e.g. `%%project%$title%%` will replace the `project` key with the value of `title` if the default is selected. Modifiers can be used on either side of the `%`, e.g. `%%project%$title:snake%%`.
66
66
 
67
+ #### Multiple choice type
68
+
69
+ If the `type` is set to `choice`, then the key `choices` can contain a hash or array of choices. The key that accepts the choice should be surrounded with parenthesis (required for each choice).
70
+
71
+ If a Hash is defined, each choice can have a result string:
72
+
73
+ ```yaml
74
+ variables:
75
+ - key: shebang
76
+ prompt: Shebang line
77
+ type: choice
78
+ default: r
79
+ choices:
80
+ (r)uby: "#! /usr/bin/env ruby"
81
+ (j)avascript: "#! /usr/bin/env node"
82
+ (p)ython: "#! /usr/bin/env python"
83
+ (b)ash: "#! /bin/bash"
84
+ (z)sh: "#! /bin/zsh"
85
+ ```
86
+
87
+ If an array is defined, the string of the choice will also be its result:
88
+
89
+ ```yaml
90
+ variables:
91
+ - key: language
92
+ prompt: Programming language
93
+ type: choice
94
+ default: 1
95
+ choices:
96
+ - 1. ruby
97
+ - 1. javascript
98
+ - 1. python
99
+ - 1. bash
100
+ - 1. zsh
101
+ ```
102
+
103
+ If the choice starts with a number (as above), then a numeric list will be generated and typing the associated index number will accept that choice. Numeric lists are automatically numbered, so the preceding digit doesn't matter, as long as it's a digit. In this case a default can be defined with an integer for its placement in the list (starting with 1), and parenthesis aren't required.
104
+
105
+ #### If/then logic
106
+
107
+ A template can use if/then logic, which is useful with multiple choice types. It can be applied to any type, though.
108
+
109
+ The format for if/then logic is:
110
+
111
+ ```
112
+ %%if KEY OPERATOR VALUE%%
113
+ content
114
+ %%else if KEY OPERATOR VALUE2%%
115
+ content 2
116
+ %%else%%
117
+ content 3
118
+ %%endif%%
119
+ ```
120
+
121
+ There should be no spaces around the comparison, e.g. `%% if language == javascript %%` won't work. The block must start with an `if` statement and end with `%%endif%%` or `%%end%%`. The `%%else%%` statement is optional -- if it doesn't exist then the entire block will be removed if no conditions are met.
122
+
123
+ The key should be an existing key defined in `variables`. The operator can be any of:
124
+
125
+ - `==` or `=` (equals)
126
+ - `=~` (matches regex)
127
+ - `*=` (contains)
128
+ - `^=` (starts with)
129
+ - `$=` (ends with)
130
+ - `>` (greater than)
131
+ - `>=` (greater than or equal)
132
+ - `<` (less than)
133
+ - `<=` (less than or equal)
134
+
135
+ The value after the operator doesn't need to be quoted, anything after the operator will be compared to the value of the key.
136
+
137
+ Logic can be used on multiple lines like the example above, or on a single line (useful for filenames):
138
+
139
+ ```
140
+ %%project%%.%%if language == javascript%%js%%else if language == ruby%%rb%%else%%sh%%endif%%
141
+ ```
142
+
143
+ Content within if/else blocks can contain variables.
144
+
145
+
67
146
  ### File-specific handling
68
147
 
69
148
  A `files` dictionary can specify how to handle specific files. Options are `copy`, `overwrite`, `merge`, or `ask`. The key for each entry is a filename or glob that matches the source filename (accounting for template variables if applicable):
@@ -74,7 +153,7 @@ files:
74
153
  "%%title%%.md": overwrite
75
154
  ```
76
155
 
77
- Filenames can include wildcards (`*`, `?`), and Bash-style globbing (`[0-9]`, `[a-z]`, `{one,two,three}`).
156
+ Filenames can include wildcards (`*`, `?`), and Bash-ish globbing (`[0-9]`, `[a-z]`, `{one,two,three}`).
78
157
 
79
158
  If `merge` is specified, then the source file is scanned for merge comments and those are merged if they don't exist in the copied/existing file. If no merge comments are defined, then the entire contents of the source file are appended to the destination file (unless the file already matches the source). Merge comments start with `merge` and end with `/merge` and can have any comment syntax preceding them, for example:
80
159
 
@@ -4,7 +4,8 @@ WORKDIR /planter
4
4
  RUN gem install bundler:2.2.29
5
5
  COPY ./docker/sources.list /etc/apt/sources.list
6
6
  RUN apt-get update -y --allow-insecure-repositories || true
7
- RUN apt-get install -y less vim
7
+ RUN apt-get install -y sudo || true
8
+ RUN sudo apt-get install -y less vim || true
8
9
  COPY ./docker/inputrc /root/.inputrc
9
10
  COPY ./docker/bash_profile /root/.bash_profile
10
11
  CMD ["/planter/scripts/runtests.sh"]
@@ -4,7 +4,8 @@ WORKDIR /planter
4
4
  RUN gem install bundler:2.2.29
5
5
  COPY ./docker/sources.list /etc/apt/sources.list
6
6
  RUN apt-get update -y --allow-insecure-repositories || true
7
- RUN apt-get install -y less vim
7
+ RUN apt-get install -y sudo || true
8
+ RUN sudo apt-get install -y less vim || true
8
9
  COPY ./docker/inputrc /root/.inputrc
9
10
  COPY ./docker/bash_profile /root/.bash_profile
10
11
  CMD ["/planter/scripts/runtests.sh"]
@@ -4,7 +4,8 @@ WORKDIR /planter
4
4
  RUN gem install bundler:2.2.29
5
5
  COPY ./docker/sources.list /etc/apt/sources.list
6
6
  RUN apt-get update -y --allow-insecure-repositories || true
7
- RUN apt-get install -y less vim
7
+ RUN apt-get install -y sudo || true
8
+ RUN sudo apt-get install -y less vim || true
8
9
  COPY ./docker/inputrc /root/.inputrc
9
10
  COPY ./docker/bash_profile /root/.bash_profile
10
11
  CMD ["/planter/scripts/runtests.sh"]
@@ -5,7 +5,8 @@ WORKDIR /planter
5
5
  RUN gem install bundler:2.2.29
6
6
  COPY ./docker/sources.list /etc/apt/sources.list
7
7
  RUN apt-get update -y --allow-insecure-repositories || true
8
- RUN apt-get install -y less vim
8
+ RUN apt-get install -y sudo || true
9
+ RUN sudo apt-get install -y less vim || true
9
10
  COPY ./docker/inputrc /root/.inputrc
10
11
  COPY ./docker/bash_profile /root/.bash_profile
11
12
  CMD ["/planter/scripts/runtests.sh"]
data/docker/bash_profile CHANGED
@@ -2,6 +2,7 @@
2
2
  export GLI_DEBUG=true
3
3
  export EDITOR="/usr/bin/vim"
4
4
  alias b="bundle exec bin/plant"
5
+ alias be="bundle exec"
5
6
  alias quit="exit"
6
7
 
7
8
  shopt -s nocaseglob
@@ -11,5 +12,5 @@ shopt -s histverify
11
12
  shopt -s cmdhist
12
13
 
13
14
  cd /planter
14
- bundle install
15
+ bundle update
15
16
  gem install pkg/*.gem
data/lib/planter/array.rb CHANGED
@@ -12,7 +12,7 @@ module Planter
12
12
  ## @param default [String] The color templated output string
13
13
  ##
14
14
  def abbr_choices(default: nil)
15
- chars = join(' ').scan(/\((.)\)/).map { |c| c[0] }
15
+ chars = join(' ').scan(/\((?:(.)\.?)\)/).map { |c| c[0] }
16
16
  out = String.new
17
17
  out << '{xdw}['
18
18
  out << chars.map do |c|
@@ -25,6 +25,61 @@ module Planter
25
25
  out << '{dw}]{x}'
26
26
  end
27
27
 
28
+ ## Convert an array of choices to a string with optional numbering
29
+ ##
30
+ ## @param numeric [Boolean] Include numbering
31
+ ##
32
+ ## @return [Array] Array of choices
33
+ ##
34
+ def to_options(numeric)
35
+ map.with_index do |c, i|
36
+ # v = c.to_s.match(/\(?([a-z]|\d+\.?)\)?/)[1].strip
37
+ if numeric
38
+ "(#{i + 1}). #{c.to_s.sub(/^\(?\d+\.?\)? +/, '')}"
39
+ else
40
+ c
41
+ end
42
+ end
43
+ end
44
+
45
+ ## Find the index of a choice in an array of choices
46
+ ##
47
+ ## @param choice [String] The choice to find
48
+ ##
49
+ ## @return [Integer] Index of the choice
50
+ ##
51
+ def option_index(choice)
52
+ index = find_index { |c| c.to_s.match(/\((.+)\)/)[1].strip.sub(/\.$/, '') == choice }
53
+ index || false
54
+ end
55
+
56
+ ## Convert an array of choices to a hash
57
+ ## - If the array contains hashes, they are converted to key/value pairs
58
+ ## - If the array contains strings, they are used as both key and value
59
+ ##
60
+ ## @return [Hash] Hash of choices
61
+ ##
62
+ def choices_to_hash
63
+ hash = {}
64
+ each do |c|
65
+ if c.is_a?(Hash)
66
+ hash[c.keys.first.to_s] = c.values.first.to_s
67
+ else
68
+ hash[c.to_s] = c.to_s
69
+ end
70
+ end
71
+
72
+ hash
73
+ end
74
+
75
+ ## Clean strings in an array by removing numbers and parentheses
76
+ ##
77
+ ## @return [Array] Array with cleaned strings
78
+ ##
79
+ def to_values
80
+ map(&:clean_value)
81
+ end
82
+
28
83
  ##
29
84
  ## Stringify keys in an array of hashes or arrays
30
85
  ##
data/lib/planter/hash.rb CHANGED
@@ -3,6 +3,30 @@
3
3
  module Planter
4
4
  ## Hash helpers
5
5
  class ::Hash
6
+ ## Turn all keys and values into string
7
+ ##
8
+ ## @return [Hash] copy of the hash where all its keys are strings
9
+ ##
10
+ def stringify
11
+ each_with_object({}) do |(k, v), hsh|
12
+ hsh[k.to_s] = if v.is_a?(Hash) || v.is_a?(Array)
13
+ v.stringify
14
+ else
15
+ v.to_s
16
+ end
17
+ end
18
+ end
19
+
20
+ ## Destructive version of #stringify
21
+ ##
22
+ ## @return [Hash] Hash with stringified keys and values
23
+ ##
24
+ ## @see #stringify
25
+ ##
26
+ def stringify!
27
+ replace stringify
28
+ end
29
+
6
30
  ## Turn all keys into string
7
31
  ##
8
32
  ## @return [Hash] copy of the hash where all its keys are strings
data/lib/planter/plant.rb CHANGED
@@ -45,7 +45,8 @@ module Planter
45
45
  default: var[:default],
46
46
  value: var[:value],
47
47
  min: var[:min],
48
- max: var[:max]
48
+ max: var[:max],
49
+ choices: var[:choices] || nil
49
50
  )
50
51
  answer = q.ask
51
52
  if answer.nil?
@@ -158,8 +159,8 @@ module Planter
158
159
  s.run
159
160
  end
160
161
  end
161
- Planter.spinner.update(title: '😄')
162
- Planter.spinner.success(' Planting complete!')
162
+ Planter.spinner.update(title: 'Planting complete!')
163
+ Planter.spinner.success
163
164
  end
164
165
 
165
166
  ##
@@ -182,19 +183,20 @@ module Planter
182
183
  next if File.binary?(file)
183
184
 
184
185
  content = IO.read(file)
185
- new_content = content.apply_variables.apply_regexes
186
+
187
+ new_content = content.apply_logic.apply_variables.apply_regexes
186
188
 
187
189
  new_content.gsub!(%r{^.{.4}/?merge *.{,4}\n}, '') if new_content =~ /^.{.4}merge *\n/
188
190
 
189
191
  unless content == new_content
190
- Planter.notify("Applying variables to #{file}", :debug, above_spinner: true)
192
+ Planter.notify("Applying variables to #{file}", :debug)
191
193
  File.open(file, 'w') { |f| f.puts new_content }
192
194
  end
193
195
  end
194
196
 
195
197
  true
196
198
  rescue StandardError => e
197
- Planter.notify("#{e}\n#{e.backtrace}", :debug, above_spinner: true)
199
+ Planter.notify("#{e}\n#{e.backtrace}", :debug)
198
200
  'Error updating files/directories'
199
201
  end
200
202
 
@@ -215,7 +217,7 @@ module Planter
215
217
 
216
218
  true
217
219
  rescue StandardError => e
218
- Planter.notify("#{e}\n#{e.backtrace}", :debug, above_spinner: true)
220
+ Planter.notify("#{e}\n#{e.backtrace}", :debug)
219
221
  'Error initializing git'
220
222
  end
221
223
  end
@@ -23,6 +23,7 @@ module Planter
23
23
  @prompt = question[:prompt] || nil
24
24
  @default = question[:default]
25
25
  @value = question[:value]
26
+ @choices = question[:choices] || []
26
27
  @gum = false # TTY::Which.exist?('gum')
27
28
  end
28
29
 
@@ -37,6 +38,8 @@ module Planter
37
38
  return @value.to_s.apply_variables.apply_regexes.coerce(@type) if @value && @type != :date
38
39
 
39
40
  res = case @type
41
+ when :choice
42
+ Prompt.choice(@choices, @prompt, default_response: @default)
40
43
  when :integer
41
44
  read_number(integer: true)
42
45
  when :float
@@ -238,18 +241,36 @@ module Planter
238
241
  ## @param default_response [String] The character of the default
239
242
  ## response
240
243
  ##
241
- ## @return [String] character of selected response, lowercased
244
+ ## @return [String] string of selected response with parenthesis removed
242
245
  ##
243
246
  def self.choice(choices, prompt = 'Make a selection', default_response: nil)
244
247
  $stdin.reopen('/dev/tty')
245
248
 
246
- default = default_response.is_a?(String) ? default_response.downcase : nil
249
+ choices = choices.choices_to_hash if choices.is_a?(Array) && choices.first.is_a?(Hash)
247
250
 
248
- # if this isn't an interactive shell, answer default
249
- return default unless $stdout.isatty
251
+ if choices.is_a?(Hash)
252
+ choices.stringify!
253
+ numeric = choices.keys.first =~ /^\(?\d+\.?\)? /
254
+ keys = choices.keys.to_options(numeric)
255
+ values = choices.values.map(&:clean_value)
256
+ choices = choices.keys
257
+ else
258
+ numeric = choices.first =~ /^\(?\d+\.?\)? /
259
+ keys = choices.to_options(numeric)
260
+ values = choices.to_values.map(&:clean_value)
261
+ end
250
262
 
251
- # If --defaults is set, return default
252
- return default if Planter.accept_defaults || ENV['PLANTER_DEBUG']
263
+ default = case default_response.to_s
264
+ when /^\d+$/
265
+ values[default.to_i]
266
+ when /^[a-z]$/i
267
+ keys.include?(default_response) ? values[keys.index(default_response)] : nil
268
+ end
269
+
270
+ # If --defaults is set or not an interactive shell, return default
271
+ return default if Planter.accept_defaults || ENV['PLANTER_DEBUG'] || !$stdout.isatty
272
+
273
+ default = default_response.to_s if default_response
253
274
 
254
275
  # clear the buffer
255
276
  if ARGV&.length
@@ -259,9 +280,10 @@ module Planter
259
280
  end
260
281
  system 'stty cbreak'
261
282
 
262
- vertical = choices.join(' ').length + 4 > TTY::Screen.cols
263
- desc = choices.map { |c| c.highlight_character(default: default) }
264
- abbr = choices.abbr_choices(default: default)
283
+ vertical = numeric || choices.join(', ').length + 4 > TTY::Screen.cols
284
+
285
+ desc = keys.map { |c| c.highlight_character(default: default) }
286
+ abbr = keys.abbr_choices(default: default)
265
287
 
266
288
  options = if vertical
267
289
  "{x}#{desc.join("\n")}\n{by}#{prompt}{x} #{abbr}{bw}? "
@@ -270,16 +292,34 @@ module Planter
270
292
  end
271
293
 
272
294
  $stdout.syswrite options.x
273
- res = $stdin.sysread 1
295
+
296
+ res = if numeric && choices.length > 9
297
+ $stdin.sysread choices.length.to_s.length
298
+ else
299
+ $stdin.sysread 1
300
+ end
301
+
274
302
  puts
275
303
  system 'stty cooked'
276
304
 
277
305
  res.chomp!
278
306
  res.downcase!
279
307
 
280
- res.empty? ? default : res
308
+ res = res.empty? ? default : res
309
+
310
+ if res.to_i.positive?
311
+ values[res.to_i - 1]
312
+ elsif res =~ /^[a-z]/ && keys&.option_index(res)
313
+ values[keys.option_index(res)]
314
+ end
281
315
  end
282
316
 
317
+ ## Determine what to do with a file
318
+ ##
319
+ ## @param entry [FileEntry] The file entry
320
+ ##
321
+ ## @return [Symbol] :merge, :overwrite, :copy, :ignore
322
+ ##
283
323
  def self.file_what?(entry)
284
324
  options = %w[(o)vewrite (m)erge]
285
325
  options << '(c)opy' unless File.exist?(entry.target)
@@ -12,7 +12,20 @@ module Planter
12
12
  ## @return [Symbol] string as variable key
13
13
  ##
14
14
  def to_var
15
- snake_case.to_sym
15
+ strip_quotes.snake_case.to_sym
16
+ end
17
+
18
+ ## Strip quotes from a string
19
+ ##
20
+ ## @return [String] string with quotes stripped
21
+ ##
22
+ def strip_quotes
23
+ sub(/^(["'])(.*)\1$/, '\2')
24
+ end
25
+
26
+ ## Destructive version of #strip_quotes
27
+ def strip_quotes!
28
+ replace strip_quotes
16
29
  end
17
30
 
18
31
  #
@@ -184,6 +197,96 @@ module Planter
184
197
  replace apply_defaults(variables)
185
198
  end
186
199
 
200
+ ## Apply logic to a string
201
+ ##
202
+ ## @param variables [Hash] Hash of variables to apply
203
+ ##
204
+ def apply_logic(variables = nil)
205
+ variables = variables.nil? ? Planter.variables : variables
206
+
207
+ gsub(/%%if .*?%%.*?%%end(if)?%%/mi) do |construct|
208
+ res = false
209
+ # Get the condition and the content
210
+ output = construct.match(/%%else%%(.*?)%%end/m) ? Regexp.last_match(1) : ''
211
+
212
+ conditions = construct.to_enum(:scan,
213
+ /%%(?<statement>(?:els(?:e )?)?if) (?<condition>.*?)%%(?<content>.*?)(?=%%)/mi).map do
214
+ Regexp.last_match
215
+ 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
+
281
+ output
282
+ end
283
+ end
284
+
285
+ ## Destructive version of #apply_logic
286
+ def apply_logic!(variables)
287
+ replace apply_logic(variables)
288
+ end
289
+
187
290
  ##
188
291
  ## Apply key/value substitutions to a string. Variables are represented as
189
292
  ## %%key%%, and the hash passed to the function is { key: value }
@@ -199,6 +302,8 @@ module Planter
199
302
 
200
303
  content = content.apply_defaults(variables)
201
304
 
305
+ content = content.apply_logic(variables)
306
+
202
307
  variables.each do |k, v|
203
308
  if last_only
204
309
  pattern = "%%#{k.to_var}"
@@ -437,15 +542,18 @@ module Planter
437
542
  # number or float
438
543
  when /^[nf]/
439
544
  :float
440
- # multiline or paragraph
441
- when /^(mu|p)/
545
+ # paragraph
546
+ when /^p/
442
547
  :multiline
443
548
  # class
444
- when /^c/
549
+ when /^cl/
445
550
  :class
446
551
  # module
447
- when /^m/
552
+ when /^mod/
448
553
  :module
554
+ # multiple choice
555
+ when /^(ch|mu)/
556
+ :choice
449
557
  # string
450
558
  else
451
559
  :string
@@ -493,6 +601,14 @@ module Planter
493
601
  replace clean_encode
494
602
  end
495
603
 
604
+ ## Clean up a string by removing leading numbers and parentheticalse
605
+ ##
606
+ ## @return [String] cleaned string
607
+ ##
608
+ def clean_value
609
+ sub(/^\(?\d+\.\)? +/, '').sub(/\((.*?)\)/, '\1')
610
+ end
611
+
496
612
  ##
497
613
  ## Highlight characters in parenthesis, with special color for default if
498
614
  ## provided. Output is color templated string, unprocessed.
@@ -3,5 +3,5 @@
3
3
  # Primary module for this gem.
4
4
  module Planter
5
5
  # Current Planter version.
6
- VERSION = '3.0.2'
6
+ VERSION = '3.0.3'
7
7
  end
data/lib/planter.rb CHANGED
@@ -92,7 +92,7 @@ module Planter
92
92
  '{bw}'
93
93
  end
94
94
  out = "#{color}#{string}{x}"
95
- out.gsub!(/\[(.*?)\]/, "{by}\\1{x}#{color}")
95
+ out = out.gsub(/\[(.*?)\]/, "{by}\\1{x}#{color}")
96
96
  out = "\n#{out}" if newline
97
97
  above_spinner ? spinner.log(out.x) : warn(out.x)
98
98
 
@@ -114,6 +114,33 @@ describe ::String do
114
114
  end
115
115
  end
116
116
 
117
+ describe '.apply_logic' do
118
+ it 'applies a single logic replacement' do
119
+ template = 'Hello %%if language == ruby%%World%%else%%There%%end%%!'
120
+ logic = { language: 'ruby' }
121
+ expect(template.apply_logic(logic)).to eq 'Hello World!'
122
+ end
123
+
124
+ it 'handles quotes in logic' do
125
+ template = 'Hello %%if language == "ruby"%%World%%else%%There%%end%%!'
126
+ logic = { language: 'ruby' }
127
+ expect(template.apply_logic(logic)).to eq 'Hello World!'
128
+ end
129
+
130
+ it 'handles no logic replacements' do
131
+ template = 'Hello, World!'
132
+ logic = {}
133
+ expect(template.apply_logic(logic)).to eq 'Hello, World!'
134
+ end
135
+
136
+ it 'Operates in place' do
137
+ template = 'Hello %%if language == "ruby"%%World%%else%%There%%end%%!'
138
+ logic = { language: 'ruby' }
139
+ template.apply_logic!(logic)
140
+ expect(template).to eq 'Hello World!'
141
+ end
142
+ end
143
+
117
144
  describe '.apply_regexes' do
118
145
  it 'applies a single regex replacement' do
119
146
  template = 'Hello, World!'
@@ -171,15 +198,15 @@ describe ::String do
171
198
  end
172
199
 
173
200
  it 'normalizes a multiline type' do
174
- expect("multi".normalize_type.to_s).to eq "multiline"
201
+ expect("para".normalize_type.to_s).to eq "multiline"
175
202
  end
176
203
 
177
204
  it 'normalizes a class type' do
178
- expect("c".normalize_type.to_s).to eq "class"
205
+ expect("cl".normalize_type.to_s).to eq "class"
179
206
  end
180
207
 
181
- it 'normalizes a module type' do
182
- expect("m".normalize_type.to_s).to eq "module"
208
+ it 'normalizes a multiple choice type' do
209
+ expect("choice".normalize_type.to_s).to eq "choice"
183
210
  end
184
211
  end
185
212
 
data/spec/spec_helper.rb CHANGED
@@ -29,7 +29,7 @@ require 'open3'
29
29
  require 'time'
30
30
 
31
31
  module PlanterHelpers
32
- PLANTER_EXEC = File.join(File.dirname(__FILE__), '..', 'bin', 'plant')
32
+ PLANTER_EXEC = File.join(File.dirname(__FILE__), '..', 'exe', 'plant')
33
33
 
34
34
  def planter_with_env(env, *args, stdin: nil)
35
35
  pread(env, 'bundle', 'exec', PLANTER_EXEC, "--base-dir=#{File.dirname(__FILE__)}", *args, stdin: stdin)
data/src/_README.md CHANGED
@@ -52,7 +52,7 @@ First, there's a `variables` section that defines variables used in the template
52
52
  variables:
53
53
  - key: var_key
54
54
  prompt: Prompt text
55
- type: string # [string,multiline,float,integer,number,date] defaults to string
55
+ type: string # [string,paragraph,float,integer,number,date,choice] defaults to string
56
56
  # value: (force value, string can include %%variables%% and regexes will be replaced. For date type can be today, time, now, etc.)
57
57
  default: Untitled
58
58
  min: 1
@@ -69,6 +69,85 @@ repo: # If a repository URL is provided, it will be pulled and duplicated instea
69
69
 
70
70
  In a template you can add a default value for a placholder by adding `%default value` to it. For example, `%%project%Default Project%%` will set the placeholder to `Default Project` if the variable value matches the default value in the configuration. This allows you to accept the default on the command line but have a different value inserted in the template. To use another variable in its place, use `$KEY` in the placeholder, e.g. `%%project%$title%%` will replace the `project` key with the value of `title` if the default is selected. Modifiers can be used on either side of the `%`, e.g. `%%project%$title:snake%%`.
71
71
 
72
+ #### Multiple choice type
73
+
74
+ If the `type` is set to `choice`, then the key `choices` can contain a hash or array of choices. The key that accepts the choice should be surrounded with parenthesis (required for each choice).
75
+
76
+ If a Hash is defined, each choice can have a result string:
77
+
78
+ ```yaml
79
+ variables:
80
+ - key: shebang
81
+ prompt: Shebang line
82
+ type: choice
83
+ default: r
84
+ choices:
85
+ (r)uby: "#! /usr/bin/env ruby"
86
+ (j)avascript: "#! /usr/bin/env node"
87
+ (p)ython: "#! /usr/bin/env python"
88
+ (b)ash: "#! /bin/bash"
89
+ (z)sh: "#! /bin/zsh"
90
+ ```
91
+
92
+ If an array is defined, the string of the choice will also be its result:
93
+
94
+ ```yaml
95
+ variables:
96
+ - key: language
97
+ prompt: Programming language
98
+ type: choice
99
+ default: 1
100
+ choices:
101
+ - 1. ruby
102
+ - 1. javascript
103
+ - 1. python
104
+ - 1. bash
105
+ - 1. zsh
106
+ ```
107
+
108
+ If the choice starts with a number (as above), then a numeric list will be generated and typing the associated index number will accept that choice. Numeric lists are automatically numbered, so the preceding digit doesn't matter, as long as it's a digit. In this case a default can be defined with an integer for its placement in the list (starting with 1), and parenthesis aren't required.
109
+
110
+ #### If/then logic
111
+
112
+ A template can use if/then logic, which is useful with multiple choice types. It can be applied to any type, though.
113
+
114
+ The format for if/then logic is:
115
+
116
+ ```
117
+ %%if KEY OPERATOR VALUE%%
118
+ content
119
+ %%else if KEY OPERATOR VALUE2%%
120
+ content 2
121
+ %%else%%
122
+ content 3
123
+ %%endif%%
124
+ ```
125
+
126
+ There should be no spaces around the comparison, e.g. `%% if language == javascript %%` won't work. The block must start with an `if` statement and end with `%%endif%%` or `%%end%%`. The `%%else%%` statement is optional -- if it doesn't exist then the entire block will be removed if no conditions are met.
127
+
128
+ The key should be an existing key defined in `variables`. The operator can be any of:
129
+
130
+ - `==` or `=` (equals)
131
+ - `=~` (matches regex)
132
+ - `*=` (contains)
133
+ - `^=` (starts with)
134
+ - `$=` (ends with)
135
+ - `>` (greater than)
136
+ - `>=` (greater than or equal)
137
+ - `<` (less than)
138
+ - `<=` (less than or equal)
139
+
140
+ The value after the operator doesn't need to be quoted, anything after the operator will be compared to the value of the key.
141
+
142
+ Logic can be used on multiple lines like the example above, or on a single line (useful for filenames):
143
+
144
+ ```
145
+ %%project%%.%%if language == javascript%%js%%else if language == ruby%%rb%%else%%sh%%endif%%
146
+ ```
147
+
148
+ Content within if/else blocks can contain variables.
149
+
150
+
72
151
  ### File-specific handling
73
152
 
74
153
  A `files` dictionary can specify how to handle specific files. Options are `copy`, `overwrite`, `merge`, or `ask`. The key for each entry is a filename or glob that matches the source filename (accounting for template variables if applicable):
@@ -79,7 +158,7 @@ files:
79
158
  "%%title%%.md": overwrite
80
159
  ```
81
160
 
82
- Filenames can include wildcards (`*`, `?`), and Bash-style globbing (`[0-9]`, `[a-z]`, `{one,two,three}`).
161
+ Filenames can include wildcards (`*`, `?`), and Bash-ish globbing (`[0-9]`, `[a-z]`, `{one,two,three}`).
83
162
 
84
163
  If `merge` is specified, then the source file is scanned for merge comments and those are merged if they don't exist in the copied/existing file. If no merge comments are defined, then the entire contents of the source file are appended to the destination file (unless the file already matches the source). Merge comments start with `merge` and end with `/merge` and can have any comment syntax preceding them, for example:
85
164
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: planter-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.2
4
+ version: 3.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-01 00:00:00.000000000 Z
11
+ date: 2024-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bump