planter-cli 3.0.2 → 3.0.4

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: 06c272d8c938a24e24d8ad5ec220ba9100f6f35fbe6e8d9765a33c982607dbd0
4
+ data.tar.gz: f9a21781e985fce37c419fcf5f4e6f33cd13c951cd882468b6c90e3a19b50fdd
5
5
  SHA512:
6
- metadata.gz: 0001cffb09df38704dbeadb6b68b42418a0b02741430fd3932fcdf845d53052945d1e51dd5bd6227c8f02cd70fbbd86f8d037bd1ef2da7547c5ae3f859320a93
7
- data.tar.gz: b612aa38f0aaa7d1746b8715e032e93550735510b9e0e5f90cbdaef35d0cb0eea3f9afbc8c1f001f03eb40c0de41f7773e5ef7f4799e1792a4543be36ff4074b
6
+ metadata.gz: ffbd475e7f56a2c96cf325cd1a681e9a7541d191c9cc9f669c06e2b3a74b4c889b2256290c5b390ea36aa408bc5e262f605ed0106e4ec409b2dece751adbbcbc
7
+ data.tar.gz: cc9241c6c6ac2a10ac2d5b82cdf2e6c204472c18834b113499af57e18615706a429c2900d19bdb9aa287c2d77bbe0f0a0a22c29288c10fceb292d623a23c86c3
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,25 @@
1
+ ### 3.0.4
2
+
3
+ 2024-09-02 08:43
4
+
5
+ #### FIXED
6
+
7
+ - Incorrect binary name in help banner
8
+
9
+ ### 3.0.3
10
+
11
+ 2024-09-02 08:02
12
+
13
+ #### CHANGED
14
+
15
+ - Multiline is now "paragraph"
16
+ - Module type now requires "mod" at minimum
17
+
18
+ #### NEW
19
+
20
+ - Multiple choice variable type (ch|mu)
21
+ - If/then logic in templates
22
+
1
23
  ### 3.0.2
2
24
 
3
25
  2024-09-01 09:46
data/README.md CHANGED
@@ -8,10 +8,6 @@ Plant a directory structure and files using templates.
8
8
 
9
9
  If you run into errors, try `gem install --user-install planter-cli`, or as a last ditch effort, `sudo gem install planter-cli`.
10
10
 
11
- ### Optional
12
-
13
- If [Gum](https://github.com/charmbracelet/gum) is available it will be used for command line input.
14
-
15
11
  ## Configuration
16
12
 
17
13
  Planter's base configuration is in `~/.config/planter/planter.yml`. This file can contain any of the keys used in templates (see below) and will serve as a base configuration for all templates. Any key defined in this file will be overridden if it exists in a template.
@@ -37,7 +33,7 @@ Scripts can be executable files in any language, and receive the template direct
37
33
 
38
34
  Templates are directories found in `~/.config/planter/templates/[TEMPLATE_NAME]`. All files and directories inside of these template directories are copied when that template is called. Filenames, directory names, and file contents can all use template placeholders.
39
35
 
40
- Template placeholders are defined with `%%KEY%%`, where key is the key defined in the `variables` section of the configuration. %%KEY%% placeholders can be used in directory/file names, and in the file contents. These work in any plain text or RTF format document, including XML, so they can be used in things like Scrivener templates and MindNode files as well.
36
+ Template placeholders are defined with `%%KEY%%`, where key is the key defined in the `variables` section of the configuration. `%%KEY%%` placeholders can be used in directory/file names, and in the file contents. These work in any plain text or RTF format document, including XML, so they can be used in things like Scrivener templates and MindNode files as well.
41
37
 
42
38
  Each template contains a `_planter.yml` file that defines variables and other configuration options. The file format for all configuration files is [YAML](https://yaml.org/spec/1.2.2/).
43
39
 
@@ -47,22 +43,108 @@ First, there's a `variables` section that defines variables used in the template
47
43
  variables:
48
44
  - key: var_key
49
45
  prompt: Prompt text
50
- type: string # [string,multiline,float,integer,number,date] defaults to string
46
+ type: string # [string,paragraph,float,integer,number,date,choice] defaults to string
51
47
  # value: (force value, string can include %%variables%% and regexes will be replaced. For date type can be today, time, now, etc.)
52
48
  default: Untitled
53
49
  min: 1
54
50
  max: 5
51
+ ```
52
+
53
+ A configuration can include additional keys:
54
+
55
+ ```yaml
55
56
  script: # array of scripts, args passed as [script and args] TEMPLATE_DIR PWD
56
57
  - process.py
57
58
  git_init: false # if true, initialize a git repository in the newly-planted directory
58
59
  files: # Dictionary for file handling (see [File-specific handling](#file-specific-handling))
59
60
  replacements: # Dictionary of pattern/replacments for regex substitution, see [Regex replacements](#regex-replacements)
60
- repo: # If a repository URL is provided, it will be pulled and duplicated instead of copying a file structure
61
+ repo: # If a repository URL is provided, it will be pulled and duplicated instead of copying an existing file structure
61
62
  ```
62
63
 
63
64
  #### Default values in template strings
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
+ 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 (or doesn't exist). 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%%`.
67
+
68
+ #### Multiple choice type
69
+
70
+ 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).
71
+
72
+ If a Hash/Dictionary is defined, each choice can have a result string:
73
+
74
+ ```yaml
75
+ variables:
76
+ - key: shebang
77
+ prompt: Shebang line
78
+ type: choice
79
+ default: r
80
+ choices:
81
+ (r)uby: "#! /usr/bin/env ruby"
82
+ (j)avascript: "#! /usr/bin/env node"
83
+ (p)ython: "#! /usr/bin/env python"
84
+ (b)ash: "#! /bin/bash"
85
+ (z)sh: "#! /bin/zsh"
86
+ ```
87
+
88
+ When a choice is selected from a dictionary, the result string will be inserted instead of the choice title.
89
+
90
+ If an array is defined, the string of the choice will also be its result (minus any parenthesis):
91
+
92
+ ```yaml
93
+ variables:
94
+ - key: language
95
+ prompt: Programming language
96
+ type: choice
97
+ default: 1
98
+ choices:
99
+ - 1. ruby
100
+ - 1. javascript
101
+ - 1. python
102
+ - 1. bash
103
+ - 1. zsh
104
+ ```
105
+
106
+ 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 (in the `defaults:` key) for its placement in the list (starting with 1), and parenthesis aren't required.
107
+
108
+ #### If/then logic
109
+
110
+ A template can use if/then logic, which is useful with multiple choice types. It can be applied to any type, though.
111
+
112
+ The format for if/then logic is:
113
+
114
+ ```
115
+ %%if KEY OPERATOR VALUE%%
116
+ content
117
+ %%else if KEY OPERATOR VALUE2%%
118
+ content 2
119
+ %%else%%
120
+ content 3
121
+ %%endif%%
122
+ ```
123
+
124
+ 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.
125
+
126
+ The key should be an existing key defined in `variables`. The operator can be any of:
127
+
128
+ - `==` or `=` (equals)
129
+ - `=~` (matches regex)
130
+ - `*=` (contains)
131
+ - `^=` (starts with)
132
+ - `$=` (ends with)
133
+ - `>` (greater than)
134
+ - `>=` (greater than or equal)
135
+ - `<` (less than)
136
+ - `<=` (less than or equal)
137
+
138
+ The value after the operator doesn't need to be quoted, anything after the operator will be compared to the value of the key.
139
+
140
+ Logic can be used on multiple lines like the example above, or on a single line (useful for filenames):
141
+
142
+ ```
143
+ %%project%%.%%if language == javascript%%js%%else if language == ruby%%rb%%else%%sh%%endif%%
144
+ ```
145
+
146
+ Content within if/else blocks can contain variables.
147
+
66
148
 
67
149
  ### File-specific handling
68
150
 
@@ -74,7 +156,7 @@ files:
74
156
  "%%title%%.md": overwrite
75
157
  ```
76
158
 
77
- Filenames can include wildcards (`*`, `?`), and Bash-style globbing (`[0-9]`, `[a-z]`, `{one,two,three}`).
159
+ Filenames can include wildcards (`*`, `?`), and Bash-ish globbing (`[0-9]`, `[a-z]`, `{one,two,three}`).
78
160
 
79
161
  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
162
 
@@ -117,7 +199,7 @@ If `preserve_tags` is set to `true` in the config (either base or template), the
117
199
  The executable for Planter is `plant`. You can run `plant TEMPLATE` in any directory and TEMPLATE will be planted in the current directory. You can also use `--in PATH` to plant in another directory.
118
200
 
119
201
  ```
120
- Usage: planter [options] TEMPLATE
202
+ Usage: plant [options] TEMPLATE
121
203
  --defaults Accept default values for all variables
122
204
  -i, --in TARGET Plant in TARGET instead of current directory
123
205
  -o, --overwrite Overwrite existing files
data/bin/plant CHANGED
@@ -26,7 +26,7 @@ Planter.base_dir = ENV['PLANTER_DIR'] || File.expand_path('~/.config/planter')
26
26
  Planter::Color.coloring = $stdout.isatty
27
27
 
28
28
  opts = OptionParser.new
29
- opts.banner = 'Usage: planter [options] TEMPLATE'
29
+ opts.banner = 'Usage: plant [options] TEMPLATE'
30
30
 
31
31
  Planter.accept_defaults = false
32
32
  opts.on('--defaults', 'Accept default values for all variables') do
@@ -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.4'
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
@@ -13,10 +13,6 @@ Plant a directory structure and files using templates.
13
13
 
14
14
  If you run into errors, try `gem install --user-install planter-cli`, or as a last ditch effort, `sudo gem install planter-cli`.
15
15
 
16
- ### Optional
17
-
18
- If [Gum](https://github.com/charmbracelet/gum) is available it will be used for command line input.
19
-
20
16
  ## Configuration
21
17
 
22
18
  Planter's base configuration is in `~/.config/planter/planter.yml`. This file can contain any of the keys used in templates (see below) and will serve as a base configuration for all templates. Any key defined in this file will be overridden if it exists in a template.
@@ -42,7 +38,7 @@ Scripts can be executable files in any language, and receive the template direct
42
38
 
43
39
  Templates are directories found in `~/.config/planter/templates/[TEMPLATE_NAME]`. All files and directories inside of these template directories are copied when that template is called. Filenames, directory names, and file contents can all use template placeholders.
44
40
 
45
- Template placeholders are defined with `%%KEY%%`, where key is the key defined in the `variables` section of the configuration. %%KEY%% placeholders can be used in directory/file names, and in the file contents. These work in any plain text or RTF format document, including XML, so they can be used in things like Scrivener templates and MindNode files as well.
41
+ Template placeholders are defined with `%%KEY%%`, where key is the key defined in the `variables` section of the configuration. `%%KEY%%` placeholders can be used in directory/file names, and in the file contents. These work in any plain text or RTF format document, including XML, so they can be used in things like Scrivener templates and MindNode files as well.
46
42
 
47
43
  Each template contains a `_planter.yml` file that defines variables and other configuration options. The file format for all configuration files is [YAML](https://yaml.org/spec/1.2.2/).
48
44
 
@@ -52,22 +48,108 @@ First, there's a `variables` section that defines variables used in the template
52
48
  variables:
53
49
  - key: var_key
54
50
  prompt: Prompt text
55
- type: string # [string,multiline,float,integer,number,date] defaults to string
51
+ type: string # [string,paragraph,float,integer,number,date,choice] defaults to string
56
52
  # value: (force value, string can include %%variables%% and regexes will be replaced. For date type can be today, time, now, etc.)
57
53
  default: Untitled
58
54
  min: 1
59
55
  max: 5
56
+ ```
57
+
58
+ A configuration can include additional keys:
59
+
60
+ ```yaml
60
61
  script: # array of scripts, args passed as [script and args] TEMPLATE_DIR PWD
61
62
  - process.py
62
63
  git_init: false # if true, initialize a git repository in the newly-planted directory
63
64
  files: # Dictionary for file handling (see [File-specific handling](#file-specific-handling))
64
65
  replacements: # Dictionary of pattern/replacments for regex substitution, see [Regex replacements](#regex-replacements)
65
- repo: # If a repository URL is provided, it will be pulled and duplicated instead of copying a file structure
66
+ repo: # If a repository URL is provided, it will be pulled and duplicated instead of copying an existing file structure
66
67
  ```
67
68
 
68
69
  #### Default values in template strings
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
+ 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 (or doesn't exist). 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%%`.
72
+
73
+ #### Multiple choice type
74
+
75
+ 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).
76
+
77
+ If a Hash/Dictionary is defined, each choice can have a result string:
78
+
79
+ ```yaml
80
+ variables:
81
+ - key: shebang
82
+ prompt: Shebang line
83
+ type: choice
84
+ default: r
85
+ choices:
86
+ (r)uby: "#! /usr/bin/env ruby"
87
+ (j)avascript: "#! /usr/bin/env node"
88
+ (p)ython: "#! /usr/bin/env python"
89
+ (b)ash: "#! /bin/bash"
90
+ (z)sh: "#! /bin/zsh"
91
+ ```
92
+
93
+ When a choice is selected from a dictionary, the result string will be inserted instead of the choice title.
94
+
95
+ If an array is defined, the string of the choice will also be its result (minus any parenthesis):
96
+
97
+ ```yaml
98
+ variables:
99
+ - key: language
100
+ prompt: Programming language
101
+ type: choice
102
+ default: 1
103
+ choices:
104
+ - 1. ruby
105
+ - 1. javascript
106
+ - 1. python
107
+ - 1. bash
108
+ - 1. zsh
109
+ ```
110
+
111
+ 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 (in the `defaults:` key) for its placement in the list (starting with 1), and parenthesis aren't required.
112
+
113
+ #### If/then logic
114
+
115
+ A template can use if/then logic, which is useful with multiple choice types. It can be applied to any type, though.
116
+
117
+ The format for if/then logic is:
118
+
119
+ ```
120
+ %%if KEY OPERATOR VALUE%%
121
+ content
122
+ %%else if KEY OPERATOR VALUE2%%
123
+ content 2
124
+ %%else%%
125
+ content 3
126
+ %%endif%%
127
+ ```
128
+
129
+ 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.
130
+
131
+ The key should be an existing key defined in `variables`. The operator can be any of:
132
+
133
+ - `==` or `=` (equals)
134
+ - `=~` (matches regex)
135
+ - `*=` (contains)
136
+ - `^=` (starts with)
137
+ - `$=` (ends with)
138
+ - `>` (greater than)
139
+ - `>=` (greater than or equal)
140
+ - `<` (less than)
141
+ - `<=` (less than or equal)
142
+
143
+ The value after the operator doesn't need to be quoted, anything after the operator will be compared to the value of the key.
144
+
145
+ Logic can be used on multiple lines like the example above, or on a single line (useful for filenames):
146
+
147
+ ```
148
+ %%project%%.%%if language == javascript%%js%%else if language == ruby%%rb%%else%%sh%%endif%%
149
+ ```
150
+
151
+ Content within if/else blocks can contain variables.
152
+
71
153
 
72
154
  ### File-specific handling
73
155
 
@@ -79,7 +161,7 @@ files:
79
161
  "%%title%%.md": overwrite
80
162
  ```
81
163
 
82
- Filenames can include wildcards (`*`, `?`), and Bash-style globbing (`[0-9]`, `[a-z]`, `{one,two,three}`).
164
+ Filenames can include wildcards (`*`, `?`), and Bash-ish globbing (`[0-9]`, `[a-z]`, `{one,two,three}`).
83
165
 
84
166
  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
167
 
@@ -122,7 +204,7 @@ If `preserve_tags` is set to `true` in the config (either base or template), the
122
204
  The executable for Planter is `plant`. You can run `plant TEMPLATE` in any directory and TEMPLATE will be planted in the current directory. You can also use `--in PATH` to plant in another directory.
123
205
 
124
206
  ```
125
- Usage: planter [options] TEMPLATE
207
+ Usage: plant [options] TEMPLATE
126
208
  --defaults Accept default values for all variables
127
209
  -i, --in TARGET Plant in TARGET instead of current directory
128
210
  -o, --overwrite Overwrite existing files
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.4
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