Skip to content

Commit f75fc32

Browse files
abhinavprashantv
andauthored
cmd: Support package patterns (#77)
Adds support to the errtrace command for recognizing import path patterns. This makes the common case for command usage: errtrace -w ./... This is an optional addition to the existing file-based behavior. If the provided argument is an existing file, it will be used as-is. Otherwise, the pattern will be expanded with`go list`. If a pattern is provided, all matching Go files are in-scope: source, test, and external tests and build-tagged files. Resolves #73 --------- Co-authored-by: Prashant V <[email protected]>
1 parent b662de8 commit f75fc32

File tree

4 files changed

+486
-41
lines changed

4 files changed

+486
-41
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## Unreleased
9+
### Added
10+
- cmd/errtrace: Support Go package patterns in addition to file paths.
11+
Use `errtrace -w ./...` to transform all files under the current package
12+
and its descendants.
13+
914
### Changed
1015
- cmd/errtrace:
1116
Print a message when reading from stdin because no arguments were given.

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ Try out errtrace with your own code:
140140
2. Switch to your Git repository and instrument your code.
141141

142142
```bash
143-
git ls-files -- '*.go' | xargs errtrace -w
143+
errtrace -w ./...
144144
```
145145
3. Let `go mod tidy` install the errtrace Go module for you.
146146

@@ -335,11 +335,13 @@ Then, run it on your code:
335335
errtrace -w path/to/file.go path/to/another/file.go
336336
```
337337

338-
To run it on all Go files in your project,
339-
if you use Git, run the following command on a Unix-like system:
338+
Instead of specifying individual files,
339+
you can also specify a Go package pattern.
340+
For example:
340341

341342
```bash
342-
git ls-files -- '*.go' | xargs errtrace -w
343+
errtrace -w example.com/path/to/package
344+
errtrace -w ./...
343345
```
344346

345347
errtrace can be set be setup as a custom formatter in your editor,

cmd/errtrace/main.go

Lines changed: 147 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@
88
//
99
// # Usage
1010
//
11-
// errtrace [options] <source files>
11+
// errtrace [options] <source files | patterns>
12+
//
13+
// This will transform source files and write them to the standard output.
14+
//
15+
// If instead of source files, Go package patterns are given,
16+
// errtrace will transform all the files that match those patterns.
17+
// For example, 'errtrace ./...' will transform all files in the current
18+
// package and all subpackages.
1219
//
13-
// This will transform the source files and write them to the standard output.
1420
// Use the following flags to control the output:
1521
//
1622
// -format
@@ -25,6 +31,7 @@ package main
2531

2632
import (
2733
"bytes"
34+
"encoding/json"
2835
"errors"
2936
"flag"
3037
"fmt"
@@ -35,6 +42,8 @@ import (
3542
"io"
3643
"log"
3744
"os"
45+
"os/exec"
46+
"path/filepath"
3847
"regexp"
3948
"sort"
4049
"strconv"
@@ -52,10 +61,10 @@ func main() {
5261
}
5362

5463
type mainParams struct {
55-
Write bool // -w
56-
List bool // -l
57-
Format format // -format
58-
Files []string // list of files to process
64+
Write bool // -w
65+
List bool // -l
66+
Format format // -format
67+
Patterns []string // list of files to process
5968

6069
ImplicitStdin bool // whether stdin was picked because there were no args
6170
}
@@ -77,7 +86,7 @@ func (p *mainParams) Parse(w io.Writer, args []string) error {
7786
flag := flag.NewFlagSet("errtrace", flag.ContinueOnError)
7887
flag.SetOutput(w)
7988
flag.Usage = func() {
80-
fmt.Fprintln(w, "usage: errtrace [options] <source files>")
89+
fmt.Fprintln(w, "usage: errtrace [options] <source files | patterns>")
8190
flag.PrintDefaults()
8291
}
8392

@@ -87,16 +96,17 @@ func (p *mainParams) Parse(w io.Writer, args []string) error {
8796
"write result to the given source files instead of stdout.")
8897
flag.BoolVar(&p.List, "l", false,
8998
"list files that would be modified without making any changes.")
99+
90100
// TODO: toolexec mode
91101

92102
if err := flag.Parse(args); err != nil {
93103
return err
94104
}
95105

96-
p.Files = flag.Args()
97-
if len(p.Files) == 0 {
106+
p.Patterns = flag.Args()
107+
if len(p.Patterns) == 0 {
98108
// Read file from stdin when there's no args, similar to gofmt.
99-
p.Files = []string{"-"}
109+
p.Patterns = []string{"-"}
100110
p.ImplicitStdin = true
101111
}
102112

@@ -178,28 +188,147 @@ func (cmd *mainCmd) Run(args []string) (exitCode int) {
178188
return 1
179189
}
180190

181-
for _, file := range p.Files {
191+
files, err := expandPatterns(p.Patterns)
192+
if err != nil {
193+
cmd.log.Println("errtrace:", err)
194+
return 1
195+
}
196+
197+
// Paths will be printed relative to CWD.
198+
// Paths outside it will be printed as-is.
199+
var workDir string
200+
if wd, err := os.Getwd(); err == nil {
201+
workDir = wd + string(filepath.Separator)
202+
}
203+
204+
for _, file := range files {
205+
display := file
206+
if workDir != "" {
207+
// Not using filepath.Rel
208+
// because we don't want any ".."s in the path.
209+
display = strings.TrimPrefix(file, workDir)
210+
}
211+
if display == "-" {
212+
display = "stdin"
213+
}
214+
182215
req := fileRequest{
183216
Format: p.shouldFormat(),
184217
Write: p.Write,
185218
List: p.List,
186-
Filename: file,
219+
Filename: display,
220+
Filepath: file,
187221
ImplicitStdin: p.ImplicitStdin,
188222
}
189223
if err := cmd.processFile(req); err != nil {
190-
cmd.log.Printf("%s:%s", file, err)
224+
cmd.log.Printf("%s:%s", display, err)
191225
exitCode = 1
192226
}
193227
}
194228

195229
return exitCode
196230
}
197231

232+
// expandPatterns turns the given list of patterns and files
233+
// into a list of paths to files.
234+
//
235+
// Arguments that are already files are returned as-is.
236+
// Arguments that are patterns are expanded using 'go list'.
237+
// As a special case for stdin, "-" is returned as-is.
238+
func expandPatterns(args []string) ([]string, error) {
239+
var files, patterns []string
240+
for _, arg := range args {
241+
if arg == "-" {
242+
files = append(files, arg)
243+
continue
244+
}
245+
246+
if info, err := os.Stat(arg); err == nil && !info.IsDir() {
247+
files = append(files, arg)
248+
continue
249+
}
250+
251+
patterns = append(patterns, arg)
252+
}
253+
254+
if len(patterns) > 0 {
255+
pkgFiles, err := goListFiles(patterns)
256+
if err != nil {
257+
return nil, fmt.Errorf("go list: %w", err)
258+
}
259+
260+
files = append(files, pkgFiles...)
261+
}
262+
263+
return files, nil
264+
}
265+
266+
var _execCommand = exec.Command
267+
268+
func goListFiles(patterns []string) (files []string, err error) {
269+
// The -e flag makes 'go list' include erroneous packages.
270+
// This will even include packages that have all files excluded
271+
// by build constraints if explicitly requested.
272+
// (with "path/to/pkg" instead of "./...")
273+
args := []string{"list", "-find", "-e", "-json"}
274+
args = append(args, patterns...)
275+
276+
var stderr bytes.Buffer
277+
cmd := _execCommand("go", args...)
278+
cmd.Stderr = &stderr
279+
280+
stdout, err := cmd.StdoutPipe()
281+
if err != nil {
282+
return nil, fmt.Errorf("create stdout pipe: %w", err)
283+
}
284+
285+
if err := cmd.Start(); err != nil {
286+
return nil, fmt.Errorf("start command: %w", err)
287+
}
288+
289+
type packageInfo struct {
290+
Dir string
291+
GoFiles []string
292+
CgoFiles []string
293+
TestGoFiles []string
294+
XTestGoFiles []string
295+
IgnoredGoFiles []string
296+
}
297+
298+
decoder := json.NewDecoder(stdout)
299+
for decoder.More() {
300+
var pkg packageInfo
301+
if err := decoder.Decode(&pkg); err != nil {
302+
return nil, fmt.Errorf("output malformed: %v", err)
303+
}
304+
305+
for _, pkgFiles := range [][]string{
306+
pkg.GoFiles,
307+
pkg.CgoFiles,
308+
pkg.TestGoFiles,
309+
pkg.XTestGoFiles,
310+
pkg.IgnoredGoFiles,
311+
} {
312+
for _, f := range pkgFiles {
313+
files = append(files, filepath.Join(pkg.Dir, f))
314+
}
315+
}
316+
}
317+
318+
if err := cmd.Wait(); err != nil {
319+
return nil, fmt.Errorf("%w\n%s", err, stderr.String())
320+
}
321+
322+
return files, nil
323+
}
324+
198325
type fileRequest struct {
199-
Format bool
200-
Write bool
201-
List bool
202-
Filename string
326+
Format bool
327+
Write bool
328+
List bool
329+
330+
Filename string // name displayed to the user
331+
Filepath string // actual location on disk, or "-" for stdin
203332

204333
ImplicitStdin bool
205334
}
@@ -432,7 +561,7 @@ func (cmd *mainCmd) processFile(r fileRequest) error {
432561
var _stdinWait = 200 * time.Millisecond
433562

434563
func (cmd *mainCmd) readFile(r fileRequest) ([]byte, error) {
435-
if r.Filename != "-" {
564+
if r.Filepath != "-" {
436565
return os.ReadFile(r.Filename)
437566
}
438567

0 commit comments

Comments
 (0)