Scott M. Mcdermott

UNIX Systems & Network Administrator
available for contract or salaried positions

gotime.sh

#!/bin/bash
#
# primitive time and task tracking from the shell
#
# invoke as:
#
#       'gostop'        (makes a uuid-named stamp in "stops/"),
#       'gostart'       (makes a uuid-named stamp in "starts/"),
#       'gostat[us]'    (reports which was the last stamp)"
#       'gormlast'      (removes whichever the last stamp was)
#       'godo'          (from this point on, track time against arg)
#
# go* where * != 'do' take an optional single argument:
# the timestamp for the touch, in $HISTTIMEFORMAT (where
# HISTTIMEFORMAT=%Y%m%d%H%M%S), otherwise the default is the
# time of script invocation
#
# uses logs based in ~/.gowhich
#

source ~/lib/sh/include
include fsetenv
require setenv
require set_invocation_name
require bomb
include time
include util

##############################################################################

fsetenv progname        set_invocation_name
setenv  progname        ${progname:2} # strip "go" prefix
setenv  custdir         ~/work/$(<~/.gowhich)
setenv  logbase         $custdir/log
setenv  taskdir         $logbase/tasks

# should get this from the shell, but would have to write too
# much code to accomodate different formats, no time right now
#
setenv  HISTTIMEFORMAT  %Y%m%d%H%M%S
setenv  now_timefmt     $(date +$HISTTIMEFORMAT)
fsetenv now_touchfmt    timefmt_to_touchfmt $now_timefmt

# we use this as either the name of the stamp file or the
# name of a tmp file if we are doing timestamp comparisons
# using "find -newer"
#
setenv  random          $(uuid)

##############################################################################

echo_last ()
{
        printf \
                "in %s as of %s, %s\n" \
                $lastmode \
                ${lasttime:-never} \
                $(
                        ls -1t $taskdir |
                        head -1 |
                        sed -r 's,\.[[:digit:]]+$,,'
                )
}

set_time_envs ()
{
        # requested time
        #
        fsetenv ttime_touchfmt          timefmt_to_touchfmt $arg
        fsetenv ttime_time_t            timefmt_to_time_t $arg

        # one second before
        #
        setenv  just_before_time_t      $((ttime_time_t - 1))
        fsetenv just_before_timefmt     time_t_to_timefmt $just_before_time_t
        fsetenv just_before_touchfmt    timefmt_to_touchfmt $just_before_timefmt

        # one second after
        #
        setenv  just_after_time_t       $((ttime_time_t + 1))
        fsetenv just_after_timefmt      time_t_to_timefmt $just_after_time_t
        fsetenv just_after_touchfmt     timefmt_to_touchfmt $just_after_timefmt
}

##############################################################################

# allow user to forcibly override caution, only if first
# argument of at least two (because we should be in
# maintenance mode i.e. doing specific things with a
# timestamp, not in work-mode defaulting to the current
# time)
#
if (($# > 1)) && [[ "$1" == '-f' ]]; then skip_sanity_check=1; shift; fi

if [[ $progname =~ '^(start|stop|rmstart|rmstop)$' ]]
then
        # argument check for this invocation variant
        #
        if   (($# == 0)); then arg=$now_timefmt # defaults to current time
        elif (($# == 1)); then arg=$1           # or allows passing a time in
        else bomb "more than one argument disallowed"
        fi

        # parse the target time given into usable format
        #
        if [[ "$arg" =~ '[[:digit:]]{14}' ]]
        then set_time_envs $arg
        else bomb "time must be in $HISTTIMEFORMAT format"
        fi
fi

# make sure we can run, and handle the case where we're the
# first invocation for a particular customer
#
test -d $custdir ||
        bomb "no such customer"
mkdir -p $logbase/{starts,stops,tasks} ||
        bomb "failed to make customer dirs"

# sanity check
#
declare -i startcount=$(ls -1 $logbase/starts | count)
declare -i stopcount=$(ls -1 $logbase/stops | count)

# allow flexibility when running maintenance commands
#
if ! ((skip_sanity_check))
then
        ((stopcount > startcount)) &&
                bomb "impossible: more stops than starts"
        (((startcount - stopcount) > 1)) &&
                bomb "impossible: delta greater than one"
        (((startcount - stopcount) < 0)) &&
                bomb "impossible: delta less than zero"
fi

# if the total count is even, then every start was stopped
# (meaning we are stopped); otherwise, we must be started.
#
if (((startcount + stopcount) % 2))
then lastmode=start
else lastmode=stop
fi

# determine the last start or stop file we touched and what time that was
#
lastdir=$logbase/${lastmode}s # lastmode + 's' (plural)
lastfile=$(ls -1t $lastdir | head -1)
lastpath=$lastdir/$lastfile
[[ "$lastfile" ]] && lasttime=$(find $lastpath -printf ${HISTTIMEFORMAT//%/%T})

##############################################################################

case $progname in

# query the current state (i.e. what was the last timestamp)
#
(stat*)

        (($# == 0)) || bomb "no argument allowed"
        echo_last
        ;;

# remove the most recent stamp that we made (i.e. the 'last' one)
#
(rmlast)

        (($# == 0)) || bomb "no argument allowed"
        rm $lastpath
        ;;

# try to remove an explicitly designated timestamp file
#
(rmstop|rmstart)

        newer_than=/tmp/newer-$random
        older_than=/tmp/older-$random
        invocation=${progname#rm*}

        (($# == 1)) || bomb "requires an argument"

        touch -t $just_before_touchfmt $newer_than
        touch -t $just_after_touchfmt $older_than

        potential_matches=($(
                for file in $(
                        find $logbase/${invocation}s/ \
                                -type f \
                                -newer $newer_than
                ); do
                        if test \
                                $file -nt $newer_than -a \
                                $file -ot $older_than
                        then
                                echo $file
                        fi
                done
        ))

        ((${#potential_matches[@]} > 1)) &&
                bomb "too many matches in removal"
        ((${#potential_matches[@]} == 0)) &&
                bomb "no matches for removal"

        rm $potential_matches

        ;;

# make new timestamp to denote "billing" or "not billing" state
#
(stop|start)

        # whichever was specified, if it's the same as the
        # current mode, we're in the wrong sequence; we
        # should be flipping between the two at each
        # invocation
        #
        if [[ "$lastmode" == $progname ]] && ! ((skip_sanity_check))
        then bomb "already $(echo_last)"
        fi

        # make the file with a random name to prevent
        # collision: the date is what we want to store,
        # however, and it must remain intact (uses mtime)
        #
        setenv logdir $logbase/${progname}s

        test -d $logdir ||
                bomb "logdir is not a directory"

        touch \
                -t ${ttime_touchfmt:-$now_touchfmt} \
                $logdir/$random
        ;;

(do)
        # no args, just display last one and exit
        # XXX merge this with gostat
        #
        (($# == 0)) && { ls -1t $taskdir | head -1; exit 0; }

        what=$(IFS=_; echo "$*")
        task_maxlen=63 # "YYMMDDHHmmss description" ends at col 78

        ((${#what} > $task_maxlen)) &&
                bomb "task length ${#what} exceeds max $task_maxlen"

        # prevent bugs from fucking us *all* up
        #
        bash ~/bin/backup-tasks.sh || bomb "backup failed"

        # seem to do this all the time...
        #
        [[ $what == "mail" ]] && bomb "messaging, you fool!"

        # make sure either this or use full paths
        #
        cd $taskdir || bomb "cannot change to task dir"

        i=0
        while true; do
                if ! test -f $what.$i
                then
                        if touch $what.$i
                        then exit 0
                        else bomb "error touching task whatfile"
                        fi
                else
                        let i++
                fi
        done

        ;;

(*)
        bomb "no such invocation name"
        ;;

esac