planter-cli 0.0.3 → 3.0.1

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rubocop.yml +5 -7
  4. data/CHANGELOG.md +21 -0
  5. data/README.md +28 -1
  6. data/Rakefile +54 -18
  7. data/bin/plant +6 -0
  8. data/docker/Dockerfile-2.6 +5 -5
  9. data/docker/Dockerfile-2.7 +3 -3
  10. data/docker/Dockerfile-3.0 +3 -3
  11. data/lib/planter/array.rb +51 -0
  12. data/lib/planter/color.rb +1 -1
  13. data/lib/planter/errors.rb +14 -0
  14. data/lib/planter/file.rb +87 -4
  15. data/lib/planter/fileentry.rb +5 -1
  16. data/lib/planter/filelist.rb +43 -7
  17. data/lib/planter/hash.rb +81 -84
  18. data/lib/planter/plant.rb +4 -10
  19. data/lib/planter/prompt.rb +6 -3
  20. data/lib/planter/script.rb +24 -12
  21. data/lib/planter/string.rb +134 -29
  22. data/lib/planter/tag.rb +54 -0
  23. data/lib/planter/version.rb +1 -1
  24. data/lib/planter.rb +60 -34
  25. data/planter-cli.gemspec +1 -0
  26. data/spec/config.yml +2 -0
  27. data/spec/planter/array_spec.rb +28 -0
  28. data/spec/planter/file_entry_spec.rb +40 -0
  29. data/spec/planter/file_spec.rb +19 -0
  30. data/spec/planter/filelist_spec.rb +15 -0
  31. data/spec/planter/hash_spec.rb +110 -0
  32. data/spec/planter/plant_spec.rb +1 -0
  33. data/spec/planter/script_spec.rb +80 -0
  34. data/spec/planter/string_spec.rb +215 -2
  35. data/spec/planter/symbol_spec.rb +23 -0
  36. data/spec/planter.yml +6 -0
  37. data/spec/planter_spec.rb +82 -0
  38. data/spec/scripts/test.sh +3 -0
  39. data/spec/scripts/test_fail.sh +3 -0
  40. data/spec/spec_helper.rb +8 -2
  41. data/spec/templates/test/%%project:snake%%.rtf +10 -0
  42. data/spec/templates/test/Rakefile +6 -0
  43. data/spec/templates/test/_planter.yml +12 -0
  44. data/spec/templates/test/_scripts/test.sh +3 -0
  45. data/spec/templates/test/_scripts/test_fail.sh +3 -0
  46. data/spec/templates/test/test.rb +5 -0
  47. data/spec/test_out/image.png +0 -0
  48. data/spec/test_out/test2.rb +5 -0
  49. data/src/_README.md +28 -1
  50. metadata +57 -2
data/lib/planter/hash.rb CHANGED
@@ -1,103 +1,100 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Hash helpers
4
- class ::Hash
5
- ## Turn all keys into string
6
- ##
7
- ## @return [Hash] copy of the hash where all its keys are strings
8
- ##
9
- def stringify_keys
10
- each_with_object({}) do |(k, v), hsh|
11
- hsh[k.to_s] = if v.is_a?(Hash)
12
- v.stringify_keys
13
- elsif v.is_a?(Array)
14
- v.map(&:symbolize_keys)
15
- else
16
- v
17
- end
18
- end
19
- end
20
-
21
- ##
22
- ## Turn all keys into symbols
23
- ##
24
- ## @return [Hash] hash with symbolized keys
25
- ##
26
- def symbolize_keys
27
- each_with_object({}) do |(k, v), hsh|
28
- hsh[k.to_sym] = if v.is_a?(Hash)
29
- v.symbolize_keys
30
- elsif v.is_a?(Array)
31
- v.map(&:symbolize_keys)
3
+ module Planter
4
+ ## Hash helpers
5
+ class ::Hash
6
+ ## Turn all keys into string
7
+ ##
8
+ ## @return [Hash] copy of the hash where all its keys are strings
9
+ ##
10
+ def stringify_keys
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_keys
32
14
  else
33
15
  v
34
16
  end
17
+ end
18
+ end
19
+
20
+ ## Destructive version of #stringify_keys
21
+ ##
22
+ ## @return [Hash] Hash with stringified keys
23
+ ##
24
+ def stringify_keys!
25
+ replace stringify_keys
35
26
  end
36
- end
37
27
 
38
- ##
39
- ## Deep merge a hash
40
- ##
41
- ## @param second [Hash] The hash to merge into self
42
- ##
43
- def deep_merge(second)
44
- merger = proc do |_, v1, v2|
45
- if v1.is_a?(Hash) && v2.is_a?(Hash)
46
- v1.merge(v2, &merger)
47
- elsif v1.is_a?(Array) && v2.is_a?(Array)
48
- v1 | v2
49
- elsif [:undefined, nil, :nil].include?(v2)
50
- v1
51
- else
52
- v2
28
+ ##
29
+ ## Turn all keys into symbols
30
+ ##
31
+ ## @return [Hash] hash with symbolized keys
32
+ ##
33
+ def symbolize_keys
34
+ each_with_object({}) do |(k, v), hsh|
35
+ hsh[k.to_sym] = if v.is_a?(Hash) || v.is_a?(Array)
36
+ v.symbolize_keys
37
+ else
38
+ v
39
+ end
53
40
  end
54
41
  end
55
- merge(second.to_h, &merger)
56
- end
57
42
 
58
- ##
59
- ## Freeze all values in a hash
60
- ##
61
- ## @return [Hash] Hash with all values frozen
62
- ##
63
- def deep_freeze
64
- chilled = {}
65
- each do |k, v|
66
- chilled[k] = v.is_a?(Hash) ? v.deep_freeze : v.freeze
43
+ ##
44
+ ## Destructive version of #symbolize_keys
45
+ ##
46
+ ## @return [Hash] Hash with symbolized keys
47
+ ##
48
+ def symbolize_keys!
49
+ replace symbolize_keys
67
50
  end
68
51
 
69
- chilled.freeze
70
- end
52
+ ##
53
+ ## Deep merge a hash
54
+ ##
55
+ ## @param second [Hash] The hash to merge into self
56
+ ##
57
+ def deep_merge(second)
58
+ merger = proc do |_, v1, v2|
59
+ if v1.is_a?(Hash) && v2.is_a?(Hash)
60
+ v1.merge(v2, &merger)
61
+ elsif v1.is_a?(Array) && v2.is_a?(Array)
62
+ v1 | v2
63
+ elsif [:undefined, nil, :nil].include?(v2)
64
+ v1
65
+ else
66
+ v2
67
+ end
68
+ end
69
+ merge(second.to_h, &merger)
70
+ end
71
71
 
72
- ##
73
- ## Destructive version of #deep_freeze
74
- ##
75
- ## @return [Hash] Hash with all values frozen
76
- ##
77
- def deep_freeze!
78
- replace deep_thaw.deep_freeze
79
- end
72
+ ##
73
+ ## Freeze all values in a hash
74
+ ##
75
+ ## @return [Hash] Hash with all values frozen
76
+ ##
77
+ def deep_freeze
78
+ chilled = {}
79
+ each do |k, v|
80
+ chilled[k] = v.is_a?(Hash) ? v.deep_freeze : v.freeze
81
+ end
80
82
 
81
- ##
82
- ## Unfreeze a hash and all nested values
83
- ##
84
- ## @return [Hash] unfrozen hash
85
- ##
86
- def deep_thaw
87
- chilled = {}
88
- each do |k, v|
89
- chilled[k] = v.is_a?(Hash) ? v.deep_thaw : v.dup
83
+ chilled.freeze
90
84
  end
91
85
 
92
- chilled.dup
93
- end
86
+ ##
87
+ ## Unfreeze a hash and all nested values
88
+ ##
89
+ ## @return [Hash] unfrozen hash
90
+ ##
91
+ def deep_thaw
92
+ chilled = {}
93
+ each do |k, v|
94
+ chilled[k] = v.is_a?(Hash) ? v.deep_thaw : v.dup
95
+ end
94
96
 
95
- ##
96
- ## Destructive version of #deep_thaw
97
- ##
98
- ## @return [Hash] unfrozen hash
99
- ##
100
- def deep_thaw!
101
- replace deep_thaw
97
+ chilled.dup
98
+ end
102
99
  end
103
100
  end
data/lib/planter/plant.rb CHANGED
@@ -13,7 +13,7 @@ module Planter
13
13
  Planter.variables = variables if variables.is_a?(Hash)
14
14
  Planter.config = template if template
15
15
 
16
- @basedir = File.join(Planter::BASE_DIR, 'templates', Planter.template)
16
+ @basedir = File.join(Planter.base_dir, 'templates', Planter.template)
17
17
  @target = Planter.target || Dir.pwd
18
18
 
19
19
  @git = Planter.config[:git_init] || false
@@ -68,6 +68,8 @@ module Planter
68
68
  ## @example Pass a GitHub-style repo path and get full url
69
69
  ## expand_repo("ttscoff/planter-cli") #=> https://github.com/ttscoff/planter-cli.git
70
70
  ##
71
+ ## @param repo [String] The repo
72
+ ##
71
73
  ## @return { description_of_the_return_value }
72
74
  ##
73
75
  def expand_repo(repo)
@@ -177,15 +179,7 @@ module Planter
177
179
  files = Dir.glob('**/*', File::FNM_DOTMATCH).reject { |f| File.directory?(f) || f =~ /^(\.git|config\.yml)/ }
178
180
 
179
181
  files.each do |file|
180
- type = `file #{file}`
181
- case type.sub(/^#{Regexp.escape(file)}: /, '').split(/:/).first
182
- when /Apple binary property list/
183
- `plutil -convert xml1 #{file}`
184
- when /data/
185
- next
186
- else
187
- next if File.binary?(file)
188
- end
182
+ next if File.binary?(file)
189
183
 
190
184
  content = IO.read(file)
191
185
  new_content = content.apply_variables.apply_regexes
@@ -34,7 +34,7 @@ module Planter
34
34
  def ask
35
35
  return nil if @prompt.nil?
36
36
 
37
- return @value.to_s.apply_variables.apply_regexes.coerce(@type) if @value
37
+ return @value.to_s.apply_variables.apply_regexes.coerce(@type) if @value && @type != :date
38
38
 
39
39
  res = case @type
40
40
  when :integer
@@ -133,7 +133,7 @@ module Planter
133
133
  ## @return [String] the single-line response
134
134
  ##
135
135
  def read_line(prompt: nil)
136
- prompt ||= read_lines @prompt
136
+ prompt ||= @prompt
137
137
  default = @default ? " {bw}[#{@default}]" : ''
138
138
  Planter.notify("{by}#{prompt}#{default}")
139
139
 
@@ -249,7 +249,7 @@ module Planter
249
249
  return default unless $stdout.isatty
250
250
 
251
251
  # If --defaults is set, return default
252
- return default if Planter.accept_defaults
252
+ return default if Planter.accept_defaults || ENV['PLANTER_DEBUG']
253
253
 
254
254
  # clear the buffer
255
255
  if ARGV&.length
@@ -316,6 +316,9 @@ module Planter
316
316
  default_response
317
317
  end
318
318
 
319
+ # if PLANTER_DEBUG is set, answer default
320
+ return true if ENV['PLANTER_DEBUG']
321
+
319
322
  # if this isn't an interactive shell, answer default
320
323
  return default unless $stdout.isatty
321
324
 
@@ -13,15 +13,24 @@ module Planter
13
13
  ## @param script [String] The script name
14
14
  ##
15
15
  def initialize(template_dir, output_dir, script)
16
- found = find_script(template_dir, output_dir, script)
17
- Planter.notify("Script #{script} not found", :error, exit_code: 10) unless found
16
+ found = find_script(template_dir, script)
17
+ raise ScriptError.new("Script #{script} not found") unless found
18
+
18
19
  @script = found
20
+ make_executable
21
+
22
+ raise ScriptError.new("Output directory #{output_dir} not found") unless File.directory?(output_dir)
19
23
 
20
- Planter.notify("Directory #{output_dir} not found", :error, exit_code: 10) unless File.directory?(output_dir)
21
24
  @template_directory = template_dir
22
25
  @directory = output_dir
23
26
  end
24
27
 
28
+ ## Make a script executable if it's not already
29
+ def make_executable
30
+ File.chmod(0o755, @script) unless File.executable?(@script)
31
+ File.executable?(@script)
32
+ end
33
+
25
34
  ##
26
35
  ## Locate a script in either the base directory or template directory
27
36
  ##
@@ -31,13 +40,15 @@ module Planter
31
40
  ## @return [String] Path to script
32
41
  ##
33
42
  def find_script(template_dir, script)
34
- parts = Shellwords.split(script).first
35
- return script if File.exist?(parts[0])
43
+ parts = Shellwords.split(script)
44
+ script_name = parts[0]
45
+ args = parts[1..-1].join(' ')
46
+ return script if File.exist?(script_name)
36
47
 
37
- if File.exist?(File.join(template_dir, '_scripts', parts[0]))
38
- return "#{File.join(template_dir, '_scripts', parts[0])} #{parts[1..-1]}"
39
- elsif File.exist?(File.join(BASE_DIR, 'scripts', parts[0]))
40
- return "#{File.join(BASE_DIR, 'scripts', parts[0])} #{parts[1..-1]}"
48
+ if File.exist?(File.join(template_dir, '_scripts', script_name))
49
+ return "#{File.join(template_dir, '_scripts', script_name)} #{args}".strip
50
+ elsif File.exist?(File.join(Planter.base_dir, 'scripts', script_name))
51
+ return "#{File.join(Planter.base_dir, 'scripts', script_name)} #{args}".strip
41
52
  end
42
53
 
43
54
  nil
@@ -49,9 +60,10 @@ module Planter
49
60
  ## @return [Boolean] true if success?
50
61
  ##
51
62
  def run
52
- `#{@script} "#{@template_directory}" "#{@directory}"`
53
-
54
- Planter.notify("Error running #{File.basename(@script)}", :error, exit_code: 128) unless $?.success?
63
+ stdout, stderr, status = Open3.capture3(@script, @template_directory, @directory)
64
+ Planter.notify("STDOUT:\n#{stdout}", :debug) unless stdout.empty?
65
+ Planter.notify("STDERR:\n#{stderr}", :debug) unless stderr.empty?
66
+ raise ScriptError.new("Error running #{@script}") unless status.success?
55
67
 
56
68
  true
57
69
  end
@@ -23,7 +23,7 @@ module Planter
23
23
  ## @return Class name representation of the object.
24
24
  ##
25
25
  def to_class_name
26
- strip.no_ext.split(/[-_ ]/).map(&:capitalize).join('').gsub(/[^a-z0-9]/i, '')
26
+ strip.no_ext.title_case.gsub(/[^a-z0-9]/i, '').sub(/^\S/, &:upcase)
27
27
  end
28
28
 
29
29
  ##
@@ -66,7 +66,7 @@ module Planter
66
66
  ## @return [String] Snake-cased version of string
67
67
  ##
68
68
  def snake_case
69
- strip.gsub(/\S[A-Z]/) { |pair| pair.split('').join('_') }
69
+ strip.gsub(/\S(?=[A-Z])/, '\0_')
70
70
  .gsub(/[ -]+/, '_')
71
71
  .gsub(/[^a-z0-9_]+/i, '')
72
72
  .gsub(/_+/, '_')
@@ -82,7 +82,7 @@ module Planter
82
82
  ## @return [String] Snake-cased version of string
83
83
  ##
84
84
  def camel_case
85
- strip.gsub(/[ _]+(\S)/) { Regexp.last_match(1).upcase }
85
+ strip.gsub(/(?<=[^a-z0-9])(\S)/) { Regexp.last_match(1).upcase }
86
86
  .gsub(/[^a-z0-9]+/i, '')
87
87
  .sub(/^(\w)/) { Regexp.last_match(1).downcase }
88
88
  end
@@ -96,20 +96,11 @@ module Planter
96
96
  ## @return [String] title cased string
97
97
  ##
98
98
  def title_case
99
- gsub(/\b(\w)/) { Regexp.last_match(1).upcase }
99
+ split(/\b(\w+)/).map(&:capitalize).join('')
100
100
  end
101
101
 
102
- ##
103
- ## Apply key/value substitutions to a string. Variables are represented as
104
- ## %%key%%, and the hash passed to the function is { key: value }
105
- ##
106
- ## @param last_only [Boolean] Only replace the last instance of %%key%%
107
- ##
108
- ## @return [String] string with variables substituted
109
- ##
110
- def apply_variables(last_only: false)
111
- content = dup.clean_encode
112
- mod_rx = '(?<mod>
102
+ # @return [String] Regular expression for matching variable modifiers
103
+ MOD_RX = '(?<mod>
113
104
  (?::
114
105
  (
115
106
  l(?:ow(?:er)?)?)?|
@@ -121,11 +112,75 @@ module Planter
121
112
  )?
122
113
  )*
123
114
  )'
115
+ # @return [String] regular expression string for default values
116
+ DEFAULT_RX = '(?:%(?<default>[^%]+))?'
117
+
118
+ #
119
+ # Apply default values to a string
120
+ #
121
+ # Default values are applied to variables that are not present in the variables hash,
122
+ # or whose value matches the default value
123
+ #
124
+ # @param variables [Hash] Hash of variable values
125
+ #
126
+ # @return [String] string with default values applied
127
+ #
128
+ def apply_defaults(variables)
129
+ # Perform an in-place substitution on the content string for default values
130
+ gsub(/%%(?<varname>[^%:]+)(?<mods>(?::[^%]+)*)%(?<default>[^%]+)%%/) do
131
+ # Capture the last match object
132
+ m = Regexp.last_match
133
+
134
+ # Check if the variable is not present in the variables hash
135
+ if !variables.key?(m['varname'].to_var)
136
+ # If the variable is not present, use the default value from the match
137
+ m['default'].apply_var_names
138
+ else
139
+ # Retrieve the default value for the variable from the configuration
140
+ vars = Planter.config[:variables].filter { |v| v[:key] == m['varname'] }
141
+ default = vars.first[:default] if vars.count.positive?
142
+ if default.nil?
143
+ m[0]
144
+ elsif variables[m['varname'].to_var] == default
145
+ # If the variable's value matches the default value, use the default value from the match
146
+ m['default'].apply_var_names
147
+ else
148
+ m[0]
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ #
155
+ # Destructive version of #apply_defaults
156
+ #
157
+ # @param variables [Hash] hash of variables to apply
158
+ #
159
+ # @return [String] string with defaults applied
160
+ #
161
+ def apply_defaults!(variables)
162
+ replace apply_defaults(variables)
163
+ end
124
164
 
125
- Planter.variables.each do |k, v|
165
+ ##
166
+ ## Apply key/value substitutions to a string. Variables are represented as
167
+ ## %%key%%, and the hash passed to the function is { key: value }
168
+ ##
169
+ ## @param last_only [Boolean] Only replace the last instance of %%key%%
170
+ ##
171
+ ## @return [String] string with variables substituted
172
+ ##
173
+ def apply_variables(variables: nil, last_only: false)
174
+ variables = variables.nil? ? Planter.variables : variables
175
+
176
+ content = dup.clean_encode
177
+
178
+ content = content.apply_defaults(variables)
179
+
180
+ variables.each do |k, v|
126
181
  if last_only
127
182
  pattern = "%%#{k.to_var}"
128
- content = content.reverse.sub(/(?mix)%%(?:(?<mod>.*?):)*(?<key>#{pattern.reverse})/) do
183
+ content = content.reverse.sub(/(?mix)%%(?:(?<mod>.*?):)*(?<key>#{pattern.reverse})/i) do
129
184
  m = Regexp.last_match
130
185
  if m['mod']
131
186
  m['mod'].reverse.split(/:/).each do |mod|
@@ -136,14 +191,16 @@ module Planter
136
191
  v.reverse
137
192
  end.reverse
138
193
  else
139
- rx = /(?mix)%%(?<key>#{k.to_var})#{mod_rx}%%/
194
+ rx = /(?mix)%%(?<key>#{k.to_var})#{MOD_RX}#{DEFAULT_RX}%%/
140
195
 
141
196
  content.gsub!(rx) do
142
197
  m = Regexp.last_match
143
198
 
144
- mods = m['mod']&.split(/:/)
145
- mods&.each do |mod|
146
- v = v.apply_mod(mod.normalize_mod)
199
+ if m['mod']
200
+ mods = m['mod']&.split(/:/)
201
+ mods&.each do |mod|
202
+ v = v.apply_mod(mod.normalize_mod)
203
+ end
147
204
  end
148
205
  v
149
206
  end
@@ -153,16 +210,49 @@ module Planter
153
210
  content
154
211
  end
155
212
 
213
+ #
214
+ # Handle $varname and ${varname} variable substitutions
215
+ #
216
+ # @return [String] String with variables substituted
217
+ #
218
+ def apply_var_names
219
+ sub(/\$\{?(?<varname>\w+)(?<mods>(?::\w+)+)?\}?/) do
220
+ m = Regexp.last_match
221
+ if Planter.variables.key?(m['varname'].to_var)
222
+ Planter.variables[m['varname'].to_var].apply_mods(m['mods'])
223
+ else
224
+ m
225
+ end
226
+ end
227
+ end
228
+
229
+ #
230
+ # Apply modifiers to a string
231
+ #
232
+ # @param mods [String] Colon separated list of modifiers to apply
233
+ #
234
+ # @return [String] string with modifiers applied
235
+ #
236
+ def apply_mods(mods)
237
+ content = dup
238
+ mods.split(/:/).each do |mod|
239
+ content.apply_mod!(mod.normalize_mod)
240
+ end
241
+ content
242
+ end
243
+
156
244
  ##
157
245
  ## Apply regex replacements from @config[:replacements]
158
246
  ##
159
247
  ## @return [String] string with regexes applied
160
248
  ##
161
- def apply_regexes
249
+ def apply_regexes(regexes = nil)
162
250
  content = dup.clean_encode
163
- return self unless Planter.config.key?(:replacements)
251
+ regexes = regexes.nil? && Planter.config.key?(:replacements) ? Planter.config[:replacements] : regexes
164
252
 
165
- Planter.config[:replacements].stringify_keys.each do |pattern, replacement|
253
+ return self unless regexes
254
+
255
+ regexes.stringify_keys.each do |pattern, replacement|
166
256
  pattern = Regexp.new(pattern) unless pattern.is_a?(Regexp)
167
257
  replacement = replacement.gsub(/\$(\d)/, '\\\1').apply_variables
168
258
  content.gsub!(pattern, replacement)
@@ -177,8 +267,8 @@ module Planter
177
267
  ##
178
268
  ## @return [String] string with variables substituted
179
269
  ##
180
- def apply_variables!(last_only: false)
181
- replace apply_variables(last_only: last_only)
270
+ def apply_variables!(variables: nil, last_only: false)
271
+ replace apply_variables(variables: variables, last_only: last_only)
182
272
  end
183
273
 
184
274
  ##
@@ -186,8 +276,8 @@ module Planter
186
276
  ##
187
277
  ## @return [String] string with variables substituted
188
278
  ##
189
- def apply_regexes!
190
- replace apply_regexes
279
+ def apply_regexes!(regexes = nil)
280
+ replace apply_regexes(regexes)
191
281
  end
192
282
 
193
283
  ##
@@ -222,6 +312,8 @@ module Planter
222
312
  ##
223
313
  ## @param mod [Symbol] The modifier to apply
224
314
  ##
315
+ ## @return [String] modified string
316
+ ##
225
317
  def apply_mod(mod)
226
318
  case mod
227
319
  when :slug
@@ -241,6 +333,17 @@ module Planter
241
333
  end
242
334
  end
243
335
 
336
+ #
337
+ # Destructive version of #apply_mod
338
+ #
339
+ # @param mod [String] modified string
340
+ #
341
+ # @return [<Type>] <description>
342
+ #
343
+ def apply_mod!(mod)
344
+ replace apply_mod(mod)
345
+ end
346
+
244
347
  ##
245
348
  ## Convert mod string to symbol
246
349
  ##
@@ -334,9 +437,11 @@ module Planter
334
437
  ##
335
438
  ##
336
439
  def coerce(type)
440
+ type = type.normalize_type
441
+
337
442
  case type
338
443
  when :date
339
- Chronic.parse(self)
444
+ Chronic.parse(self).strftime('%Y-%m-%d %H:%M')
340
445
  when :integer || :number
341
446
  to_i
342
447
  when :float
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Planter
4
+ module Tag
5
+ class << self
6
+ def set(target, tags)
7
+ tags = [tags] unless tags.is_a?(Array)
8
+
9
+ set_tags(target, tags)
10
+ $? == 0
11
+ end
12
+
13
+ # Add tags to a directory.
14
+ #
15
+ # @param dir [String] The directory to tag.
16
+ # @param tags [Array<String>] The tags to add.
17
+ def add(target, tags)
18
+ tags = [tags] unless tags.is_a?(Array)
19
+ existing_tags = get(target)
20
+ tags.concat(existing_tags).uniq!
21
+
22
+ set_tags(target, tags)
23
+ $? == 0
24
+ end
25
+
26
+ def get(target)
27
+ res = `xattr -p com.apple.metadata:_kMDItemUserTags "#{target}" 2>/dev/null`.clean_encode
28
+ return [] if res =~ /no such xattr/ || res.empty?
29
+
30
+ tags = Plist.parse_xml(res)
31
+
32
+ return false if tags.nil?
33
+
34
+ tags
35
+ end
36
+
37
+ def copy(source, target)
38
+ tags = `xattr -px com.apple.metadata:_kMDItemUserTags "#{source}" 2>/dev/null`
39
+ `xattr -wx com.apple.metadata:_kMDItemUserTags "#{tags}" "#{target}"`
40
+ $? == 0
41
+ end
42
+
43
+ private
44
+
45
+ def set_tags(target, tags)
46
+ tags.map! { |tag| "<string>#{tag}</string>" }
47
+ `xattr -w com.apple.metadata:_kMDItemUserTags '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
48
+ <plist version="1.0">
49
+ <array>#{tags.join}</array>
50
+ </plist>' "#{target}"`
51
+ end
52
+ end
53
+ end
54
+ end
@@ -3,5 +3,5 @@
3
3
  # Primary module for this gem.
4
4
  module Planter
5
5
  # Current Planter version.
6
- VERSION = '0.0.3'
6
+ VERSION = '3.0.1'
7
7
  end