#!/usr/bin/perl -w # # Tool to parse todo files. # use strict; use Template; use Getopt::Long qw(:config no_ignore_case no_auto_abbrev bundling); # Parse a todo file and add tasks to \%todos. sub parse_todo { my ($todos, $path, $file, $filter) = @_; #print "$path...\n"; my $owners; local $_; local *FILE; # Check for duplicates and create task list. die "duplicate path: $path" if exists $$todos{''}; $$todos{''} = [ ]; my $tasks = $$todos{''}; # Open file. open FILE, "<$file" or die "$file: $!"; # Read each lines. while () { chomp; # Task line. /^([-+=x]) (?:\[(\d\d\/\d\d\/\d{4})\] |)(?:\(([^)]+)\) |)(.+)$/ and do { my ($state, $deadline, $owners, $text) = ($1, $2, $3 || $owners, $4); my @owners; @owners = split /, */, $owners if defined $owners; # Read next lines. while () { chomp; # Continued task, new paragraph. /^ $/ and $text .= "\n", next; # Continued task. /^ (.*)$/ and $text .= ' ' . $1, next; last; } # Add task. my %task = ( state => $state, deadline => $deadline, owners => [ @owners ], text => $text, ); push @$tasks, \%task if &$filter (%task); last unless defined $_; redo; }; # Default owner line. /^\(([^)]+)\)$/ and $owners = $1, next; # Subtask line. /^[-\w\d]+$/ and do { my $path = $path . '/' . $_; $$todos{$_} ||= { }; my $todos = $$todos{$_}; die "duplicate path: $path" if exists $$todos{''}; $$todos{''} = [ ]; $tasks = $$todos{''}; next; }; # Empty line. /^$/ and next; # Else, die. die 'Invalid format'; } # Close file. close FILE; } # Read dir and parse each todo files it contains. sub parse_dir { my ($todos, $path, $dir, $filter) = @_; local $_; local @_; local *DIR; # Read files list. opendir DIR, $dir or die "$dir: $!"; @_ = readdir DIR; closedir DIR; # Filter todo files & dirs lists. my @todofiles = grep { !/^\./ && /\.todo$/ && -f "$dir/$_" } @_; s/\.todo$// foreach @todofiles; my @tododirs = grep { !/^\./ && -d "$dir/$_" } @_; # Create empty hashes. $$todos{$_} ||= { } foreach @todofiles, @tododirs; # Process each todo files. parse_todo ($$todos{$_}, "$path/$_", "$dir/$_.todo", $filter) foreach @todofiles; # Recurse into each dirs. parse_dir ($$todos{$_}, "$path/$_", "$dir/$_", $filter) foreach @tododirs; } # Drop empty nodes. sub drop_empty { my ($todos) = @_; for (keys %$todos) { if ($_) { # Recurse. drop_empty ($$todos{$_}); # Drop if empty. delete $$todos{$_} unless scalar keys %{$$todos{$_}}; } else { delete $$todos{$_} unless scalar @{$$todos{$_}}; } } } # Output todo tree file. sub output_tree { my ($config, $t) = @_; my $tt = new Template ({ INCLUDE_PATH => $$config{'template-dir'}, PRE_CHOMP => 1, POST_CHOMP => 1, INTERPOLATE => 1, }); $tt->process ('todo.' . $$config{'format'} . '.tt', { 'tasks' => $t, 'owner' => !defined $$config{'owner'}, }) or die $tt->error; } # Print short help. sub usage { print < '../todo', 'template-dir' => '.', 'format' => 'text', ); GetOptions (\%config, 'todo-dir|t=s', 'template-dir|T=s', 'format|f=s', 'keep-empty|k', 'owner|o=s', 'open|O', 'scheduled|s', 'dump|d', 'help|h', ) or die; usage () and exit 0 if (exists ($config{help})); return %config; } my %todos; my %config = config (); my $filter = sub { my %t = @_; if ($config{'owner'}) { return 0 unless grep { $_ eq $config{'owner'} } @{$t{'owners'}}; } if ($config{'open'}) { return 0 unless $t{'state'} =~ /[-=]/; } if ($config{'scheduled'}) { return 0 unless defined $t{'deadline'}; } 1; }; parse_dir (\%todos, '', $config{'todo-dir'}, $filter); drop_empty \%todos unless $config{'keep-empty'}; if (exists ($config{'dump'})) { use Data::Dumper; print Dumper \%todos; } else { output_tree (\%config, \%todos); }