doing 2.0.5.pre → 2.0.9.pre

Sign up to get free protection for your applications and to get access to all the features.
data/bin/doing CHANGED
@@ -57,12 +57,15 @@ config = Doing.config
57
57
  settings = config.settings
58
58
  wwid.config = settings
59
59
 
60
- if config.settings.dig('plugins', 'command_path')
61
- commands_from File.expand_path(config.settings.dig('plugins', 'command_path'))
60
+ if settings.dig('plugins', 'command_path')
61
+ commands_from File.expand_path(settings.dig('plugins', 'command_path'))
62
62
  end
63
63
 
64
64
  program_desc 'A CLI for a What Was I Doing system'
65
- program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text record of what you've been doing, complete with tag-based time tracking. The command line tool allows you to add entries, annotate with tags and notes, and view your entries with myriad options, with a focus on a "natural" language syntax.)
65
+ program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text
66
+ record of what you've been doing, complete with tag-based time tracking. The
67
+ command line tool allows you to add entries, annotate with tags and notes, and
68
+ view your entries with myriad options, with a focus on a "natural" language syntax.)
66
69
 
67
70
  default_command :recent
68
71
  # sort_help :manually
@@ -137,7 +140,7 @@ command %i[now next] do |c|
137
140
  if options[:back]
138
141
  date = wwid.chronify(options[:back], guess: :begin)
139
142
 
140
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
143
+ raise InvalidTimeExpression.new('unable to parse date string', topic: 'Date parser:') if date.nil?
141
144
  else
142
145
  date = Time.now
143
146
  end
@@ -149,13 +152,13 @@ command %i[now next] do |c|
149
152
  end
150
153
 
151
154
  if options[:e] || (args.empty? && $stdin.stat.size.zero?)
152
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
155
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
153
156
 
154
157
  input = ''
155
158
  input += args.join(' ') unless args.empty?
156
159
  input = wwid.fork_editor(input).strip
157
160
 
158
- raise Doing::Errors::EmptyInput, 'No content' if input.empty?
161
+ raise EmptyInput, 'No content' if input.empty?
159
162
 
160
163
  title, note = wwid.format_input(input)
161
164
  note.push(options[:n]) if options[:n]
@@ -173,14 +176,14 @@ command %i[now next] do |c|
173
176
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
174
177
  wwid.write(wwid.doing_file)
175
178
  else
176
- raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
179
+ raise EmptyInput, 'You must provide content when creating a new entry'
177
180
  end
178
181
  end
179
182
  end
180
183
 
181
184
  desc 'Reset the start time of an entry'
182
185
  command %i[reset begin] do |c|
183
- c.desc 'Set the start date of an item to now'
186
+ c.desc 'Limit search to section'
184
187
  c.arg_name 'NAME'
185
188
  c.flag %i[s section], default_value: 'All'
186
189
 
@@ -195,6 +198,16 @@ command %i[reset begin] do |c|
195
198
  c.arg_name 'QUERY'
196
199
  c.flag [:search]
197
200
 
201
+ c.desc 'Force exact search string matching (case sensitive)'
202
+ c.switch %i[x exact], default_value: false, negatable: false
203
+
204
+ c.desc 'Reset items that *don\'t* match search/tag filters'
205
+ c.switch [:not], default_value: false, negatable: false
206
+
207
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
208
+ c.arg_name 'TYPE'
209
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
210
+
198
211
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
199
212
  c.arg_name 'BOOLEAN'
200
213
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -207,7 +220,16 @@ command %i[reset begin] do |c|
207
220
  options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
208
221
  end
209
222
 
210
- options[:tag_bool] = options[:bool].normalize_bool
223
+ options[:bool] = options[:bool].normalize_bool
224
+
225
+ options[:case] = options[:case].normalize_case
226
+
227
+ if options[:search]
228
+ search = options[:search]
229
+ search.sub!(/^'?/, "'") if options[:exact]
230
+ options[:search] = search
231
+ end
232
+
211
233
 
212
234
  items = wwid.filter_items([], opt: options)
213
235
 
@@ -248,6 +270,11 @@ long_desc %(
248
270
  )
249
271
  arg_name 'NOTE_TEXT'
250
272
  command :note do |c|
273
+ c.example 'doing note', desc: 'Open the last entry in $EDITOR to append a note'
274
+ c.example 'doing note "Just a quick annotation"', desc: 'Add a quick note to the last entry'
275
+ c.example 'doing note --tag done "Keeping it real or something"', desc: 'Add a note to the last item tagged @done'
276
+ c.example 'doing note --search "late night" -e', desc: 'Open $EDITOR to add a note to the last item containing "late night" (fuzzy matched)'
277
+
251
278
  c.desc 'Section'
252
279
  c.arg_name 'NAME'
253
280
  c.flag %i[s section], default_value: 'All'
@@ -266,6 +293,16 @@ command :note do |c|
266
293
  c.arg_name 'QUERY'
267
294
  c.flag [:search]
268
295
 
296
+ c.desc 'Force exact search string matching (case sensitive)'
297
+ c.switch %i[x exact], default_value: false, negatable: false
298
+
299
+ c.desc 'Add note to item that *doesn\'t* match search/tag filters'
300
+ c.switch [:not], default_value: false, negatable: false
301
+
302
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
303
+ c.arg_name 'TYPE'
304
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
305
+
269
306
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
270
307
  c.arg_name 'BOOLEAN'
271
308
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -280,6 +317,15 @@ command :note do |c|
280
317
 
281
318
  options[:tag_bool] = options[:bool].normalize_bool
282
319
 
320
+ options[:case] = options[:case].normalize_case
321
+
322
+ if options[:search]
323
+ search = options[:search]
324
+ search.sub!(/^'?/, "'") if options[:exact]
325
+ options[:search] = search
326
+ end
327
+
328
+
283
329
  last_entry = wwid.last_entry(options)
284
330
 
285
331
  unless last_entry
@@ -291,7 +337,7 @@ command :note do |c|
291
337
  new_note = Doing::Note.new
292
338
 
293
339
  if options[:e] || (args.empty? && $stdin.stat.size.zero? && !options[:r])
294
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
340
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
295
341
 
296
342
  input = !args.empty? ? args.join(' ') : ''
297
343
 
@@ -312,7 +358,7 @@ command :note do |c|
312
358
  elsif $stdin.stat.size.positive?
313
359
  new_note.add($stdin.read)
314
360
  else
315
- raise Doing::Errors::EmptyInput, 'You must provide content when adding a note' unless options[:remove]
361
+ raise EmptyInput, 'You must provide content when adding a note' unless options[:remove]
316
362
  end
317
363
 
318
364
  if last_note.equal?(new_note)
@@ -330,6 +376,11 @@ end
330
376
  desc 'Finish any running @meanwhile tasks and optionally create a new one'
331
377
  arg_name 'ENTRY'
332
378
  command :meanwhile do |c|
379
+ c.example 'doing meanwhile "Long task that will have others after it before it\'s done"', desc: 'Add a new long-running entry, completing any current @meanwhile entry'
380
+ c.example 'doing meanwhile', desc: 'Finish any open @meanwhile entry'
381
+ c.example 'doing meanwhile --archive', desc: 'Finish any open @meanwhile entry and archive it'
382
+ c.example 'doing meanwhile --back 2h "Something I\'ve been working on for a while', desc: 'Add a @meanwhile entry with a start date 2 hours ago'
383
+
333
384
  c.desc 'Section'
334
385
  c.arg_name 'NAME'
335
386
  c.flag %i[s section]
@@ -338,7 +389,7 @@ command :meanwhile do |c|
338
389
  c.switch %i[e editor], negatable: false, default_value: false
339
390
 
340
391
  c.desc 'Archive previous @meanwhile entry'
341
- c.switch %i[a archive], default_value: false
392
+ c.switch %i[a archive], negatable: false, default_value: false
342
393
 
343
394
  c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
344
395
  c.arg_name 'DATE_STRING'
@@ -352,7 +403,7 @@ command :meanwhile do |c|
352
403
  if options[:back]
353
404
  date = wwid.chronify(options[:back], guess: :begin)
354
405
 
355
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
406
+ raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
356
407
  else
357
408
  date = Time.now
358
409
  end
@@ -365,7 +416,7 @@ command :meanwhile do |c|
365
416
  input = ''
366
417
 
367
418
  if options[:e]
368
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
419
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
369
420
 
370
421
  input += args.join(' ') unless args.empty?
371
422
  input = wwid.fork_editor(input).strip
@@ -396,14 +447,14 @@ end
396
447
  desc 'Output HTML, CSS, and Markdown (ERB) templates for customization'
397
448
  long_desc %(
398
449
  Templates are printed to STDOUT for piping to a file.
399
- Save them and use them in the configuration file under html_template.
400
-
401
- Example `doing template haml > ~/styles/my_doing.haml`
450
+ Save them and use them in the configuration file under export_templates.
402
451
  )
403
452
  arg_name 'TYPE', must_match: Doing::Plugins.template_regex
404
453
  command :template do |c|
454
+ c.example 'doing template haml > ~/styles/my_doing.haml', desc: 'Output the haml template and save it to a file'
455
+
405
456
  c.desc 'List all available templates'
406
- c.switch %i[l list]
457
+ c.switch %i[l list], negatable: false
407
458
 
408
459
  c.desc 'List in single column for completion'
409
460
  c.switch %i[c]
@@ -424,7 +475,7 @@ command :template do |c|
424
475
  type = args[0]
425
476
  end
426
477
 
427
- raise Doing::Errors::InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type
478
+ raise InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type
428
479
 
429
480
  $stdout.puts Doing::Plugins.template_for_trigger(type)
430
481
 
@@ -473,6 +524,16 @@ command :select do |c|
473
524
  c.arg_name 'QUERY'
474
525
  c.flag %i[q query search]
475
526
 
527
+ c.desc 'Force exact search string matching (case sensitive)'
528
+ c.switch %i[x exact], default_value: false, negatable: false
529
+
530
+ c.desc 'Select items that *don\'t* match search/tag filters'
531
+ c.switch [:not], default_value: false, negatable: false
532
+
533
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
534
+ c.arg_name 'TYPE'
535
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
536
+
476
537
  c.desc 'Use --no-menu to skip the interactive menu. Use with --query to filter items and act on results automatically. Test with `--output doing` to preview matches.'
477
538
  c.switch %i[menu], negatable: true, default_value: true
478
539
 
@@ -503,12 +564,14 @@ command :select do |c|
503
564
  c.flag %i[o output]
504
565
 
505
566
  c.desc "Copy selection as a new entry with current time and no @done tag. Only works with single selections. Can be combined with --editor."
506
- c.switch %i[again resume]
567
+ c.switch %i[again resume], negatable: false, default_value: false
507
568
 
508
569
  c.action do |_global_options, options, args|
509
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
570
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
510
571
 
511
- raise Doing::Errors::InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query]
572
+ raise InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query]
573
+
574
+ options[:case] = options[:case].normalize_case
512
575
 
513
576
  wwid.interactive(options)
514
577
  end
@@ -517,6 +580,9 @@ end
517
580
  desc 'Add an item to the Later section'
518
581
  arg_name 'ENTRY'
519
582
  command :later do |c|
583
+ c.example 'doing later "Something I\'ll think about tomorrow"', desc: 'Add an entry to the Later section'
584
+ c.example 'doing later -e', desc: 'Open $EDITOR to create an entry and optional note'
585
+
520
586
  c.desc "Edit entry with #{Doing::Util.default_editor}"
521
587
  c.switch %i[e editor], negatable: false, default_value: false
522
588
 
@@ -531,17 +597,17 @@ command :later do |c|
531
597
  c.action do |_global_options, options, args|
532
598
  if options[:back]
533
599
  date = wwid.chronify(options[:back], guess: :begin)
534
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
600
+ raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
535
601
  else
536
602
  date = Time.now
537
603
  end
538
604
 
539
605
  if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
540
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
606
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
541
607
 
542
608
  input = args.empty? ? '' : args.join(' ')
543
609
  input = wwid.fork_editor(input).strip
544
- raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty?
610
+ raise EmptyInput, 'No content' unless input && !input.empty?
545
611
 
546
612
  title, note = wwid.format_input(input)
547
613
  note.push(options[:n]) if options[:n]
@@ -558,7 +624,7 @@ command :later do |c|
558
624
  wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
559
625
  wwid.write(wwid.doing_file)
560
626
  else
561
- raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
627
+ raise EmptyInput, 'You must provide content when creating a new entry'
562
628
  end
563
629
  end
564
630
  end
@@ -566,6 +632,11 @@ end
566
632
  desc 'Add a completed item with @done(date). No argument finishes last entry.'
567
633
  arg_name 'ENTRY'
568
634
  command %i[done did] do |c|
635
+ c.example 'doing done', desc: 'Tag the last entry @done'
636
+ c.example 'doing done I already finished this', desc: 'Add a new entry and immediately mark it @done'
637
+ c.example 'doing done --back 30m This took me half an hour', desc: 'Add an entry with a start date 30 minutes ago and a @done date of right now'
638
+ c.example 'doing done --at 3pm --took 1h Started and finished this afternoon', desc: 'Add an entry with a @done date of 3pm and a start date of 2pm (3pm - 1h)'
639
+
569
640
  c.desc 'Remove @done tag'
570
641
  c.switch %i[r remove], negatable: false, default_value: false
571
642
 
@@ -614,19 +685,19 @@ command %i[done did] do |c|
614
685
 
615
686
  if options[:took]
616
687
  took = wwid.chronify_qty(options[:took])
617
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
688
+ raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
618
689
  end
619
690
 
620
691
  if options[:back]
621
692
  date = wwid.chronify(options[:back], guess: :begin)
622
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
693
+ raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
623
694
  else
624
695
  date = options[:took] ? Time.now - took : Time.now
625
696
  end
626
697
 
627
698
  if options[:at]
628
699
  finish_date = wwid.chronify(options[:at], guess: :begin)
629
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
700
+ raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
630
701
 
631
702
  date = options[:took] ? finish_date - took : finish_date
632
703
  elsif options[:took]
@@ -651,7 +722,7 @@ command %i[done did] do |c|
651
722
  note.add(options[:note]) if options[:note]
652
723
 
653
724
  if options[:editor]
654
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
725
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
655
726
  is_new = false
656
727
 
657
728
  if args.empty?
@@ -659,7 +730,7 @@ command %i[done did] do |c|
659
730
 
660
731
  unless last_entry
661
732
  Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done')
662
- raise Doing::Errors::NoResults, 'No results'
733
+ raise NoResults, 'No results'
663
734
  end
664
735
 
665
736
  old_entry = last_entry.dup
@@ -671,7 +742,7 @@ command %i[done did] do |c|
671
742
  end
672
743
 
673
744
  input = wwid.fork_editor(input).strip
674
- raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty?
745
+ raise EmptyInput, 'No content' unless input && !input.empty?
675
746
 
676
747
  title, note = wwid.format_input(input)
677
748
  new_entry = Doing::Item.new(date, title, section, note)
@@ -746,7 +817,7 @@ command %i[done did] do |c|
746
817
  wwid.write(wwid.doing_file)
747
818
  Doing.logger.info('Entry Added:', new_entry.title)
748
819
  else
749
- raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
820
+ raise EmptyInput, 'You must provide content when creating a new entry'
750
821
  end
751
822
  end
752
823
  end
@@ -755,6 +826,9 @@ desc 'End last X entries with no time tracked'
755
826
  long_desc 'Adds @done tag without datestamp so no elapsed time is recorded. Alias for `doing finish --no-date`.'
756
827
  arg_name 'COUNT'
757
828
  command :cancel do |c|
829
+ c.example 'doing cancel', desc: 'Cancel the last entry'
830
+ c.example 'doing cancel --tag project1 -u 5', desc: 'Cancel the last 5 unfinished entries containing @project1'
831
+
758
832
  c.desc 'Archive entries'
759
833
  c.switch %i[a archive], negatable: false, default_value: false
760
834
 
@@ -770,6 +844,20 @@ command :cancel do |c|
770
844
  c.arg_name 'BOOLEAN'
771
845
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
772
846
 
847
+ c.desc 'Cancel the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
848
+ c.arg_name 'QUERY'
849
+ c.flag [:search]
850
+
851
+ c.desc 'Force exact search string matching (case sensitive)'
852
+ c.switch %i[x exact], default_value: false, negatable: false
853
+
854
+ c.desc 'Finish items that *don\'t* match search/tag filters'
855
+ c.switch [:not], default_value: false, negatable: false
856
+
857
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
858
+ c.arg_name 'TYPE'
859
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
860
+
773
861
  c.desc 'Cancel last entry (or entries) not already marked @done'
774
862
  c.switch %i[u unfinished], negatable: false, default_value: false
775
863
 
@@ -789,9 +877,9 @@ command :cancel do |c|
789
877
  tags = options[:tag].to_tags
790
878
  end
791
879
 
792
- raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
880
+ raise InvalidArgument, 'Only one argument allowed' if args.length > 1
793
881
 
794
- raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
882
+ raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
795
883
 
796
884
  if options[:interactive]
797
885
  count = 0
@@ -799,7 +887,17 @@ command :cancel do |c|
799
887
  count = args[0] ? args[0].to_i : 1
800
888
  end
801
889
 
890
+ search = nil
891
+
892
+ if options[:search]
893
+ search = options[:search]
894
+ search.sub!(/^'?/, "'") if options[:exact]
895
+ end
896
+
802
897
  opts = {
898
+ search: search,
899
+ case: options[:case].normalize_case,
900
+ not: options[:not],
803
901
  archive: options[:a],
804
902
  count: count,
805
903
  date: false,
@@ -820,6 +918,10 @@ desc 'Mark last X entries as @done'
820
918
  long_desc 'Marks the last X entries with a @done tag and current date. Does not alter already completed entries.'
821
919
  arg_name 'COUNT'
822
920
  command :finish do |c|
921
+ c.example 'doing finish', desc: 'Mark the last entry @done'
922
+ c.example 'doing finish --auto --section Later 10', desc: 'Add @done to any unfinished entries in the last 10 in Later, setting the finish time based on the start time of the task after it'
923
+ c.example 'doing finish --search "a specific entry" --at "yesterday 3pm"', desc: 'Search for an entry containing string and set its @done time to yesterday at 3pm'
924
+
823
925
  c.desc 'Include date'
824
926
  c.switch [:date], negatable: true, default_value: true
825
927
 
@@ -844,6 +946,16 @@ command :finish do |c|
844
946
  c.arg_name 'QUERY'
845
947
  c.flag [:search]
846
948
 
949
+ c.desc 'Force exact search string matching (case sensitive)'
950
+ c.switch %i[x exact], default_value: false, negatable: false
951
+
952
+ c.desc 'Finish items that *don\'t* match search/tag filters'
953
+ c.switch [:not], default_value: false, negatable: false
954
+
955
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
956
+ c.arg_name 'TYPE'
957
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
958
+
847
959
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
848
960
  c.arg_name 'BOOLEAN'
849
961
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -873,22 +985,22 @@ command :finish do |c|
873
985
  unless options[:auto]
874
986
  if options[:took]
875
987
  took = wwid.chronify_qty(options[:took])
876
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
988
+ raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
877
989
  end
878
990
 
879
- raise Doing::Errors::InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
991
+ raise InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
880
992
 
881
- raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
993
+ raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
882
994
 
883
995
  if options[:at]
884
996
  finish_date = wwid.chronify(options[:at], guess: :begin)
885
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
997
+ raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
886
998
 
887
999
  date = options[:took] ? finish_date - took : finish_date
888
1000
  elsif options[:back]
889
1001
  date = wwid.chronify(options[:back])
890
1002
 
891
- raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
1003
+ raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
892
1004
  elsif options[:took]
893
1005
  date = wwid.chronify_qty(options[:took])
894
1006
  else
@@ -902,9 +1014,9 @@ command :finish do |c|
902
1014
  tags = options[:tag].to_tags
903
1015
  end
904
1016
 
905
- raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
1017
+ raise InvalidArgument, 'Only one argument allowed' if args.length > 1
906
1018
 
907
- raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
1019
+ raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
908
1020
 
909
1021
  if options[:interactive]
910
1022
  count = 0
@@ -912,12 +1024,21 @@ command :finish do |c|
912
1024
  count = args[0] ? args[0].to_i : 1
913
1025
  end
914
1026
 
1027
+ search = nil
1028
+
1029
+ if options[:search]
1030
+ search = options[:search]
1031
+ search.sub!(/^'?/, "'") if options[:exact]
1032
+ end
1033
+
915
1034
  opts = {
916
1035
  archive: options[:archive],
917
1036
  back: date,
918
1037
  count: count,
919
1038
  date: options[:date],
920
- search: options[:search],
1039
+ search: search,
1040
+ case: options[:case].normalize_case,
1041
+ not: options[:not],
921
1042
  section: options[:section],
922
1043
  sequential: options[:auto],
923
1044
  tag: tags,
@@ -951,6 +1072,16 @@ command %i[again resume] do |c|
951
1072
  c.arg_name 'QUERY'
952
1073
  c.flag [:search]
953
1074
 
1075
+ c.desc 'Force exact search string matching (case sensitive)'
1076
+ c.switch %i[x exact], default_value: false, negatable: false
1077
+
1078
+ c.desc 'Resume items that *don\'t* match search/tag filters'
1079
+ c.switch [:not], default_value: false, negatable: false
1080
+
1081
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
1082
+ c.arg_name 'TYPE'
1083
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1084
+
954
1085
  c.desc 'Boolean used to combine multiple tags'
955
1086
  c.arg_name 'BOOLEAN'
956
1087
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -967,7 +1098,17 @@ command %i[again resume] do |c|
967
1098
 
968
1099
  c.action do |_global_options, options, _args|
969
1100
  tags = options[:tag].nil? ? [] : options[:tag].to_tags
1101
+
1102
+ options[:case] = options[:case].normalize_case
1103
+
1104
+ if options[:search]
1105
+ search = options[:search]
1106
+ search.sub!(/^'?/, "'") if options[:exact]
1107
+ options[:search] = search
1108
+ end
1109
+
970
1110
  opts = options
1111
+
971
1112
  opts[:tag] = tags
972
1113
  opts[:tag_bool] = options[:bool].normalize_bool
973
1114
  opts[:interactive] = options[:interactive]
@@ -1037,6 +1178,16 @@ command :tag do |c|
1037
1178
  c.arg_name 'QUERY'
1038
1179
  c.flag [:search]
1039
1180
 
1181
+ c.desc 'Force exact search string matching (case sensitive)'
1182
+ c.switch %i[x exact], default_value: false, negatable: false
1183
+
1184
+ c.desc 'Tag items that *don\'t* match search/tag filters'
1185
+ c.switch [:not], default_value: false, negatable: false
1186
+
1187
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
1188
+ c.arg_name 'TYPE'
1189
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1190
+
1040
1191
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1041
1192
  c.arg_name 'BOOLEAN'
1042
1193
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -1045,9 +1196,9 @@ command :tag do |c|
1045
1196
  c.switch %i[i interactive], negatable: false, default_value: false
1046
1197
 
1047
1198
  c.action do |_global_options, options, args|
1048
- raise Doing::Errors::MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:a]
1199
+ raise MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]
1049
1200
 
1050
- raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1201
+ raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1051
1202
 
1052
1203
  section = 'All'
1053
1204
 
@@ -1081,6 +1232,13 @@ command :tag do |c|
1081
1232
  count = options[:count].to_i
1082
1233
  end
1083
1234
 
1235
+ options[:case] = options[:case].normalize_case
1236
+
1237
+ if options[:search]
1238
+ search = options[:search]
1239
+ search.sub!(/^'?/, "'") if options[:exact]
1240
+ options[:search] = search
1241
+ end
1084
1242
 
1085
1243
  if count.zero? && !options[:force]
1086
1244
  if options[:search]
@@ -1104,7 +1262,7 @@ command :tag do |c|
1104
1262
 
1105
1263
  res = wwid.yn(question, default_response: false)
1106
1264
 
1107
- exit_now! 'Cancelled' unless res
1265
+ raise UserCancelled unless res
1108
1266
  end
1109
1267
 
1110
1268
  options[:count] = count
@@ -1117,19 +1275,6 @@ command :tag do |c|
1117
1275
  end
1118
1276
  end
1119
1277
 
1120
- # desc 'Autotag last X entries'
1121
- # arg_name 'COUNT'
1122
- # command :autotag do |c|
1123
- # c.action do |global_options, options, args|
1124
- # options = {
1125
- # autotag: true,
1126
- # count: args[0].to_i
1127
- # }
1128
- # cmd = commands[:tag]
1129
- # cmd.action.(global_options, options, [])
1130
- # end
1131
- # end
1132
-
1133
1278
  desc 'Mark last entry as flagged'
1134
1279
  command [:mark, :flag] do |c|
1135
1280
  c.example 'doing flag', desc: 'Add @flagged to the last entry created'
@@ -1165,6 +1310,16 @@ command [:mark, :flag] do |c|
1165
1310
  c.arg_name 'QUERY'
1166
1311
  c.flag [:search]
1167
1312
 
1313
+ c.desc 'Force exact search string matching (case sensitive)'
1314
+ c.switch %i[x exact], default_value: false, negatable: false
1315
+
1316
+ c.desc 'Flag items that *don\'t* match search/tag/date filters'
1317
+ c.switch [:not], default_value: false, negatable: false
1318
+
1319
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
1320
+ c.arg_name 'TYPE'
1321
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1322
+
1168
1323
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
1169
1324
  c.arg_name 'BOOLEAN'
1170
1325
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
@@ -1175,7 +1330,7 @@ command [:mark, :flag] do |c|
1175
1330
  c.action do |_global_options, options, _args|
1176
1331
  mark = settings['marker_tag'] || 'flagged'
1177
1332
 
1178
- raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1333
+ raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
1179
1334
 
1180
1335
  section = 'All'
1181
1336
 
@@ -1196,6 +1351,14 @@ command [:mark, :flag] do |c|
1196
1351
  count = options[:count].to_i
1197
1352
  end
1198
1353
 
1354
+ options[:case] = options[:case].normalize_case
1355
+
1356
+ if options[:search]
1357
+ search = options[:search]
1358
+ search.sub!(/^'?/, "'") if options[:exact]
1359
+ options[:search] = search
1360
+ end
1361
+
1199
1362
  if count.zero? && !options[:force]
1200
1363
  if options[:search]
1201
1364
  section_q = ' matching your search terms'
@@ -1270,6 +1433,16 @@ command :show do |c|
1270
1433
  c.arg_name 'QUERY'
1271
1434
  c.flag [:search]
1272
1435
 
1436
+ c.desc 'Force exact search string matching (case sensitive)'
1437
+ c.switch %i[x exact], default_value: false, negatable: false
1438
+
1439
+ c.desc 'Show items that *don\'t* match search/tag/date filters'
1440
+ c.switch [:not], default_value: false, negatable: false
1441
+
1442
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
1443
+ c.arg_name 'TYPE'
1444
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1445
+
1273
1446
  c.desc 'Sort order (asc/desc)'
1274
1447
  c.arg_name 'ORDER'
1275
1448
  c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
@@ -1307,8 +1480,8 @@ command :show do |c|
1307
1480
  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
1308
1481
  c.arg_name 'FORMAT'
1309
1482
  c.flag %i[o output]
1310
- c.action do |_global_options, options, args|
1311
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1483
+ c.action do |global_options, options, args|
1484
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1312
1485
 
1313
1486
  tag_filter = false
1314
1487
  tags = []
@@ -1323,8 +1496,15 @@ command :show do |c|
1323
1496
  when /^@/
1324
1497
  section = 'All'
1325
1498
  else
1326
- section = wwid.guess_section(args[0])
1327
- raise Doing::Errors::InvalidSection, "No such section: #{args[0]}" unless section
1499
+ begin
1500
+ section = wwid.guess_section(args[0])
1501
+ rescue WrongCommand => exception
1502
+ cmd = commands[:view]
1503
+ action = cmd.send(:get_action, nil)
1504
+ return action.call(global_options, options, args)
1505
+ end
1506
+
1507
+ raise InvalidSection, "No such section: #{args[0]}" unless section
1328
1508
 
1329
1509
  args.shift
1330
1510
  end
@@ -1359,7 +1539,7 @@ command :show do |c|
1359
1539
  start = wwid.chronify(date_string, guess: :begin)
1360
1540
  finish = false
1361
1541
  end
1362
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1542
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
1363
1543
  dates = [start, finish]
1364
1544
  end
1365
1545
 
@@ -1367,6 +1547,14 @@ command :show do |c|
1367
1547
 
1368
1548
  tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1369
1549
 
1550
+ options[:case] = options[:case].normalize_case
1551
+
1552
+ if options[:search]
1553
+ search = options[:search]
1554
+ search.sub!(/^'?/, "'") if options[:exact]
1555
+ options[:search] = search
1556
+ end
1557
+
1370
1558
  opt = options.dup
1371
1559
 
1372
1560
  opt[:sort_tags] = options[:tag_sort] =~ /^n/i
@@ -1429,20 +1617,35 @@ command %i[grep search] do |c|
1429
1617
  c.desc 'Only show items with recorded time intervals'
1430
1618
  c.switch [:only_timed], default_value: false, negatable: false
1431
1619
 
1620
+ c.desc 'Force exact string matching (case sensitive)'
1621
+ c.switch %i[x exact], default_value: false, negatable: false
1622
+
1623
+ c.desc 'Show items that *don\'t* match search string'
1624
+ c.switch [:not], default_value: false, negatable: false
1625
+
1626
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
1627
+ c.arg_name 'TYPE'
1628
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1629
+
1432
1630
  c.desc 'Display an interactive menu of results to perform further operations'
1433
1631
  c.switch %i[i interactive], default_value: false, negatable: false
1434
1632
 
1435
1633
  c.action do |_global_options, options, args|
1436
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1634
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1437
1635
 
1438
1636
  tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
1439
1637
 
1440
1638
  section = wwid.guess_section(options[:section]) if options[:section]
1441
1639
 
1640
+ options[:case] = options[:case].normalize_case
1641
+
1642
+ search = args.join(' ')
1643
+ search.sub!(/^'?/, "'") if options[:exact]
1644
+
1442
1645
  options[:times] = true if options[:totals]
1443
1646
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1444
1647
  options[:highlight] = true
1445
- options[:search] = args.join(' ')
1648
+ options[:search] = search
1446
1649
  options[:section] = section
1447
1650
  options[:tags_color] = tags_color
1448
1651
 
@@ -1548,7 +1751,7 @@ command :today do |c|
1548
1751
  c.flag [:after]
1549
1752
 
1550
1753
  c.action do |_global_options, options, _args|
1551
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1754
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1552
1755
 
1553
1756
  options[:t] = true if options[:totals]
1554
1757
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
@@ -1595,9 +1798,9 @@ command :on do |c|
1595
1798
  c.flag %i[o output]
1596
1799
 
1597
1800
  c.action do |_global_options, options, args|
1598
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1801
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1599
1802
 
1600
- raise Doing::Errors::MissingArgument, 'Missing date argument' if args.empty?
1803
+ raise MissingArgument, 'Missing date argument' if args.empty?
1601
1804
 
1602
1805
  date_string = args.join(' ')
1603
1806
 
@@ -1610,7 +1813,7 @@ command :on do |c|
1610
1813
  finish = false
1611
1814
  end
1612
1815
 
1613
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1816
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
1614
1817
 
1615
1818
  message = "Date interpreted as #{start}"
1616
1819
  message += " to #{finish}" if finish
@@ -1653,9 +1856,9 @@ command :since do |c|
1653
1856
  c.flag %i[o output]
1654
1857
 
1655
1858
  c.action do |_global_options, options, args|
1656
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1859
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1657
1860
 
1658
- raise Doing::Errors::MissingArgument, 'Missing date argument' if args.empty?
1861
+ raise MissingArgument, 'Missing date argument' if args.empty?
1659
1862
 
1660
1863
  date_string = args.join(' ')
1661
1864
 
@@ -1665,7 +1868,7 @@ command :since do |c|
1665
1868
  start = wwid.chronify(date_string, guess: :begin)
1666
1869
  finish = Time.now
1667
1870
 
1668
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
1871
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
1669
1872
 
1670
1873
  Doing.logger.debug("Date interpreted as #{start} through the current time")
1671
1874
 
@@ -1698,7 +1901,6 @@ command :yesterday do |c|
1698
1901
  c.switch [:totals], default_value: false, negatable: false
1699
1902
 
1700
1903
  c.desc 'Sort tags by (name|time)'
1701
- default = 'time'
1702
1904
  default = settings['tag_sort'] || 'name'
1703
1905
  c.arg_name 'KEY'
1704
1906
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
@@ -1716,7 +1918,7 @@ command :yesterday do |c|
1716
1918
  c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
1717
1919
 
1718
1920
  c.action do |_global_options, options, _args|
1719
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1921
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1720
1922
 
1721
1923
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
1722
1924
 
@@ -1761,8 +1963,18 @@ command :last do |c|
1761
1963
  c.arg_name 'QUERY'
1762
1964
  c.flag [:search]
1763
1965
 
1966
+ c.desc 'Force exact search string matching (case sensitive)'
1967
+ c.switch %i[x exact], default_value: false, negatable: false
1968
+
1969
+ c.desc 'Show items that *don\'t* match search string or tag filter'
1970
+ c.switch [:not], default_value: false, negatable: false
1971
+
1972
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
1973
+ c.arg_name 'TYPE'
1974
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
1975
+
1764
1976
  c.action do |global_options, options, _args|
1765
- raise Doing::Errors::InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1977
+ raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1766
1978
 
1767
1979
  if options[:tag].nil?
1768
1980
  tags = []
@@ -1779,11 +1991,20 @@ command :last do |c|
1779
1991
 
1780
1992
  end
1781
1993
 
1994
+ options[:case] = options[:case].normalize_case
1995
+
1996
+ search = nil
1997
+
1998
+ if options[:search]
1999
+ search = options[:search]
2000
+ search.sub!(/^'?/, "'") if options[:exact]
2001
+ end
2002
+
1782
2003
  if options[:editor]
1783
- wwid.edit_last(section: options[:s], options: { search: options[:search], tag: tags, tag_bool: options[:bool] })
2004
+ wwid.edit_last(section: options[:s], options: { search: search, case: options[:case], tag: tags, tag_bool: options[:bool], not: options[:not] })
1784
2005
  else
1785
2006
  Doing::Pager::page wwid.last(times: true, section: options[:s],
1786
- options: { search: options[:search], tag: tags, tag_bool: options[:bool] }).strip
2007
+ options: { search: search, case: options[:case], negate: options[:not], tag: tags, tag_bool: options[:bool] }).strip
1787
2008
  end
1788
2009
  end
1789
2010
  end
@@ -1791,7 +2012,7 @@ end
1791
2012
  desc 'List sections'
1792
2013
  command :sections do |c|
1793
2014
  c.desc 'List in single column'
1794
- c.switch %i[c column], default_value: false
2015
+ c.switch %i[c column], negatable: false, default_value: false
1795
2016
 
1796
2017
  c.action do |_global_options, options, _args|
1797
2018
  joiner = options[:c] ? "\n" : "\t"
@@ -1814,7 +2035,7 @@ command :add_section do |c|
1814
2035
  c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'
1815
2036
 
1816
2037
  c.action do |_global_options, _options, args|
1817
- raise Doing::Errors::InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
2038
+ raise InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
1818
2039
 
1819
2040
  wwid.add_section(args.join(' ').cap_first)
1820
2041
  wwid.write(wwid.doing_file)
@@ -1853,16 +2074,54 @@ command :plugins do |c|
1853
2074
 
1854
2075
  c.desc 'List plugins of type (import, export)'
1855
2076
  c.arg_name 'TYPE'
1856
- c.flag %i[t type], must_match: /^[iea].*$/i, default_value: 'all'
2077
+ c.flag %i[t type], must_match: /^(?:[iea].*)$/i, default_value: 'all'
1857
2078
 
1858
2079
  c.desc 'List in single column for completion'
1859
- c.switch %i[c column], default_value: false
2080
+ c.switch %i[c column], negatable: false, default_value: false
1860
2081
 
1861
2082
  c.action do |_global_options, options, _args|
1862
2083
  Doing::Plugins.list_plugins(options)
1863
2084
  end
1864
2085
  end
1865
2086
 
2087
+ desc 'Generate shell completion scripts'
2088
+ command :completion do |c|
2089
+ c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
2090
+ c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
2091
+ c.example 'doing completion --type fish --file ~/.config/fish/completions/doing.fish', desc: 'Output fish completions to file'
2092
+ c.example 'doing completion --type bash --file ~/.bash_it/completion/enabled/doing.bash', desc: 'Output bash completions to file'
2093
+
2094
+ c.desc 'Shell to generate for (bash, zsh, fish)'
2095
+ c.arg_name 'SHELL'
2096
+ c.flag %i[t type], must_match: /^[bzf](?:[ai]?sh)?$/i, default_value: 'zsh'
2097
+
2098
+ c.desc 'File to write output to'
2099
+ c.arg_name 'PATH'
2100
+ c.flag %i[f file], default_value: 'stdout'
2101
+
2102
+ c.action do |_global_options, options, _args|
2103
+ script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')
2104
+
2105
+ case options[:type]
2106
+ when /^b/
2107
+ result = `ruby #{File.join(script_dir, 'generate_bash_completions.rb')}`
2108
+ when /^z/
2109
+ result = `ruby #{File.join(script_dir, 'generate_zsh_completions.rb')}`
2110
+ when /^f/
2111
+ result = `ruby #{File.join(script_dir, 'generate_fish_completions.rb')}`
2112
+ end
2113
+
2114
+ if options[:file] =~ /^stdout$/i
2115
+ $stdout.puts result
2116
+ else
2117
+ File.open(File.expand_path(options[:file]), 'w') do |f|
2118
+ f.puts result
2119
+ end
2120
+ Doing.logger.warn('File written:', "#{options[:type]} completions written to #{options[:file]}")
2121
+ end
2122
+ end
2123
+ end
2124
+
1866
2125
  desc 'Display a user-created view'
1867
2126
  long_desc 'Command line options override view configuration'
1868
2127
  arg_name 'VIEW_NAME'
@@ -1903,6 +2162,16 @@ command :view do |c|
1903
2162
  c.arg_name 'QUERY'
1904
2163
  c.flag [:search]
1905
2164
 
2165
+ c.desc 'Force exact search string matching (case sensitive)'
2166
+ c.switch %i[x exact], default_value: false, negatable: false
2167
+
2168
+ c.desc 'Show items that *don\'t* match search string'
2169
+ c.switch [:not], default_value: false, negatable: false
2170
+
2171
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
2172
+ c.arg_name 'TYPE'
2173
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2174
+
1906
2175
  c.desc 'Sort tags by (name|time)'
1907
2176
  c.arg_name 'KEY'
1908
2177
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i
@@ -1925,15 +2194,22 @@ command :view do |c|
1925
2194
  c.desc 'Select from a menu of matching entries to perform additional operations'
1926
2195
  c.switch %i[i interactive], negatable: false, default_value: false
1927
2196
 
1928
- c.action do |_global_options, options, args|
1929
- raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
2197
+ c.action do |global_options, options, args|
2198
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
1930
2199
 
1931
- raise Doing::Errors::InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
2200
+ raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
1932
2201
 
1933
2202
  title = if args.empty?
1934
2203
  wwid.choose_view
1935
2204
  else
1936
- wwid.guess_view(args[0])
2205
+ begin
2206
+ wwid.guess_view(args[0])
2207
+ rescue WrongCommand => exception
2208
+ cmd = commands[:show]
2209
+ options[:sort] = 'asc'
2210
+ action = cmd.send(:get_action, nil)
2211
+ return action.call(global_options, options, args)
2212
+ end
1937
2213
  end
1938
2214
 
1939
2215
  if options[:section]
@@ -2023,11 +2299,21 @@ command :view do |c|
2023
2299
  start = wwid.chronify(date_string, guess: :begin)
2024
2300
  finish = false
2025
2301
  end
2026
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
2302
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
2027
2303
  dates = [start, finish]
2028
2304
  end
2029
2305
 
2030
- opts = options
2306
+ options[:case] = options[:case].normalize_case
2307
+
2308
+ search = nil
2309
+
2310
+ if options[:search]
2311
+ search = options[:search]
2312
+ search.sub!(/^'?/, "'") if options[:exact]
2313
+ end
2314
+
2315
+ opts = options.dup
2316
+ opts[:search] = search
2031
2317
  opts[:output] = output_format
2032
2318
  opts[:count] = count
2033
2319
  opts[:format] = date_format
@@ -2046,9 +2332,9 @@ command :view do |c|
2046
2332
 
2047
2333
  Doing::Pager.page wwid.list_section(opts)
2048
2334
  elsif title.instance_of?(FalseClass)
2049
- exit_now! 'Cancelled'
2335
+ raise UserCancelled, 'Cancelled' unless res
2050
2336
  else
2051
- raise Doing::Errors::InvalidView, "View #{title} not found in config"
2337
+ raise InvalidView, "View #{title} not found in config"
2052
2338
  end
2053
2339
  end
2054
2340
  end
@@ -2100,6 +2386,16 @@ command %i[archive move] do |c|
2100
2386
  c.arg_name 'QUERY'
2101
2387
  c.flag [:search]
2102
2388
 
2389
+ c.desc 'Force exact search string matching (case sensitive)'
2390
+ c.switch %i[x exact], default_value: false, negatable: false
2391
+
2392
+ c.desc 'Show items that *don\'t* match search string'
2393
+ c.switch [:not], default_value: false, negatable: false
2394
+
2395
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
2396
+ c.arg_name 'TYPE'
2397
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2398
+
2103
2399
  c.desc 'Archive entries older than date
2104
2400
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
2105
2401
  c.arg_name 'DATE_STRING'
@@ -2119,11 +2415,21 @@ command %i[archive move] do |c|
2119
2415
  tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
2120
2416
  end
2121
2417
 
2122
- raise Doing::Errors::InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
2418
+ raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
2123
2419
 
2124
2420
  tags.concat(options[:tag].to_tags) if options[:tag]
2125
2421
 
2126
- opts = options
2422
+ search = nil
2423
+
2424
+ options[:case] = options[:case].normalize_case
2425
+
2426
+ if options[:search]
2427
+ search = options[:search]
2428
+ search.sub!(/^'?/, "'") if options[:exact]
2429
+ end
2430
+
2431
+ opts = options.dup
2432
+ opts[:search] = search
2127
2433
  opts[:bool] = options[:bool].normalize_bool
2128
2434
  opts[:destination] = options[:to]
2129
2435
  opts[:tags] = tags
@@ -2158,6 +2464,16 @@ command :rotate do |c|
2158
2464
  c.arg_name 'QUERY'
2159
2465
  c.flag [:search]
2160
2466
 
2467
+ c.desc 'Force exact search string matching (case sensitive)'
2468
+ c.switch %i[x exact], default_value: false, negatable: false
2469
+
2470
+ c.desc 'Rotate items that *don\'t* match search string or tag filter'
2471
+ c.switch [:not], default_value: false, negatable: false
2472
+
2473
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
2474
+ c.arg_name 'TYPE'
2475
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2476
+
2161
2477
  c.desc 'Rotate entries older than date
2162
2478
  (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
2163
2479
  c.arg_name 'DATE_STRING'
@@ -2170,6 +2486,16 @@ command :rotate do |c|
2170
2486
 
2171
2487
  options[:bool] = options[:bool].normalize_bool
2172
2488
 
2489
+ options[:case] = options[:case].normalize_case
2490
+
2491
+ search = nil
2492
+
2493
+ if options[:search]
2494
+ search = options[:search]
2495
+ search.sub!(/^'?/, "'") if options[:exact]
2496
+ options[:search] = search
2497
+ end
2498
+
2173
2499
  wwid.rotate(options)
2174
2500
  end
2175
2501
  end
@@ -2208,7 +2534,7 @@ command :open do |c|
2208
2534
  system %(open "#{File.expand_path(wwid.doing_file)}")
2209
2535
  end
2210
2536
  else
2211
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
2537
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
2212
2538
 
2213
2539
  system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
2214
2540
  end
@@ -2236,7 +2562,7 @@ command :config do |c|
2236
2562
  c.flag %i[e editor], default_value: nil
2237
2563
 
2238
2564
  c.desc 'Show a config key value based on arguments. Separate key paths with colons or dots, e.g. "export_templates.html". Empty arguments outputs the entire config.'
2239
- c.switch %i[d dump]
2565
+ c.switch %i[d dump], negatable: false
2240
2566
 
2241
2567
  c.desc 'Format for --dump (json|yaml|raw)'
2242
2568
  c.arg_name 'FORMAT'
@@ -2261,7 +2587,6 @@ command :config do |c|
2261
2587
  c.action do |_global_options, options, args|
2262
2588
  if options[:update]
2263
2589
  config.configure({rewrite: true, ignore_local: true})
2264
- # Doing.logger.warn("Config file rewritten: #{config.config_file}")
2265
2590
  return
2266
2591
  end
2267
2592
 
@@ -2276,7 +2601,6 @@ command :config do |c|
2276
2601
  when /^r/
2277
2602
  cfg
2278
2603
  else
2279
- # cfg = { last_key => cfg } unless last_key.nil?
2280
2604
  YAML.dump(cfg)
2281
2605
  end
2282
2606
  else
@@ -2291,7 +2615,7 @@ command :config do |c|
2291
2615
  choices.concat(config.additional_configs)
2292
2616
  res = wwid.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to edit > ')
2293
2617
 
2294
- raise Doing::Errors::UserCancelled, 'Cancelled' unless res
2618
+ raise UserCancelled, 'Cancelled' unless res
2295
2619
 
2296
2620
  config_file = res.strip || config.config_file
2297
2621
  else
@@ -2308,7 +2632,7 @@ command :config do |c|
2308
2632
  `open -a "#{editor}" "#{config_file}"`
2309
2633
  end
2310
2634
  else
2311
- raise Doing::Errors::InvalidArgument, 'No viable editor found in config or environment.'
2635
+ raise InvalidArgument, 'No viable editor found in config or environment.'
2312
2636
  end
2313
2637
  elsif options[:a] || options[:b]
2314
2638
  if options[:a]
@@ -2319,7 +2643,7 @@ command :config do |c|
2319
2643
  else
2320
2644
  editor = options[:e] || Doing::Util.find_default_editor('config')
2321
2645
 
2322
- raise Doing::Errors::MissingEditor, 'No viable editor defined in config or environment' unless editor
2646
+ raise MissingEditor, 'No viable editor defined in config or environment' unless editor
2323
2647
 
2324
2648
  if Doing::Util.exec_available(editor)
2325
2649
  system %(#{editor} "#{config_file}")
@@ -2329,7 +2653,7 @@ command :config do |c|
2329
2653
  end
2330
2654
  else
2331
2655
  editor = options[:e] || Doing::Util.default_editor
2332
- raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
2656
+ raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
2333
2657
 
2334
2658
  system %(#{editor} "#{config_file}")
2335
2659
  end
@@ -2360,6 +2684,16 @@ command :import do |c|
2360
2684
  c.arg_name 'QUERY'
2361
2685
  c.flag [:search]
2362
2686
 
2687
+ c.desc 'Force exact search string matching (case sensitive)'
2688
+ c.switch %i[x exact], default_value: false, negatable: false
2689
+
2690
+ c.desc 'Import items that *don\'t* match search/tag/date filters'
2691
+ c.switch [:not], default_value: false, negatable: false
2692
+
2693
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)nsensitive, (s)mart]'
2694
+ c.arg_name 'TYPE'
2695
+ c.flag [:case], must_match: /^[csi]/, default_value: 'smart'
2696
+
2363
2697
  c.desc 'Only import items with recorded time intervals'
2364
2698
  c.switch [:only_timed], default_value: false, negatable: false
2365
2699
 
@@ -2413,17 +2747,19 @@ command :import do |c|
2413
2747
  start = wwid.chronify(date_string, guess: :begin)
2414
2748
  finish = false
2415
2749
  end
2416
- raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
2750
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
2417
2751
  dates = [start, finish]
2418
2752
  end
2419
2753
 
2754
+ options[:case] = options[:case].normalize_case
2755
+
2420
2756
  if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
2421
2757
  options[:no_overlap] = !options[:overlap]
2422
2758
  options[:date_filter] = dates
2423
2759
  wwid.import(args, options)
2424
2760
  wwid.write(wwid.doing_file)
2425
2761
  else
2426
- raise Doing::Errors::InvalidPluginType, "Invalid import type: #{options[:type]}"
2762
+ raise InvalidPluginType, "Invalid import type: #{options[:type]}"
2427
2763
  end
2428
2764
  end
2429
2765
  end
@@ -2448,14 +2784,13 @@ pre do |global, _command, _options, _args|
2448
2784
  end
2449
2785
 
2450
2786
  on_error do |exception|
2451
- # if exception.kind_of?(SystemExit)
2452
- # false
2453
- # else
2454
- # p exception.inspect
2455
- # Doing.logger.output_results
2456
- # true
2457
- # end
2458
- false
2787
+ if exception.kind_of?(SystemExit)
2788
+ false
2789
+ else
2790
+ # Doing.logger.error('Fatal:', exception)
2791
+ Doing.logger.output_results
2792
+ true
2793
+ end
2459
2794
  end
2460
2795
 
2461
2796
  post do |global, _command, _options, _args|