planter-cli 0.0.3 → 3.0.1

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