In my "A Vim and GTD Workflow" post I mentioned a helper script that I was working on to help keep everything in shape. Here are some of the details…


I wrote the script in bash because it is doing a fairly simple set of tasks. As the features grow I’ll probably want to re-write this in python at some point.

I’ve called the script check_status.sh for now and it should be executed with your CWD set to the status directory that you set up for your GTD stuff.

The beginning part of the script is fairly generic and follows the format of many of my other bash scripts:

#!/bin/bash

# Copyright 2015 Curtis Sand
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

SCRIPT=$0

STATUS_FILE='status.txt'

usage () {
    echo -e "$SCRIPT [-h|--help|-f|--file-presence|-c|--status-contents]\n"
    local msg="Validate the contents of the ${STATUS_FILE} file compared to"
    msg="${msg} the task files present on the filesystem."
    msg="${msg} The default behaviour is to execute all checks"
    msg="${msg} but can be overridden with one of the options."
    echo $msg|fmt
    echo "  Options:"
    echo "    -h|--help            : print this help message"
    echo "    -f|--file-presence   : Check that the sub-directories"
    echo "                           contain all task files."
    echo "    -c|--status-contents : Check that ${STATUS_FILE} contains"
    echo "                           references to all task files."
    exit 1;
}

It starts out with the usual shebang, license preamble and usage function. Here I’ve defined the name of the status file so that you can easily change it if necessary.

Following is the command line parsing code:

ARGS=`getopt -o "h,f,c" -l "help,file-presence,status-contents" \
    -n "check_status.sh" -- "$@"`
if [ $? -ne 0 ]; then
    # bad arguments found
    exit 1;
fi
eval set -- "$ARGS"

# logic flags
FILE_PRESENCE=false
EXECUTE_BOTH=true

while true ; do
    case "$1" in
        -h|--help) usage ; shift ;;
        -f|--file-presence) FILE_PRESENCE=true; EXECUTE_BOTH=false; shift ;;
        -c|--status-contents) EXECUTE_BOTH=false; shift ;;
        --) shift ; break ;;
        *) echo "Internal error!" ; exit 1 ;;
    esac
done

Next comes an if statement that performs either the "file-presence" check or the "status-contents" check:

RC=0

if ! $FILE_PRESENCE; then
    echo "Default Logic: checking that \"${STATUS_FILE}\" is complete..."
    IFS=$'\n'  # use newline delimiter instead of space
    task_dirs=$(find . -maxdepth 1 -type d|sed -e 's@./@@'|grep -v "\.")
    tasks=$(find $task_dirs -type f -not -iname ".*swp" | \
            sed -e 's@^./@@' -e 's@`<@@g' -e 's@>`_@@')
    missing=
    for task in ${tasks}; do
        grep ${task} ${STATUS_FILE} -q
        if [[ "$?" -eq "0" ]]; then
            echo -n " ."
        else
            missing="${missing}\"${task}\"...\tNO \"${STATUS_FILE}\" ENTRY"
            RC=1
        fi
    done
    echo -e "${missing}"
else
    echo "Reverse Logic: checking all files referenced in \"${STATUS_FILE}\" exist..."
    tasks=$(grep "\`<" ${STATUS_FILE}|sed -e "s@\`<@@" -e "s@>\`_@@")
    missing=""
    for task in ${tasks}; do
        if [[ -f $task ]]; then
            echo -n " ."
        else
            missing="${missing}\n\"$task\"...\tFILE MISSING";
            RC=1
        fi;
    done
    echo -e "${missing}"
fi

That is the main meat of the script so lets disect it a little bit.

The first part of the if statement is the code to verify that the status file contains references to all the task files. First the IFS variable is set so that the output of the following subcommands can be delimited on \n characters instead of on spaces. Next the list of task directories is retrieved by running find with the test "-type d". Then the task directories are searched for task files and the filenames are cleaned up by piping the find output through sed. Finally the task files are iterated through and the status file is grep‘d through to determine if it contains an appropriate reference to each task.

The else portion of the if statement is the check to ensure that for each reference in the status file there exists a file in one of the state directories. The parsing code here is a bit simpler; the status file is searched via grep for any links of the form:

`<task_dir/task_name>`_

Then the paths are cleaned up and then checked to ensure they exist.

Finally, the last part of the script recursively calls the script again if the user did not specify which specific test to use. This is how the default "execute all checks" behavior is implemented:

if $EXECUTE_BOTH; then
    if ! $FILE_PRESENCE; then
        args="--file-presence"
    else
        args="--status-contents"
    fi
    $SCRIPT $args
fi
exit ${RC}

I’ll also include an example of my GTD setup so it is more clear how things look when it’s all set up. First here is the filesystem that was set up:

../status/
../status/status.txt   # This is the status file.
../status/backlog/
../status/backlog/do_something
../status/in-progress/
../status/in-progress/some_project
../status/done/
../status/done/another_task

And the contents of the status.txt file would then look something like:

Status
======

TOC
---

- `in-progress`_
- `backlog`_
- `done`_

in-progress
-----------

- `<in-progress/some_project>`_

backlog
-------

- `<backlog/do_something>`_

done
----

- `<done/another_task>`_
" "