summaryrefslogtreecommitdiff
path: root/cmd/krino/main.go
blob: e204b5f7551f14322b13c2b1593302a98a324e06 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// SPDX-License-Identifier: GPL-3.0-or-later

// Command krino sorts files in configured directories by rules.
package main

import (
	"errors"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"
)

// version is stamped by the Makefile with -ldflags "-X main.version=...".
var version = "dev"

const usage = `usage: krino [-y | -n] [-v] [--json] [-c FILE] [NAME...]
       krino init
       krino new NAME PATH
       krino check [NAME...]
       krino explain FILE
       krino log [-n N]
       krino undo [RUN]

Sort the files in the directories listed in krino.conf by their rules.

  -y           apply without asking
  -n           dry run: show the plan, change nothing
  -v           also list unmatched, ignored and busy files
  --json       with -n: print the plan as JSON
  -c FILE      use FILE instead of ~/.config/krino/krino.conf
  -h, --help   show this help
  --version    print the version
`

// globals holds the flags that may appear before or after a subcommand.
type globals struct {
	yes, dry, verbose, json bool
	conf                    string
}

// command is a subcommand: it gets the parsed globals and its own arguments.
type command func(g *globals, args []string, stdout, stderr io.Writer) int

// commands maps subcommand names to their functions; each cmd file adds itself.
var commands = map[string]command{}

func main() {
	os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
}

// run is main without the process exit, so tests can drive it.
func run(args []string, stdout, stderr io.Writer) int {
	g := &globals{}
	fs := flagSet("krino", g)
	fs.BoolVar(&g.yes, "y", false, "")
	fs.BoolVar(&g.dry, "n", false, "")
	fs.BoolVar(&g.verbose, "v", false, "")
	fs.BoolVar(&g.json, "json", false, "")
	showVersion := fs.Bool("version", false, "")
	if code, ok := parse(fs, args, stdout, stderr); !ok {
		return code
	}
	if *showVersion {
		fmt.Fprintf(stdout, "krino %s\n", version)
		return 0
	}
	rest := fs.Args()
	if len(rest) > 0 {
		if cmd, ok := commands[rest[0]]; ok {
			return cmd(g, rest[1:], stdout, stderr)
		}
	}
	// Ruling 2026-09-12/4: Go's flag package stops parsing at the first
	// non-flag argument, so "krino dl -n" leaves "-n" in rest as a second
	// directory name instead of a flag - the dry run is silently never
	// honoured. A leftover argument that still looks like a flag is a
	// usage error rather than a guess; there is deliberately no second
	// pass that re-parses trailing flags, which would make "krino --
	// -weird-dir" ambiguous between the two syntaxes.
	for _, a := range rest {
		if strings.HasPrefix(a, "-") {
			fmt.Fprintf(stderr, "krino: %s: flags must come before directory names\nrun 'krino -h' for help\n", a)
			return 2
		}
	}
	return cmdSort(g, rest, stdout, stderr)
}

// flagSet returns a silent flag set with -c bound to g, shared by every command.
func flagSet(name string, g *globals) *flag.FlagSet {
	fs := flag.NewFlagSet(name, flag.ContinueOnError)
	fs.SetOutput(io.Discard)
	fs.StringVar(&g.conf, "c", g.conf, "")
	return fs
}

// parse parses args into fs. On -h it prints the usage and returns (0, false);
// on a bad flag it reports it and returns (2, false).
func parse(fs *flag.FlagSet, args []string, stdout, stderr io.Writer) (int, bool) {
	err := fs.Parse(args)
	switch {
	case err == nil:
		return 0, true
	case errors.Is(err, flag.ErrHelp):
		fmt.Fprint(stdout, usage)
		return 0, false
	default:
		fmt.Fprintf(stderr, "krino: %v\nrun 'krino -h' for help\n", err)
		return 2, false
	}
}