/
main.go
148 lines (137 loc) · 3.82 KB
/
main.go
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package main
import (
"context"
"flag"
"io"
"os"
"path"
"github.com/gobwas/cli"
"github.com/gobwas/flagutil"
"github.com/gobwas/flagutil/parse/file"
"github.com/gobwas/flagutil/parse/file/yaml"
"github.com/gobwas/flagutil/parse/pargs"
)
type core struct {
verbose bool
}
func main() {
var core core
r := cli.Runner{
// Override flags parsing to use flagutil package.
// It allows us to have fancy things like flag shortucts and
// posix-compatible flags syntax.
DoParseFlags: func(ctx context.Context, fs *flag.FlagSet, args []string) ([]string, error) {
opts, rest := parseOptions(fs, args)
err := flagutil.Parse(ctx, fs, opts...)
if err != nil {
return nil, err
}
return rest(), err
},
// Override help message printing. It will print pretty help message
// which is aware of flag shortcuts used by flagutil package.
DoPrintFlags: func(ctx context.Context, w io.Writer, fs *flag.FlagSet) error {
// Be kind and restore original output writer.
orig := fs.Output()
fs.SetOutput(w)
defer fs.SetOutput(orig)
// Note that to print right help message we have to use same parse
// options we used in DoParseFlags() above. That's why here is
// parseOptions() helper func.
opts, _ := parseOptions(fs, nil)
return flagutil.PrintDefaults(ctx, fs, opts...)
},
}
r.Main(&cli.Container{
Command: cli.Commands{
// NOTE: we wrap original command here to make it parse
// configuration file before actual Run() happens.
//
// This is an easy way to determine the "target" command. That is,
// the command which is the latest in the execution path.
// We need this since we don't want to parse configuration file per
// each command in the path because not all of commands defined
// their flags yet.
"ping": wrap(&ping{
core: &core,
}),
},
// Define the "core" global flags which we can use then in sub commands
// (if the core struct were injected).
DoDefineFlags: func(fs *flag.FlagSet) {
fs.BoolVar(&core.verbose,
"verbose", false,
"be verbose",
)
},
})
}
func wrap(cmd cli.Command) cli.Command {
return &cli.Container{
Command: cmd,
DoRun: func(ctx context.Context, args []string) error {
if err := parseConfigFile(ctx); err != nil {
return err
}
return cmd.Run(ctx, args)
},
}
}
func parseConfigFile(ctx context.Context) error {
all := mergeFlags(ctx)
return flagutil.Parse(ctx, all,
flagutil.WithParser(
&file.Parser{
Lookup: file.PathLookup(configPath()),
Syntax: new(yaml.Syntax),
},
),
)
}
// mergeFlags prepares merge of every command's flag set into one superset.
// It adds command name as a prefix for every subset.
func mergeFlags(ctx context.Context) *flag.FlagSet {
all := flag.NewFlagSet("all", flag.PanicOnError)
for i, cmd := range cli.ContextCommandsInfo(ctx) {
if cmd.FlagSet == nil {
continue
}
name := cmd.Name
if i == 0 {
name = "core"
}
flagutil.Subset(all, name, func(sub *flag.FlagSet) {
// Combine command flag set into a new empty subset.
// This makes setting flag value of a subset also change original
// command flag set.
*sub = *flagutil.CombineSets(sub, cmd.FlagSet)
})
// Mark already specified flags in command flag set as specified in
// superset as well. This makes command line options prioritized over
// file configuration.
cmd.FlagSet.Visit(func(f *flag.Flag) {
flagutil.SetActual(all, name+"."+f.Name)
})
}
return all
}
func parseOptions(fs *flag.FlagSet, args []string) (
opts []flagutil.ParseOption,
rest func() []string,
) {
posix := &pargs.Parser{
Args: args,
Shorthand: true,
}
opts = []flagutil.ParseOption{
flagutil.WithParser(posix),
}
return opts, posix.NonOptionArgs
}
func configPath() string {
dir, err := os.Getwd()
if err != nil {
panic(err)
}
return path.Join(dir, "examples/complex/config.yaml")
}