12 files changed,
497 insertions(+),
441 deletions(-)
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.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/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 +}
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 }