all repos

clerk @ 3fa39dfa16c8185a8bc74cbcce9d4af35d0e8964

missing tooling for ledger/hledger
12 files changed, 497 insertions(+), 441 deletions(-)
change cli frameworks
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-06-27 17:53:42 +0300
Authored at: 2026-06-27 16:51:40 +0300
Change ID: qkvzmvpppnnrlquvlmzpmrvmnxozwlyr
Parent: 0c15ec4
D format.go
···
        1
        
        -package main

      
        2
        
        -

      
        3
        
        -import (

      
        4
        
        -	"bytes"

      
        5
        
        -	"flag"

      
        6
        
        -	"fmt"

      
        7
        
        -	"io"

      
        8
        
        -	"os"

      
        9
        
        -

      
        10
        
        -	"olexsmir.xyz/clerk/journal"

      
        11
        
        -	"olexsmir.xyz/clerk/journal/printer"

      
        12
        
        -)

      
        13
        
        -

      
        14
        
        -func runFormat(args []string) {

      
        15
        
        -	fs := flag.NewFlagSet("format", flag.ExitOnError)

      
        16
        
        -	check := fs.Bool("c", false, "Exit code 0 if already formatted, 1 otherwise")

      
        17
        
        -	diff := fs.Bool("d", false, "Display diffs instead of rewriting files")

      
        18
        
        -	list := fs.Bool("l", false, "List files whose formatting differs")

      
        19
        
        -	write := fs.Bool("w", false, "Write result back to file instead of stdout")

      
        20
        
        -	fs.Usage = func() {

      
        21
        
        -		fmt.Fprintf(os.Stderr, "Usage: clerk format [flags] [path ...]\n")

      
        22
        
        -		fs.PrintDefaults()

      
        23
        
        -	}

      
        24
        
        -	fs.Parse(args)

      
        25
        
        -

      
        26
        
        -	paths := fs.Args()

      
        27
        
        -

      
        28
        
        -	// Read from stdin if no paths given

      
        29
        
        -	if len(paths) == 0 {

      
        30
        
        -		src, err := io.ReadAll(os.Stdin)

      
        31
        
        -		if err != nil {

      
        32
        
        -			fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err)

      
        33
        
        -			os.Exit(1)

      
        34
        
        -		}

      
        35
        
        -

      
        36
        
        -		pf, err := journal.NewLoader().LoadBytes("stdin", src)

      
        37
        
        -		if err != nil {

      
        38
        
        -			fmt.Fprintf(os.Stderr, "parse error: %v\n", err)

      
        39
        
        -			os.Exit(1)

      
        40
        
        -		}

      
        41
        
        -		if len(pf.Errors) > 0 {

      
        42
        
        -			fmt.Fprintf(os.Stderr, "parse error: %v\n", pf.Errors[0].Message)

      
        43
        
        -			os.Exit(1)

      
        44
        
        -		}

      
        45
        
        -

      
        46
        
        -		var buf bytes.Buffer

      
        47
        
        -		if err := printer.Fprint(&buf, pf.Ast); err != nil {

      
        48
        
        -			fmt.Fprintf(os.Stderr, "format error: %v\n", err)

      
        49
        
        -			os.Exit(1)

      
        50
        
        -		}

      
        51
        
        -

      
        52
        
        -		formatted := buf.Bytes()

      
        53
        
        -

      
        54
        
        -		switch {

      
        55
        
        -		case *check:

      
        56
        
        -			if bytes.Equal(src, formatted) {

      
        57
        
        -				os.Exit(0)

      
        58
        
        -			}

      
        59
        
        -			os.Exit(1)

      
        60
        
        -		case *diff:

      
        61
        
        -			diffLines("stdin", src, formatted)

      
        62
        
        -		default:

      
        63
        
        -			os.Stdout.Write(formatted)

      
        64
        
        -		}

      
        65
        
        -		return

      
        66
        
        -	}

      
        67
        
        -

      
        68
        
        -	// Process each file

      
        69
        
        -	exitCode := 0

      
        70
        
        -	for _, path := range paths {

      
        71
        
        -		info, err := os.Stat(path)

      
        72
        
        -		if err != nil {

      
        73
        
        -			fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)

      
        74
        
        -			exitCode = 1

      
        75
        
        -			continue

      
        76
        
        -		}

      
        77
        
        -		if info.IsDir() {

      
        78
        
        -			fmt.Fprintf(os.Stderr, "error: %s: is a directory\n", path)

      
        79
        
        -			exitCode = 1

      
        80
        
        -			continue

      
        81
        
        -		}

      
        82
        
        -

      
        83
        
        -		src, err := os.ReadFile(path)

      
        84
        
        -		if err != nil {

      
        85
        
        -			fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)

      
        86
        
        -			exitCode = 1

      
        87
        
        -			continue

      
        88
        
        -		}

      
        89
        
        -

      
        90
        
        -		pf, err := journal.NewLoader().LoadBytes(path, src)

      
        91
        
        -		if err != nil {

      
        92
        
        -			fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)

      
        93
        
        -			exitCode = 1

      
        94
        
        -			continue

      
        95
        
        -		}

      
        96
        
        -		if len(pf.Errors) > 0 || len(pf.FileErrors) > 0 {

      
        97
        
        -			fmt.Fprintf(os.Stderr, "error: %s: has errors, refusing to format\n", path)

      
        98
        
        -			exitCode = 1

      
        99
        
        -			continue

      
        100
        
        -		}

      
        101
        
        -

      
        102
        
        -		var buf bytes.Buffer

      
        103
        
        -		if err := printer.Fprint(&buf, pf.Ast); err != nil {

      
        104
        
        -			fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)

      
        105
        
        -			exitCode = 1

      
        106
        
        -			continue

      
        107
        
        -		}

      
        108
        
        -

      
        109
        
        -		formatted := buf.Bytes()

      
        110
        
        -		changed := !bytes.Equal(src, formatted)

      
        111
        
        -

      
        112
        
        -		switch {

      
        113
        
        -		case *check:

      
        114
        
        -			if changed {

      
        115
        
        -				fmt.Fprintf(os.Stderr, "%s: not formatted\n", path)

      
        116
        
        -				exitCode = 1

      
        117
        
        -			}

      
        118
        
        -		case *list:

      
        119
        
        -			if changed {

      
        120
        
        -				fmt.Println(path)

      
        121
        
        -			}

      
        122
        
        -		case *diff:

      
        123
        
        -			if changed {

      
        124
        
        -				diffLines(path, src, formatted)

      
        125
        
        -			}

      
        126
        
        -		case *write:

      
        127
        
        -			if changed {

      
        128
        
        -				if err := os.WriteFile(path, formatted, 0o644); err != nil {

      
        129
        
        -					fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)

      
        130
        
        -					exitCode = 1

      
        131
        
        -				}

      
        132
        
        -			}

      
        133
        
        -		default:

      
        134
        
        -			if _, err := os.Stdout.Write(formatted); err != nil {

      
        135
        
        -				fmt.Fprintf(os.Stderr, "error writing stdout: %v\n", err)

      
        136
        
        -				exitCode = 1

      
        137
        
        -			}

      
        138
        
        -		}

      
        139
        
        -	}

      
        140
        
        -	os.Exit(exitCode)

      
        141
        
        -}

      
        142
        
        -

      
        143
        
        -func diffLines(path string, src, formatted []byte) {

      
        144
        
        -	fmt.Printf("--- %s\n+++ %s\n", path, path)

      
        145
        
        -	srcLines := bytes.Split(src, []byte("\n"))

      
        146
        
        -	fmtLines := bytes.Split(formatted, []byte("\n"))

      
        147
        
        -	lines := max(len(fmtLines), len(srcLines))

      
        148
        
        -	for i := range lines {

      
        149
        
        -		var sLine, fLine []byte

      
        150
        
        -		if i < len(srcLines) {

      
        151
        
        -			sLine = srcLines[i]

      
        152
        
        -		}

      
        153
        
        -		if i < len(fmtLines) {

      
        154
        
        -			fLine = fmtLines[i]

      
        155
        
        -		}

      
        156
        
        -		if !bytes.Equal(sLine, fLine) {

      
        157
        
        -			if len(sLine) > 0 {

      
        158
        
        -				fmt.Printf("-%s\n", sLine)

      
        159
        
        -			} else {

      
        160
        
        -				fmt.Println("-")

      
        161
        
        -			}

      
        162
        
        -			if len(fLine) > 0 {

      
        163
        
        -				fmt.Printf("+%s\n", fLine)

      
        164
        
        -			} else {

      
        165
        
        -				fmt.Println("+")

      
        166
        
        -			}

      
        167
        
        -		}

      
        168
        
        -	}

      
        169
        
        -}

      
M go.mod
···
        4
        4
         

      
        5
        5
         tool golang.org/x/tools/cmd/stringer

      
        6
        6
         

      
        
        7
        +require github.com/urfave/cli/v3 v3.10.0

      
        
        8
        +

      
        7
        9
         require (

      
        8
        10
         	golang.org/x/mod v0.36.0 // indirect

      
        9
        11
         	golang.org/x/sync v0.20.0 // indirect

      
M go.sum
···
        
        1
        +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=

      
        
        2
        +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

      
        1
        3
         github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=

      
        2
        4
         github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=

      
        
        5
        +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

      
        
        6
        +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

      
        
        7
        +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=

      
        
        8
        +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=

      
        
        9
        +github.com/urfave/cli/v3 v3.10.0 h1:0aU8yOObVDMkM13Cj4G+zb4P0PdeJMec65f81Ak1ioM=

      
        
        10
        +github.com/urfave/cli/v3 v3.10.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=

      
        3
        11
         golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=

      
        4
        12
         golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=

      
        5
        13
         golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

      
        6
        14
         golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=

      
        7
        15
         golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=

      
        8
        16
         golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=

      
        
        17
        +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

      
        
        18
        +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

      
A internal/cli/cli.go
···
        
        1
        +package cli

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +

      
        
        6
        +	"github.com/urfave/cli/v3"

      
        
        7
        +)

      
        
        8
        +

      
        
        9
        +type Cli struct {

      
        
        10
        +	version string

      
        
        11
        +}

      
        
        12
        +

      
        
        13
        +func New(version string) *Cli {

      
        
        14
        +	return &Cli{

      
        
        15
        +		version: version,

      
        
        16
        +	}

      
        
        17
        +}

      
        
        18
        +

      
        
        19
        +func (c *Cli) Run(ctx context.Context, args []string) error {

      
        
        20
        +	cmd := &cli.Command{

      
        
        21
        +		Name:                   "clerk",

      
        
        22
        +		Usage:                  "missing pta tooling",

      
        
        23
        +		Version:                c.version,

      
        
        24
        +		EnableShellCompletion:  true,

      
        
        25
        +		UseShortOptionHandling: true,

      
        
        26
        +		Commands: []*cli.Command{

      
        
        27
        +			{

      
        
        28
        +				Name:   "format",

      
        
        29
        +				Usage:  "reformat journal files",

      
        
        30
        +				Action: c.formatAction,

      
        
        31
        +				Arguments: []cli.Argument{&cli.StringArgs{

      
        
        32
        +					Name:      "journals",

      
        
        33
        +					UsageText: "(path to journal files/directories (stdin if empty))",

      
        
        34
        +					Min:       0,

      
        
        35
        +					Max:       -1,

      
        
        36
        +				}},

      
        
        37
        +				Flags: []cli.Flag{

      
        
        38
        +					&cli.BoolFlag{

      
        
        39
        +						Name:    "write",

      
        
        40
        +						Aliases: []string{"w"},

      
        
        41
        +						Usage:   "write result back to file instead of stdout",

      
        
        42
        +					},

      
        
        43
        +					&cli.BoolFlag{

      
        
        44
        +						Name:    "check",

      
        
        45
        +						Aliases: []string{"c"},

      
        
        46
        +						Usage:   "exit code 0 if already formatted, 1 otherwise",

      
        
        47
        +					},

      
        
        48
        +					&cli.BoolFlag{

      
        
        49
        +						Name:    "diff",

      
        
        50
        +						Aliases: []string{"d"},

      
        
        51
        +						Usage:   "display diffs instead of rewriting files",

      
        
        52
        +					},

      
        
        53
        +					&cli.BoolFlag{

      
        
        54
        +						Name:    "list",

      
        
        55
        +						Aliases: []string{"l"},

      
        
        56
        +						Usage:   "list files whose formatting differs",

      
        
        57
        +					},

      
        
        58
        +				},

      
        
        59
        +			},

      
        
        60
        +			{

      
        
        61
        +				Name:   "tags",

      
        
        62
        +				Usage:  "generate a tags file for journal entries",

      
        
        63
        +				Action: c.tagsAction,

      
        
        64
        +				Flags: []cli.Flag{&cli.StringFlag{

      
        
        65
        +					Name:    "out",

      
        
        66
        +					Aliases: []string{"o"},

      
        
        67
        +					Usage:   "output file, set to - for stdout",

      
        
        68
        +					Value:   "tags",

      
        
        69
        +				}},

      
        
        70
        +				Arguments: []cli.Argument{&cli.StringArgs{

      
        
        71
        +					Name:      "journals",

      
        
        72
        +					UsageText: "(path to journal files/directories)",

      
        
        73
        +					Min:       0,

      
        
        74
        +					Max:       -1,

      
        
        75
        +				}},

      
        
        76
        +			},

      
        
        77
        +			{

      
        
        78
        +				Name:   "lint",

      
        
        79
        +				Usage:  "lint journal files for common mistakes",

      
        
        80
        +				Action: c.lintAction,

      
        
        81
        +				Flags: []cli.Flag{

      
        
        82
        +					&cli.StringFlag{

      
        
        83
        +						Name:    "format",

      
        
        84
        +						Aliases: []string{"f"},

      
        
        85
        +						Usage:   "output format: text, json",

      
        
        86
        +						Value:   "text",

      
        
        87
        +					},

      
        
        88
        +					&cli.StringFlag{

      
        
        89
        +						Name:  "path-style",

      
        
        90
        +						Usage: "file path style: basename, relative, absolute",

      
        
        91
        +						Value: "relative",

      
        
        92
        +					},

      
        
        93
        +				},

      
        
        94
        +				Arguments: []cli.Argument{&cli.StringArgs{

      
        
        95
        +					Name:      "journals",

      
        
        96
        +					UsageText: "(path to journal files/directories (stdin if empty))",

      
        
        97
        +					Min:       0,

      
        
        98
        +					Max:       -1,

      
        
        99
        +				}},

      
        
        100
        +			},

      
        
        101
        +		},

      
        
        102
        +	}

      
        
        103
        +	return cmd.Run(ctx, args)

      
        
        104
        +}

      
A internal/cli/cmd_format.go
···
        
        1
        +package cli

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"bytes"

      
        
        5
        +	"context"

      
        
        6
        +	"fmt"

      
        
        7
        +	"os"

      
        
        8
        +

      
        
        9
        +	"github.com/urfave/cli/v3"

      
        
        10
        +

      
        
        11
        +	"olexsmir.xyz/clerk/journal"

      
        
        12
        +	"olexsmir.xyz/clerk/journal/printer"

      
        
        13
        +)

      
        
        14
        +

      
        
        15
        +func (c *Cli) formatAction(ctx context.Context, cmd *cli.Command) error {

      
        
        16
        +	check := cmd.Bool("check")

      
        
        17
        +	diff := cmd.Bool("diff")

      
        
        18
        +	list := cmd.Bool("list")

      
        
        19
        +	write := cmd.Bool("write")

      
        
        20
        +	paths := cmd.StringArgs("journals")

      
        
        21
        +

      
        
        22
        +	loader := journal.NewLoader()

      
        
        23
        +	if len(paths) == 0 {

      
        
        24
        +		pf, err := loadStdin(loader)

      
        
        25
        +		if err != nil {

      
        
        26
        +			return err

      
        
        27
        +		}

      
        
        28
        +		if len(pf.Errors) > 0 || len(pf.FileErrors) > 0 {

      
        
        29
        +			for _, e := range pf.Errors {

      
        
        30
        +				fmt.Fprintf(os.Stderr, "error: stdin: %s\n", e.Message)

      
        
        31
        +			}

      
        
        32
        +			for _, fe := range pf.FileErrors {

      
        
        33
        +				fmt.Fprintf(os.Stderr, "error: stdin: %s\n", fe.Message)

      
        
        34
        +			}

      
        
        35
        +			return fmt.Errorf("stdin: has errors, refusing to format")

      
        
        36
        +		}

      
        
        37
        +		return c.formatFile("stdin", pf, check, diff, list, write)

      
        
        38
        +	}

      
        
        39
        +

      
        
        40
        +	files, err := resolvePaths(paths)

      
        
        41
        +	if err != nil {

      
        
        42
        +		return err

      
        
        43
        +	}

      
        
        44
        +

      
        
        45
        +	var errs []error

      
        
        46
        +	for _, path := range files {

      
        
        47
        +		pf, err := loadFile(loader, path)

      
        
        48
        +		if err != nil {

      
        
        49
        +			fmt.Fprintf(os.Stderr, "error: %v\n", err)

      
        
        50
        +			errs = append(errs, err)

      
        
        51
        +			continue

      
        
        52
        +		}

      
        
        53
        +		if len(pf.Errors) > 0 || len(pf.FileErrors) > 0 {

      
        
        54
        +			for _, e := range pf.Errors {

      
        
        55
        +				fmt.Fprintf(os.Stderr, "error: %s: %s\n", path, e.Message)

      
        
        56
        +			}

      
        
        57
        +			for _, fe := range pf.FileErrors {

      
        
        58
        +				fmt.Fprintf(os.Stderr, "error: %s: %s\n", path, fe.Message)

      
        
        59
        +			}

      
        
        60
        +			errs = append(errs, fmt.Errorf("%s: has errors, refusing to format", path))

      
        
        61
        +			continue

      
        
        62
        +		}

      
        
        63
        +

      
        
        64
        +		if err := c.formatFile(path, pf, check, diff, list, write); err != nil {

      
        
        65
        +			fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)

      
        
        66
        +			errs = append(errs, err)

      
        
        67
        +		}

      
        
        68
        +	}

      
        
        69
        +	if len(errs) > 0 {

      
        
        70
        +		return fmt.Errorf("format: %d error(s)", len(errs))

      
        
        71
        +	}

      
        
        72
        +	return nil

      
        
        73
        +}

      
        
        74
        +

      
        
        75
        +func (c *Cli) formatFile(path string, pf *journal.ParsedFile, check, wantDiff, list, write bool) error {

      
        
        76
        +	var buf bytes.Buffer

      
        
        77
        +	if err := printer.Fprint(&buf, pf.Ast); err != nil {

      
        
        78
        +		return fmt.Errorf("format: %w", err)

      
        
        79
        +	}

      
        
        80
        +	formatted := buf.Bytes()

      
        
        81
        +	changed := !bytes.Equal(pf.Src, formatted)

      
        
        82
        +

      
        
        83
        +	switch {

      
        
        84
        +	case check:

      
        
        85
        +		if changed {

      
        
        86
        +			return fmt.Errorf("not formatted")

      
        
        87
        +		}

      
        
        88
        +	case list:

      
        
        89
        +		if changed {

      
        
        90
        +			fmt.Println(path)

      
        
        91
        +		}

      
        
        92
        +	case wantDiff:

      
        
        93
        +		if changed {

      
        
        94
        +			diffLines(path, pf.Src, formatted)

      
        
        95
        +		}

      
        
        96
        +	case write:

      
        
        97
        +		if changed {

      
        
        98
        +			if err := os.WriteFile(path, formatted, 0o644); err != nil {

      
        
        99
        +				return fmt.Errorf("write %s: %w", path, err)

      
        
        100
        +			}

      
        
        101
        +		}

      
        
        102
        +	default:

      
        
        103
        +		if _, err := os.Stdout.Write(formatted); err != nil {

      
        
        104
        +			return fmt.Errorf("write stdout: %w", err)

      
        
        105
        +		}

      
        
        106
        +	}

      
        
        107
        +	return nil

      
        
        108
        +}

      
        
        109
        +

      
        
        110
        +func diffLines(path string, src, formatted []byte) {

      
        
        111
        +	fmt.Printf("--- %s\n+++ %s\n", path, path)

      
        
        112
        +	srcLines := bytes.Split(src, []byte("\n"))

      
        
        113
        +	fmtLines := bytes.Split(formatted, []byte("\n"))

      
        
        114
        +	lines := max(len(fmtLines), len(srcLines))

      
        
        115
        +	for i := range lines {

      
        
        116
        +		var sLine, fLine []byte

      
        
        117
        +		if i < len(srcLines) {

      
        
        118
        +			sLine = srcLines[i]

      
        
        119
        +		}

      
        
        120
        +		if i < len(fmtLines) {

      
        
        121
        +			fLine = fmtLines[i]

      
        
        122
        +		}

      
        
        123
        +		if !bytes.Equal(sLine, fLine) {

      
        
        124
        +			if len(sLine) > 0 {

      
        
        125
        +				fmt.Printf("-%s\n", sLine)

      
        
        126
        +			} else {

      
        
        127
        +				fmt.Println("-")

      
        
        128
        +			}

      
        
        129
        +			if len(fLine) > 0 {

      
        
        130
        +				fmt.Printf("+%s\n", fLine)

      
        
        131
        +			} else {

      
        
        132
        +				fmt.Println("+")

      
        
        133
        +			}

      
        
        134
        +		}

      
        
        135
        +	}

      
        
        136
        +}

      
A internal/cli/cmd_lint.go
···
        
        1
        +package cli

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"fmt"

      
        
        6
        +	"os"

      
        
        7
        +	"strings"

      
        
        8
        +

      
        
        9
        +	"github.com/urfave/cli/v3"

      
        
        10
        +

      
        
        11
        +	"olexsmir.xyz/clerk/internal/linter"

      
        
        12
        +	"olexsmir.xyz/clerk/journal"

      
        
        13
        +	"olexsmir.xyz/clerk/journal/semantic"

      
        
        14
        +)

      
        
        15
        +

      
        
        16
        +func (c *Cli) lintAction(ctx context.Context, cmd *cli.Command) error {

      
        
        17
        +	format := cmd.String("format")

      
        
        18
        +	pathStyle := cmd.String("path-style")

      
        
        19
        +

      
        
        20
        +	lint := linter.NewLinter(linter.Rules)

      
        
        21
        +	reporter := linter.NewReporter(os.Stdout, parsePathStyle(pathStyle))

      
        
        22
        +

      
        
        23
        +	journals := cmd.StringArgs("journals")

      
        
        24
        +	if len(journals) == 0 {

      
        
        25
        +		return c.lintStdin(lint, reporter, format)

      
        
        26
        +	}

      
        
        27
        +

      
        
        28
        +	journalFiles, err := resolvePaths(journals)

      
        
        29
        +	if err != nil {

      
        
        30
        +		return err

      
        
        31
        +	}

      
        
        32
        +	if len(journalFiles) == 0 {

      
        
        33
        +		return nil

      
        
        34
        +	}

      
        
        35
        +

      
        
        36
        +	loader := journal.NewLoader()

      
        
        37
        +

      
        
        38
        +	var errs []error

      
        
        39
        +	for _, f := range journalFiles {

      
        
        40
        +		if _, err := loadFile(loader, f); err != nil {

      
        
        41
        +			fmt.Fprintf(os.Stderr, "error: %v\n", err)

      
        
        42
        +			errs = append(errs, err)

      
        
        43
        +		}

      
        
        44
        +	}

      
        
        45
        +

      
        
        46
        +	sema := semantic.Build(loader.Ordered())

      
        
        47
        +	reporter.Collect(lint.Run(sema))

      
        
        48
        +

      
        
        49
        +	if err := reporter.Flush(format); err != nil {

      
        
        50
        +		return fmt.Errorf("flushing report: %w", err)

      
        
        51
        +	}

      
        
        52
        +

      
        
        53
        +	if reporter.HasIssues() || len(errs) > 0 {

      
        
        54
        +		return fmt.Errorf("lint found issues")

      
        
        55
        +	}

      
        
        56
        +	return nil

      
        
        57
        +}

      
        
        58
        +

      
        
        59
        +func (c *Cli) lintStdin(lint *linter.Linter, reporter *linter.Reporter, format string) error {

      
        
        60
        +	loader := journal.NewLoader()

      
        
        61
        +	if _, err := loadStdin(loader); err != nil {

      
        
        62
        +		return err

      
        
        63
        +	}

      
        
        64
        +

      
        
        65
        +	sema := semantic.Build(loader.Ordered())

      
        
        66
        +	reporter.Collect(lint.Run(sema))

      
        
        67
        +

      
        
        68
        +	if err := reporter.Flush(format); err != nil {

      
        
        69
        +		return fmt.Errorf("flushing report: %w", err)

      
        
        70
        +	}

      
        
        71
        +	if reporter.HasIssues() {

      
        
        72
        +		return fmt.Errorf("lint found issues")

      
        
        73
        +	}

      
        
        74
        +	return nil

      
        
        75
        +}

      
        
        76
        +

      
        
        77
        +func parsePathStyle(s string) linter.PathStyle {

      
        
        78
        +	switch strings.ToLower(s) {

      
        
        79
        +	case "basename":

      
        
        80
        +		return linter.PathBasename

      
        
        81
        +	case "absolute":

      
        
        82
        +		return linter.PathAbsolute

      
        
        83
        +	default:

      
        
        84
        +		return linter.PathRelative

      
        
        85
        +	}

      
        
        86
        +}

      
A internal/cli/cmd_tags.go
···
        
        1
        +package cli

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"fmt"

      
        
        6
        +	"io"

      
        
        7
        +	"os"

      
        
        8
        +

      
        
        9
        +	"github.com/urfave/cli/v3"

      
        
        10
        +

      
        
        11
        +	"olexsmir.xyz/clerk/journal"

      
        
        12
        +	"olexsmir.xyz/clerk/journal/tags"

      
        
        13
        +)

      
        
        14
        +

      
        
        15
        +func (c *Cli) tagsAction(ctx context.Context, cmd *cli.Command) error {

      
        
        16
        +	cwd, err := os.Getwd()

      
        
        17
        +	if err != nil {

      
        
        18
        +		return fmt.Errorf("getting working directory: %w", err)

      
        
        19
        +	}

      
        
        20
        +

      
        
        21
        +	journals := cmd.StringArgs("journals")

      
        
        22
        +	if len(journals) == 0 {

      
        
        23
        +		journals = []string{"."}

      
        
        24
        +	}

      
        
        25
        +

      
        
        26
        +	journalFiles, err := resolvePaths(journals)

      
        
        27
        +	if err != nil {

      
        
        28
        +		return err

      
        
        29
        +	}

      
        
        30
        +

      
        
        31
        +	if len(journalFiles) == 0 {

      
        
        32
        +		return fmt.Errorf("no journal files found")

      
        
        33
        +	}

      
        
        34
        +

      
        
        35
        +	loader := journal.NewLoader()

      
        
        36
        +

      
        
        37
        +	var errs []error

      
        
        38
        +	for _, file := range journalFiles {

      
        
        39
        +		if _, err := loadFile(loader, file); err != nil {

      
        
        40
        +			fmt.Fprintf(os.Stderr, "error: %v\n", err)

      
        
        41
        +			errs = append(errs, err)

      
        
        42
        +		}

      
        
        43
        +	}

      
        
        44
        +	if len(errs) > 0 {

      
        
        45
        +		return fmt.Errorf("tags: %d errors", len(errs))

      
        
        46
        +	}

      
        
        47
        +

      
        
        48
        +	tagger := tags.New(loader, cwd)

      
        
        49
        +

      
        
        50
        +	out := cmd.String("out")

      
        
        51
        +	var w io.Writer = os.Stdout

      
        
        52
        +	if out != "-" {

      
        
        53
        +		f, err := os.Create(out)

      
        
        54
        +		if err != nil {

      
        
        55
        +			return fmt.Errorf("creating output file: %w", err)

      
        
        56
        +		}

      
        
        57
        +		defer f.Close()

      
        
        58
        +		w = f

      
        
        59
        +	}

      
        
        60
        +

      
        
        61
        +	if err := tagger.Write(w); err != nil {

      
        
        62
        +		return fmt.Errorf("writing tags: %w", err)

      
        
        63
        +	}

      
        
        64
        +

      
        
        65
        +	return nil

      
        
        66
        +}

      
A internal/cli/helpers.go
···
        
        1
        +package cli

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"fmt"

      
        
        5
        +	"io"

      
        
        6
        +	"os"

      
        
        7
        +	"path/filepath"

      
        
        8
        +

      
        
        9
        +	"olexsmir.xyz/clerk/journal"

      
        
        10
        +)

      
        
        11
        +

      
        
        12
        +func resolvePaths(paths []string) ([]string, error) {

      
        
        13
        +	if len(paths) == 0 {

      
        
        14
        +		return nil, nil

      
        
        15
        +	}

      
        
        16
        +

      
        
        17
        +	seen := make(map[string]bool)

      
        
        18
        +

      
        
        19
        +	var files []string

      
        
        20
        +	for _, p := range paths {

      
        
        21
        +		info, err := os.Stat(p)

      
        
        22
        +		if err != nil {

      
        
        23
        +			return nil, fmt.Errorf("%s: %w", p, err)

      
        
        24
        +		}

      
        
        25
        +

      
        
        26
        +		if info.IsDir() {

      
        
        27
        +			filepath.Walk(p, func(fpath string, finfo os.FileInfo, err error) error {

      
        
        28
        +				if err != nil || finfo.IsDir() || !journal.IsJournalFile(fpath) {

      
        
        29
        +					return nil

      
        
        30
        +				}

      
        
        31
        +				if !seen[fpath] {

      
        
        32
        +					seen[fpath] = true

      
        
        33
        +					files = append(files, fpath)

      
        
        34
        +				}

      
        
        35
        +				return nil

      
        
        36
        +			})

      
        
        37
        +			continue

      
        
        38
        +		}

      
        
        39
        +

      
        
        40
        +		if journal.IsJournalFile(p) && !seen[p] {

      
        
        41
        +			seen[p] = true

      
        
        42
        +			files = append(files, p)

      
        
        43
        +		}

      
        
        44
        +	}

      
        
        45
        +

      
        
        46
        +	return files, nil

      
        
        47
        +}

      
        
        48
        +

      
        
        49
        +func loadFile(loader *journal.Loader, path string) (*journal.ParsedFile, error) {

      
        
        50
        +	src, err := os.ReadFile(path)

      
        
        51
        +	if err != nil {

      
        
        52
        +		return nil, fmt.Errorf("reading %s: %w", path, err)

      
        
        53
        +	}

      
        
        54
        +	pf, err := loader.LoadBytes(path, src)

      
        
        55
        +	if err != nil {

      
        
        56
        +		return nil, fmt.Errorf("parsing %s: %w", path, err)

      
        
        57
        +	}

      
        
        58
        +	return pf, nil

      
        
        59
        +}

      
        
        60
        +

      
        
        61
        +func readStdin() ([]byte, error) {

      
        
        62
        +	src, err := io.ReadAll(os.Stdin)

      
        
        63
        +	if err != nil {

      
        
        64
        +		return nil, fmt.Errorf("reading stdin: %w", err)

      
        
        65
        +	}

      
        
        66
        +	return src, nil

      
        
        67
        +}

      
        
        68
        +

      
        
        69
        +func loadStdin(loader *journal.Loader) (*journal.ParsedFile, error) {

      
        
        70
        +	src, err := readStdin()

      
        
        71
        +	if err != nil {

      
        
        72
        +		return nil, err

      
        
        73
        +	}

      
        
        74
        +	pf, err := loader.LoadBytes("stdin", src)

      
        
        75
        +	if err != nil {

      
        
        76
        +		return nil, fmt.Errorf("parsing stdin: %w", err)

      
        
        77
        +	}

      
        
        78
        +	return pf, nil

      
        
        79
        +}

      
M internal/linter/report.go
···
        71
        71
         	r.finds = append(r.finds, finds...)

      
        72
        72
         }

      
        73
        73
         

      
        
        74
        +func (r *Reporter) HasIssues() bool {

      
        
        75
        +	return len(r.finds) > 0

      
        
        76
        +}

      
        
        77
        +

      
        74
        78
         func (r *Reporter) Flush(format string) error {

      
        75
        79
         	switch format {

      
        76
        80
         	case "json":

      
D lint.go
···
        1
        
        -package main

      
        2
        
        -

      
        3
        
        -import (

      
        4
        
        -	"flag"

      
        5
        
        -	"fmt"

      
        6
        
        -	"io"

      
        7
        
        -	"os"

      
        8
        
        -	"path/filepath"

      
        9
        
        -	"strings"

      
        10
        
        -

      
        11
        
        -	"olexsmir.xyz/clerk/internal/linter"

      
        12
        
        -	"olexsmir.xyz/clerk/journal"

      
        13
        
        -	"olexsmir.xyz/clerk/journal/semantic"

      
        14
        
        -)

      
        15
        
        -

      
        16
        
        -func runLint(args []string) {

      
        17
        
        -	fs := flag.NewFlagSet("lint", flag.ExitOnError)

      
        18
        
        -	format := fs.String("format", "text", "output format: text, json")

      
        19
        
        -	pathStyle := fs.String("path-style", "relative", "file path style: basename, relative, absolute")

      
        20
        
        -	fs.Usage = func() {

      
        21
        
        -		fmt.Fprintf(os.Stderr, "Usage: clerk lint [flags] [path...]\n")

      
        22
        
        -		fs.PrintDefaults()

      
        23
        
        -	}

      
        24
        
        -	fs.Parse(args)

      
        25
        
        -

      
        26
        
        -	style := parsePathStyle(*pathStyle)

      
        27
        
        -	l := linter.NewLinter(linter.Rules)

      
        28
        
        -	reporter := linter.NewReporter(os.Stdout, style)

      
        29
        
        -	paths := fs.Args()

      
        30
        
        -

      
        31
        
        -	if len(paths) == 0 {

      
        32
        
        -		finds := lintStdin(l)

      
        33
        
        -		reporter.Collect(finds)

      
        34
        
        -		reporter.Flush(*format)

      
        35
        
        -		if hasFatal(finds) {

      
        36
        
        -			os.Exit(1)

      
        37
        
        -		}

      
        38
        
        -		os.Exit(0)

      
        39
        
        -	}

      
        40
        
        -

      
        41
        
        -	exitCode := 0

      
        42
        
        -	for _, path := range paths {

      
        43
        
        -		info, err := os.Stat(path)

      
        44
        
        -		if err != nil {

      
        45
        
        -			fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)

      
        46
        
        -			exitCode = 1

      
        47
        
        -			continue

      
        48
        
        -		}

      
        49
        
        -		if info.IsDir() {

      
        50
        
        -			exitCode = lintDir(l, reporter, path)

      
        51
        
        -			continue

      
        52
        
        -		}

      
        53
        
        -		finds, fatal := lintFile(l, path)

      
        54
        
        -		reporter.Collect(finds)

      
        55
        
        -		if fatal {

      
        56
        
        -			exitCode = 1

      
        57
        
        -		}

      
        58
        
        -	}

      
        59
        
        -	reporter.Flush(*format)

      
        60
        
        -	os.Exit(exitCode)

      
        61
        
        -}

      
        62
        
        -

      
        63
        
        -func lintDir(l *linter.Linter, reporter *linter.Reporter, dir string) int {

      
        64
        
        -	ldr := journal.NewLoader()

      
        65
        
        -	var paths []string

      
        66
        
        -	filepath.Walk(dir, func(fpath string, finfo os.FileInfo, err error) error {

      
        67
        
        -		if err != nil || finfo.IsDir() || !journal.IsJournalFile(fpath) {

      
        68
        
        -			return nil

      
        69
        
        -		}

      
        70
        
        -		paths = append(paths, fpath)

      
        71
        
        -		return nil

      
        72
        
        -	})

      
        73
        
        -	if len(paths) == 0 {

      
        74
        
        -		return 0

      
        75
        
        -	}

      
        76
        
        -	var lastErr error

      
        77
        
        -	for _, fpath := range paths {

      
        78
        
        -		src, err := os.ReadFile(fpath)

      
        79
        
        -		if err != nil {

      
        80
        
        -			fmt.Fprintf(os.Stderr, "error: %s: %v\n", fpath, err)

      
        81
        
        -			lastErr = err

      
        82
        
        -			continue

      
        83
        
        -		}

      
        84
        
        -		if _, err := ldr.LoadBytes(fpath, src); err != nil {

      
        85
        
        -			fmt.Fprintf(os.Stderr, "error: %s: %v\n", fpath, err)

      
        86
        
        -			lastErr = err

      
        87
        
        -		}

      
        88
        
        -	}

      
        89
        
        -	ctx := semantic.Build(ldr.Ordered())

      
        90
        
        -	finds := l.Run(ctx)

      
        91
        
        -	reporter.Collect(finds)

      
        92
        
        -	if lastErr != nil {

      
        93
        
        -		return 1

      
        94
        
        -	}

      
        95
        
        -	if hasFatal(finds) {

      
        96
        
        -		return 1

      
        97
        
        -	}

      
        98
        
        -	return 0

      
        99
        
        -}

      
        100
        
        -

      
        101
        
        -func lintFile(l *linter.Linter, path string) ([]linter.Find, bool) {

      
        102
        
        -	src, err := os.ReadFile(path)

      
        103
        
        -	if err != nil {

      
        104
        
        -		fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)

      
        105
        
        -		return nil, true

      
        106
        
        -	}

      
        107
        
        -	ldr := journal.NewLoader()

      
        108
        
        -	pf, err := ldr.LoadBytes(path, src)

      
        109
        
        -	if err != nil {

      
        110
        
        -		fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)

      
        111
        
        -		return nil, true

      
        112
        
        -	}

      
        113
        
        -	_ = pf // top-level file kept via ldr, not directly needed

      
        114
        
        -	ctx := semantic.Build(ldr.Ordered())

      
        115
        
        -	finds := l.Run(ctx)

      
        116
        
        -	return finds, hasFatal(finds)

      
        117
        
        -}

      
        118
        
        -

      
        119
        
        -func lintStdin(l *linter.Linter) []linter.Find {

      
        120
        
        -	src, err := io.ReadAll(os.Stdin)

      
        121
        
        -	if err != nil {

      
        122
        
        -		fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err)

      
        123
        
        -		os.Exit(1)

      
        124
        
        -	}

      
        125
        
        -	ldr := journal.NewLoader()

      
        126
        
        -	_, err = ldr.LoadBytes("stdin", src)

      
        127
        
        -	if err != nil {

      
        128
        
        -		fmt.Fprintf(os.Stderr, "error: %v\n", err)

      
        129
        
        -		os.Exit(1)

      
        130
        
        -	}

      
        131
        
        -	ctx := semantic.Build(ldr.Ordered())

      
        132
        
        -	return l.Run(ctx)

      
        133
        
        -}

      
        134
        
        -

      
        135
        
        -func hasFatal(finds []linter.Find) bool {

      
        136
        
        -	for _, f := range finds {

      
        137
        
        -		if f.Severity <= 2 {

      
        138
        
        -			return true

      
        139
        
        -		}

      
        140
        
        -	}

      
        141
        
        -	return false

      
        142
        
        -}

      
        143
        
        -

      
        144
        
        -func parsePathStyle(s string) linter.PathStyle {

      
        145
        
        -	switch strings.ToLower(s) {

      
        146
        
        -	default:

      
        147
        
        -		return linter.PathRelative

      
        148
        
        -	case "basename":

      
        149
        
        -		return linter.PathBasename

      
        150
        
        -	case "absolute":

      
        151
        
        -		return linter.PathAbsolute

      
        152
        
        -	}

      
        153
        
        -}

      
M main.go
···
        1
        1
         package main

      
        2
        2
         

      
        3
        3
         import (

      
        4
        
        -	"fmt"

      
        
        4
        +	"context"

      
        
        5
        +	"log/slog"

      
        5
        6
         	"os"

      
        
        7
        +

      
        
        8
        +	"olexsmir.xyz/clerk/internal/cli"

      
        6
        9
         )

      
        
        10
        +

      
        
        11
        +// NOTE: sets during build

      
        
        12
        +// go build -ldflags="-X 'main.version=v1.0.0'"

      
        
        13
        +var version = "develop"

      
        7
        14
         

      
        8
        15
         func main() {

      
        9
        
        -	if len(os.Args) < 2 {

      
        10
        
        -		usage()

      
        
        16
        +	if err := cli.New(version).Run(context.Background(), os.Args); err != nil {

      
        
        17
        +		slog.Error("mugit", "err", err)

      
        11
        18
         		os.Exit(1)

      
        12
        19
         	}

      
        13
        
        -

      
        14
        
        -	switch os.Args[1] {

      
        15
        
        -	case "fmt", "format":

      
        16
        
        -		runFormat(os.Args[2:])

      
        17
        
        -	case "tags":

      
        18
        
        -		runTags(os.Args[2:])

      
        19
        
        -	case "lint":

      
        20
        
        -		runLint(os.Args[2:])

      
        21
        
        -	default:

      
        22
        
        -		usage()

      
        23
        
        -		os.Exit(1)

      
        24
        
        -	}

      
        25
        
        -}

      
        26
        
        -

      
        27
        
        -func usage() {

      
        28
        
        -	fmt.Fprintf(os.Stderr, `Usage: clerk <command> [options]

      
        29
        
        -

      
        30
        
        -Commands:

      
        31
        
        -  tags       Generate ctags-compatible tag file

      
        32
        
        -  format     Format journal files

      
        33
        
        -  lint       Lint journal files

      
        34
        
        -`)

      
        35
        20
         }

      
D tags.go
···
        1
        
        -package main

      
        2
        
        -

      
        3
        
        -import (

      
        4
        
        -	"flag"

      
        5
        
        -	"fmt"

      
        6
        
        -	"os"

      
        7
        
        -	"path/filepath"

      
        8
        
        -

      
        9
        
        -	"olexsmir.xyz/clerk/journal"

      
        10
        
        -	"olexsmir.xyz/clerk/journal/tags"

      
        11
        
        -)

      
        12
        
        -

      
        13
        
        -func runTags(args []string) {

      
        14
        
        -	fs := flag.NewFlagSet("tags", flag.ExitOnError)

      
        15
        
        -	output := fs.String("o", "tags", "output file, set to - for stdout")

      
        16
        
        -	fs.Usage = func() {

      
        17
        
        -		fmt.Fprintf(os.Stderr, "Usage: clerk tags [-o tags] [<journals>...]\n")

      
        18
        
        -		fs.PrintDefaults()

      
        19
        
        -	}

      
        20
        
        -	fs.Parse(args)

      
        21
        
        -

      
        22
        
        -	cwd, err := os.Getwd()

      
        23
        
        -	if err != nil {

      
        24
        
        -		fmt.Fprintf(os.Stderr, "error getting working directory: %v\n", err)

      
        25
        
        -		os.Exit(1)

      
        26
        
        -	}

      
        27
        
        -

      
        28
        
        -	rawPaths := fs.Args()

      
        29
        
        -	if len(rawPaths) == 0 {

      
        30
        
        -		rawPaths = []string{"."}

      
        31
        
        -	}

      
        32
        
        -

      
        33
        
        -	var journals []string

      
        34
        
        -	seen := make(map[string]bool)

      
        35
        
        -	for _, p := range rawPaths {

      
        36
        
        -		info, err := os.Stat(p)

      
        37
        
        -		if err != nil {

      
        38
        
        -			fmt.Fprintf(os.Stderr, "error reading %s: %v\n", p, err)

      
        39
        
        -			os.Exit(1)

      
        40
        
        -		}

      
        41
        
        -

      
        42
        
        -		if info.IsDir() {

      
        43
        
        -			entries, err := os.ReadDir(p)

      
        44
        
        -			if err != nil {

      
        45
        
        -				fmt.Fprintf(os.Stderr, "error reading directory %s: %v\n", p, err)

      
        46
        
        -				os.Exit(1)

      
        47
        
        -			}

      
        48
        
        -			for _, entry := range entries {

      
        49
        
        -				if entry.IsDir() { // TODO: keep traversing?

      
        50
        
        -					continue

      
        51
        
        -				}

      
        52
        
        -				fpath := filepath.Join(p, entry.Name())

      
        53
        
        -				if journal.IsJournalFile(fpath) && !seen[fpath] {

      
        54
        
        -					seen[fpath] = true

      
        55
        
        -					journals = append(journals, fpath)

      
        56
        
        -				}

      
        57
        
        -			}

      
        58
        
        -		} else if journal.IsJournalFile(p) && !seen[p] {

      
        59
        
        -			seen[p] = true

      
        60
        
        -			journals = append(journals, p)

      
        61
        
        -		}

      
        62
        
        -	}

      
        63
        
        -

      
        64
        
        -	if len(journals) == 0 {

      
        65
        
        -		fmt.Fprintf(os.Stderr, "no journal files found\n")

      
        66
        
        -		os.Exit(1)

      
        67
        
        -	}

      
        68
        
        -

      
        69
        
        -	loader := journal.NewLoader()

      
        70
        
        -	for _, path := range journals {

      
        71
        
        -		if _, err := loader.Load(path); err != nil {

      
        72
        
        -			fmt.Fprintf(os.Stderr, "error loading %s: %v\n", path, err)

      
        73
        
        -			os.Exit(1)

      
        74
        
        -		}

      
        75
        
        -	}

      
        76
        
        -

      
        77
        
        -	tagger := tags.New(loader, cwd)

      
        78
        
        -

      
        79
        
        -	w := os.Stdout

      
        80
        
        -	if *output != "-" {

      
        81
        
        -		f, err := os.Create(*output)

      
        82
        
        -		if err != nil {

      
        83
        
        -			fmt.Fprintf(os.Stderr, "error creating output file: %v\n", err)

      
        84
        
        -			os.Exit(1)

      
        85
        
        -		}

      
        86
        
        -		defer f.Close()

      
        87
        
        -		w = f

      
        88
        
        -	}

      
        89
        
        -

      
        90
        
        -	if err := tagger.Write(w); err != nil {

      
        91
        
        -		fmt.Fprintf(os.Stderr, "error writing tags: %v\n", err)

      
        92
        
        -		os.Exit(1)

      
        93
        
        -	}

      
        94
        
        -}