ADDED src/0dev.org/commands/tbd/tbd.go Index: src/0dev.org/commands/tbd/tbd.go ================================================================== --- src/0dev.org/commands/tbd/tbd.go +++ src/0dev.org/commands/tbd/tbd.go @@ -0,0 +1,236 @@ +// tbd, a #tag-based dependency tool for low ceremony task tracking +// see README.md for usage tips +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "regexp" + "sort" + "strings" +) + +var ( + // Can be used to extract tags from a line of text + extract *regexp.Regexp +) + +func init() { + // (#)([^\\s]+?) - capture the tag's type and value + // (?:\\.|\\s|$) - complete the match, non-capturing + extract = regexp.MustCompile("(#)([^\\s]+?)(?:[,\\.\\s]|$)") +} + +func main() { + + var ( + exitCode int + handlers []handler = []handler{counting(), tracing()} + ) + + // Parse command line elements and register handlers + if len(os.Args) == 1 { + handlers = append(handlers, cutoff()) + } else { + handlers = append(handlers, matching(os.Args[1:])) + } + + collect, output := collecting() + handlers = append(handlers, collect) + + // Open default input + input, err := os.Open("tbdata") + if err != nil { + fmt.Fprintln(os.Stderr, "Unable to open tbdata.", err.Error()) + os.Exit(1) + } + + exitCode = process(input, handlers) + + for _, v := range output() { + fmt.Println(v) + } + + if err := input.Close(); err != nil { + fmt.Fprintln(os.Stderr, "Unable to close input.", err.Error()) + exitCode = 1 + } + + os.Exit(exitCode) +} + +// Processes input through the provided handlers +func process(input io.Reader, handlers []handler) int { + reader := bufio.NewReader(input) + for { + line, err := reader.ReadString('\n') + + line = strings.TrimSpace(line) + if len(line) > 0 { + handle(handlers, &task{tags: parse(line), value: line}) + } + + // Process errors after handling as ReadString can return both data and error f + if err != nil { + if err == io.EOF { + return 0 + } + fmt.Fprintln(os.Stderr, "Error reading input.", err.Error()) + return 1 + } + } +} + +// The tags struct contains hash (#) and at (@) tags +type tags struct { + hash []string +} + +// as per fmt.Stringer +func (t tags) String() string { + var output bytes.Buffer + for _, tag := range t.hash { + output.WriteByte('[') + output.WriteString(tag) + output.WriteByte(']') + } + return output.String() +} + +// Parse the input for any tags +func parse(line string) (result tags) { + for _, submatch := range extract.FindAllStringSubmatch(line, -1) { + switch submatch[1] { + // Other tag types can be added as well + case "#": + result.hash = append(result.hash, submatch[2]) + } + } + return +} + +// The task struct contains a task description and its tags +type task struct { + tags + nth int + value string + depends tasks +} + +// as per fmt.Stringer +func (t *task) String() string { + return fmt.Sprintf("%d) %s %s", t.nth, t.tags, t.value) +} + +// Alias for a slice of task pointer +type tasks []*task + +// as per sort.Interface +func (t tasks) Len() int { + return len(t) +} + +// as per sort.Interface +func (t tasks) Less(i, j int) bool { + return t[i].nth < t[j].nth +} + +// as per sort.Interface +func (t tasks) Swap(i, j int) { + t[i], t[j] = t[j], t[i] +} + +// The action struct contains flags that determine what to do with a task +type action struct { + // Stop further processing of the task + stop bool +} + +// An alias interface for all task handler functions +type handler func(*task) action + +// Execute handlers against a task +func handle(handlers []handler, t *task) { + for _, h := range handlers { + act := h(t) + if act.stop { + return + } + } +} + +// Returns a counting handler closure that sets the tasks' nth field +func counting() handler { + var at int = 1 + return func(t *task) action { + t.nth = at + at++ + return action{} + } +} + +// Returns a tracing handler closure that sets the tasks' depends field +func tracing() handler { + // Store the last task per tag type + var last = make(map[string]*task) + return func(t *task) action { + for _, tag := range t.hash { + if prev, ok := last[tag]; ok { + t.depends = append(t.depends, prev) + } + last[tag] = t + } + return action{} // default, no action + } +} + +// Returns handler closure that rejects tasks with already-seen tags +func cutoff() handler { + var seen = make(map[string]bool) + return func(t *task) (result action) { + for _, tag := range t.hash { + if seen[tag] { + result.stop = true + } else { + seen[tag] = true + } + } + return + } +} + +// Returns a matching handler closure that filters tasks not matching atleast one tag +func matching(tags []string) handler { + var allowed = make(map[string]bool) + for _, tag := range tags { + allowed[tag] = true + } + + return func(t *task) action { + for _, tag := range t.hash { + if allowed[tag] { + return action{} + } + } + return action{stop: true} + } +} + +// Returns a handler that stores every seen task and an ancillary function to retrieve those +func collecting() (handler, func() tasks) { + var seen = make(map[*task]struct{}) + return func(t *task) action { + seen[t] = struct{}{} + return action{} + }, func() tasks { + seq := make(tasks, 0, len(seen)) + for k, _ := range seen { + seq = append(seq, k) + } + sort.Stable(seq) + return seq + } +} ADDED src/0dev.org/commands/tbd/tbd_test.go Index: src/0dev.org/commands/tbd/tbd_test.go ================================================================== --- src/0dev.org/commands/tbd/tbd_test.go +++ src/0dev.org/commands/tbd/tbd_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "strings" + "testing" +) + +var ( + tbdata string = `implement more #tests +extract code out of #main +support cgi/fast-cgi in #main` +) + +func TestNoArgs(t *testing.T) { + // Configure the handlers ... + // Use a collecting handler to catch all tasks right after they are parsed + // so we have something to easily compare against the final stage's output + collectTest, outputTest := collecting() + collectFinal, outputFinal := collecting() + handlers := []handler{collectTest, counting(), tracing(), cutoff(), collectFinal} + + // ... and execute them against the sample tbdata + reader := strings.NewReader(tbdata) + exit := process(reader, handlers) + + if exit != 0 { + t.Error("Unexpected exit code.", exit) + } + + initial := outputTest() + result := outputFinal() + if len(result) != 2 { + t.Error("Unexpected result length", 2) + } + if result[0] != initial[0] { + t.Error("Unexpected task at result[0]", result[0]) + } + if result[1] != initial[1] { + t.Error("Unexpected task at result[1]", result[1]) + } +} + +func TestArgs(t *testing.T) { + // Configure the handlers ... + // Use a collecting handler to catch all tasks right after they are parsed + // so we have something to easily compare against the final stage's output + collectTest, outputTest := collecting() + collectFinal, outputFinal := collecting() + handlers := []handler{collectTest, counting(), tracing(), matching([]string{"main"}), collectFinal} + + // ... and execute them against the sample tbdata + reader := strings.NewReader(tbdata) + exit := process(reader, handlers) + + if exit != 0 { + t.Error("Unexpected exit code.", exit) + } + + initial := outputTest() + result := outputFinal() + if len(result) != 2 { + t.Error("Unexpected result length", 2) + } + if result[0] != initial[1] { + t.Error("Unexpected task at result[0]", result[0]) + } + if result[1] != initial[2] { + t.Error("Unexpected task at result[1]", result[1]) + } +}