PageRenderTime 96ms CodeModel.GetById 11ms app.highlight 77ms RepoModel.GetById 1ms app.codeStats 0ms

/w2do.pl

http://w2do.googlecode.com/
Perl | 872 lines | 810 code | 27 blank | 35 comment | 13 complexity | 1a46e37c5395a9593316e24e4e9b024e MD5 | raw file
  1#!/usr/bin/env perl
  2
  3# w2do, a simple text-based todo manager
  4# Copyright (C) 2008, 2009 Jaromir Hradilek
  5
  6# This program is  free software:  you can redistribute it and/or modify it
  7# under  the terms  of the  GNU General Public License  as published by the
  8# Free Software Foundation, version 3 of the License.
  9# 
 10# This program  is  distributed  in the hope  that it will  be useful,  but
 11# WITHOUT  ANY WARRANTY;  without  even the implied  warranty of MERCHANTA-
 12# BILITY  or  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
 13# License for more details.
 14# 
 15# You should have received a copy of the  GNU General Public License  along
 16# with this program. If not, see <http://www.gnu.org/licenses/>.
 17
 18use strict;
 19use warnings;
 20use locale;
 21use Text::Wrap;
 22use File::Copy;
 23use File::Basename;
 24use File::Spec::Functions;
 25use Term::ANSIColor;
 26use Getopt::Long;
 27
 28# General script information:
 29use constant NAME    => basename($0, '.pl');       # Script name.
 30use constant VERSION => '2.1.1';                   # Script version.
 31
 32# Global script settings:
 33our $HOMEDIR   = $ENV{HOME} || $ENV{USERPROFILE} || '.';
 34our $savefile  = $ENV{W2DO_SAVEFILE} || catfile($HOMEDIR, '.w2do');
 35our $backext   = '.bak';                           # Backup file extension.
 36our $coloured  = 0;                                # Colour output setup.
 37our $verbose   = 1;                                # Verbosity level.
 38our $headcol   = 'bold white on_green';            # Table header colour.
 39our $donecol   = 'green';                          # Done task colour.
 40our $todaycol  = 'bold';                           # Today's task colour.
 41$Text::Wrap::columns = $ENV{W2DO_WIDTH} || 75;     # Default table width.
 42
 43# Command line options:
 44my $identifier = undef;                            # Task identifier.
 45my $action     = 0;                                # Default action.
 46my %args       = ();                               # Specifying options.
 47
 48# Signal handlers:
 49$SIG{__WARN__} = sub { 
 50  exit_with_error((shift) . "Try `--help' for more information.", 22);
 51};
 52
 53# Display script usage:
 54sub display_help {
 55  my $NAME = NAME;
 56
 57  # Print the message:
 58  print << "END_HELP";
 59Usage: $NAME [-l] [-t task] [-g group] [-d date] [-p priority] [-f|-u]
 60       $NAME -a task [-g group] [-d date] [-p priority] [-f|-u]
 61       $NAME -c id [-t task] [-g group] [-d date] [-p priority] [-f|-u]
 62       $NAME -r id
 63       $NAME [options]
 64
 65General options:
 66
 67  -l, --list               display items in the task list
 68  -a, --add task           add new item to the task list
 69  -c, --change id          change selected item in the task list
 70  -r, --remove id          remove selected item from the task list
 71
 72  --change-group group     change all items in the selected group
 73  --remove-group group     remove all items from the selected group
 74  --purge-group group      remove all finished items in the selected group
 75
 76  --change-date date       change all items with selected due date
 77  --remove-date date       remove all items with selected due date
 78  --purge-date date        remove all finished items with selected due date
 79
 80  --change-old             change all items with passed due date
 81  --remove-old             remove all items with passed due date
 82  --purge-old              remove all finished items with passed due date
 83
 84  --change-all             change all items in the task list
 85  --remove-all             remove all items from the task list
 86  --purge-all              remove all finished items from the task list
 87
 88  -U, --undo               revert last action
 89  -G, --groups             display all groups in the task list
 90  -S, --stats              display detailed task list statistics
 91  -h, --help               display this help and exit
 92  -v, --version            display version information and exit
 93
 94Specifying options:
 95
 96  -t, --task task          specify the task name
 97  -g, --group group        specify the group name
 98  -d, --date date          specify the due date; available options are
 99                           anytime, today, yesterday, tomorrow, month,
100                           year, or an exact date in the YYYY-MM-DD format
101  -p, --priority priority  specify the priority; available options are 1-5
102                           where 1 represents the highest priority
103  -f, --finished           specify the finished task
104  -u, --unfinished         specify the unfinished task
105
106Additional options:
107
108  -s, --savefile file      use selected file instead of the default ~/.w2do
109  -w, --width width        use selected line width; the minimal value is 75
110  -q, --quiet              avoid displaying messages that are not necessary
111  -C, --colour             use coloured output instead of the default plain
112                           text version
113END_HELP
114}
115
116# Display script version:
117sub display_version {
118  my ($NAME, $VERSION) = (NAME, VERSION);
119
120  # Print the message:
121  print << "END_VERSION";
122$NAME $VERSION
123
124Copyright (C) 2008, 2009 Jaromir Hradilek
125This program is free software; see the source for copying conditions. It is
126distributed in the hope  that it will be useful,  but WITHOUT ANY WARRANTY;
127without even the implied warranty of  MERCHANTABILITY or FITNESS FOR A PAR-
128TICULAR PURPOSE.
129END_VERSION
130}
131
132# Display all groups in the task list:
133sub display_groups {
134  my $stats = {};
135
136  # Get task list statistics:
137  get_stats($stats);
138
139  # Check whether the list is not empty:
140  if (%$stats) {
141    # Display the list of groups:
142    print join(', ', map { "$_ (" . $stats->{$_}->{done}  . '/'
143                                  . $stats->{$_}->{tasks} . ')' }
144                     sort keys(%$stats)),
145          "\n";
146  }
147  else {
148    # Report empty list:
149    print "The task list is empty.\n" if $verbose;
150  }
151}
152
153# Display detailed task list statistics:
154sub display_statistics {
155  my $stats = {};
156  my $per;
157
158  # Get task list statistics:
159  my ($groups, $tasks, $undone) = get_stats($stats);
160
161  # Display overall statistics:
162  printf "%d group%s, %d task%s, %d unfinished\n\n",
163         $groups, (($groups != 1) ? 's' : ''),
164         $tasks,  (($tasks  != 1) ? 's' : ''),
165         $undone;
166
167  # Process each group:
168  foreach my $group (sort (keys %$stats)) {
169    # Count group percentage:
170    $per = int($stats->{$group}->{done} * 100 / $stats->{$group}->{tasks});
171
172    # Display group progress:
173    printf "%-11s %s %d%%\n", "$group:", draw_progressbar($per), $per;
174  }
175
176  # Count overall percentage:
177  $per = $tasks ? int(($tasks - $undone) * 100 / $tasks) : 0;
178
179  # Display overall progress:
180  printf "---\n%-11s %s %d%%\n", "total:", draw_progressbar($per), $per;
181}
182
183# Display items in the task list:
184sub display_tasks {
185  my $args = shift;
186  my @data;
187
188  # Load matching tasks:
189  load_selection(\@data, undef, $args);
190
191  # Check whether the list is not empty:
192  if (@data) {
193    my $current = '';
194    my ($id, $group, $date, $priority, $state, $task);
195
196    # Prepare the table layout:
197    my $format  = " %-4s  %-10s  %-10s   %s    %s   %s\n";
198    my $caption = " id    group       date        pri  sta  task" .
199                  ' 'x ($Text::Wrap::columns - 45) . "\n";
200    my $divider = '-'x  $Text::Wrap::columns . "\n";
201    my $border  = '='x  $Text::Wrap::columns . "\n";
202    my $indent  = ' 'x  41;
203
204    # Set up the line wrapper:
205    $Text::Wrap::columns++;
206
207    # Display table header:
208    $coloured ? print colored ($caption, $headcol)
209              : print $border, $caption, $border;
210
211    # Process each task:
212    foreach my $line (sort @data) {
213      # Parse the task record:
214      $line =~ /^([^:]*):([^:]*):([1-5]):([ft]):(.*):(\d+)$/;
215
216      # Check whether the group has changed:
217      if (lc($1) ne $current) {
218        # Display the divider unless the first group is being listed:
219        if ($group) {
220          $coloured ? print colored ($caption, $headcol) : print $divider;
221        }
222
223        # Remember the current group:
224        $current = lc($1);
225      }
226
227      # If possible, use relative date reference:
228      if ($2 eq date_to_string(time)) { $date = 'today'; }
229      elsif ($2 eq date_to_string(time - 86400)) { $date = 'yesterday'; }
230      elsif ($2 eq date_to_string(time + 86400)) { $date = 'tomorrow';  }
231      else { $date = $2; }
232
233      # Prepare the rest of the task entry:
234      $id       = $6;
235      $group    = $1;
236      $priority = $3;
237      $state    = ($4 eq 'f') ? '-' : 'f';
238      $task     =  wrap($indent, $indent, $5); $task =~ s/\s+//;
239
240      # Set up colours:
241      print color $donecol  if $coloured && $state eq 'f';
242      print color $todaycol if $coloured && $state ne 'f'
243                                         && $date  eq 'today';
244
245      # Display the task entry:
246      printf($format, $id, $group, $date, $priority, $state, $task);
247
248      # Reset colours:
249      print color 'reset'   if $coloured;
250    }
251  }
252  else {
253    # Report empty list:
254    print "No matching task found.\n" if $verbose;
255  }
256}
257
258# Add new item to the task list:
259sub add_task {
260  my $args     = shift;
261
262  # Use default value when none is provided:
263  my $group    = $args->{group}    || 'general';
264  my $date     = $args->{date}     || 'anytime';
265  my $priority = $args->{priority} || '3';
266  my $state    = $args->{state}    || 'f';
267  my $task     = $args->{task}     || '';
268  my $id       = choose_id();
269
270  # Create the task record:
271  my @data     = ("$group:$date:$priority:$state:$task:$id\n"); 
272
273  # Add data to the end of the save file:
274  add_data(\@data);
275
276  # Report success:
277  print "Task has been successfully added with id $id.\n" if $verbose;
278}
279
280# Change selected item in the task list:
281sub change_task {
282  my (@selected, @data);
283
284  # Load selected task:
285  load_selection(\@selected, \@data, { id => shift });
286
287  # Change selected item:
288  change_selection(\@selected, \@data, shift);
289}
290
291# Remove selected item from the task list:
292sub remove_task {
293  my (@selected, @data);
294
295  # Load selected task:
296  load_selection(\@selected, \@data, { id => shift });
297
298  # Remove selected item:
299  remove_selection(\@selected, \@data);
300}
301
302# Change all items in the selected group:
303sub change_group {
304  my (@selected, @data);
305
306  # Load selected tasks:
307  load_selection(\@selected, \@data, { group => shift });
308
309  # Change selected items:
310  change_selection(\@selected, \@data, shift);
311}
312
313# Remove all items in the selected group:
314sub remove_group {
315  my (@selected, @data);
316
317  # Load selected tasks:
318  load_selection(\@selected, \@data, { group => shift });
319
320  # Remove selected items:
321  remove_selection(\@selected, \@data);
322}
323
324# Remove all finished items in the selected group:
325sub purge_group {
326  my (@selected, @data);
327
328  # Load selected tasks:
329  load_selection(\@selected, \@data, { group => shift });
330
331  # Purge selected items:
332  purge_selection(\@selected, \@data);
333}
334
335# Change all items with selected due date:
336sub change_date {
337  my (@selected, @data);
338
339  # Load selected tasks:
340  load_selection(\@selected, \@data, { date => shift });
341
342  # Change selected items:
343  change_selection(\@selected, \@data, shift);
344}
345
346# Remove all items with the selected due date:
347sub remove_date {
348  my (@selected, @data);
349
350  # Load selected tasks:
351  load_selection(\@selected, \@data, { date => shift });
352
353  # Remove selected items:
354  remove_selection(\@selected, \@data);
355}
356
357# Remove all finished items with selected due date:
358sub purge_date {
359  my (@selected, @data);
360
361  # Load selected tasks:
362  load_selection(\@selected, \@data, { date => shift });
363
364  # Purge selected items:
365  purge_selection(\@selected, \@data);
366}
367
368# Change all items with passed due date:
369sub change_old {
370  my (@selected, @data);
371
372  # Load selected tasks:
373  load_old(\@selected, \@data);
374
375  # Change selected items:
376  change_selection(\@selected, \@data, shift);
377}
378
379# Remove all items with passed due date:
380sub remove_old {
381  my (@selected, @data);
382
383  # Load selected tasks:
384  load_old(\@selected, \@data);
385
386  # Change selected items:
387  remove_selection(\@selected, \@data);
388}
389
390# Purge all items with passed due date:
391sub purge_old {
392  my (@selected, @data);
393
394  # Load selected tasks:
395  load_old(\@selected, \@data);
396
397  # Purge selected tasks:
398  purge_selection(\@selected, \@data);
399}
400
401# Change all items in the task list:
402sub change_all {
403  my (@selected, @data);
404
405  # Load all tasks:
406  load_selection(\@selected, \@data);
407
408  # Change all items:
409  change_selection(\@selected, \@data, shift);
410}
411
412# Remove all items from the task list:
413sub remove_all {
414  my (@selected, @data);
415
416  # Load all tasks:
417  load_selection(\@selected, \@data);
418
419  # Remove all items:
420  remove_selection(\@selected, \@data);
421}
422
423# Remove all finished items from the task list:
424sub purge_all {
425  my (@selected, @data);
426
427  # Load all tasks:
428  load_selection(\@selected, \@data);
429
430  # Purge all tasks:
431  purge_selection(\@selected, \@data);
432}
433
434# Revert last action:
435sub revert_last_action {
436  # Try to restore data from the backup file:
437  if (move("$savefile$backext", $savefile)) {
438    # Report success:
439    print "Last action has been successfully reverted.\n" if $verbose;
440  }
441  else {
442    # If not present, we are probably at oldest change:
443    print "Already at oldest change.\n" if $verbose;
444  }
445}
446
447# Change selected items in the task list:
448sub change_selection {
449  my ($selected, $data, $args) = @_;
450
451  # Check whether the selection is not empty:
452  if (@$selected) {
453    # Check whether the changed item is suplied:
454    if (%$args) {
455      # Process each item:
456      foreach my $item (@$selected) {
457        # Parse the task record:
458        if ($item =~ /^([^:]*):([^:]*):([1-5]):([ft]):(.*):(\d+)$/) {
459          # Use existing value when none is supplied:
460          my $group    = $args->{group}    || $1;
461          my $date     = $args->{date}     || $2;
462          my $priority = $args->{priority} || $3;
463          my $state    = $args->{state}    || $4;
464          my $task     = $args->{task}     || $5;
465          my $id       = $6;
466
467          # Update the task record:
468          push(@$data, "$group:$date:$priority:$state:$task:$id\n");
469        }
470      }
471      
472      # Store data to the save file:
473      save_data($data);
474
475      # Report success:
476      print "Selected tasks have been successfully changed.\n" if $verbose;
477    }
478    else {
479      # Report missing option:
480      print "You have to specify what to change.\n" if $verbose;
481    }
482  }
483  else {
484    # Report empty selection:
485    print "No matching task found.\n" if $verbose;
486  }
487}
488
489# Remove selected items from the task list:
490sub remove_selection {
491  my ($selected, $data) = @_;
492
493  # Check whether the selection is not empty:
494  if (@$selected) {
495    # Store data to the save file:
496    save_data($data);
497
498    # Report success:
499    print "Selected tasks have been successfully removed.\n" if $verbose;
500  }
501  else {
502    # Report empty selection:
503    print "No matching task found.\n" if $verbose;
504  }
505}
506
507# Remove all finished items in the selection:
508sub purge_selection {
509  my ($selected, $data) = @_;
510
511  # Check whether the selection is not empty:
512  if (@$selected) {
513    # Process each item:
514    foreach my $item (@$selected) {
515      # Add unfinished tasks back to the list:
516      if ($item =~ /^[^:]*:[^:]*:[1-5]:f:.*:\d+$/) {
517        push(@$data, $item);
518      }
519    }
520
521    # Store data to the save file:
522    save_data($data);
523
524    # Report success:
525    print "Selected tasks have been successfully purged.\n" if $verbose;
526  }
527  else {
528    # Report empty selection:
529    print "No matching task found.\n" if $verbose;
530  }
531}
532
533# Load selected data from the save file:
534sub load_selection {
535  my ($selected, $rest, $args) = @_;
536  my  $reserved  = '[\\\\\^\.\$\|\(\)\[\]\*\+\?\{\}]';
537
538  # Escape reserved characters:
539  $args->{group} =~ s/($reserved)/\\$1/g if $args->{group};
540  $args->{task}  =~ s/($reserved)/\\$1/g if $args->{task};
541
542  # Use default pattern when none is provided:
543  my $group      = $args->{group}    || '[^:]*';
544  my $date       = $args->{date}     || '[^:]*';
545  my $priority   = $args->{priority} || '[1-5]';
546  my $state      = $args->{state}    || '[ft]';
547  my $task       = $args->{task}     || '';
548  my $id         = $args->{id}       || '\d+';
549
550  # Create the mask:
551  my $mask       = "^$group:$date:$priority:$state:.*$task.*:$id\$";
552
553  # Open the save file for reading:
554  if (open(SAVEFILE, "$savefile")) {
555    # Process each line:
556    while (my $line = <SAVEFILE>) {
557      # Check whether the line matches given pattern:
558      if ($line =~ /$mask/i) {
559        # Add line to the selected items list:
560        push(@$selected, $line);
561      }
562      else {
563        # Add line to the other items list:
564        push(@$rest, $line);
565      }
566    }
567
568    # Close the save file:
569    close(SAVEFILE);
570  }
571}
572
573# Load data with passed due date from the save file:
574sub load_old {
575  my ($selected, $rest) = @_;
576
577  # Open the save file for reading:
578  if (open(SAVEFILE, "$savefile")) {
579    # Process each line:
580    while (my $line = <SAVEFILE>) {
581      # Parse the task record:
582      $line =~ /^[^:]*:([^:]*):[1-5]:[ft]:.*:\d+$/;
583
584      # Check whether the line matches given pattern:
585      if ("$1" lt date_to_string(time) && "$1" ne 'anytime') {
586        # Add line to the selected items list:
587        push(@$selected, $line);
588      }
589      else {
590        # Add line to the other items list:
591        push(@$rest, $line);
592      }
593    }
594
595    # Close the save file:
596    close(SAVEFILE);
597  }
598}
599
600# Save data to the save file:
601sub save_data {
602  my $data = shift;
603
604  # Backup the save file:
605  copy($savefile, "$savefile$backext") if (-r $savefile);
606
607  # Open the save file for writing:
608  if (open(SAVEFILE, ">$savefile")) {
609    # Process each item:
610    foreach my $item (@$data) {
611      # Write data to the save file:
612      print SAVEFILE $item;
613    }
614
615    # Close the save file:
616    close(SAVEFILE);
617  }
618  else {
619    # Report failure and exit:
620    exit_with_error("Unable to write to `$savefile'.", 13);
621  }
622}
623
624# Add data to the end of the save file:
625sub add_data {
626  my $data = shift;
627
628  # Backup the save file:
629  copy($savefile, "$savefile$backext") if (-r $savefile);
630
631  # Open the save file for appending:
632  if (open(SAVEFILE, ">>$savefile")) {
633    # Process each item:
634    foreach my $item (@$data) {
635      # Write data to the save file:
636      print SAVEFILE $item;
637    }
638
639    # Close the save file:
640    close(SAVEFILE);
641  }
642  else {
643    # Report failure and exit:
644    exit_with_error("Unable to write to `$savefile'.", 13);
645  }
646}
647
648# Get task list statistics:
649sub get_stats {
650  my $stats  = shift;
651  my $groups = 0;
652  my $tasks  = 0;
653  my $undone = 0;
654
655  # Open the save file for reading:
656  if (open(SAVEFILE, "$savefile")) {
657    # Process each line:
658    while (my $line = <SAVEFILE>) {
659      # Parse the task record:
660      if ($line =~ /^([^:]*):[^:]*:[1-5]:([ft]):.*:\d+$/) {
661        my $group = lc($1);
662
663        # Count group statistics:
664        if ($stats->{$group}) {
665          # Increment counters:
666          $stats->{$group}->{tasks} += 1;
667          $stats->{$group}->{done}  += ($2 eq 't') ? 1 : 0;
668        }
669        else {
670          # Initialize counters:
671          $stats->{$group}->{tasks}  = 1;
672          $stats->{$group}->{done}   = ($2 eq 't') ? 1 : 0;
673
674          # Increment number of counted groups:
675          $groups++;
676        }
677
678        # Count overall statistics:
679        $tasks++;
680        $undone++ unless ($2 eq 't');
681      }
682    }
683  }
684
685  # Return overall statistics:
686  return $groups, $tasks, $undone;
687}
688
689# Choose first available ID:
690sub choose_id {
691  my @used   = ();
692  my $chosen = 1;
693
694  # Open the save file for reading:
695  if (open(SAVEFILE, "$savefile")) {
696    # Build the list of used IDs:
697    while (my $line = <SAVEFILE>) {
698      push(@used, int($1)) if ($line =~ /:(\d+)$/);
699    }
700
701    # Close the save file:
702    close(SAVEFILE);
703
704    # Find first unused ID:
705    foreach my $id (sort {$a <=> $b} @used) {
706      $chosen++ if ($chosen == $id);
707    }
708  }
709
710  # Return the result:
711  return $chosen;
712}
713
714# Translate due date alias to mask:
715sub translate_mask {
716  my $date = shift;
717
718  if ($date eq 'month') { 
719    return substr(date_to_string(time), 0, 8) . '..';
720  }
721  elsif ($date eq 'year')  { 
722    return substr(date_to_string(time), 0, 5) . '..-..';
723  }
724  else  { 
725    return translate_date($date);
726  }
727}
728
729# Translate due date alias to YYYY-MM-DD string:
730sub translate_date {
731  my $date = shift;
732
733  if    ($date =~ /^\d{4}-[01]\d-[0-3]\d$/) { return $date }
734  elsif ($date eq 'anytime')   { return $date }
735  elsif ($date eq 'today')     { return date_to_string(time) }
736  elsif ($date eq 'yesterday') { return date_to_string(time - 86400) }
737  elsif ($date eq 'tomorrow')  { return date_to_string(time + 86400) }
738  elsif ($date eq 'month')     { return date_to_string(time + 2678400)  }
739  elsif ($date eq 'year')      { return date_to_string(time + 31536000) }
740  else  { exit_with_error("Invalid due date `$date'.", 22) }
741}
742
743# Translate given date to YYYY-MM-DD string:
744sub date_to_string {
745  my @date = localtime(shift);
746  return sprintf("%d-%02d-%02d", ($date[5] + 1900), ++$date[4], $date[3]);
747}
748
749# Draw progress bar:
750sub draw_progressbar {
751  my $percent = shift;
752  my $pointer = ($percent > 0 && $percent < 100) ? '>' : '';
753  return '[' . '=' x int($percent/10) . $pointer .
754         ' ' x ($percent ? (9 - int($percent/10)) : 10) . ']';
755}
756
757# Display given message and immediately terminate the script:
758sub exit_with_error {
759  my $message = shift || 'An unspecified error has occurred.';
760  my $retval  = shift || 1;
761
762  print STDERR NAME . ": $message\n";
763  exit $retval;
764}
765
766# Set up the option parser:
767Getopt::Long::Configure('no_auto_abbrev', 'no_ignore_case', 'bundling');
768
769# Parse command line options:
770GetOptions(
771  # Specifying options:
772  'task|t=s'       => sub { $args{task}     = $_[1] },
773  'group|g=s'      => sub { $args{group}    = $_[1] },
774  'date|d=s'       => sub { $args{date}     = $_[1] },
775  'priority|p=i'   => sub { $args{priority} = $_[1] },
776  'finished|f'     => sub { $args{state}    = 't' },
777  'unfinished|u'   => sub { $args{state}    = 'f' },
778
779  # Additional options:
780  'quiet|q'        => sub { $verbose             = 0 },
781  'verbose|V'      => sub { $verbose             = 1 },
782  'plain|P'        => sub { $coloured            = 0 },
783  'colour|color|C' => sub { $coloured            = 1 },
784  'savefile|s=s'   => sub { $savefile            = $_[1] },
785  'width|w=i'      => sub { $Text::Wrap::columns = $_[1] },
786
787  # General options:
788  'list|l'         => sub { $action = 0 },
789  'add|a=s'        => sub { $action = 1;  $args{task} = $_[1] },
790  'change|c=i'     => sub { $action = 2;  $identifier = $_[1] },
791  'remove|r=i'     => sub { $action = 3;  $identifier = $_[1] },
792
793  'change-group=s' => sub { $action = 12; $identifier = $_[1] },
794  'remove-group=s' => sub { $action = 13; $identifier = $_[1] },
795  'purge-group=s'  => sub { $action = 14; $identifier = $_[1] },
796
797  'change-date=s'  => sub { $action = 22; $identifier = $_[1] },
798  'remove-date=s'  => sub { $action = 23; $identifier = $_[1] },
799  'purge-date=s'   => sub { $action = 24; $identifier = $_[1] },
800
801  'change-old'     => sub { $action = 32 },
802  'remove-old'     => sub { $action = 33 },
803  'purge-old'      => sub { $action = 34 },
804
805  'change-all'     => sub { $action = 42 },
806  'remove-all'     => sub { $action = 43 },
807  'purge-all'      => sub { $action = 44 },
808
809  'undo|U'         => sub { $action = 95 },
810  'groups|G'       => sub { $action = 96 },
811  'stats|S'        => sub { $action = 97 },
812
813  'help|h'         => sub { display_help();    exit 0 },
814  'version|v'      => sub { display_version(); exit 0 },
815);
816
817# Detect superfluous options:
818if (scalar(@ARGV) != 0) {
819  exit_with_error("Invalid option `$ARGV[0]'.", 22);
820}
821
822# Trim group option:
823if (my $value = $args{group}) {
824  $args{group} = substr($value, 0, 10);
825}
826
827# Translate due date option:
828if (my $value = $args{date}) {
829  if ($action == 0) { $args{date} = translate_mask($value) }
830  else { $args{date} = translate_date($value) }
831}
832
833# Translate due date identifier:
834if ($action >= 22 && $action <= 24) {
835  $identifier = translate_mask($identifier);
836}
837
838# Check priority option:
839if (my $value = $args{priority}) {
840  unless ($value =~ /^[1-5]$/) {
841    exit_with_error("Invalid priority `$value'.", 22);
842  }
843}
844
845# Check line width option:
846if ($Text::Wrap::columns < 75) {
847  exit_with_error("Invalid line width `$Text::Wrap::columns'.", 22);
848}
849
850# Perform appropriate action:
851if    ($action ==  0) { display_tasks(\%args) }
852elsif ($action ==  1) { add_task(\%args) }
853elsif ($action ==  2) { change_task($identifier, \%args) }
854elsif ($action ==  3) { remove_task($identifier) }
855elsif ($action == 12) { change_group($identifier, \%args) }
856elsif ($action == 13) { remove_group($identifier) }
857elsif ($action == 14) { purge_group($identifier) }
858elsif ($action == 22) { change_date($identifier, \%args) }
859elsif ($action == 23) { remove_date($identifier) }
860elsif ($action == 24) { purge_date($identifier) }
861elsif ($action == 32) { change_old(\%args) }
862elsif ($action == 33) { remove_old() }
863elsif ($action == 34) { purge_old() }
864elsif ($action == 42) { change_all(\%args) }
865elsif ($action == 43) { remove_all() }
866elsif ($action == 44) { purge_all() }
867elsif ($action == 95) { revert_last_action() }
868elsif ($action == 96) { display_groups() }
869elsif ($action == 97) { display_statistics() }
870
871# Return success:
872exit 0;