diff --git a/cli.go b/cli.go index 1e90e8f..0d91303 100644 --- a/cli.go +++ b/cli.go @@ -2,14 +2,16 @@ // // Example usage // -// Declare a struct type that embeds *cli.Flagger, along with an fields you want to capture as flags. +// Declare a struct type which implement cli.Command interface. // // type Echo struct { -// *cli.Flagger // Echoed string `flag:"echoed, echo this string"` // } // -// Package understands all basic types supported by flag's package xxxVar functions: int, int64, uint, uint64, float64, bool, string, time.Duration. Types implementing flag.Value interface are also supported. +// Package understands all basic types supported by flag's package xxxVar functions: +// int, int64, uint, uint64, float64, bool, string, time.Duration. +// Types implementing flag.Value interface are also supported. +// (Useful package: https://github.com/sgreben/flagvar) // // type CustomDate string // @@ -27,26 +29,24 @@ // } // // type EchoWithDate struct { -// *cli.Flagger // Echoed string `flag:"echoed, echo this string"` // EchoWithDate CustomDate `flag:"echoDate, echo this date too"` // } // -// Now we need to make our type implement the cli.Command interface. That requires three methods that aren't already provided by *cli.Flagger: +// Now we need to make our type implement the cli.Command interface. // -// func (c *Echo) Desc() string { +// func (c *Echo) Help() string { // return "Echo the input string." // } // -// func (c *Echo) Run() { -// fmt.Println(c.Echoed) +// func (c *Echo) Synopsis() string { +// return "Short one liner about the command" // } // // Maybe we write sample command runs: // -// func (c *Echo) Samples() []string { -// return []string{"echoprogram -echoed=\"echo this\"", -// "echoprogram -echoed=\"or echo this\""} +// func (c *Echo) Run(ctx_ context.Context) error { +// return nil // } // // We can set default command to run @@ -56,123 +56,80 @@ // After all of this, we can run them like this: // // func main() { -// c := cli.New("echoer", "1.0.0") -// c.Authors = []string{"authors goes here"} -// c.Add( -// &Echo{ -// Echoed: "default string", -// }) -// //c.SetDefaults("echo") -// c.Run(os.Args) +// c := cli.New("archiver", "1.0.0") +// cli.RootCommand().Authors = []string{"authors goes here"} +// cli.RootCommand().Description = `Lorem Ipsum is simply dummy text of the printing and typesetting industry. +// Lorem Ipsum has been the industry's standard dummy text ever since the 1500s` +// +// cli.RootCommand().AddCommand("echo", &Echo{}) +// c.Run(context.Background(), os.Args) // } package cli import ( + "bytes" "context" "errors" "flag" "fmt" + "io" "os" "reflect" + "strconv" "strings" "text/template" "time" ) +const ( + completeLine = "COMP_LINE" + completePoint = "COMP_POINT" + defaultHelpTemplate = `{{.Help}} +{{with $flags := flagSet .Command}}{{if ne $flags ""}} +Options: +{{$flags}}{{- end }}{{end}}{{if gt (len .SubCommands) 0}} +Commands: +{{- range $name, $value := .SubCommands }} + {{$value.NameAligned}} {{$value.Synopsis}}{{with $flags := flagSet $value.Command}}{{if ne $flags ""}} + Options: + {{replace $flags "\n" "\n " -1}}{{- end }}{{end}} +{{- end }}{{end}} +` +) + // CLI defines a new command line interface type CLI struct { - Name string - Version string - Description string - Authors []string - Commands *commands - commandSets map[string]*commands - defaultCommand string - template *template.Template - commandTemplate *template.Template -} - -type commands struct { - Command - SubCommands map[string]*commands - flagSetDone bool + // HelpWriter is used to print help text and version when requested. + HelpWriter io.Writer + // ErrorWriter used to output errors when a command can not be run. + ErrorWriter io.Writer + // AutoComplete used to handle autocomplete request from bash or zsh. + AutoComplete bool + root *Root + defaultCommand string + flagSet Flagger + flagSetOut bytes.Buffer + template string + lastCommandsName []string } -// New returns new CLI struct +// New returns a new CLI struct func New(name string, version string) *CLI { cli := &CLI{ - Name: name, - commandSets: make(map[string]*commands), - defaultCommand: "help", - Version: version, - } - cli.commandSets["root"] = &commands{ - SubCommands: make(map[string]*commands), - flagSetDone: true, - } - cli.SetTemplate(`USAGE: - {{.Name}}{{if .Commands}} command [command options]{{end}} -VERSION: - {{.Version}}{{if .Description}} -DESCRIPTION: - {{.Description}}{{end}}{{if len .Authors}} -AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: - {{range $index, $author := .Authors}}{{if $index}} - {{end}}{{$author}}{{end}}{{end}}{{if .Commands.Command}} -COMMAND: - {{.Name}}: {{printCommand .Commands}} -{{with $subCommands := .Commands.SubCommands}}{{with $subCommandsLength := len $subCommands}}{{if gt $subCommandsLength 0}}SUB COMMANDS DESCRIPTION{{if gt $subCommandsLength 1}}S{{end}}:{{range $subName, $subCommand := $subCommands}} - {{$subName}}: {{printCommand $subCommand}} -{{end}}{{end}}{{end}}{{end}}{{else}} -COMMAND{{with $length := len .Commands.SubCommands}}{{if ne 1 $length}}S{{end}}{{end}}:{{range $subName, $subCommands := .Commands.SubCommands}} - {{$subName}}: {{printCommand $subCommands}} -{{end}}{{end}} -`, `{{.Command.Desc}}{{with $help := .Command.Help}}{{if ne $help ""}} - Options: - {{replace $help "\n" "\n " -1}}{{end}}{{end}}{{with $samples := .Command.Samples}}{{with $sampleLen := len $samples}}{{if gt $sampleLen 1}} - Samples: - {{join $samples "\n "}}{{end}}{{end}}{{end}}{{with $subCommands := .SubCommands}}{{with $subCommandsLength := len $subCommands}}{{if gt $subCommandsLength 0}} - SUB COMMAND{{if ne 1 $subCommandsLength}}S{{end}}:{{range $subName, $subCommand := $subCommands}} - {{$subName}}: {{$subCommand.Desc}}{{end}}{{end}}{{end}}{{end}}`) - return cli -} - -// SetTemplate sets the template for the console output -func (cli *CLI) SetTemplate(temp string, commandTemp string) error { - funcs := template.FuncMap{ - "join": strings.Join, - "replace": strings.Replace, - "printSubCommands": func(commands *commands) string { - err := cli.commandTemplate.Execute(os.Stderr, commands) - if err != nil { - return err.Error() - } - return "" - }, - "printCommand": func(commands *commands) string { - err := cli.commandTemplate.Execute(os.Stderr, commands) - if err != nil { - return err.Error() - } - return "" + root: RootCommand(), + HelpWriter: os.Stdout, + AutoComplete: true, + ErrorWriter: os.Stderr, + flagSet: &flag.FlagSet{ + Usage: func() {}, }, + template: defaultHelpTemplate, } - ct, err := template.New("command").Funcs(funcs).Parse(commandTemp) - if err != nil { - return err - } - t, err := template.New("help").Funcs(funcs).Parse(temp) - if err != nil { - return err - } - cli.template = t - cli.commandTemplate = ct - return nil -} + cli.flagSet.SetOutput(&cli.flagSetOut) + cli.root.Name = name + cli.root.Version = version -// Add commands -func (cli *CLI) Add(commands ...Command) { - cli.addTo(commands, cli.commandSets["root"]) + return cli } // SetDefault sets default command @@ -182,50 +139,93 @@ func (cli *CLI) SetDefault(command string) { // Run parses the arguments and runs the applicable command func (cli *CLI) Run(ctx context.Context, args []string) { + doComplete := false + if line, ok := cli.isCompleteStarted(); ok { + if !cli.AutoComplete { + return + } + args = strings.Split(line, " ") + doComplete = true + } if len(args) == 1 { args = append(args, cli.defaultCommand) } - c, err := cli.getSubCommand(cli.commandSets["root"], args[1:]) + c, err := cli.getSubCommand(cli.root, args[1:]) + if doComplete { + lastArg := strings.TrimLeft(args[len(args)-1], "-") + if len(cli.lastCommandsName) > 0 { + for _, name := range cli.lastCommandsName { + if strings.HasPrefix(name, lastArg) { + cli.HelpWriter.Write([]byte(name + "\n")) + } + } + } else { + cli.flagSet.VisitAll(func(f *flag.Flag) { + if strings.HasPrefix(f.Name, lastArg) { + cli.HelpWriter.Write([]byte("-" + f.Name + "\n")) + } + }) + } + return + } if err != nil { - fmt.Println("INVALID PARAMETER:") - fmt.Println(" ", err) - fmt.Println() - cli.help(c) + cli.help(c, err) return } if c == nil { - cli.help(cli.commandSets["root"]) + cli.help(cli.root, nil) return } - c.Command.Run(ctx) + if err := c.Run(ctx); err != nil { + cli.help(c, err) + } +} + +// SetTemplate set a new template for commands +func (cli *CLI) SetTemplate(template string) { + cli.template = template } -func (cli *CLI) getSubCommand(commandSet *commands, args []string) (*commands, error) { - for name, c := range commandSet.SubCommands { +// SetFlagSet set an different flag parser +func (cli *CLI) SetFlagSet(flagSet Flagger) { + cli.flagSet = flagSet + cli.flagSet.SetOutput(&cli.flagSetOut) +} + +func (cli *CLI) getSubCommand(command SubCommands, args []string) (Command, error) { + cli.lastCommandsName = []string{} + for name, c := range command.SubCommands() { + cli.lastCommandsName = append(cli.lastCommandsName, name) if name == args[0] { - cli.getFlagSet(c) - if len(c.SubCommands) > 0 { + if err := cli.getFlagSet(c); err != nil { + return c, err + } + if subC, ok := c.(SubCommands); ok { if len(args) <= 1 { return c, errors.New("missing sub command") } - subC, err := cli.getSubCommand(c, args[1:]) + subC, err := cli.getSubCommand(subC, args[1:]) + if subC != nil { + cli.lastCommandsName = []string{} + } if err != nil { - return c, err + if subC == nil { + subC = c + } + return subC, err } if subC == nil { - return c, errors.New("missing sub command") + return c, errors.New("wrong sub command") } return subC, nil } + var parseArg []string if len(args) > 1 { - if err := c.Command.Parse(args[1:]); err != nil { - return c, err - } - } else { - if err := c.Command.Parse([]string{}); err != nil { - return c, err - } + parseArg = args[1:] + } + if err := cli.flagSet.Parse(parseArg); err != nil { + return c, err } return c, nil } @@ -233,72 +233,91 @@ func (cli *CLI) getSubCommand(commandSet *commands, args []string) (*commands, e return nil, nil } -func (cli *CLI) addTo(commandList []Command, commandSet *commands) { - for _, c := range commandList { - if reflect.TypeOf(c).Kind() != reflect.Ptr { - continue - } - t := reflect.TypeOf(c).Elem() - v := reflect.ValueOf(c).Elem() - flagger := v.FieldByNameFunc(func(s string) bool { return strings.Contains(s, "Flagger") }) - if !flagger.CanSet() { - continue - } - flagger.Set(reflect.ValueOf(&Flagger{FlagSet: &flag.FlagSet{}})) - name := strings.ToLower(t.Name()) - - commandSet.SubCommands[name] = &commands{ - Command: c, - SubCommands: make(map[string]*commands), +func (cli *CLI) help(c Command, err error) { + output := cli.HelpWriter + if err != nil { + output = cli.ErrorWriter + output.Write([]byte(err.Error() + "\n\n")) + } + t, err := template.New("root").Funcs(template.FuncMap{ + "replace": strings.Replace, + "flagSet": func(c Command) string { + fs := &flag.FlagSet{ + Usage: func() {}, + } + var out bytes.Buffer + fs.SetOutput(&out) + st := reflect.ValueOf(c) + if st.Kind() != reflect.Ptr { + return err.Error() + } + if err := cli.defineFlagSet(fs, st, ""); err != nil { + return err.Error() + } + fs.PrintDefaults() + return out.String() + }}).Parse(defaultHelpTemplate) + if err != nil { + cli.ErrorWriter.Write([]byte(fmt.Sprintf( + "Internal error! Failed to parse command help template: %s\n", err))) + return + } + s := struct { + Command + SubCommands map[string]interface{} + }{ + Command: c, + SubCommands: make(map[string]interface{}), + } + if subCs, ok := c.(SubCommands); ok { + longest := 0 + subC := subCs.SubCommands() + for k, _ := range subC { + if v := len(k); v > longest { + longest = v + } } - if subCommands := c.SubCommands(); len(subCommands) > 0 { - cli.addTo(subCommands, commandSet.SubCommands[name]) + for name, command := range subC { + c := command + s.SubCommands[name] = map[string]interface{}{ + "Command": c, + "Synopsis": c.Synopsis(), + "Help": c.Help(), + "NameAligned": name + strings.Repeat(" ", longest-len(name)), + } } } + t.Execute(output, s) } -func (cli *CLI) help(command *commands) { - cli.Commands = command - cli.getFlagSet(command) - for _, c := range command.SubCommands { - cli.getFlagSet(c) - } - cli.template.Execute(os.Stderr, cli) -} - -func (cli *CLI) getFlagSet(c *commands) error { - if c.flagSetDone { - return nil - } - c.flagSetDone = true - fs := c.Command.getFlagSet() - st := reflect.ValueOf(c.Command) +func (cli *CLI) getFlagSet(c Command) error { + st := reflect.ValueOf(c) if st.Kind() != reflect.Ptr { return errors.New("pointer expected") } - if err := cli.defineFlagSet(fs, st); err != nil { + if err := cli.defineFlagSet(cli.flagSet, st, ""); err != nil { return err } return nil } -func (cli *CLI) defineFlagSet(fs *flag.FlagSet, st reflect.Value) error { +func (cli *CLI) defineFlagSet(fs Flagger, st reflect.Value, subName string) error { st = reflect.Indirect(st) if !st.IsValid() || st.Type().Kind() != reflect.Struct { - return errors.New("non-nil pointer to struct expected") + return errors.New("non-nil pointer for struct expected") } flagValueType := reflect.TypeOf((*flag.Value)(nil)).Elem() for i := 0; i < st.NumField(); i++ { typ := st.Type().Field(i) - if typ.Type.Kind() == reflect.Struct { - if err := cli.defineFlagSet(fs, st.Field(i)); err != nil { - return err - } - continue - } var name, usage string tag := typ.Tag.Get("flag") if tag == "" { + if typ.Type.Kind() == reflect.Struct { + if err := cli.defineFlagSet(fs, st.Field(i), ""); err != nil { + return err + } + continue + } continue } val := st.Field(i) @@ -306,7 +325,7 @@ func (cli *CLI) defineFlagSet(fs *flag.FlagSet, st reflect.Value) error { return errors.New("field is unexported") } if !val.CanAddr() { - return errors.New("field is of unsupported type") + return errors.New("field is unsupported type") } flagData := strings.SplitN(tag, ",", 2) switch len(flagData) { @@ -315,10 +334,21 @@ func (cli *CLI) defineFlagSet(fs *flag.FlagSet, st reflect.Value) error { case 2: name, usage = flagData[0], flagData[1] } + if name == "-" { + continue + } + if subName != "" { + name = subName + "." + name + } addr := val.Addr() if addr.Type().Implements(flagValueType) { fs.Var(addr.Interface().(flag.Value), name, usage) continue + } else if typ.Type.Kind() == reflect.Struct { + if err := cli.defineFlagSet(fs, st.Field(i), name); err != nil { + return err + } + continue } switch d := val.Interface().(type) { case int: @@ -343,3 +373,15 @@ func (cli *CLI) defineFlagSet(fs *flag.FlagSet, st reflect.Value) error { } return nil } + +func (cli *CLI) isCompleteStarted() (string, bool) { + line := os.Getenv(completeLine) + if line == "" { + return "", false + } + point, err := strconv.Atoi(os.Getenv(completePoint)) + if err == nil && point > 0 && point < len(line) { + line = line[:point] + } + return line, true +} diff --git a/command/completion.go b/command/completion.go new file mode 100644 index 0000000..298c9c7 --- /dev/null +++ b/command/completion.go @@ -0,0 +1,180 @@ +package command + +import ( + "bufio" + "context" + "errors" + "fmt" + "os" + "os/user" + "path/filepath" +) + +type Completion struct { + UnInstall bool `flag:"uninstall, uninstall auto complete functionality"` + Bash bool `flag:"bash, add auto complete for bash"` + Zsh bool `flag:"zsh, add auto complete for zsh"` + name string + binPath string + bashCmd string + zshCmd string + homeDir string +} + +func New(name string) *Completion { + return &Completion{ + name: name, + bashCmd: "complete -C %s %s", + zshCmd: "complete -o nospace -C %s %s", + } +} + +func (c *Completion) Help() string { + return `Install auto complete ability for your bash and zsh terminal. +(un)install in bash basically adds/remove from .bashrc: + complete -C + +(un)install in zsh basically adds/remove from .zshrc: + autoload -U +X bashcompinit && bashcompinit" + complete -C +` +} + +func (c *Completion) Synopsis() string { + return `Install/uninstall auto complete ability for your bash, fish or zsh terminal.` +} + +func (c *Completion) Run(_ context.Context) error { + bin, err := os.Executable() + if err != nil { + return err + } + c.binPath, err = filepath.Abs(bin) + if err != nil { + return err + } + + u, err := user.Current() + if err != nil { + return err + } + c.homeDir = u.HomeDir + + functs := []func() error{c.installBash, c.installZsh} + if c.UnInstall { + functs = []func() error{c.unInstallBash, c.unInstallZsh} + } + for _, fn := range functs { + if err := fn(); err != nil { + return err + } + } + + return nil +} + +func (c *Completion) installBash() error { + cmd := fmt.Sprintf(c.bashCmd, c.binPath, c.name) + file := filepath.Join(c.homeDir, ".bashrc") + if c.isLineExists(file, cmd) { + return errors.New("already installed") + } + err := c.appendFile(file, cmd) + if err == nil { + fmt.Println("Bash install done") + } + return err +} + +func (c *Completion) installZsh() error { + cmd := fmt.Sprintf(c.zshCmd, c.binPath, c.name) + file := filepath.Join(c.homeDir, ".zshrc") + if c.isLineExists(file, cmd) { + return errors.New("already installed") + } + cmdAutoload := "autoload -U +X bashcompinit && bashcompinit" + if c.isLineExists(file, cmdAutoload) { + if err := c.appendFile(".zshrc", cmdAutoload); err != nil { + return err + } + } + err := c.appendFile(file, cmd) + if err == nil { + fmt.Println("Zsh install done") + } + return err +} + +func (c *Completion) unInstallBash() error { + cmd := fmt.Sprintf(c.bashCmd, c.binPath, c.name) + file := filepath.Join(c.homeDir, ".bashrc") + if c.isLineExists(file, cmd) { + return errors.New("already installed") + } + return c.appendFile(file, cmd) +} + +func (c *Completion) unInstallZsh() error { + cmd := fmt.Sprintf(c.zshCmd, c.binPath, c.name) + file := filepath.Join(c.homeDir, ".zshrc") + if c.isLineExists(file, cmd) { + return errors.New("already installed") + } + return c.appendFile(file, cmd) +} + +func (c *Completion) isLineExists(file string, line string) bool { + f, err := os.Open(file) + if err != nil { + return false + } + defer f.Close() + scanner := bufio.NewScanner(f) + for scanner.Scan() { + if scanner.Text() == line { + return true + } + } + return false +} + +func (c *Completion) appendFile(file string, cmd string) error { + f, err := os.OpenFile(file, os.O_RDWR|os.O_APPEND, 0) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteString(fmt.Sprintf("\n%s\n", cmd)) + return err +} + +func (c *Completion) removeFromFile(file string, cmd []string) error { + wf, err := os.OpenFile(file+".tmp", os.O_WRONLY|os.O_CREATE, 0) + if err != nil { + return err + } + defer wf.Close() + rf, err := os.Open(file) + if err != nil { + return err + } + defer rf.Close() + scanner := bufio.NewScanner(rf) + for scanner.Scan() { + text := scanner.Text() + found := false + for _, s := range cmd { + if text == s { + found = true + break + } + } + if found { + continue + } + if _, err := wf.WriteString(fmt.Sprintf("%s\n", text)); err != nil { + return err + } + } + return os.Rename(file+".tmp", file) +} diff --git a/flagger.go b/flagger.go deleted file mode 100644 index 8ff393d..0000000 --- a/flagger.go +++ /dev/null @@ -1,43 +0,0 @@ -package cli - -import ( - "bytes" - "context" - "flag" -) - -// Command is an interface for cli commands -type Command interface { - Desc() string - Samples() []string - Run(ctx context.Context) - Parse([]string) error - Help() string - SubCommands() []Command - getFlagSet() *flag.FlagSet -} - -// Flagger is a helper struct for commands -type Flagger struct { - *flag.FlagSet - out bytes.Buffer -} - -func (f *Flagger) getFlagSet() *flag.FlagSet { - f.FlagSet.Usage = func() {} - f.FlagSet.SetOutput(&f.out) - return f.FlagSet -} - -func (f *Flagger) Help() string { - f.PrintDefaults() - return f.out.String() -} - -func (f *Flagger) Samples() []string { - return []string{} -} - -func (f *Flagger) SubCommands() []Command { - return nil -} diff --git a/flagger_test.go b/flagger_test.go deleted file mode 100644 index 5cb2ba5..0000000 --- a/flagger_test.go +++ /dev/null @@ -1,270 +0,0 @@ -package cli - -import ( - "context" - "errors" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/suite" -) - -type FlaggerTestSuite struct { - suite.Suite - cli *CLI -} - -type TestCommand struct { - *Flagger - Verbose bool `flag:"v"` -} - -func (c TestCommand) Desc() string { - return "Test description" -} -func (c TestCommand) Run(ctx context.Context) { - fmt.Println("its okey") -} -func (c TestCommand) Samples() []string { - return []string{"test samle"} -} - -func (suite *FlaggerTestSuite) SetupTest() { - suite.cli = New("test", "1.0.0") -} - -func TestSafeList(t *testing.T) { - suite.Run(t, new(FlaggerTestSuite)) -} - -func (suite *FlaggerTestSuite) TestEmptyFlag() { - suite.cli.Add(&TestCommand{}) - suite.Nil(suite.cli.getFlagSet(suite.cli.commandSets["root"].SubCommands["testcommand"])) -} - -func (suite *FlaggerTestSuite) TestWrongFlag() { - type testcommand2 struct { - TestCommand - Apple *interface{} `flag:"apple"` - } - suite.cli.Add(&testcommand2{}) - suite.NotNil(suite.cli.getFlagSet(suite.cli.commandSets["root"].SubCommands["testcommand2"])) -} - -func (suite *FlaggerTestSuite) TestUnexportedFlag() { - type testcommand2 struct { - TestCommand - Apple *interface{} `flag:"apple"` - } - suite.cli.Add(&testcommand2{}) - suite.NotNil(suite.cli.getFlagSet(suite.cli.commandSets["root"].SubCommands["testcommand2"])) -} - -type CustomFlag []string - -func (c *CustomFlag) String() string { return fmt.Sprint(*c) } -func (c *CustomFlag) Set(value string) error { - *c = append(*c, value) - return nil -} - -func (suite *FlaggerTestSuite) TestFlag() { - type testcommand2 struct { - TestCommand - String string `flag:"string,string flag example"` - Int int `flag:"int,int flag example"` - Int64 int64 `flag:"int64,int64 flag example"` - Uint uint `flag:"uint,uint flag example"` - Uint64 uint64 `flag:"uint64"` - Float64 float64 `flag:"float64"` - Bool bool `flag:"bool"` - Duration time.Duration `flag:"duration"` - MySlice CustomFlag `flag:"slice"` // custom flag.Value implementation - - Empty bool `flag:""` // empty flag definition - NonExposed int // does not have flag attached - } - reference := testcommand2{ - TestCommand: TestCommand{ - Verbose: true, - }, - String: "whales", - Int: 42, - Int64: 100 << 30, - Uint: 7, - Uint64: 24, - Float64: 1.55, - Bool: true, - Duration: 15 * time.Minute, - MySlice: CustomFlag{"a", "b"}, - } - conf := testcommand2{} - suite.cli.Add(&conf) - suite.Nil(suite.cli.getFlagSet(suite.cli.commandSets["root"].SubCommands["testcommand2"])) - - args := []string{ - "-string", "whales", "-int", "42", - "-int64", "107374182400", "-uint", "7", - "-uint64", "24", "-float64", "1.55", "-bool", - "-duration", "15m", - "-slice", "a", - "-slice", "b", - "-v", - } - suite.Nil(conf.Parse(args)) - conf.Flagger = nil - suite.Equal(reference, conf) -} - -func (suite *FlaggerTestSuite) TestDefaultValueFlag() { - type testcommand2 struct { - TestCommand - String string `flag:"string,string flag example"` - Int int `flag:"int,int flag example"` - Int64 int64 `flag:"int64,int64 flag example"` - Uint uint `flag:"uint,uint flag example"` - Uint64 uint64 `flag:"uint64"` - Float64 float64 `flag:"float64"` - Bool bool `flag:"bool"` - Duration time.Duration `flag:"duration"` - MySlice CustomFlag `flag:"slice"` // custom flag.Value implementation - - Empty bool `flag:""` // empty flag definition - NonExposed int // does not have flag attached - } - reference := testcommand2{ - String: "whales", - Int: 42, - Int64: 100 << 30, - Uint: 7, - Uint64: 24, - Float64: 1.55, - Bool: true, - Duration: 15 * time.Minute, - MySlice: CustomFlag{"a", "b"}, - } - conf := testcommand2{ - String: "whales", - } - suite.cli.Add(&conf) - suite.Nil(suite.cli.getFlagSet(suite.cli.commandSets["root"].SubCommands["testcommand2"])) - - args := []string{ - "-int", "42", - "-int64", "107374182400", "-uint", "7", - "-uint64", "24", "-float64", "1.55", "-bool", - "-duration", "15m", - "-slice", "a", - "-slice", "b", - } - suite.Nil(conf.Parse(args)) - conf.Flagger = nil - suite.Equal(reference, conf) -} - -func (suite *FlaggerTestSuite) TestParseError() { - type testcommand2 struct { - TestCommand - Int int `flag:"int,int flag example"` - } - - conf := testcommand2{} - suite.cli.Add(&conf) - suite.Nil(suite.cli.getFlagSet(suite.cli.commandSets["root"].SubCommands["testcommand2"])) - - args := []string{ - "-int", "fasdf", - } - suite.NotNil(conf.Parse(args)) -} - -type CustomFlagError []string - -func (c *CustomFlagError) String() string { return fmt.Sprint(*c) } -func (c *CustomFlagError) Set(value string) error { - *c = append(*c, value) - return errors.New("its a trap") -} - -func (suite *FlaggerTestSuite) TestParseErrorCustom() { - type testcommand2 struct { - TestCommand - Int CustomFlagError `flag:"int,int flag example"` - } - - conf := testcommand2{} - suite.cli.Add(&conf) - suite.Nil(suite.cli.getFlagSet(suite.cli.commandSets["root"].SubCommands["testcommand2"])) - - args := []string{ - "-int", "fasdf", - } - suite.NotNil(conf.Parse(args)) -} - -type TestCommandWithSubCommand struct { - *Flagger - MainProgramParam bool `flag:"mainParam,string flag example"` - subCommands []Command -} - -func (c *TestCommandWithSubCommand) Desc() string { - return "Test description" -} -func (c *TestCommandWithSubCommand) Run(ctx context.Context) { - fmt.Println("its okey") -} -func (c *TestCommandWithSubCommand) Samples() []string { - return []string{"test samle"} -} -func (c *TestCommandWithSubCommand) SubCommands() []Command { - return c.subCommands -} - -func (suite *FlaggerTestSuite) TestSubcommandFlag() { - type testcommand2 struct { - TestCommand - String string `flag:"string,string flag example"` - Int int `flag:"int,int flag example"` - Int64 int64 `flag:"int64,int64 flag example"` - Uint uint `flag:"uint,uint flag example"` - Uint64 uint64 `flag:"uint64"` - Float64 float64 `flag:"float64"` - Bool bool `flag:"bool"` - Duration time.Duration `flag:"duration"` - MySlice CustomFlag `flag:"slice"` // custom flag.Value implementation - - Empty bool `flag:""` // empty flag definition - NonExposed int // does not have flag attached - } - reference2 := &testcommand2{ - String: "whales", - Int: 42, - Int64: 100 << 30, - Uint: 7, - Uint64: 24, - Float64: 1.55, - Bool: true, - Duration: 15 * time.Minute, - MySlice: CustomFlag{"a", "b"}, - } - conf := TestCommandWithSubCommand{ - subCommands: []Command{reference2}, - } - suite.cli.Add(&conf) - - args := []string{"testcommand", "testcommand2", - "-string", "whales", "-int", "42", - "-int64", "107374182400", "-uint", "7", - "-uint64", "24", "-float64", "1.55", "-bool", - "-duration", "15m", - "-slice", "a", - "-slice", "b", - "-v", - } - _, err := suite.cli.getSubCommand(suite.cli.commandSets["root"], args) - suite.Nil(err) - conf.Flagger = nil - suite.Equal(conf, conf) -} diff --git a/interface.go b/interface.go new file mode 100644 index 0000000..7f3b80d --- /dev/null +++ b/interface.go @@ -0,0 +1,49 @@ +package cli + +import ( + "context" + "flag" + "io" + "time" +) + +// A command is a runnable command of a CLI. +type Command interface { + // Help should return long-form help text that includes the command-line + // usage, a brief few sentences explaining the function of the command. + Help() string + + // Synopsis should return a one-line, short synopsis of the command. + Synopsis() string + + // Run should run the actual command with the given Context + Run(ctx context.Context) error +} + +type SubCommands interface { + // SubCommand should return a list of sub commands + SubCommands() map[string]Command +} + +type ParseHelper interface { + // Parse should help to validate flags, and add extra options + Parse([]string) error +} + +// Flagger is an interface satisfied by flag.FlagSet and other implementations +// of flags. +type Flagger interface { + Parse([]string) error + StringVar(p *string, name string, value string, usage string) + IntVar(p *int, name string, value int, usage string) + Int64Var(p *int64, name string, value int64, usage string) + BoolVar(p *bool, name string, value bool, usage string) + UintVar(p *uint, name string, value uint, usage string) + Uint64Var(p *uint64, name string, value uint64, usage string) + Float64Var(p *float64, name string, value float64, usage string) + DurationVar(p *time.Duration, name string, value time.Duration, usage string) + Set(name string, value string) error + SetOutput(output io.Writer) + Var(value flag.Value, name string, usage string) + VisitAll(fn func(*flag.Flag)) +} diff --git a/root.go b/root.go new file mode 100644 index 0000000..62d50c4 --- /dev/null +++ b/root.go @@ -0,0 +1,81 @@ +package cli + +import ( + "context" + "strings" +) + +type Root struct { + Name string + Version string + Description string + Authors []string + subCommands map[string]Command +} + +var defaultRoot = &Root{ + subCommands: make(map[string]Command), +} + +// RootCommand returns the default Root command +func RootCommand() *Root { + return defaultRoot +} + +// SetRoot should set default root command +func SetRoot(root *Root) { + defaultRoot = root +} + +// AddCommand add a main command +func (r *Root) AddCommand(name string, command Command) bool { + if _, ok := r.subCommands[name]; ok { + return false + } + r.subCommands[name] = command + return true +} + +// SubCommands return the main commands +func (r *Root) SubCommands() map[string]Command { + return r.subCommands +} + +func (r *Root) Help() string { + buff := strings.Builder{} + buff.WriteString("Usage: " + r.Name) + if len(r.SubCommands()) > 0 { + buff.WriteString(" command [command options]") + } + buff.WriteString("\n") + if r.Version != "" { + buff.WriteString("Version: " + r.Version + "\n") + } + authorsLen := len(r.Authors) + if authorsLen > 0 { + if authorsLen > 1 { + buff.WriteString("Authors: ") + } else { + buff.WriteString("Author: ") + } + buff.WriteString(strings.Join(r.Authors, ", ") + "\n") + } + if r.Description != "" { + buff.WriteString("Description:\n" + r.Description + "\n") + } + return buff.String() +} + +func (r *Root) Synopsis() string { + buff := strings.Builder{} + buff.WriteString(r.Name) + if len(r.SubCommands()) > 0 { + buff.WriteString(" command [command options]") + } + buff.WriteString("\n") + return buff.String() +} + +func (r *Root) Run(_ context.Context) error { + return nil +}