1// Package cli parses the neferwl command line and runs the selected command.
2package cli
3
4import (
5 "context"
6 "errors"
7 "flag"
8 "fmt"
9 "io"
10 "os"
11)
12
13// Env holds what a command uses besides its arguments.
14type Env struct {
15 Stdout, Stderr io.Writer
16 // Version is the packaged build version; empty for source builds.
17 Version string
18}
19
20// Runner runs a command with its positional arguments, after flag parsing.
21type Runner func(ctx context.Context, env Env, args []string) error
22
23// Command is one neferwl command. Setup registers the command's flags on fs
24// and returns its Runner, which reads the parsed flag values.
25type Command struct {
26 Name string // empty for the root command
27 Args string // positional arguments synopsis, e.g. "[path]"
28 Summary string
29 Setup func(fs *flag.FlagSet) Runner
30}
31
32// root runs the compositor; commands are the subcommands.
33var (
34 root = Command{Args: "[flags]", Summary: "Run the compositor.", Setup: setupRun}
35 commands = []Command{validateConfigCmd, stateCmd, versionCmd}
36)
37
38// Main runs the command line args (without the program name) and returns
39// the process exit code: 0 on success, 2 on usage errors, 1 otherwise.
40func Main(args []string, version string) int {
41 env := Env{Stdout: os.Stdout, Stderr: os.Stderr, Version: version}
42 return exitCode(env.Stderr, Execute(context.Background(), env, args))
43}
44
45// Execute selects the command named by args[0], or the root command, parses
46// its flags and runs it. -h and --help print the command usage to Stdout.
47func Execute(ctx context.Context, env Env, args []string) error {
48 cmd := lookup(args)
49 if cmd.Name != "" {
50 args = args[1:]
51 }
52 fs := flag.NewFlagSet(cmd.path(), flag.ContinueOnError)
53 fs.SetOutput(io.Discard) // errors and usage are printed once, here and in exitCode
54 run := cmd.Setup(fs)
55 if err := fs.Parse(args); err != nil {
56 if errors.Is(err, flag.ErrHelp) {
57 printUsage(env.Stdout, cmd, fs)
58 return nil
59 }
60 return cmd.usageError(err)
61 }
62 err := run(ctx, env, fs.Args())
63 var usage usageError
64 if errors.As(err, &usage) {
65 return cmd.usageError(usage.error)
66 }
67 return err
68}
69
70func lookup(args []string) Command {
71 if len(args) > 0 {
72 for _, c := range commands {
73 if args[0] == c.Name {
74 return c
75 }
76 }
77 }
78 return root
79}
80
81func (c Command) path() string {
82 if c.Name == "" {
83 return "neferwl"
84 }
85 return "neferwl " + c.Name
86}
87
88func (c Command) synopsis() string {
89 if c.Args == "" {
90 return c.path()
91 }
92 return c.path() + " " + c.Args
93}
94
95// usageError marks a bad command line; it exits with code 2.
96type usageError struct{ error }
97
98// usagef returns a usage error; Execute appends the command synopsis.
99func usagef(format string, a ...any) error {
100 return usageError{fmt.Errorf(format, a...)}
101}
102
103func (c Command) usageError(err error) error {
104 return usageError{fmt.Errorf("%w\nusage: %s (see %s --help)", err, c.synopsis(), c.path())}
105}
106
107func printUsage(w io.Writer, cmd Command, fs *flag.FlagSet) {
108 fmt.Fprintf(w, "usage: %s\n\n%s\n", cmd.synopsis(), cmd.Summary)
109 if cmd.Name == "" {
110 fmt.Fprintln(w, "\nCommands:")
111 for _, c := range commands {
112 fmt.Fprintf(w, " %-34s %s\n", c.synopsis(), c.Summary)
113 }
114 }
115 hasFlags := false
116 fs.VisitAll(func(*flag.Flag) { hasFlags = true })
117 if hasFlags {
118 fmt.Fprintln(w, "\nFlags:")
119 fs.SetOutput(w)
120 fs.PrintDefaults()
121 }
122}
123
124// exitCode prints err to w, unless the run log already has it, and maps
125// it to the process exit code. A canceled context (SIGINT, SIGTERM) is a
126// clean exit.
127func exitCode(w io.Writer, err error) int {
128 if err == nil || errors.Is(err, context.Canceled) {
129 return 0
130 }
131 if !errors.As(err, new(loggedError)) {
132 fmt.Fprintln(w, err)
133 }
134 var usage usageError
135 if errors.As(err, &usage) {
136 return 2
137 }
138 return 1
139}