Check-in [61b0c5b57a]
Overview
SHA1:61b0c5b57a33c6457aa09fcd481cf4706f7155ff
Date: 2015-01-26 20:30:51
User: spaskalev
Comment:Added tbd from gophergala/tbd :)
Timelines: family | ancestors | descendants | both | trunk
Downloads: Tarball | ZIP archive
Other Links: files | file ages | folders | manifest
Tags And Properties
Context
2015-01-26
20:40
[96db071674] Added tbd.slide as well (user: spaskalev, tags: trunk)
20:30
[61b0c5b57a] Added tbd from gophergala/tbd :) (user: spaskalev, tags: trunk)
2015-01-14
20:46
[3ea011cf07] Use for range in mtf's encoder for the position table. (user: spaskalev, tags: trunk)
Changes

Added src/0dev.org/commands/tbd/tbd.go version [bf4f228fed].

            1  +// tbd, a #tag-based dependency tool for low ceremony task tracking
            2  +// see README.md for usage tips
            3  +package main
            4  +
            5  +import (
            6  +	"bufio"
            7  +	"bytes"
            8  +	"fmt"
            9  +	"io"
           10  +	"os"
           11  +	"regexp"
           12  +	"sort"
           13  +	"strings"
           14  +)
           15  +
           16  +var (
           17  +	// Can be used to extract tags from a line of text
           18  +	extract *regexp.Regexp
           19  +)
           20  +
           21  +func init() {
           22  +	// (#)([^\\s]+?)   - capture the tag's type and value
           23  +	// (?:\\.|\\s|$)   - complete the match, non-capturing
           24  +	extract = regexp.MustCompile("(#)([^\\s]+?)(?:[,\\.\\s]|$)")
           25  +}
           26  +
           27  +func main() {
           28  +
           29  +	var (
           30  +		exitCode int
           31  +		handlers []handler = []handler{counting(), tracing()}
           32  +	)
           33  +
           34  +	// Parse command line elements and register handlers
           35  +	if len(os.Args) == 1 {
           36  +		handlers = append(handlers, cutoff())
           37  +	} else {
           38  +		handlers = append(handlers, matching(os.Args[1:]))
           39  +	}
           40  +
           41  +	collect, output := collecting()
           42  +	handlers = append(handlers, collect)
           43  +
           44  +	// Open default input
           45  +	input, err := os.Open("tbdata")
           46  +	if err != nil {
           47  +		fmt.Fprintln(os.Stderr, "Unable to open tbdata.", err.Error())
           48  +		os.Exit(1)
           49  +	}
           50  +
           51  +	exitCode = process(input, handlers)
           52  +
           53  +	for _, v := range output() {
           54  +		fmt.Println(v)
           55  +	}
           56  +
           57  +	if err := input.Close(); err != nil {
           58  +		fmt.Fprintln(os.Stderr, "Unable to close input.", err.Error())
           59  +		exitCode = 1
           60  +	}
           61  +
           62  +	os.Exit(exitCode)
           63  +}
           64  +
           65  +// Processes input through the provided handlers
           66  +func process(input io.Reader, handlers []handler) int {
           67  +	reader := bufio.NewReader(input)
           68  +	for {
           69  +		line, err := reader.ReadString('\n')
           70  +
           71  +		line = strings.TrimSpace(line)
           72  +		if len(line) > 0 {
           73  +			handle(handlers, &task{tags: parse(line), value: line})
           74  +		}
           75  +
           76  +		// Process errors after handling as ReadString can return both data and error f
           77  +		if err != nil {
           78  +			if err == io.EOF {
           79  +				return 0
           80  +			}
           81  +			fmt.Fprintln(os.Stderr, "Error reading input.", err.Error())
           82  +			return 1
           83  +		}
           84  +	}
           85  +}
           86  +
           87  +// The tags struct contains hash (#) and at (@) tags
           88  +type tags struct {
           89  +	hash []string
           90  +}
           91  +
           92  +// as per fmt.Stringer
           93  +func (t tags) String() string {
           94  +	var output bytes.Buffer
           95  +	for _, tag := range t.hash {
           96  +		output.WriteByte('[')
           97  +		output.WriteString(tag)
           98  +		output.WriteByte(']')
           99  +	}
          100  +	return output.String()
          101  +}
          102  +
          103  +// Parse the input for any tags
          104  +func parse(line string) (result tags) {
          105  +	for _, submatch := range extract.FindAllStringSubmatch(line, -1) {
          106  +		switch submatch[1] {
          107  +		// Other tag types can be added as well
          108  +		case "#":
          109  +			result.hash = append(result.hash, submatch[2])
          110  +		}
          111  +	}
          112  +	return
          113  +}
          114  +
          115  +// The task struct contains a task description and its tags
          116  +type task struct {
          117  +	tags
          118  +	nth     int
          119  +	value   string
          120  +	depends tasks
          121  +}
          122  +
          123  +// as per fmt.Stringer
          124  +func (t *task) String() string {
          125  +	return fmt.Sprintf("%d) %s %s", t.nth, t.tags, t.value)
          126  +}
          127  +
          128  +// Alias for a slice of task pointer
          129  +type tasks []*task
          130  +
          131  +// as per sort.Interface
          132  +func (t tasks) Len() int {
          133  +	return len(t)
          134  +}
          135  +
          136  +// as per sort.Interface
          137  +func (t tasks) Less(i, j int) bool {
          138  +	return t[i].nth < t[j].nth
          139  +}
          140  +
          141  +// as per sort.Interface
          142  +func (t tasks) Swap(i, j int) {
          143  +	t[i], t[j] = t[j], t[i]
          144  +}
          145  +
          146  +// The action struct contains flags that determine what to do with a task
          147  +type action struct {
          148  +	// Stop further processing of the task
          149  +	stop bool
          150  +}
          151  +
          152  +// An alias interface for all task handler functions
          153  +type handler func(*task) action
          154  +
          155  +// Execute handlers against a task
          156  +func handle(handlers []handler, t *task) {
          157  +	for _, h := range handlers {
          158  +		act := h(t)
          159  +		if act.stop {
          160  +			return
          161  +		}
          162  +	}
          163  +}
          164  +
          165  +// Returns a counting handler closure that sets the tasks' nth field
          166  +func counting() handler {
          167  +	var at int = 1
          168  +	return func(t *task) action {
          169  +		t.nth = at
          170  +		at++
          171  +		return action{}
          172  +	}
          173  +}
          174  +
          175  +// Returns a tracing handler closure that sets the tasks' depends field
          176  +func tracing() handler {
          177  +	// Store the last task per tag type
          178  +	var last = make(map[string]*task)
          179  +	return func(t *task) action {
          180  +		for _, tag := range t.hash {
          181  +			if prev, ok := last[tag]; ok {
          182  +				t.depends = append(t.depends, prev)
          183  +			}
          184  +			last[tag] = t
          185  +		}
          186  +		return action{} // default, no action
          187  +	}
          188  +}
          189  +
          190  +// Returns handler closure that rejects tasks with already-seen tags
          191  +func cutoff() handler {
          192  +	var seen = make(map[string]bool)
          193  +	return func(t *task) (result action) {
          194  +		for _, tag := range t.hash {
          195  +			if seen[tag] {
          196  +				result.stop = true
          197  +			} else {
          198  +				seen[tag] = true
          199  +			}
          200  +		}
          201  +		return
          202  +	}
          203  +}
          204  +
          205  +// Returns a matching handler closure that filters tasks not matching atleast one tag
          206  +func matching(tags []string) handler {
          207  +	var allowed = make(map[string]bool)
          208  +	for _, tag := range tags {
          209  +		allowed[tag] = true
          210  +	}
          211  +
          212  +	return func(t *task) action {
          213  +		for _, tag := range t.hash {
          214  +			if allowed[tag] {
          215  +				return action{}
          216  +			}
          217  +		}
          218  +		return action{stop: true}
          219  +	}
          220  +}
          221  +
          222  +// Returns a handler that stores every seen task and an ancillary function to retrieve those
          223  +func collecting() (handler, func() tasks) {
          224  +	var seen = make(map[*task]struct{})
          225  +	return func(t *task) action {
          226  +			seen[t] = struct{}{}
          227  +			return action{}
          228  +		}, func() tasks {
          229  +			seq := make(tasks, 0, len(seen))
          230  +			for k, _ := range seen {
          231  +				seq = append(seq, k)
          232  +			}
          233  +			sort.Stable(seq)
          234  +			return seq
          235  +		}
          236  +}

Added src/0dev.org/commands/tbd/tbd_test.go version [174270376e].

            1  +package main
            2  +
            3  +import (
            4  +	"strings"
            5  +	"testing"
            6  +)
            7  +
            8  +var (
            9  +	tbdata string = `implement more #tests
           10  +extract code out of #main
           11  +support cgi/fast-cgi in #main`
           12  +)
           13  +
           14  +func TestNoArgs(t *testing.T) {
           15  +	// Configure the handlers ...
           16  +	// Use a collecting handler to catch all tasks right after they are parsed
           17  +	// so we have something to easily compare against the final stage's output
           18  +	collectTest, outputTest := collecting()
           19  +	collectFinal, outputFinal := collecting()
           20  +	handlers := []handler{collectTest, counting(), tracing(), cutoff(), collectFinal}
           21  +
           22  +	// ... and execute them against the sample tbdata
           23  +	reader := strings.NewReader(tbdata)
           24  +	exit := process(reader, handlers)
           25  +
           26  +	if exit != 0 {
           27  +		t.Error("Unexpected exit code.", exit)
           28  +	}
           29  +
           30  +	initial := outputTest()
           31  +	result := outputFinal()
           32  +	if len(result) != 2 {
           33  +		t.Error("Unexpected result length", 2)
           34  +	}
           35  +	if result[0] != initial[0] {
           36  +		t.Error("Unexpected task at result[0]", result[0])
           37  +	}
           38  +	if result[1] != initial[1] {
           39  +		t.Error("Unexpected task at result[1]", result[1])
           40  +	}
           41  +}
           42  +
           43  +func TestArgs(t *testing.T) {
           44  +	// Configure the handlers ...
           45  +	// Use a collecting handler to catch all tasks right after they are parsed
           46  +	// so we have something to easily compare against the final stage's output
           47  +	collectTest, outputTest := collecting()
           48  +	collectFinal, outputFinal := collecting()
           49  +	handlers := []handler{collectTest, counting(), tracing(), matching([]string{"main"}), collectFinal}
           50  +
           51  +	// ... and execute them against the sample tbdata
           52  +	reader := strings.NewReader(tbdata)
           53  +	exit := process(reader, handlers)
           54  +
           55  +	if exit != 0 {
           56  +		t.Error("Unexpected exit code.", exit)
           57  +	}
           58  +
           59  +	initial := outputTest()
           60  +	result := outputFinal()
           61  +	if len(result) != 2 {
           62  +		t.Error("Unexpected result length", 2)
           63  +	}
           64  +	if result[0] != initial[1] {
           65  +		t.Error("Unexpected task at result[0]", result[0])
           66  +	}
           67  +	if result[1] != initial[2] {
           68  +		t.Error("Unexpected task at result[1]", result[1])
           69  +	}
           70  +}