Skip to content

Commit 48fee98

Browse files
committed
feat: implement (*cliz.Command).Run() func
1 parent 2d25abd commit 48fee98

File tree

10 files changed

+665
-524
lines changed

10 files changed

+665
-524
lines changed

exp/cli/cli.go

Lines changed: 0 additions & 481 deletions
This file was deleted.

exp/cli/command.go

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
package cliz
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
"strconv"
9+
"strings"
10+
11+
errorz "github.com/kunitsucom/util.go/errors"
12+
)
13+
14+
//nolint:gochecknoglobals
15+
var (
16+
// Stdout is the writer to be used for standard output.
17+
Stdout io.Writer = os.Stdout
18+
// Stderr is the writer to be used for standard error.
19+
Stderr io.Writer = os.Stderr
20+
)
21+
22+
const (
23+
breakArg = "--"
24+
longOptionPrefix = "--"
25+
shortOptionPrefix = "-"
26+
)
27+
28+
type (
29+
// Command is a structure for building command lines. Please fill in each field for the structure you are facing.
30+
Command struct {
31+
// Name is the name of the command.
32+
Name string
33+
// Usage is the usage of the command.
34+
//
35+
// If you want to use the default usage, remain empty.
36+
// Otherwise, set the custom usage.
37+
Usage string
38+
// UsageFunc is custom usage function.
39+
//
40+
// If you want to use the default usage function, remain nil.
41+
// Otherwise, set the custom usage function.
42+
UsageFunc func(c *Command)
43+
// Description is the description of the command.
44+
Description string
45+
// Options is the options of the command.
46+
Options []Option
47+
// Func is the function to be executed when the command is executed.
48+
Func func(ctx context.Context, remainingArgs []string) error
49+
// SubCommands is the subcommands of the command.
50+
SubCommands []*Command
51+
52+
calledCommands []string
53+
remainingArgs []string
54+
}
55+
56+
// Option is the interface for the option.
57+
Option interface {
58+
// GetName returns the name of the option.
59+
GetName() string
60+
// GetShort returns the short name of the option.
61+
GetShort() string
62+
// GetEnvironment returns the environment variable name of the option.
63+
GetEnvironment() string
64+
// GetDescription returns the description of the option.
65+
GetDescription() string
66+
// HasDefault returns whether the option has a default value.
67+
HasDefault() bool
68+
// getDefault returns the default value of the option.
69+
getDefault() interface{}
70+
// HasValue returns whether the option has a value.
71+
HasValue() bool
72+
73+
// private is the private method for internal interface.
74+
private()
75+
}
76+
)
77+
78+
func (cmd *Command) getDescription() string {
79+
if cmd.Description != "" {
80+
return cmd.Description
81+
}
82+
return fmt.Sprintf("command %q description", strings.Join(cmd.calledCommands, " "))
83+
}
84+
85+
func (cmd *Command) Next() *Command {
86+
if cmd == nil {
87+
return nil
88+
}
89+
if len(cmd.calledCommands) == 0 {
90+
return nil
91+
}
92+
for _, subcmd := range cmd.SubCommands {
93+
if len(subcmd.calledCommands) > 0 {
94+
return subcmd
95+
}
96+
}
97+
return nil
98+
}
99+
100+
func (cmd *Command) GetCalledCommands() []string {
101+
if cmd == nil {
102+
return nil
103+
}
104+
105+
for _, subcmd := range cmd.SubCommands {
106+
if len(subcmd.calledCommands) > 0 {
107+
return subcmd.GetCalledCommands()
108+
}
109+
}
110+
111+
return cmd.calledCommands
112+
}
113+
114+
// getSubcommand returns the subcommand if cmd contains the subcommand.
115+
func (cmd *Command) getSubcommand(arg string) (subcmd *Command) {
116+
if cmd == nil {
117+
return nil
118+
}
119+
120+
for _, subcmd := range cmd.SubCommands {
121+
if subcmd.Name == arg {
122+
return subcmd
123+
}
124+
}
125+
return nil
126+
}
127+
128+
func equalOptionArg(o Option, arg string) bool {
129+
return longOptionPrefix+o.GetName() == arg || shortOptionPrefix+o.GetShort() == arg
130+
}
131+
132+
func hasPrefixOptionEqualArg(o Option, arg string) bool {
133+
return strings.HasPrefix(arg, longOptionPrefix+o.GetName()+"=") || strings.HasPrefix(arg, shortOptionPrefix+o.GetShort()+"=")
134+
}
135+
136+
func extractValueOptionEqualArg(arg string) string {
137+
return strings.Join(strings.Split(arg, "=")[1:], "=")
138+
}
139+
140+
func hasOptionValue(args []string, i int) bool {
141+
lastIndex := len(args) - 1
142+
return i+1 > lastIndex
143+
}
144+
145+
//nolint:funlen,gocognit,cyclop
146+
func (cmd *Command) parseArgs(args []string) (remaining []string, err error) {
147+
cmd.calledCommands = append(cmd.calledCommands, cmd.Name)
148+
cmd.remainingArgs = make([]string, 0)
149+
150+
i := 0
151+
argsLoop:
152+
for ; i < len(args); i++ {
153+
arg := args[i]
154+
155+
switch {
156+
case arg == breakArg:
157+
cmd.remainingArgs = append(cmd.remainingArgs, args[i+1:]...)
158+
break argsLoop
159+
case strings.HasPrefix(arg, shortOptionPrefix):
160+
for _, opt := range cmd.Options {
161+
switch o := opt.(type) {
162+
case *StringOption:
163+
switch {
164+
case equalOptionArg(o, arg):
165+
DebugLog.Printf("%s: option: %s: %s", cmd.Name, o.Name, arg)
166+
if hasOptionValue(args, i) {
167+
return nil, errorz.Errorf("%s: %w", arg, ErrMissingOptionValue)
168+
}
169+
o.value = ptr(args[i+1])
170+
i++
171+
TraceLog.Printf("%s: parsed option: %s: %v", cmd.Name, o.Name, *o.value)
172+
continue argsLoop
173+
case hasPrefixOptionEqualArg(o, arg):
174+
DebugLog.Printf("%s: option: %s: %s", cmd.Name, o.Name, arg)
175+
o.value = ptr(extractValueOptionEqualArg(arg))
176+
TraceLog.Printf("%s: parsed option: %s: %v", cmd.Name, o.Name, *o.value)
177+
continue argsLoop
178+
}
179+
case *BoolOption:
180+
switch {
181+
case equalOptionArg(o, arg):
182+
DebugLog.Printf("%s: option: %s: %s", cmd.Name, o.Name, arg)
183+
o.value = ptr(true)
184+
TraceLog.Printf("%s: parsed option: %s: %v", cmd.Name, o.Name, *o.value)
185+
continue argsLoop
186+
case hasPrefixOptionEqualArg(o, arg):
187+
DebugLog.Printf("%s: option: %s: %s", cmd.Name, o.Name, arg)
188+
optVal, err := strconv.ParseBool(extractValueOptionEqualArg(arg))
189+
if err != nil {
190+
return nil, errorz.Errorf("%s: %w", arg, err)
191+
}
192+
o.value = &optVal
193+
TraceLog.Printf("%s: parsed option: %s: %v", cmd.Name, o.Name, *o.value)
194+
continue argsLoop
195+
}
196+
case *IntOption:
197+
switch {
198+
case equalOptionArg(o, arg):
199+
DebugLog.Printf("%s: option: %s: %s", cmd.Name, o.Name, arg)
200+
if hasOptionValue(args, i) {
201+
return nil, errorz.Errorf("%s: %w", arg, ErrMissingOptionValue)
202+
}
203+
optVal, err := strconv.Atoi(args[i+1])
204+
if err != nil {
205+
return nil, errorz.Errorf("%s: %w", arg, err)
206+
}
207+
o.value = &optVal
208+
i++
209+
TraceLog.Printf("%s: parsed option: %s: %v", cmd.Name, o.Name, *o.value)
210+
continue argsLoop
211+
case hasPrefixOptionEqualArg(o, arg):
212+
DebugLog.Printf("%s: option: %s: %s", cmd.Name, o.Name, arg)
213+
optVal, err := strconv.Atoi(extractValueOptionEqualArg(arg))
214+
if err != nil {
215+
return nil, errorz.Errorf("%s: %w", arg, err)
216+
}
217+
o.value = &optVal
218+
TraceLog.Printf("%s: parsed option: %s: %v", cmd.Name, o.Name, *o.value)
219+
continue argsLoop
220+
}
221+
case *Float64Option:
222+
switch {
223+
case equalOptionArg(o, arg):
224+
DebugLog.Printf("%s: option: %s: %s", cmd.Name, o.Name, arg)
225+
if hasOptionValue(args, i) {
226+
return nil, errorz.Errorf("%s: %w", arg, ErrMissingOptionValue)
227+
}
228+
optVal, err := strconv.ParseFloat(args[i+1], 64)
229+
if err != nil {
230+
return nil, errorz.Errorf("%s: %w", arg, err)
231+
}
232+
o.value = &optVal
233+
i++
234+
TraceLog.Printf("%s: parsed option: %s: %v", cmd.Name, o.Name, *o.value)
235+
continue argsLoop
236+
case hasPrefixOptionEqualArg(o, arg):
237+
DebugLog.Printf("%s: option: %s: %s", cmd.Name, o.Name, arg)
238+
optVal, err := strconv.ParseFloat(extractValueOptionEqualArg(arg), 64)
239+
if err != nil {
240+
return nil, errorz.Errorf("%s: %w", arg, err)
241+
}
242+
o.value = &optVal
243+
TraceLog.Printf("%s: parsed option: %s: %v", cmd.Name, o.Name, *o.value)
244+
continue argsLoop
245+
}
246+
default:
247+
return nil, errorz.Errorf("%s: %w", arg, ErrInvalidOptionType)
248+
}
249+
}
250+
return nil, errorz.Errorf("%s: %w", arg, ErrUnknownOption)
251+
default:
252+
if subcmd := cmd.getSubcommand(arg); subcmd != nil {
253+
TraceLog.Printf("parse: subcommand: %s", subcmd.Name)
254+
subcmd.calledCommands = append(subcmd.calledCommands, cmd.calledCommands...)
255+
cmd.remainingArgs, err = subcmd.parseArgs(args[i+1:])
256+
if err != nil {
257+
return nil, errorz.Errorf("%s: %w", arg, err)
258+
}
259+
return cmd.remainingArgs, nil
260+
}
261+
262+
// If subcmd is nil, it is not a subcommand.
263+
cmd.remainingArgs = append(cmd.remainingArgs, arg)
264+
continue argsLoop
265+
}
266+
}
267+
268+
return cmd.remainingArgs, nil
269+
}
270+
271+
func (cmd *Command) initCommand() {
272+
cmd.calledCommands = make([]string, 0)
273+
cmd.remainingArgs = make([]string, 0)
274+
275+
for _, subcmd := range cmd.SubCommands {
276+
subcmd.initCommand()
277+
}
278+
}
279+
280+
// Parse parses the arguments as commands and sub commands and options.
281+
//
282+
// If the "--help" option is specified, it will be displayed and ErrHelp will be returned.
283+
//
284+
// If the option is not specified, the default value will be used.
285+
//
286+
// If the environment variable is specified, it will be used as the value of the option.
287+
//
288+
//nolint:cyclop
289+
func (cmd *Command) Parse(args []string) (remainingArgs []string, err error) {
290+
if len(args) > 0 && (args[0] == os.Args[0] || args[0] == cmd.Name) {
291+
args = args[1:]
292+
}
293+
294+
cmd.initCommand()
295+
cmd.initAppendHelpOption()
296+
297+
if err := cmd.preCheckSubCommands(); err != nil {
298+
return nil, errorz.Errorf("failed to pre-check commands: %w", err)
299+
}
300+
301+
if err := cmd.preCheckOptions(); err != nil {
302+
return nil, errorz.Errorf("failed to pre-check options: %w", err)
303+
}
304+
305+
if err := cmd.loadDefaults(); err != nil {
306+
return nil, errorz.Errorf("failed to load default: %w", err)
307+
}
308+
309+
if err := cmd.loadEnvironments(); err != nil {
310+
return nil, errorz.Errorf("failed to load environment: %w", err)
311+
}
312+
313+
remaining, err := cmd.parseArgs(args)
314+
if err != nil {
315+
return nil, errorz.Errorf("failed to parse commands and options: %w", err)
316+
}
317+
318+
// NOTE: help
319+
if err := cmd.checkHelp(); err != nil {
320+
return nil, err //nolint:wrapcheck
321+
}
322+
323+
if err := cmd.postCheckOptions(); err != nil {
324+
return nil, errorz.Errorf("failed to post-check options: %w", err)
325+
}
326+
327+
return remaining, nil
328+
}
329+
330+
func (cmd *Command) Run(ctx context.Context, args []string) error {
331+
remainingArgs, err := cmd.Parse(args)
332+
if err != nil {
333+
return errorz.Errorf("%s: %w", cmd.Name, err)
334+
}
335+
336+
execCmd := cmd
337+
for len(execCmd.Next().GetCalledCommands()) > 0 {
338+
execCmd = execCmd.Next()
339+
}
340+
341+
if execCmd.Func == nil {
342+
return errorz.Errorf("%s: %w", strings.Join(execCmd.calledCommands, " "), ErrCommandFuncNotSet)
343+
}
344+
345+
return execCmd.Func(WithContext(ctx, execCmd), remainingArgs)
346+
}

0 commit comments

Comments
 (0)