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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
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 version [174270376e].













































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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])
	}
}