Skip to content

Commit 51640c8

Browse files
authored
Merge pull request #314 from katexochen/cli-no-global-state
cli: remove global state, init function usage
2 parents ab2b373 + c07305e commit 51640c8

File tree

5 files changed

+72
-73
lines changed

5 files changed

+72
-73
lines changed

cli/cli.go

+15-5
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ package cli
33
import (
44
"os"
55

6+
"git.numtide.com/numtide/treefmt/format"
67
"git.numtide.com/numtide/treefmt/walk"
78
"github.com/alecthomas/kong"
89
"github.com/charmbracelet/log"
10+
"github.com/gobwas/glob"
911
)
1012

11-
var Cli = Format{}
13+
func New() *Format {
14+
return &Format{}
15+
}
1216

1317
type Format struct {
1418
AllowMissingFormatter bool `default:"false" help:"Do not exit with error if a configured formatter is missing."`
@@ -31,17 +35,23 @@ type Format struct {
3135
Stdin bool `help:"Format the context passed in via stdin."`
3236

3337
CpuProfile string `optional:"" help:"The file into which a cpu profile will be written."`
38+
39+
excludes []glob.Glob
40+
formatters map[string]*format.Formatter
41+
42+
filesCh chan *walk.File
43+
processedCh chan *walk.File
3444
}
3545

36-
func configureLogging() {
46+
func (f *Format) configureLogging() {
3747
log.SetReportTimestamp(false)
3848
log.SetOutput(os.Stderr)
3949

40-
if Cli.Verbosity == 0 {
50+
if f.Verbosity == 0 {
4151
log.SetLevel(log.WarnLevel)
42-
} else if Cli.Verbosity == 1 {
52+
} else if f.Verbosity == 1 {
4353
log.SetLevel(log.InfoLevel)
44-
} else if Cli.Verbosity > 1 {
54+
} else if f.Verbosity > 1 {
4555
log.SetLevel(log.DebugLevel)
4656
}
4757
}

cli/format.go

+53-62
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414

1515
"git.numtide.com/numtide/treefmt/format"
1616
"git.numtide.com/numtide/treefmt/stats"
17-
"github.com/gobwas/glob"
1817

1918
"git.numtide.com/numtide/treefmt/cache"
2019
"git.numtide.com/numtide/treefmt/config"
@@ -28,23 +27,15 @@ const (
2827
BatchSize = 1024
2928
)
3029

31-
var (
32-
excludes []glob.Glob
33-
formatters map[string]*format.Formatter
34-
35-
filesCh chan *walk.File
36-
processedCh chan *walk.File
37-
38-
ErrFailOnChange = errors.New("unexpected changes detected, --fail-on-change is enabled")
39-
)
30+
var ErrFailOnChange = errors.New("unexpected changes detected, --fail-on-change is enabled")
4031

4132
func (f *Format) Run() (err error) {
4233
// set log level and other options
43-
configureLogging()
34+
f.configureLogging()
4435

4536
// cpu profiling
46-
if Cli.CpuProfile != "" {
47-
cpuProfile, err := os.Create(Cli.CpuProfile)
37+
if f.CpuProfile != "" {
38+
cpuProfile, err := os.Create(f.CpuProfile)
4839
if err != nil {
4940
return fmt.Errorf("failed to open file for writing cpu profile: %w", err)
5041
} else if err = pprof.StartCPUProfile(cpuProfile); err != nil {
@@ -69,66 +60,66 @@ func (f *Format) Run() (err error) {
6960
}()
7061

7162
// find the config file unless specified
72-
if Cli.ConfigFile == "" {
63+
if f.ConfigFile == "" {
7364
pwd, err := os.Getwd()
7465
if err != nil {
7566
return err
7667
}
77-
Cli.ConfigFile, _, err = findUp(pwd, "treefmt.toml")
68+
f.ConfigFile, _, err = findUp(pwd, "treefmt.toml")
7869
if err != nil {
7970
return err
8071
}
8172
}
8273

8374
// default the tree root to the directory containing the config file
84-
if Cli.TreeRoot == "" {
85-
Cli.TreeRoot = filepath.Dir(Cli.ConfigFile)
75+
if f.TreeRoot == "" {
76+
f.TreeRoot = filepath.Dir(f.ConfigFile)
8677
}
8778

8879
// search the tree root using the --tree-root-file if specified
89-
if Cli.TreeRootFile != "" {
80+
if f.TreeRootFile != "" {
9081
pwd, err := os.Getwd()
9182
if err != nil {
9283
return err
9384
}
94-
_, Cli.TreeRoot, err = findUp(pwd, Cli.TreeRootFile)
85+
_, f.TreeRoot, err = findUp(pwd, f.TreeRootFile)
9586
if err != nil {
9687
return err
9788
}
9889
}
9990

100-
log.Debugf("config-file=%s tree-root=%s", Cli.ConfigFile, Cli.TreeRoot)
91+
log.Debugf("config-file=%s tree-root=%s", f.ConfigFile, f.TreeRoot)
10192

10293
// read config
103-
cfg, err := config.ReadFile(Cli.ConfigFile, Cli.Formatters)
94+
cfg, err := config.ReadFile(f.ConfigFile, f.Formatters)
10495
if err != nil {
105-
return fmt.Errorf("failed to read config file %v: %w", Cli.ConfigFile, err)
96+
return fmt.Errorf("failed to read config file %v: %w", f.ConfigFile, err)
10697
}
10798

10899
// compile global exclude globs
109-
if excludes, err = format.CompileGlobs(cfg.Global.Excludes); err != nil {
100+
if f.excludes, err = format.CompileGlobs(cfg.Global.Excludes); err != nil {
110101
return fmt.Errorf("failed to compile global excludes: %w", err)
111102
}
112103

113104
// initialise formatters
114-
formatters = make(map[string]*format.Formatter)
105+
f.formatters = make(map[string]*format.Formatter)
115106

116107
for name, formatterCfg := range cfg.Formatters {
117-
formatter, err := format.NewFormatter(name, Cli.TreeRoot, formatterCfg, excludes)
108+
formatter, err := format.NewFormatter(name, f.TreeRoot, formatterCfg, f.excludes)
118109

119-
if errors.Is(err, format.ErrCommandNotFound) && Cli.AllowMissingFormatter {
110+
if errors.Is(err, format.ErrCommandNotFound) && f.AllowMissingFormatter {
120111
log.Debugf("formatter command not found: %v", name)
121112
continue
122113
} else if err != nil {
123114
return fmt.Errorf("%w: failed to initialise formatter: %v", err, name)
124115
}
125116

126117
// store formatter by name
127-
formatters[name] = formatter
118+
f.formatters[name] = formatter
128119
}
129120

130121
// open the cache
131-
if err = cache.Open(Cli.TreeRoot, Cli.ClearCache, formatters); err != nil {
122+
if err = cache.Open(f.TreeRoot, f.ClearCache, f.formatters); err != nil {
132123
return err
133124
}
134125

@@ -151,28 +142,28 @@ func (f *Format) Run() (err error) {
151142

152143
// create a channel for files needing to be processed
153144
// we use a multiple of batch size here as a rudimentary concurrency optimization based on the host machine
154-
filesCh = make(chan *walk.File, BatchSize*runtime.NumCPU())
145+
f.filesCh = make(chan *walk.File, BatchSize*runtime.NumCPU())
155146

156147
// create a channel for files that have been processed
157-
processedCh = make(chan *walk.File, cap(filesCh))
148+
f.processedCh = make(chan *walk.File, cap(f.filesCh))
158149

159150
// start concurrent processing tasks in reverse order
160-
eg.Go(updateCache(ctx))
161-
eg.Go(applyFormatters(ctx))
162-
eg.Go(walkFilesystem(ctx))
151+
eg.Go(f.updateCache(ctx))
152+
eg.Go(f.applyFormatters(ctx))
153+
eg.Go(f.walkFilesystem(ctx))
163154

164155
// wait for everything to complete
165156
return eg.Wait()
166157
}
167158

168-
func updateCache(ctx context.Context) func() error {
159+
func (f *Format) updateCache(ctx context.Context) func() error {
169160
return func() error {
170161
// used to batch updates for more efficient txs
171162
batch := make([]*walk.File, 0, BatchSize)
172163

173164
// apply a batch
174165
processBatch := func() error {
175-
if Cli.Stdin {
166+
if f.Stdin {
176167
// do nothing
177168
return nil
178169
}
@@ -190,13 +181,13 @@ func updateCache(ctx context.Context) func() error {
190181
case <-ctx.Done():
191182
return ctx.Err()
192183
// respond to processed files
193-
case file, ok := <-processedCh:
184+
case file, ok := <-f.processedCh:
194185
if !ok {
195186
// channel has been closed, no further files to process
196187
break LOOP
197188
}
198189

199-
if Cli.Stdin {
190+
if f.Stdin {
200191
// dump file into stdout
201192
f, err := os.Open(file.Path)
202193
if err != nil {
@@ -229,38 +220,38 @@ func updateCache(ctx context.Context) func() error {
229220
}
230221

231222
// if fail on change has been enabled, check that no files were actually formatted, throwing an error if so
232-
if Cli.FailOnChange && stats.Value(stats.Formatted) != 0 {
223+
if f.FailOnChange && stats.Value(stats.Formatted) != 0 {
233224
return ErrFailOnChange
234225
}
235226

236227
// print stats to stdout unless we are processing stdin and printing the results to stdout
237-
if !Cli.Stdin {
228+
if !f.Stdin {
238229
stats.Print()
239230
}
240231

241232
return nil
242233
}
243234
}
244235

245-
func walkFilesystem(ctx context.Context) func() error {
236+
func (f *Format) walkFilesystem(ctx context.Context) func() error {
246237
return func() error {
247238
eg, ctx := errgroup.WithContext(ctx)
248239
pathsCh := make(chan string, BatchSize)
249240

250241
// By default, we use the cli arg, but if the stdin flag has been set we force a filesystem walk
251242
// since we will only be processing one file from a temp directory
252-
walkerType := Cli.Walk
243+
walkerType := f.Walk
253244

254-
if Cli.Stdin {
245+
if f.Stdin {
255246
walkerType = walk.Filesystem
256247

257248
// check we have only received one path arg which we use for the file extension / matching to formatters
258-
if len(Cli.Paths) != 1 {
249+
if len(f.Paths) != 1 {
259250
return fmt.Errorf("only one path should be specified when using the --stdin flag")
260251
}
261252

262253
// read stdin into a temporary file with the same file extension
263-
pattern := fmt.Sprintf("*%s", filepath.Ext(Cli.Paths[0]))
254+
pattern := fmt.Sprintf("*%s", filepath.Ext(f.Paths[0]))
264255
file, err := os.CreateTemp("", pattern)
265256
if err != nil {
266257
return fmt.Errorf("failed to create a temporary file for processing stdin: %w", err)
@@ -270,45 +261,45 @@ func walkFilesystem(ctx context.Context) func() error {
270261
return fmt.Errorf("failed to copy stdin into a temporary file")
271262
}
272263

273-
Cli.Paths[0] = file.Name()
264+
f.Paths[0] = file.Name()
274265
}
275266

276267
walkPaths := func() error {
277268
defer close(pathsCh)
278269

279270
var idx int
280-
for idx < len(Cli.Paths) {
271+
for idx < len(f.Paths) {
281272
select {
282273
case <-ctx.Done():
283274
return ctx.Err()
284275
default:
285-
pathsCh <- Cli.Paths[idx]
276+
pathsCh <- f.Paths[idx]
286277
idx += 1
287278
}
288279
}
289280

290281
return nil
291282
}
292283

293-
if len(Cli.Paths) > 0 {
284+
if len(f.Paths) > 0 {
294285
eg.Go(walkPaths)
295286
} else {
296287
// no explicit paths to process, so we only need to process root
297-
pathsCh <- Cli.TreeRoot
288+
pathsCh <- f.TreeRoot
298289
close(pathsCh)
299290
}
300291

301292
// create a filesystem walker
302-
walker, err := walk.New(walkerType, Cli.TreeRoot, pathsCh)
293+
walker, err := walk.New(walkerType, f.TreeRoot, pathsCh)
303294
if err != nil {
304295
return fmt.Errorf("failed to create walker: %w", err)
305296
}
306297

307298
// close the files channel when we're done walking the file system
308-
defer close(filesCh)
299+
defer close(f.filesCh)
309300

310301
// if no cache has been configured, or we are processing from stdin, we invoke the walker directly
311-
if Cli.NoCache || Cli.Stdin {
302+
if f.NoCache || f.Stdin {
312303
return walker.Walk(ctx, func(file *walk.File, err error) error {
313304
select {
314305
case <-ctx.Done():
@@ -318,7 +309,7 @@ func walkFilesystem(ctx context.Context) func() error {
318309
if !(file.Info.IsDir() || file.Info.Mode()&os.ModeSymlink == os.ModeSymlink) {
319310
stats.Add(stats.Traversed, 1)
320311
stats.Add(stats.Emitted, 1)
321-
filesCh <- file
312+
f.filesCh <- file
322313
}
323314
return nil
324315
}
@@ -327,14 +318,14 @@ func walkFilesystem(ctx context.Context) func() error {
327318

328319
// otherwise we pass the walker to the cache and have it generate files for processing based on whether or not
329320
// they have been added/changed since the last invocation
330-
if err = cache.ChangeSet(ctx, walker, filesCh); err != nil {
321+
if err = cache.ChangeSet(ctx, walker, f.filesCh); err != nil {
331322
return fmt.Errorf("failed to generate change set: %w", err)
332323
}
333324
return nil
334325
}
335326
}
336327

337-
func applyFormatters(ctx context.Context) func() error {
328+
func (f *Format) applyFormatters(ctx context.Context) func() error {
338329
// create our own errgroup for concurrent formatting tasks
339330
fg, ctx := errgroup.WithContext(ctx)
340331
// simple optimization to avoid too many concurrent formatting tasks
@@ -371,7 +362,7 @@ func applyFormatters(ctx context.Context) func() error {
371362

372363
// pass each file to the processed channel
373364
for _, task := range tasks {
374-
processedCh <- task.File
365+
f.processedCh <- task.File
375366
}
376367

377368
return nil
@@ -393,26 +384,26 @@ func applyFormatters(ctx context.Context) func() error {
393384
return func() error {
394385
defer func() {
395386
// close processed channel
396-
close(processedCh)
387+
close(f.processedCh)
397388
}()
398389

399390
// iterate the files channel
400-
for file := range filesCh {
391+
for file := range f.filesCh {
401392

402393
// determine a list of formatters that are interested in file
403394
var matches []*format.Formatter
404-
for _, formatter := range formatters {
395+
for _, formatter := range f.formatters {
405396
if formatter.Wants(file) {
406397
matches = append(matches, formatter)
407398
}
408399
}
409400

410401
if len(matches) == 0 {
411-
if Cli.OnUnmatched == log.FatalLevel {
402+
if f.OnUnmatched == log.FatalLevel {
412403
return fmt.Errorf("no formatter for path: %s", file.Path)
413404
}
414-
log.Logf(Cli.OnUnmatched, "no formatter for path: %s", file.Path)
415-
processedCh <- file
405+
log.Logf(f.OnUnmatched, "no formatter for path: %s", file.Path)
406+
f.processedCh <- file
416407
} else {
417408
// record the match
418409
stats.Add(stats.Matched, 1)

cli/helpers_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func cmd(t *testing.T, args ...string) ([]byte, error) {
3434
t.Helper()
3535

3636
// create a new kong context
37-
p := newKong(t, &Cli, Options...)
37+
p := newKong(t, New(), NewOptions()...)
3838
ctx, err := p.Parse(args)
3939
if err != nil {
4040
return nil, err

0 commit comments

Comments
 (0)