jtag 0.2.0 → 1.0.0

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.
data/bin/jtag CHANGED
@@ -10,123 +10,85 @@ program_desc "Autotagging for Jekyll"
10
10
 
11
11
  version Jtag::VERSION
12
12
 
13
- @config_files = %w{blacklist.txt config.yml stopwords.txt synonyms.yml}
14
- @config_target = File.expand_path("~/.config/jtag")
15
13
  @piped_content = nil
16
14
 
17
- def config_files_complete?
18
- @config_files.each do |file|
19
- return false unless File.exist?(File.join(@config_target, file))
20
- end
21
- true
22
- end
15
+ ##
16
+ ## Define global flags and switches
17
+ ##
18
+ desc "Expect input from STDIN to be null (0)-delimited: Useful for piping file lists from `find` or `grep`"
19
+ default_value false
20
+ switch %i[0 null], negatable: false
23
21
 
24
- class ::String
25
- def file_list?
26
- self.strip.split("\n").each do |line|
27
- return false unless File.exist?(line)
28
- end
29
- true
30
- end
31
- end
22
+ desc "Path to alternate configuration directory"
23
+ arg_name "config_dir"
24
+ flag %i[c config_dir], :type => String
32
25
 
33
- desc "Debug level"
34
- default_value "0"
26
+ desc "Debug level: Set the debug level for the application. 0 is silent, 1 is errors only, 2 is errors and warnings, 3 is errors, warnings and info"
35
27
  arg_name "debug_level"
36
- flag %i[d debug], :must_match => /\d+/, :type => Integer, :default_value => 0
28
+ flag %i[d debug], :must_match => /\d+/, :type => Integer, :default_value => 3
37
29
 
38
- desc "Run silently"
39
- switch %i[s silent]
30
+ desc "Perform case-insensitive matches and searches: Useful for case-insensitive filesystems or when you're not sure of the case of your tags"
31
+ switch %i[i case_insensitive], negatable: false
40
32
 
41
- desc "Perform case-insensitive matches and searches"
42
- switch %i[i case_insensitive]
33
+ desc "Run silently: Suppress all output to STDOUT, same as debug level 0"
34
+ switch %i[s silent], negatable: false
43
35
 
44
- desc "Test (dry run, don't update files)"
45
- long_desc "Run all commands and show results on the command line, but don't overwrite/update any files"
36
+ desc "Test (dry run, don't update files): Run all commands and show results on the command line, but don't overwrite/update any files"
46
37
  default_value false
47
- switch %i[t test]
48
-
49
- def console_log(msg = "", options = {})
50
- err = options[:err] || false
51
- options[:log] ||= false
52
-
53
- if options[:log]
54
- if err
55
- @log.warn(msg)
56
- else
57
- @log.info(msg)
58
- end
59
- end
60
-
61
- return if @silent
62
-
63
- unless err
64
- $stdout.puts msg
65
- else
66
- warn msg
67
- end
68
- end
69
-
70
- def migrate_old_config
71
- old_config = File.expand_path("~/.jtag")
72
- new_config = File.expand_path("~/.config/jtag")
73
-
74
- if File.directory?(old_config) && !File.directory?(new_config)
75
- # Create parent directory if needed
76
- FileUtils.mkdir_p(File.dirname(new_config))
77
-
78
- # Move the directory
79
- FileUtils.mv(old_config, new_config)
80
-
81
- # ANSI color codes: yellow=\033[0;33m, boldyellow=\033[1;33m, default=\033[0;39m
82
- console_log "\033[0;33mConfiguration directory has been moved from \033[1;33m~/.jtag\033[0;33m to \033[1;33m~/.config/jtag\033[0;33m.\033[0;39m", :err => true
83
- console_log "\033[0;33mYour existing configuration and files have been preserved.\033[0;39m", :err => true
84
- end
85
- end
38
+ switch %i[t test], negatable: false
86
39
 
40
+ # Add a command to update and notify the user of the configuration files location
87
41
  desc "Update and notify user of configuration files location"
88
42
  command :config do |c|
43
+ # Add a switch to reset all configuration files to default values
89
44
  c.desc "Reset all configuration files to default values"
90
- c.switch [:r, "reset"]
45
+ c.switch %i[r reset]
46
+
47
+ c.desc "Ignore prompts and answer yes to all questions."
48
+ c.switch %i[y yes], negatable: false
49
+
50
+ c.desc "Set a specific configuration value"
51
+ c.arg_name "key"
52
+ c.flag %i[s set]
91
53
 
54
+ c.desc "The value to set for key"
55
+ c.arg_name "value"
56
+ c.flag %i[v value]
57
+
58
+ # Skip the pre block for this command
92
59
  c.skips_pre
60
+
61
+ # Define the action to be taken when the command is executed
93
62
  c.action do |global_options, options, args|
94
- if options[:r]
95
- print "Are you sure you want to reset all config files? y/N: "
96
- response = STDIN.gets.strip
97
- if response =~ /^y/i
98
- write_config(true)
99
- console_log "Config files reset"
63
+ if options[:set]
64
+ config = YAML.load_file(File.join(@util.config_target, "config.yml"))
65
+ config[options[:set]] = options[:value]
66
+ File.open(File.join(@util.config_target, "config.yml"), "w") do |f|
67
+ f.write(config.to_yaml)
100
68
  end
101
- else
102
- write_config(false)
69
+ return
103
70
  end
104
- end
105
- end
106
-
107
- def write_config(atomic = false)
108
- gem_root = Gem.loaded_specs["jtag"].full_gem_path
109
- gem_lib = File.join(gem_root, "lib")
110
- config_source = File.join(gem_lib, "/jtag/config_files")
111
71
 
112
- unless File.directory?(@config_target) || atomic
113
- FileUtils.cp_r(config_source, @config_target)
114
- console_log "Configuration files are located in the folder: " + @config_target
115
- console_log %Q{Make sure that "tags_location" in config.yml is set to your tags.json url.}
116
- console_log "Configuration files written to #{@config_target}"
117
- end
72
+ if options[:reset]
73
+ # Prompt the user for confirmation before resetting config files
74
+ if options[:yes]
75
+ res = true
76
+ else
77
+ print "Are you sure you want to reset all config files? y/N: "
78
+ response = STDIN.gets.strip
79
+ res = response =~ /^y/i
80
+ end
118
81
 
119
- @config_files.each do |file|
120
- unless File.exist?(File.join(@config_target, file))
121
- source_file = File.join(config_source, file)
122
- target_file = File.join(@config_target, file)
123
- FileUtils.cp(source_file, target_file)
124
- console_log "Config file #{file} added."
82
+ if res
83
+ # Reset config files to default values
84
+ @util.write_config(true)
85
+ @util.console_log "Config files reset"
86
+ end
87
+ else
88
+ # Update config files without overwriting existing files
89
+ @util.write_config(false)
125
90
  end
126
91
  end
127
- console_log
128
- console_log "Configuration files are located in the folder: " + @config_target
129
- console_log %Q{Make sure that "tags_location" in the config.yml file is set to your tags json file.}
130
92
  end
131
93
 
132
94
  desc "List tags, optionally filter for keywords/regular expressions (OR)"
@@ -143,32 +105,125 @@ command :search do |c|
143
105
  c.default_value false
144
106
  c.switch %i[c counts]
145
107
 
108
+ c.desc "Boolean operator for multiple tags (AND/OR/NOT)"
109
+ c.arg_name "bool"
110
+ c.default_value "OR"
111
+ c.flag %i[b bool], :must_match => /^([aon])(?:.*)$/i, :type => String
112
+
113
+ c.desc "Match type: exact, fuzzy, starts_with, contains"
114
+ c.arg_name "match_type"
115
+ c.default_value "contains"
116
+ c.flag %i[m match], :must_match => /^([exfsc])(?:.*)$/i, :type => String
117
+
118
+ c.desc "Case-sensitive matching"
119
+ c.switch %i[I s case_sensitive], negatable: true
120
+
121
+ c.desc "Fuzzy match distance"
122
+ c.arg_name "distance"
123
+ c.default_value 2
124
+ c.flag %i[d distance], :must_match => /^\d+$/, :type => Integer
125
+
146
126
  c.action do |global_options, options, args|
147
- tags = @jt.get_tags({ :counts => true })
148
- if args.length > 0
149
- re = args.join("|")
150
- tags.delete_if { |tag|
151
- if tag && tag["name"] =~ /(#{re})/i
152
- false
153
- else
154
- true
127
+ # Initialize arrays to store files and keywords
128
+ files = []
129
+ keywords = []
130
+ bool_op = options[:bool].to_bool
131
+
132
+ # Iterate over the arguments and separate files from keywords
133
+ args.length.times do
134
+ arg = args.pop
135
+ if File.file?(File.expand_path(arg))
136
+ files.push(File.expand_path(arg)) # Add existing files to the files array
137
+ else
138
+ keywords.push(arg) # Add non-file arguments to the keywords array
139
+ end
140
+ end
141
+
142
+ # If piped content is provided and it is a list of files, add them to the files array
143
+ files.concat(@piped_content.split(global_options[:null] ? "\x00" : "\n")) if @piped_content && @piped_content.file_list?(global_options[:null])
144
+
145
+ # If no files are provided, use the default post location
146
+ if files.empty?
147
+ if File.exist?(File.expand_path(@jt.tags_loc))
148
+ files = nil
149
+ else
150
+ if @jt.default_post_location && File.exist?(File.expand_path(@jt.default_post_location))
151
+ # Retrieves a list of files from the default post location with the specified post extension.
152
+ # The files are located using a glob pattern that matches all files with the given extension.
153
+ #
154
+ # @return [Array<String>] an array of file paths matching the specified pattern
155
+ files = Dir.glob(File.join(File.expand_path(@jt.default_post_location), "**", "*.#{@jt.post_extension}"))
155
156
  end
156
- }
157
- if options[:c]
158
- tags.map! { |tag| "#{tag["name"]} (#{tag["count"]})" }
157
+ end
158
+ else
159
+ tags = {}
160
+ end
161
+
162
+ # Get tags with counts for the specified files
163
+ if @piped_content && !@piped_content.file_list?(global_options[:null])
164
+ if @piped_content.yaml?
165
+ tags = YAML.load(@piped_content).to_counts
166
+ elsif @piped_content.json?
167
+ tags = JSON.parse(@piped_content).to_counts
168
+ elsif @piped_content.tag_list?
169
+ tags = @piped_content.split("\n").map(&:strip).to_counts
159
170
  else
160
- tags.map! { |tag| tag["name"] }
171
+ tags = @jt.tags({ :counts => true, :files => files })
161
172
  end
162
- output_tags(tags, { :format => options[:format] })
163
173
  else
164
- tags.delete_if { |tag| !tag }
165
- if options[:c]
166
- tags.map! { |tag| "#{tag["name"]} (#{tag["count"]})" }
174
+ tags = @jt.tags({ :counts => true, :files => files })
175
+ end
176
+
177
+ exit_now! "No source tag list found" if !tags || tags.empty?
178
+
179
+ match_options = {
180
+ exact: false,
181
+ contains: false,
182
+ starts_with: false,
183
+ fuzzy: false,
184
+ case_sensitive: options[:case_sensitive],
185
+ distance: options[:distance],
186
+ }
187
+
188
+ if options[:match] =~ /^[ex]/i
189
+ match_options[:exact] = true
190
+ elsif options[:match] =~ /^s/i
191
+ match_options[:starts_with] = true
192
+ elsif options[:match] =~ /^f/i
193
+ match_options[:fuzzy] = true
194
+ else
195
+ match_options[:contains] = true
196
+ end
197
+
198
+ # If keywords are provided, filter tags based on the keywords
199
+ if keywords.length > 0
200
+ if bool_op == :and
201
+ tags.delete_if { |tag| tag && !keywords.all? { |keyword| tag["name"].match_keyword(keyword, match_options) } }
202
+ elsif bool_op == :not
203
+ re = keywords.join("|") # Join keywords with OR operator
204
+ # Remove tags that match the keywords
205
+ tags.delete_if { |tag| tag && keywords.any? { |keyword| tag["name"].match_keyword(keyword, match_options) } }
167
206
  else
168
- tags.map! { |tag| tag["name"] }
207
+ # :or
208
+ # Filter tags based on any keywords
209
+ tags.delete_if { |tag| tag && !keywords.any? { |keyword| tag["name"].match_keyword(keyword, match_options) } }
169
210
  end
170
- output_tags(tags, { :format => options[:format] })
171
211
  end
212
+
213
+ # Remove nil and false values from the tags array
214
+ tags.delete_if { |tag| !tag }
215
+
216
+ exit_now! "No matching tags found" if tags.nil? || tags.empty?
217
+
218
+ # Format tags with counts if the counts option is enabled
219
+ if options[:count]
220
+ tags.map! { |tag| "#{tag["name"]} (#{tag["count"]})" }
221
+ else
222
+ tags.map! { |tag| tag["name"] }
223
+ end
224
+
225
+ # Output the formatted tags
226
+ @util.output_tags(tags, { :format => options[:format] })
172
227
  end
173
228
  end
174
229
 
@@ -189,61 +244,73 @@ command :posts_tagged do |c|
189
244
  c.switch [:print0]
190
245
 
191
246
  c.action do |global_options, options, args|
247
+ # Convert the boolean operator to uppercase for consistency
192
248
  bool = options[:bool].upcase
249
+
250
+ # Initialize arrays to store files, tags, and matches
193
251
  files = []
194
252
  tags = []
195
253
  matches = []
254
+
255
+ # Separate files and tags from the arguments
196
256
  args.length.times do
197
257
  arg = args.pop
198
258
  if File.exist?(arg)
199
- files.push(arg)
259
+ files.push(arg) # Add existing files to the files array
200
260
  else
201
- tags.push(arg)
261
+ tags.push(arg) # Add non-file arguments to the tags array
202
262
  end
203
263
  end
204
264
 
205
- files.concat(@piped_content.split("\n")) if @piped_content
265
+ # If piped content is provided and it is a list of files, add them to the files array
266
+ files.concat(@piped_content.split(global_options[:null] ? "\x00" : "\n")) if @piped_content && @piped_content.file_list?(global_options[:null])
206
267
 
268
+ # If no files are provided, use the default post location
207
269
  if files.empty?
208
- if @jt.default_post_location && File.exist?(File.dirname(@jt.default_post_location))
209
- files = Dir.glob(@jt.default_post_location)
270
+ if @jt.default_post_location && File.exist?(@jt.default_post_location)
271
+ # Retrieves a list of files from the default post location with the specified post extension.
272
+ # The files are located using a glob pattern that matches all files with the given extension.
273
+ files = Dir.glob(File.join(@jt.default_post_location, "*.#{@jt.post_extension}"))
210
274
  end
211
275
  end
212
- exit_now! "No valid filename in arguments" if files.empty?
213
- files.each { |file|
276
+
277
+ # Exit if no valid filenames are found in the arguments
278
+ raise NoValidFile if files.empty?
279
+
280
+ # Iterate over each file to find matches based on the boolean operator
281
+ files.each do |file|
214
282
  if File.exist?(file)
215
- post_tags = @jt.post_tags(file)
283
+ post_tags = @jt.post_tags(file) # Get tags for the current file
216
284
 
217
285
  if bool == "AND"
286
+ # Check if all tags are present in the post tags
218
287
  matched = 0
219
- tags.each { |tag|
220
- matched += 1 if post_tags.include?(tag)
221
- }
288
+ tags.each { |tag| matched += 1 if post_tags.include?(tag) }
222
289
  matches.push(file) if matched == tags.length
223
290
  elsif bool == "NOT"
291
+ # Check if none of the tags are present in the post tags
224
292
  matched = false
225
- tags.each { |tag|
226
- matched = true if post_tags.include?(tag)
227
- }
293
+ tags.each { |tag| matched = true if post_tags.include?(tag) }
228
294
  matches.push(file) unless matched
229
295
  else
230
- tags.each { |tag|
231
- if post_tags.include?(tag)
232
- matches.push(file) unless matches.include?(file)
233
- end
234
- }
296
+ # Default to OR: Check if any of the tags are present in the post tags
297
+ tags.each { |tag| matches.push(file) unless matches.include?(file) if post_tags.include?(tag) }
235
298
  end
236
299
  else
237
- raise "File not found: #{file}"
300
+ # Exit if the file is not found
301
+ raise FileNotFound, "File not found: #{file}"
238
302
  end
239
- }
303
+ end
240
304
 
305
+ # Create a search string from the tags and boolean operator
241
306
  search_string = tags.join(" #{bool} ")
307
+
308
+ # Output the results
242
309
  if matches.empty?
243
- console_log "No matching files found for #{search_string}"
310
+ @util.console_log "No matching files found for #{search_string}"
244
311
  else
245
- console_log "(#{search_string})", { :err => true }
246
- output_tags(matches, { :format => options[:format], :print0 => options[:print0], :grouping => "files" })
312
+ @util.console_log "(#{search_string})", { err: true }
313
+ @util.output_tags(matches, { format: options[:format], print0: options[:print0], grouping: "files" })
247
314
  end
248
315
  end
249
316
  end
@@ -272,15 +339,48 @@ command :loners do |c|
272
339
  c.flag %i[e edit], :default_value => false, :type => String
273
340
 
274
341
  c.action do |global_options, options, args|
342
+ # Convert the max option to an integer
275
343
  max = options[:m].to_i
276
- loner_tags = @jt.get_tags({ :counts => true })
344
+
345
+ # Initialize arrays to store files and keywords
346
+ files = []
347
+ keywords = []
348
+
349
+ # Separate files and keywords from the arguments
350
+ args.length.times do
351
+ arg = args.pop
352
+ if File.exist?(arg)
353
+ files.push(arg) # Add existing files to the files array
354
+ else
355
+ keywords.push(arg) # Add non-file arguments to the keywords array
356
+ end
357
+ end
358
+
359
+ # If piped content is provided and it is a list of files, add them to the files array
360
+ files.concat(@piped_content.split(global_options[:null] ? "\x00" : "\n")) if @piped_content && @piped_content.file_list?(global_options[:null])
361
+
362
+ # If no files are provided, use the default post location
363
+ if files.empty?
364
+ if @jt.default_post_location && File.exist?(@jt.default_post_location)
365
+ files = Dir.glob(File.join(@jt.default_post_location, "*.#{@jt.post_extension}"))
366
+ end
367
+ end
368
+
369
+ # Get tags with counts for the specified files
370
+ loner_tags = @jt.tags({ :counts => true, :files => files })
371
+
372
+ # Remove tags that are not loners (i.e., tags with counts greater than max)
277
373
  loner_tags.delete_if { |tag|
278
374
  tag.class == FalseClass || tag["count"] > max
279
375
  }
376
+
377
+ # Sort loner tags by count
280
378
  loner_tags.sort_by! { |tag| tag["count"] }
281
379
 
380
+ # Exit if no tags matched the criteria
282
381
  exit_now! "No tags matched the criteria" if loner_tags.empty? || loner_tags.nil?
283
382
 
383
+ # If the edit option is provided, write the loner tags to a file for editing
284
384
  if options[:e]
285
385
  path = File.expand_path(options[:e])
286
386
  while File.exist?(path)
@@ -295,7 +395,7 @@ command :loners do |c|
295
395
  File.open(path, "w+") do |f|
296
396
  f.puts "# Edit this file to remove tags you want to keep,"
297
397
  f.puts "# then run `jtag remove -p '#{path}' [/path/to/posts/*.md]`"
298
- f.puts "# to remove any tags left in the file."
398
+ f.puts "# to remove any tags left in this file."
299
399
  f.puts "#"
300
400
  f.puts "# The post counts are included for your convenience, and will"
301
401
  f.puts "# be automatically ignored when reading the list back in."
@@ -306,15 +406,17 @@ command :loners do |c|
306
406
  }
307
407
  end
308
408
 
309
- console_log "A list of results and instructions for use have been written to #{path}."
409
+ @util.console_log "A list of results and instructions for use have been written to #{path}."
310
410
  if ENV["EDITOR"]
311
- console_log
411
+ @util.console_log
312
412
  print "Would you like to open the file in #{ENV["EDITOR"]} now? (y/N) "
313
413
  input = STDIN.gets
314
414
  if input =~ /^y/i
315
415
  system "#{ENV["EDITOR"]} '#{path}'"
316
416
  end
317
417
  end
418
+
419
+ # If the remove option is provided, remove the loner tags from the specified files
318
420
  elsif options[:r]
319
421
  files = []
320
422
  args.length.times do
@@ -322,14 +424,15 @@ command :loners do |c|
322
424
  files.push(arg) if File.exist?(arg)
323
425
  end
324
426
 
325
- files.concat(@piped_content.split("\n")) if @piped_content
427
+ files.concat(@piped_content.split(global_options[:null] ? "\x00" : "\n")) if @piped_content && @piped_content.file_list?(global_options[:null])
326
428
 
327
429
  if files.empty?
328
- if @jt.default_post_location && File.exist?(File.dirname(@jt.default_post_location))
329
- files = Dir.glob(@jt.default_post_location)
430
+ if @jt.default_post_location && File.exist?(@jt.default_post_location)
431
+ files = Dir.glob(File.join(@jt.default_post_location, "*.#{@jt.post_extension}"))
330
432
  end
331
433
  end
332
434
  exit_now! "No valid filename in arguments" if files.empty?
435
+
333
436
  files.each { |file|
334
437
  tags = @jt.post_tags(file)
335
438
  loner_tags.each { |d|
@@ -343,15 +446,17 @@ command :loners do |c|
343
446
  }
344
447
  unless global_options[:t]
345
448
  @jt.update_file_tags(file, tags)
346
- console_log "Updated tags for #{file}", :log => true
449
+ @util.console_log "Updated tags for #{file}", :log => true, level: :warn
347
450
  end
348
451
 
349
- console_log
350
- console_log File.basename(file) + ":"
351
- output_tags(tags, :format => options[:format], :filename => file)
452
+ @util.console_log level: :info
453
+ @util.console_log File.basename(file) + ":", level: :info
454
+ @util.output_tags(tags, :format => options[:format], :filename => file)
352
455
  }
456
+
457
+ # Otherwise, output the loner tags
353
458
  else
354
- output_tags(loner_tags.map { |tag|
459
+ @util.output_tags(loner_tags.map { |tag|
355
460
  count = options[:no_counts] ? "" : " (#{tag["count"]})"
356
461
  "#{tag["name"]}#{count}"
357
462
  }, :format => options[:format])
@@ -376,24 +481,24 @@ command :tags do |c|
376
481
  end
377
482
  end
378
483
 
379
- files.concat(@piped_content.split("\n")) if @piped_content && @piped_content.file_list?
484
+ files.concat(@piped_content.split(global_options[:null] ? "\x00" : "\n")) if @piped_content && @piped_content.file_list?(global_options[:null])
380
485
 
381
486
  tags = []
382
487
  files.each do |file|
383
488
  tags.concat(@jt.post_tags(file)) if File.exist?(file)
384
489
 
385
490
  if args.length > 0
386
- console_log
387
- console_log "STDIN:"
491
+ @util.console_log level: :info
492
+ @util.console_log "STDIN:", level: :info
388
493
  end
389
494
  end
390
495
 
391
496
  if tags.empty? || tags.nil?
392
- console_log "No tags in post", { :err => true }
497
+ @util.console_log "No tags in post", err: true, level: :info
393
498
  else
394
499
  tags.sort!
395
500
  tags.uniq!
396
- output_tags(tags, { :format => options[:format] })
501
+ @util.output_tags(tags, { :format => options[:format] })
397
502
  end
398
503
  end
399
504
  end
@@ -415,7 +520,7 @@ command :sort do |c|
415
520
  end
416
521
  end
417
522
 
418
- files.concat(@piped_content.split("\n")) if @piped_content
523
+ files.concat(@piped_content.split(global_options[:null] ? "\x00" : "\n")) if @piped_content && @piped_content.file_list?(global_options[:null])
419
524
 
420
525
  files.each do |file|
421
526
  tags = @jt.post_tags(file)
@@ -424,13 +529,13 @@ command :sort do |c|
424
529
  unless global_options[:t]
425
530
  @jt.update_file_tags(file, tags)
426
531
  end
427
- console_log
428
- console_log File.basename(file) + ":"
532
+ @util.console_log level: :info
533
+ @util.console_log "#{File.basename(file)}:", level: :info
429
534
 
430
535
  if tags.empty? || tags.nil?
431
- console_log "No tags in post", { :err => true }
536
+ @util.console_log "No tags in post", err: true, level: :info
432
537
  else
433
- output_tags(tags, { :format => options[:format], :filename => file })
538
+ @util.output_tags(tags, { :format => options[:format], :filename => file })
434
539
  end
435
540
  end
436
541
  end
@@ -457,31 +562,31 @@ command :merge do |c|
457
562
  end
458
563
  end
459
564
 
460
- files.concat(@piped_content.split("\n")) if @piped_content
565
+ files.concat(@piped_content.split(global_options[:null] ? "\x00" : "\n")) if @piped_content && @piped_content.file_list?(global_options[:null])
461
566
 
462
567
  if files.empty?
463
- if @jt.default_post_location && File.exist?(File.dirname(@jt.default_post_location))
464
- files = Dir.glob(@jt.default_post_location)
568
+ if @jt.default_post_location && File.exist?(@jt.default_post_location)
569
+ files = Dir.glob(File.join(@jt.default_post_location, "*.#{@jt.post_extension}"))
465
570
  end
466
571
  end
467
572
  exit_now! "No valid filename in arguments" if files.empty?
468
573
  exit_now! "Needs at least two tag inputs, one or more to merge, one to merge to" if tags.length < 2
469
574
  tags.reverse!
470
575
  merge_tag = tags.pop
471
- console_log %Q{Merging #{tags.join(", ")} to #{merge_tag}}
472
- files.each { |file|
576
+ @util.console_log %(Merging #{tags.join(", ")} to #{merge_tag}), level: :info
577
+ files.each do |file|
473
578
  new_tags = @jt.merge_tags(tags, merge_tag, file)
474
579
  next unless new_tags
475
- unless global_options[:t]
580
+ unless global_options[:test]
476
581
  @jt.update_file_tags(file, new_tags)
477
- console_log
478
- console_log "Updated tags for #{file}", :log => true
582
+ @util.console_log level: :info
583
+ @util.console_log "Updated tags for #{file}", log: true, level: :warn
479
584
  end
480
585
 
481
- console_log
482
- console_log File.basename(file) + ":"
483
- output_tags(new_tags, { :format => options[:format], :filename => file })
484
- }
586
+ @util.console_log level: :info
587
+ @util.console_log "#{File.basename(file)}:", level: :info
588
+ @util.output_tags(new_tags, { format: options[:format], filename: file })
589
+ end
485
590
  end
486
591
  end
487
592
 
@@ -494,22 +599,22 @@ command :blacklist do |c|
494
599
  c.action do |global_options, options, args|
495
600
  if options[:r]
496
601
  @jt.unblacklist(args)
497
- console_log "Removed #{args.join(", ")} from blacklist."
602
+ @util.console_log "Removed #{args.join(", ")} from blacklist.", level: :info, err: true
498
603
  else
499
604
  @jt.blacklist(args)
500
- console_log "Blacklisted #{args.join(", ")}."
605
+ @util.console_log "Blacklisted #{args.join(", ")}.", level: :info, err: true
501
606
  end
502
607
  end
503
608
  end
504
609
 
505
610
  desc "Add tags to post(s)"
506
- arg_name "tags", :multiple
507
- arg_name "file_pattern"
611
+ arg_name "tags... file(s)", :multiple
508
612
  command :add do |c|
509
- c.desc "Format to use when outputting tags to console: list, json, plist, csv or yaml. Defaults to yaml."
613
+ c.desc "Format to use when outputting tags to console: list, json, plist, csv, yaml, or complete. Defaults to yaml."
614
+ c.long_desc "Use 'complete' to output full text when input is STDIN. First two letters of format are also accepted."
510
615
  c.arg_name "output_format"
511
616
  c.default_value "yaml"
512
- c.flag %i[f format], :must_match => /^(csv|list|yaml|json|plist)$/, :type => String
617
+ c.flag %i[f format], :must_match => /^(csv?|l(?:i(?:st)?)?|y(?:a(?:ml)?)?|j(?:s(?:on)?)?|p(?:l(?:ist)?)?|co(?:mp(?:lete)?)?)$/, :type => String
513
618
 
514
619
  c.action do |global_options, options, args|
515
620
  files = []
@@ -523,30 +628,40 @@ command :add do |c|
523
628
  end
524
629
  end
525
630
 
526
- files.concat(@piped_content.split("\n")) if @piped_content
631
+ files.concat(@piped_content.split(global_options[:null] ? "\x00" : "\n")) if @piped_content && @piped_content.file_list?(global_options[:null])
527
632
 
528
- if files.empty?
529
- if @jt.default_post_location && File.exist?(File.dirname(@jt.default_post_location))
530
- files = Dir.glob(@jt.default_post_location)
531
- end
532
- end
533
- exit_now! "No valid filename in arguments" if files.empty?
534
- exit_now! "No tags found in arguments" if new_tags.empty?
633
+ # if files.empty?
634
+ # if @jt.default_post_location && File.exist?(@jt.default_post_location)
635
+ # files = Dir.glob(File.join(@jt.default_post_location, "*.#{@jt.post_extension}"))
636
+ # end
637
+ # end
638
+ # raise NoValidFile if files.empty?
639
+ raise NoTagsFound, "No tags to add specified" if new_tags.empty?
535
640
 
536
- files.each { |file|
537
- tags = @jt.post_tags(file)
641
+ if @piped_content && !@piped_content.file_list?(global_options[:null])
642
+ tags = @jt.post_tags(@piped_content, true)
538
643
  tags.concat(new_tags)
539
- tags.uniq!
540
644
  tags.sort!
541
- unless global_options[:t]
542
- @jt.update_file_tags(file, tags)
543
- console_log "Updated tags for #{file}", :log => true
544
- end
645
+ tags.uniq!
646
+ @util.console_log "STDIN:", level: :debug
647
+ # TODO: Add debug level to console_log
648
+ @util.output_tags(tags, format: options[:format], content: @piped_content)
649
+ else
650
+ files.each do |file|
651
+ tags = @jt.post_tags(file)
652
+ tags.concat(new_tags)
653
+ tags.sort!
654
+ tags.uniq!
655
+ unless global_options[:t]
656
+ @jt.update_file_tags(file, tags)
657
+ @util.console_log "Updated tags for #{file}", log: true, level: :warn
658
+ end
545
659
 
546
- console_log
547
- console_log File.basename(file) + ":"
548
- output_tags(tags, :format => options[:format], :filename => file)
549
- }
660
+ @util.console_log level: :info
661
+ @util.console_log "#{File.basename(file)}:", level: :info
662
+ @util.output_tags(tags, :format => options[:format], :filename => file)
663
+ end
664
+ end
550
665
  end
551
666
  end
552
667
 
@@ -575,11 +690,11 @@ command :remove do |c|
575
690
  end
576
691
  end
577
692
 
578
- files.concat(@piped_content.split("\n")) if @piped_content
693
+ files.concat(@piped_content.split(global_options[:null] ? "\x00" : "\n")) if @piped_content && @piped_content.file_list?(global_options[:null])
579
694
 
580
695
  if files.empty?
581
- if @jt.default_post_location && File.exist?(File.dirname(@jt.default_post_location))
582
- files = Dir.glob(@jt.default_post_location)
696
+ if @jt.default_post_location && File.exist?(@jt.default_post_location)
697
+ files = Dir.glob(File.join(@jt.default_post_location, "*.#{@jt.post_extension}"))
583
698
  end
584
699
  end
585
700
  exit_now! "No valid filename in arguments" if files.empty?
@@ -593,10 +708,10 @@ command :remove do |c|
593
708
  remove_tags.push($1.strip)
594
709
  end
595
710
  }
596
- console_log "Found #{remove_tags.length} tags in #{File.basename(path)}..."
711
+ @util.console_log "Found #{remove_tags.length} tags in #{File.basename(path)}...", level: :info, err: true
597
712
  end
598
713
 
599
- exit_now! "No tags found in input, my work here is done" if remove_tags.empty?
714
+ raise NoTagsFound if remove_tags.empty?
600
715
 
601
716
  files.each { |file|
602
717
  tags = @jt.post_tags(file)
@@ -611,12 +726,12 @@ command :remove do |c|
611
726
  }
612
727
  unless global_options[:t]
613
728
  @jt.update_file_tags(file, tags)
614
- console_log "Updated tags for #{file}", :log => true
729
+ @util.console_log "Updated tags for #{file}", log: true, level: :warn, err: true
615
730
  end
616
731
 
617
- console_log
618
- console_log File.basename(file) + ":"
619
- output_tags(tags, { :format => options[:format], :filename => file })
732
+ @util.console_log level: :info
733
+ @util.console_log "#{File.basename(file)}:", level: :info
734
+ @util.output_tags(tags, { :format => options[:format], :filename => file })
620
735
  }
621
736
  end
622
737
  end
@@ -629,17 +744,14 @@ command :tag do |c|
629
744
  c.flag %i[f format], :must_match => /^(csv|list|yaml|json|plist|complete)$/, :type => String, :default_value => "yaml"
630
745
 
631
746
  c.action do |global_options, options, args|
632
- if @piped_content && !@piped_content.file_list?
747
+ if @piped_content && !@piped_content.file_list?(global_options[:null])
633
748
  suggestions = @jt.suggest(@piped_content)
634
749
  if args.length > 0
635
- console_log
636
- console_log "STDIN:", :err => true
637
- end
638
- if options[:format] == "complete"
639
- @jt.update_file_tags(@piped_content, suggestions, true)
640
- else
641
- output_tags(suggestions, :format => options[:format], :filename => nil)
750
+ @util.console_log level: :info
751
+ @util.console_log "STDIN:", level: info, err: true
642
752
  end
753
+
754
+ @util.output_tags(suggestions, :format => options[:format], :filename => nil)
643
755
  end
644
756
 
645
757
  files = []
@@ -648,7 +760,7 @@ command :tag do |c|
648
760
  files.push(arg) if File.exist?(arg)
649
761
  end
650
762
 
651
- files.concat(@piped_content.split("\n")) if @piped_content && @piped_content.file_list?
763
+ files.concat(@piped_content.split(global_options[:null] ? "\x00" : "\n")) if @piped_content && @piped_content.file_list?(global_options[:null])
652
764
 
653
765
  files.each do |file|
654
766
  if File.exist?(File.expand_path(file))
@@ -657,61 +769,40 @@ command :tag do |c|
657
769
 
658
770
  unless global_options[:t]
659
771
  if @jt.update_file_tags(file, suggestions)
660
- console_log
661
- console_log "Updated file #{file} with:", :log => true
772
+ @util.console_log level: :info
773
+ @util.console_log "Updated file #{file} with:", log: true, level: :warn
662
774
  else
663
- console_log
664
- console_log "Failed to update #{file} with:", :log => true
775
+ @util.console_log level: :info
776
+ @util.console_log "Failed to update #{file} with:", log: true, level: :error
665
777
  end
666
778
  end
667
779
  if !global_options[:s] || global_options[:t]
668
780
  if args.length > 1
669
- console_log
670
- console_log File.basename(file) + ":", :err => true, :log => true
781
+ @util.console_log level: :info
782
+ @util.console_log "#{File.basename(file)}:", err: true, level: :info
671
783
  end
672
- output_tags(suggestions, :format => options[:format], :filename => file)
784
+ @util.output_tags(suggestions, :format => options[:format], :filename => file)
673
785
  end
674
786
  suggestions = nil
675
787
  else
676
- raise "No such file: #{file}"
788
+ raise FileNotFound, "No such file: #{file}"
677
789
  end
678
790
  end
679
791
  end
680
792
  end
681
793
 
682
- def output_tags(tags, options)
683
- format = options[:format] || "yaml"
684
- print0 = options[:print0] || false
685
- filename = options[:filename] || false
686
- case format
687
- when "list"
688
- unless print0
689
- console_log tags.join("\n")
690
- else
691
- console_log tags.map { |tag|
692
- if tag.strip =~ /\b\s\b/
693
- %Q{"#{tag.strip}"}
694
- else
695
- tag.strip
696
- end
697
- }.join(" ")
698
- end
699
- when "csv"
700
- console_log tags.to_csv
701
- when "json"
702
- out = {}
703
- out["tags"] = tags
704
- out["path"] = filename if filename
705
- console_log out.to_json
706
- when "plist"
707
- out = {}
708
- out["path"] = filename if filename
709
- console_log tags.to_plist
710
- else
711
- out = {}
712
- options[:grouping] ||= "tags"
713
- out[options[:grouping]] = tags
714
- console_log out.to_yaml
794
+ ##
795
+ ## Move deprecated config files to new location
796
+ ##
797
+ ## @return [void]
798
+ ##
799
+ def update_deprecated_config
800
+ @util = JekyllTag::Util.new
801
+ if File.exist?(File.expand_path("~/.jtag"))
802
+ FileUtils.mkdir_p(File.expand_path("~/.config/jtag"))
803
+ FileUtils.cp_r(File.expand_path("~/.jtag"), File.expand_path("~/.config/jtag"), noop: true)
804
+ FileUtils.rm_rf(File.expand_path("~/.jtag"))
805
+ @util.console_log "Moved ~/.jtag to ~/.config/jtag", level: :info, err: true, log: false
715
806
  end
716
807
  end
717
808
 
@@ -726,33 +817,43 @@ end
726
817
  # end
727
818
 
728
819
  pre do |global, command, options, args|
729
- # Use skips_pre before a command to skip this block
730
- # on that command only
731
- @silent = global[:silent]
732
-
733
820
  Signal.trap("PIPE", "EXIT")
734
821
 
735
- @logfile = File.open(File.join(Dir.tmpdir, "jtag_actions.log"), "a")
822
+ @util = JekyllTag::Util.new
823
+ @util.config_target = if global[:config_dir]
824
+ File.expand_path(global[:config_dir])
825
+ else
826
+ File.expand_path("~/.config/jtag")
827
+ end
828
+
829
+ # if global[:debug] == 0 or global[:silent], run silently
830
+ @util.silent = global[:debug] == 0 || global[:silent]
736
831
 
737
- @log = Logger.new(@logfile, shift_age = 7, shift_size = 1048576)
832
+ # set up logging
833
+ logfile = File.open(File.join(Dir.tmpdir, "jtag_actions.log"), "a")
738
834
 
739
- # Check for old config directory and migrate if needed
740
- migrate_old_config
835
+ # set up logger
836
+ @util.log = Logger.new(logfile, shift_age = 7, shift_size = 1048576)
741
837
 
742
- unless config_files_complete?
743
- write_config
744
- console_log "Missing config files written to #{@config_target}. Please check your configuration.", { :err => true }
838
+ # set log level based on flags
839
+ @util.debug_level = @silent ? 0 : global[:debug]
840
+
841
+ # check for config files, write them if they don't exist
842
+ unless @util.config_files_complete?
843
+ @util.write_config
844
+ @util.console_log "Missing config files written to #{@util.config_target}. Please check your configuration.", err: true, level: :error
745
845
  return false
746
846
  end
747
847
 
748
- configfile = File.expand_path("~/.config/jtag/config.yml")
848
+ configfile = File.join(@util.config_target, "config.yml")
749
849
 
750
- global[:config] = YAML::load(File.open(configfile, "r"))
751
- global[:support] = File.expand_path("~/.config/jtag")
850
+ # load config file
851
+ config = YAML.load_file(configfile)
852
+ global[:config] = config.symbolize_keys
752
853
 
753
854
  @piped_content = $stdin.read if $stdin.fcntl(Fcntl::F_GETFL, 0) == 0 || $stdin.stat.pipe?
754
855
 
755
- @jt = JTag.new(global[:support], global[:config])
856
+ @jt = JekyllTag::JTag.new(@util.config_target, global[:config])
756
857
 
757
858
  true
758
859
  end
@@ -764,7 +865,13 @@ post do |global, command, options, args|
764
865
  end
765
866
 
766
867
  on_error do |exception|
767
- # Error logic here
868
+ @util ||= JekyllTag::Util.new
869
+ if exception.is_a?(NoValidFile) || exception.is_a?(FileNotFound) || exception.is_a?(NoTagsFound)
870
+ prefix = @jt ? @jt.color(:red) : ""
871
+ @util.console_log "#{prefix}#{exception.message}", err: true, level: :error
872
+ return false
873
+ end
874
+
768
875
  # return false to skip default error handling
769
876
  true
770
877
  end