#include #include #include #include #include #include #include #include #include #include #define ACTS_MAX 20 #define HASH_SIZE 971 #define CONFIGFILE "/.timeplanrc" /* HOME prepended */ #define DEF_TIMEPLAN "/.timeplan" /* HOME prepended */ #define MARK_DAY "_days" #define DEF_FORMTOTAL ":6" #define LENGTH(x) (sizeof((x))/sizeof(*(x))) #define WHERE1 " in \"%s\" on line %d!\n" #define WHERE2 finame,line #define NL "\r\n" #define ERRH1 stderr,"%s: " #define ERRH2 pname #define ERRNO1 ": %s!\n" #define ERRNO2 strerror(errno) #define FAKEUSE =0 static const char version[]="This is TimePlan, version 1.0.1\n"; static const char *pname; static char *finame; static FILE *fi; static int verbose=0,tree=0,doaverage=0; static char *formtotal=DEF_FORMTOTAL; static char buf[LINE_MAX]; static int line=0; static enum { SORT_NO,SORT_TIMETOT,SORT_STORES } sortby=SORT_TIMETOT; static unsigned lifetime_days=0; static unsigned long lifetime_seq=1; static void usage(void) { fprintf(stderr,"\ %s\ This command summarizes the timelog information:\n\ \n\ Usage: timeplan [-u|--unsort] [-s|--stores] [-T|--timetot] [-t|--tree]\n\ [-c|--condition ] [-f|--formtotal ]\n\ [-r|--rule :]\n\ [-v|--verbose] [-h|--help] [-V|--version]\n\ \n\ -u, --unsort\t\tDon't sort the result in any way\n\ -s, --stores\t\tSort the result by stores count\n\ -T, --timetot\t\tSort the result by total time (default)\n\ -t, --tree\t\tOrganize data as hierarchy tree\n\ -a, --average\t\tDisplay all times as average-per-day value\n\ -c, --condition\tDefine condition variable\n\ -f, --formtotal\tFormat \"Total\" column (\"text *val/val:width text\")\n\ -r, --rule\t\tAdd to the end of .rc file this rule (':'->'\\t')\n\ -v, --verbose\t\tInform about phases of transfer\n\ -h, --help\t\tPrint a summary of the options\n\ -V, --version\t\tPrint the version number\n\ ",version); exit(EXIT_FAILURE); } static const struct option longopts[]={ {"unsort" ,0,0,'u'}, {"stores" ,0,0,'s'}, {"timetot" ,0,0,'T'}, {"tree" ,0,0,'t'}, {"average" ,0,0,'a'}, {"condition",1,0,'c'}, {"formtotal",1,0,'f'}, {"rule" ,1,0,'r'}, {"verbose" ,0,0,'v'}, {"help" ,0,0,'h'}, {"version" ,0,0,'V'}}; static int calctime(int timetot,int at,int of) { /* FIXME: better distribution */ return timetot/of; } static struct action { struct action *next; int timetot,stores; unsigned long last_seq; char what[1]; } *hashtable[HASH_SIZE]; static int hashtable_tot=0; static int dumpaction(const struct action *action) { int tot,mins FAKEUSE,hours FAKEUSE,days FAKEUSE,origtot; char *s,fmt[]="%?d"; if (action) { tot=(action->timetot+30)/60; if (doaverage && lifetime_days) tot/=lifetime_days; mins=tot; hours=mins/60; days=hours/24; mins%=60; hours%=24; } else tot=0; origtot=tot; for (s=formtotal;*s;s++) switch (*s) { case '*': case '/': { long l; char *end,*s2; if (s[1]==*s) { s++; goto dump; } for (s2=s+1;isdigit(*s2);s2++); l=strtol(s+1,&end,10); if (end!=s2) { fprintf(ERRH1"Number parse error at column %d of formtotal string!\n",ERRH2,s-formtotal); exit(EXIT_FAILURE); } if (*s=='*') tot*=l; else tot/=l; s=s2-1; break; } case ':': if (s[1]==*s) { s++; goto dump; } if (!isdigit(s[1])) goto dump; if (action) { fmt[1]=s[1]; printf(fmt,tot); tot=origtot; } else origtot+=s[1]-'0'; s+=1; break; default: dump: if (action) putchar(*s); else origtot++; } if (action) printf("=%03d/%02d:%02d %4d\t%s\n",days,hours,mins, action->stores,action->what); return origtot; } #define A (*Ap) #define B (*Bp) #define FUNC(which) \ static int sort_##which(const struct action **Ap,const struct action **Bp) \ { return (B->which>A->which)-(A->which>B->which); } FUNC(timetot) FUNC(stores) #undef FUNC #undef B #undef A static void dumphashtable(void) { int item; struct action *action; struct action **sorta FAKEUSE,**sorti FAKEUSE; if (sortby!=SORT_NO) { int totalwidth=dumpaction(NULL); while (totalwidth-->5) putchar(' '); puts("Total Day Hr Mi Stor\tDescription"); if (!(sorta=malloc(sizeof(*sorta)*hashtable_tot))) { fprintf(ERRH1"malloc() of %d pointers"ERRNO1,ERRH2,hashtable_tot,ERRNO2); exit(EXIT_FAILURE); } sorti=sorta; } for (item=0;itemnext) if (sortby==SORT_NO) dumpaction(action); else *sorti++=action; if (sortby==SORT_NO) return; assert(sorti==sorta+hashtable_tot); qsort(sorta,hashtable_tot,sizeof(*sorta), (int (*)(const void *,const void *))(sortby==SORT_TIMETOT ? sort_timetot : sort_stores)); for (sorti=sorta;sortinext) if (!strcasecmp(action->what,what)) break; if (!action) { if (!(action=malloc(sizeof(*action)+strlen(what)))) { fprintf(ERRH1"malloc() for \"%s\""ERRNO1,ERRH2,what,ERRNO2); exit(EXIT_FAILURE); } action->next=NULL; action->timetot=0; action->stores=0; strcpy(action->what,what); *actionp=action; hashtable_tot++; } else { if (action->last_seq==lifetime_seq) { if (verbose) puts("storeone: preventing duplicate"); return; /* prevent duplicates like: TV-Music --> TV-Fun-Music-Fun */ } } action->last_seq=lifetime_seq; action->timetot+=length; action->stores++; } struct textlist { struct textlist *next; int line; char text[1]; }; struct textlist *conditions,**conditionstail=&conditions; static int iscondition(const char *text) { struct textlist *cond; for (cond=conditions;cond;cond=cond->next) if (!strcasecmp(text,cond->text)) return 1; return 0; } static void addlist(struct textlist ***tail,const char *text,int line) { struct textlist *item; if (!(item=malloc(sizeof(*item)+strlen(text)))) { fprintf(ERRH1"malloc() for \"%s\""ERRNO1,ERRH2,text,ERRNO2); exit(EXIT_FAILURE); } strcpy(item->text,text); item->next=NULL; **tail=item; *tail=&item->next; } static struct textlist *modifies,**modifiestail=&modifies,*modifiescmdl,**modifiescmdltail=&modifiescmdl; static int modifies_tot; static struct modistruct { regex_t regex; char *dst; } *modistructs; static void modify_load(void) { static int inited=0; char *finame,*s,*s2; FILE *fi; int line=0,m; struct textlist *item; char buf[LINE_MAX]; if (inited) return; inited=1; if (asprintf(&finame,"%s"CONFIGFILE,getenv("HOME"))==-1) { fprintf(ERRH1"Config filename allocation",ERRH2); exit(EXIT_FAILURE); } errno=0; if (!(fi=fopen(finame,"rt"))) { if (errno!=ENOENT) fprintf(ERRH1"Config file \"%s\" read"ERRNO1,ERRH2,finame,ERRNO2); return; } while (clearerr(fi),fgets(buf,sizeof(buf),fi)==buf) { line++; if ((s=strchr(buf,'\n')) && !s[1]) *s='\0'; else { fprintf(ERRH1"fgets(3) results not newline-terminated"WHERE1,ERRH2,WHERE2); exit(EXIT_FAILURE); } if (!*buf || *buf=='#') { nextline: continue; } if (*buf!='R') { fprintf(ERRH1"Unrecognized syntax"WHERE1,ERRH2,WHERE2); exit(EXIT_FAILURE); } s=buf+1; for (;;) { int isplus; while (isspace(*s)) s++; if (*s==':') break; if (!isalpha(*s)) { fprintf(ERRH1"Invalid character at offset %d"WHERE1,ERRH2,s-buf+1,WHERE2); exit(EXIT_FAILURE); } for (s2=s+1;isalpha(*s2);s2++); if (*s2!='+' && *s2!='-') { fprintf(ERRH1"Only plus ('+') or minus ('-'), not '%c' expected at offset %d"WHERE1,ERRH2,*s2,s2-buf+1,WHERE2); exit(EXIT_FAILURE); } isplus=(*s2=='+'); *s2++='\0'; if (iscondition(s)!=isplus) goto nextline; s=s2; } s++; /* Skip ':' */ addlist(&modifiestail,s,line); modifies_tot++; } if (!feof(fi) || ferror(fi)) { fprintf(ERRH1"fgets(3) \"%s\""ERRNO1,ERRH2,finame,ERRNO2); exit(EXIT_FAILURE); } if (fclose(fi)) fprintf(ERRH1"fclose(3) \"%s\""ERRNO1,ERRH2,finame,ERRNO2); *modifiestail=modifiescmdl; /* add all command-line arguments to the end */ if (!(modistructs=malloc(sizeof(*modistructs)*modifies_tot))) { fprintf(ERRH1"malloc() of %d modistructs's"ERRNO1,ERRH2,modifies_tot,ERRNO2); exit(EXIT_FAILURE); } for (m=0,item=modifies;mnext) { #define line (item->line) if (!(s=strchr(item->text,'\t'))) { fprintf(ERRH1"No delimiting tab-character found"WHERE1,ERRH2,WHERE2); exit(EXIT_FAILURE); } *s++='\0'; modistructs[m].dst=s; if (verbose) printf("regcomp: %s -> %s\n",item->text,s); if (regcomp(&modistructs[m].regex,item->text,REG_EXTENDED|REG_ICASE)) { fprintf(ERRH1"regcomp() failed for \"%s\" (%s)"WHERE1,ERRH2,item->text,ERRNO2,WHERE2); exit(EXIT_FAILURE); } #undef line } assert(!item); free(finame); } static char *modify(char *what) { regmatch_t matches[10]; int i,m; int doprep,willprep; static char modbuf1[sizeof(buf)],modbuf2[sizeof(modbuf1)]; char *src=what,*dstbase=modbuf1,*dst=dstbase; const char *start FAKEUSE,*end FAKEUSE,*patt; const struct textlist *item; if (verbose) printf("modify: %s\n",what); modify_load(); for (m=0,item=modifies,willprep=1;;m++,item=item->next) { enum { PATT_START,PATT_MID,PATT_END,PATT_TERM } pattpos; for (doprep=willprep;doprep>=0;doprep--) { if (doprep) { *dst++='-'; for (patt=src;;patt++) { if (*patt=='-' || !*patt) { if (dst[-1]!='-') *dst++='-'; if (!*patt) break; } else *dst++=*patt; } } else { if (!item) return src; i=regexec(&modistructs[m].regex,src,LENGTH(matches),matches,0); if (i==REG_NOMATCH) { willprep=0; break; } if (i) { fprintf(ERRH1"regexec() failed for \"%s\""ERRNO1,ERRH2,item->text,ERRNO2); exit(EXIT_FAILURE); } willprep=1; if (verbose) printf("matched: %s -> %s\n",item->text,modistructs[m].dst); pattpos=PATT_START; patt=NULL; while (pattpos!=PATT_TERM) { switch (pattpos) { case PATT_START: start=src; end=src+matches->rm_so; pattpos=PATT_MID; patt=modistructs[m].dst; break; case PATT_MID: if (!*patt) { pattpos=PATT_END; patt=NULL; continue; } else if (*patt=='@') { start=src; end=src+strlen(src); } else if (*patt>='0' && *patt<='9') { regmatch_t *match=matches+(*patt-'0'); if (match->rm_so==-1 || match->rm_eo==-1) { fprintf(ERRH1"Trying to substitute '%c' but no \"matches\" entry not set for \"%s\""WHERE1, ERRH2,*patt,item->text,WHERE2); exit(EXIT_FAILURE); } if (match->rm_so>match->rm_eo) { fprintf(ERRH1"Trying to substitute '%c' start>end (%d>%d) for \"%s\""WHERE1, ERRH2,*patt,match->rm_so,match->rm_eo,item->text,WHERE2); exit(EXIT_FAILURE); } start=src+match->rm_so; end=src+match->rm_eo; } else { start=patt; end=patt+1; } patt++; break; case PATT_END: start=src+matches->rm_eo; end=src+strlen(src); pattpos=PATT_TERM; /* assumed: patt=NULL; */ break; default: assert(0); } if ((dst-dstbase+(end-start))>=sizeof(modbuf1)-1/* -1 for '-' during prepping */) { fprintf(ERRH1"Maximum buffer size exceeded during substition for \"%s\""WHERE1, ERRH2,item->text,WHERE2); exit(EXIT_FAILURE); } memcpy(dst,start,end-start); dst+=end-start; } } /* SWAP buffers: */ if (dst==dstbase || (dst==dstbase+1 && *dstbase=='-')) return NULL; *dst='\0'; if (src==what) { assert(dstbase==modbuf1); src=dstbase; dst=dstbase=modbuf2; } else { char *swap; assert((src==modbuf1 && dstbase==modbuf2) ||(src==modbuf2 && dstbase==modbuf1)); swap=src; src=dstbase; dst=dstbase=swap; } } /* for (doprep) */ } /* NOTREACHED */ } static struct modifycache { struct modifycache *next; char src[1]; /* dst[1] follows after '\0' */ } *mcachetable[HASH_SIZE]; static unsigned long modifycache_hits; static char *modify_cached(char *what) { struct modifycache **mcachep,*mcache; char *dst; size_t whatl; if (!*what) return(NULL); whatl=strlen(what); if (verbose) printf("modify_cached: %s\n",what); for (mcachep=mcachetable+calchash(what);(mcache=*mcachep);mcachep=&mcache->next) if (!strcasecmp(mcache->src,what)) break; if (!mcache) { dst=modify(what); if (!(mcache=malloc(sizeof(*mcache)+whatl+1+(dst?strlen(dst):0)))) { fprintf(ERRH1"malloc() for \"%s\""ERRNO1,ERRH2,what,ERRNO2); exit(EXIT_FAILURE); } mcache->next=NULL; memcpy(mcache->src,what,whatl+1); if (dst) strcpy(mcache->src+whatl+1,dst); else mcache->src[whatl+1]='\0'; /* dst will be empty */ *mcachep=mcache; } else { if (verbose) printf("cache hit.\n"); /* if (verbose) would be performance hit */ modifycache_hits++; } if (!mcache->src[whatl+1]) return(NULL); return(mcache->src+whatl+1); } static void modify_cached_stats(void) { struct modifycache **mcachep,*mcache; unsigned long depth,maxdepth=0,entries_total=0; #define MODIFYCACHE_CALLS (modifycache_hits+entries_total) for (mcachep=mcachetable;mcachepnext) depth++; if (depth>maxdepth) maxdepth=depth; entries_total+=depth; } printf("modify_cached cache stats: hits=%u.%02u%% (%lu/%lu), HASH_SIZE=%d, maxdepth=%lu\n", (unsigned)( 100*modifycache_hits/MODIFYCACHE_CALLS ), (unsigned)((10000*modifycache_hits/MODIFYCACHE_CALLS)%100), modifycache_hits,MODIFYCACHE_CALLS, HASH_SIZE,maxdepth); #undef MODIFYCACHE_CALLS } static void store(char *what,int length) { char ce_trash,*ce,*ceo=&ce_trash; if (!(what=modify_cached(what))) { if (verbose) puts("discarded."); return; } if (verbose) printf("store: %d: %s\n",length,what); while ((ce=(tree?strrchr(what,'-'):strchr(what,'-')))) { if (!tree) *ce='\0'; storeone(what,length); if (!tree) { *ce='-'; what=ce+1; } else { *ceo='-'; *(ceo=ce)='\0'; } } storeone(what,length); if (tree) *ceo='-'; lifetime_seq++; } static void hit(time_t t,char *bufaction) { static time_t last=-1; static char bufbackup[sizeof(buf)]; char *acts[ACTS_MAX],*s; int timetot,acti,i; if (verbose) printf("hit: %ld: %s\n",t,bufaction); if (last!=-1) { if (t=LENGTH(acts)) { fprintf(ERRH1"Too many '+'-delimited actions (%d)"WHERE1,ERRH2,acti,WHERE2); exit(EXIT_FAILURE); } } timetot=t-last; for (i=0;i=7) fprintf(ERRH1"Non-parsable week-day name \"%s\""WHERE1,ERRH2,wday,WHERE2); tm.tm_sec=tm.tm_min=tm.tm_hour=0; tm.tm_wday=tm.tm_yday=-1; tm.tm_isdst=0; /* FIXME */ tm.tm_mon--; tm.tm_year-=1900; t=mktime(&tm); if (t==-1 || tm.tm_wday<0 || tm.tm_wday>6 || tm.tm_mday<1 || tm.tm_mday>31 || tm.tm_mon <0 || tm.tm_mon >11 || tm.tm_year<80 || tm.tm_year>150 ) { fprintf(ERRH1"Incorrect timestamp \"%s\""WHERE1,ERRH2,buf,WHERE2); exit(EXIT_FAILURE); } if (i<7 && tm.tm_wday!=i) fprintf(ERRH1"Non-matching week-day, given \"%s\", calculated \"%s\""WHERE1,ERRH2,days[i],days[tm.tm_wday],WHERE2); #define WANTED (60*60*24) if (basetime!=-1 && basetime+WANTED!=t) fprintf(ERRH1"Non-continuous timestamp (%ld, wanted %d) \"%s\""WHERE1,ERRH2,t-basetime,WANTED,buf,WHERE2); #undef WANTED basetime=t; store(MARK_DAY,0); lifetime_days++; continue; } if (!(isdigit(buf[0]) && isdigit(buf[1]) && buf[2]==':' && isdigit(buf[3]) && isdigit(buf[4]) && (buf[5]=='-' || (buf[5]==':' && isdigit(buf[6]) && isdigit(buf[7]) && buf[8]=='-')) ) || (sec=0,sscanf(buf,(buf[5]=='-'?"%d:%d-":"%d:%d:%d-"),&hour,&min,&sec)!=2+(buf[5]==':')) || hour<0 || hour>23 || min <0 || min >59 || sec <0 || sec >59 ) { fprintf(ERRH1"Incorrect day-time \"%s\""WHERE1,ERRH2,buf,WHERE2); exit(EXIT_FAILURE); } if (basetime==-1) { fprintf(ERRH1"Day-time found but no basetime timestamp set"WHERE1,ERRH2,WHERE2); exit(EXIT_FAILURE); } currtime=basetime+(hour*60+min)*60+sec; hit(currtime,buf+(buf[5]=='-'?6:9)); } if (!feof(fi) || ferror(fi)) { fprintf(ERRH1"fgets(3) \"%s\""ERRNO1,ERRH2,finame,ERRNO2); exit(EXIT_FAILURE); } if (fi!=stdin && fclose(fi)) fprintf(ERRH1"fclose(3) \"%s\""ERRNO1,ERRH2,finame,ERRNO2); if (verbose) modify_cached_stats(); dumphashtable(); return EXIT_SUCCESS; }