-
-
Notifications
You must be signed in to change notification settings - Fork 19
/
cmd_todo.go
251 lines (196 loc) · 5.37 KB
/
cmd_todo.go
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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
package main
import (
"bufio"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
// Structure for our options and state.
type todoCommand struct {
// The current date/time
now time.Time
// regular expression to find (nn/NN...)
reg *regexp.Regexp
// silent?
silent bool
// verbose?
verbose bool
}
// Arguments adds per-command args to the object.
func (t *todoCommand) Arguments(f *flag.FlagSet) {
f.BoolVar(&t.silent, "silent", false, "Should we be silent in the case of permission-errors?")
f.BoolVar(&t.verbose, "verbose", false, "Should we report on what we're doing?")
}
// Info returns the name of this subcommand.
func (t *todoCommand) Info() (string, string) {
return "todo", `Flag TODO-notes past their expiry date.
Details:
This command recursively examines files beneath the current directory,
or the named directory, and outputs any comments which have an associated
date which is in the past.
Two comment-types are supported 'TODO' and 'FIXME' - these must occur
literally, and in upper-case only. To find comments which should be
reported the line must also contain a date, enclosed in parenthesis.
The following examples show the kind of comments that will be reported
when the given date(s) are in the past:
// TODO (10/03/2022) - Raise this after 10th March 2022.
// TODO (03/2022) - Raise this after March 2022.
// TODO (02/06/2022) - Raise this after 2nd June 2022.
// FIXME - This will break at the end of the year (2023).
// FIXME - RootCA must be renewed & replaced before (10/2025).
Usage:
$ sysbox todo
$ sysbox todo ~/Projects/
`
}
// Process all the files beneath the given path
func (t *todoCommand) scanPath(path string) error {
err := filepath.Walk(path,
func(path string, info os.FileInfo, err error) error {
if err != nil {
if !os.IsPermission(err) {
return err
}
if !t.silent {
fmt.Fprintf(os.Stderr, "permission denied: %s\n", path)
}
return nil
}
// We only want to read files
isDir := info.IsDir()
if !isDir {
err := t.processFile(path)
return err
}
return nil
})
return err
}
// processLine outputs any matching lines; those that contain a date and a TODO/FIXME reference.
func (t *todoCommand) processLine(path string, line string) error {
// Does this line contain TODO, or FIXME? If not return
if !strings.Contains(line, "TODO") && !strings.Contains(line, "FIXME") {
return nil
}
// remove leading/trailing space
line = strings.TrimSpace(line)
// Does it contain a date?
match := t.reg.FindStringSubmatch(line)
// OK we have a date.
if len(match) >= 2 {
// The date we've found
date := match[1]
var found time.Time
var err error
// Split by "/" to find the number
// of values we've got:
//
// "DD/MM/YYYY"
// "MM/YYYY"
// "YYYY"
parts := strings.Split(date, "/")
switch len(parts) {
case 3:
found, err = time.Parse("02/01/2006", date)
if err != nil {
return fmt.Errorf("failed to parse %s:%s", date, err)
}
case 2:
found, err = time.Parse("01/2006", date)
if err != nil {
return fmt.Errorf("failed to parse %s:%s", date, err)
}
case 1:
found, err = time.Parse("2006", date)
if err != nil {
return fmt.Errorf("failed to parse %s:%s", date, err)
}
default:
return fmt.Errorf("unknown date-format %s", date)
}
// If the date we've parsed is before today
// then we alert on the line.
if found.Before(t.now) {
fmt.Printf("%s:%s\n", path, line)
}
}
return nil
}
// processFile opens a file and reads line by line for a date.
func (t *todoCommand) processFile(path string) error {
if t.verbose {
fmt.Printf("examining %s\n", path)
}
// open the file
file, err := os.Open(path)
if err != nil {
// error - is it permission-denied? If so we can swallow that
if os.IsPermission(err) {
if !t.silent {
fmt.Fprintf(os.Stderr, "permission denied opening: %s\n", path)
}
return nil
}
// ok another error
return fmt.Errorf("failed to scan file %s:%s", path, err)
}
defer file.Close()
// prepare to read the file
scanner := bufio.NewScanner(file)
// 64k is the default max length of the line-buffer - double it.
const maxCapacity int = 128 * 1024 * 1024
buf := make([]byte, maxCapacity)
scanner.Buffer(buf, maxCapacity)
// Process each line
for scanner.Scan() {
err := t.processLine(path, scanner.Text())
if err != nil {
return err
}
}
if err := scanner.Err(); err != nil {
return err
}
return nil
}
// Execute is invoked if the user specifies `todo` as the subcommand.
func (t *todoCommand) Execute(args []string) int {
// Save today's date/time which we'll use for comparison.
t.now = time.Now()
// Create the capture regexp
var err error
t.reg, err = regexp.Compile(`\(([0-9/]+)\)`)
if err != nil {
fmt.Printf("internal error compiling regular expression:%s\n", err)
return 1
}
// If we got any directories ..
if len(args) > 0 {
failed := false
// process each path
for _, path := range args {
// error? then report it, but continue
err = t.scanPath(path)
if err != nil {
fmt.Printf("error handling %s: %s\n", path, err)
failed = true
}
}
// exit-code will reveal errors
if failed {
return 1
}
return 0
}
// No named directory/directories - just handle the PWD
err = t.scanPath(".")
if err != nil {
fmt.Printf("error handling search:%s\n", err)
return 1
}
return 0
}