Merge branch 'master' into forkMaster, tested and working

This commit is contained in:
Raphael Tiersch 2017-07-31 14:20:08 +02:00
commit 9eb53fff86
28 changed files with 517 additions and 80 deletions

View file

@ -2,8 +2,8 @@ language: go
matrix: matrix:
include: include:
- go: 1.7.5 - go: 1.7.6
- go: 1.8.1 - go: 1.8.3
- go: tip - go: tip
allow_failures: allow_failures:
- go: tip - go: tip

View file

@ -485,6 +485,36 @@ when the `--author` flag is not provided by user.
More in [viper documentation](https://github.com/spf13/viper#working-with-flags). More in [viper documentation](https://github.com/spf13/viper#working-with-flags).
## Positional and Custom Arguments
Validation of positional arguments can be specified using the `Args` field.
The follow validators are built in:
- `NoArgs` - the command will report an error if there are any positional args.
- `ArbitraryArgs` - the command will accept any args.
- `OnlyValidArgs` - the command will report an error if there are any positional args that are not in the ValidArgs list.
- `MinimumNArgs(int)` - the command will report an error if there are not at least N positional args.
- `MaximumNArgs(int)` - the command will report an error if there are more than N positional args.
- `ExactArgs(int)` - the command will report an error if there are not exactly N positional args.
- `RangeArgs(min, max)` - the command will report an error if the number of args is not between the minimum and maximum number of expected args.
A custom validator can be provided like this:
```go
Args: func validColorArgs(cmd *cobra.Command, args []string) error {
if err := cli.RequiresMinArgs(1)(cmd, args); err != nil {
return err
}
if myapp.IsValidColor(args[0]) {
return nil
}
return fmt.Errorf("Invalid color specified: %s", args[0])
}
```
## Example ## Example
In the example below, we have defined three commands. Two are at the top level In the example below, we have defined three commands. Two are at the top level

98
args.go Normal file
View file

@ -0,0 +1,98 @@
package cobra
import (
"fmt"
)
type PositionalArgs func(cmd *Command, args []string) error
// Legacy arg validation has the following behaviour:
// - root commands with no subcommands can take arbitrary arguments
// - root commands with subcommands will do subcommand validity checking
// - subcommands will always accept arbitrary arguments
func legacyArgs(cmd *Command, args []string) error {
// no subcommand, always take args
if !cmd.HasSubCommands() {
return nil
}
// root command with subcommands, do subcommand checking
if !cmd.HasParent() && len(args) > 0 {
return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.findSuggestions(args[0]))
}
return nil
}
// NoArgs returns an error if any args are included
func NoArgs(cmd *Command, args []string) error {
if len(args) > 0 {
return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath())
}
return nil
}
// OnlyValidArgs returns an error if any args are not in the list of ValidArgs
func OnlyValidArgs(cmd *Command, args []string) error {
if len(cmd.ValidArgs) > 0 {
for _, v := range args {
if !stringInSlice(v, cmd.ValidArgs) {
return fmt.Errorf("invalid argument %q for %q%s", v, cmd.CommandPath(), cmd.findSuggestions(args[0]))
}
}
}
return nil
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// ArbitraryArgs never returns an error
func ArbitraryArgs(cmd *Command, args []string) error {
return nil
}
// MinimumNArgs returns an error if there is not at least N args
func MinimumNArgs(n int) PositionalArgs {
return func(cmd *Command, args []string) error {
if len(args) < n {
return fmt.Errorf("requires at least %d arg(s), only received %d", n, len(args))
}
return nil
}
}
// MaximumNArgs returns an error if there are more than N args
func MaximumNArgs(n int) PositionalArgs {
return func(cmd *Command, args []string) error {
if len(args) > n {
return fmt.Errorf("accepts at most %d arg(s), received %d", n, len(args))
}
return nil
}
}
// ExactArgs returns an error if there are not exactly n args
func ExactArgs(n int) PositionalArgs {
return func(cmd *Command, args []string) error {
if len(args) != n {
return fmt.Errorf("accepts %d arg(s), received %d", n, len(args))
}
return nil
}
}
// RangeArgs returns an error if the number of args is not within the expected range
func RangeArgs(min int, max int) PositionalArgs {
return func(cmd *Command, args []string) error {
if len(args) < min || len(args) > max {
return fmt.Errorf("accepts between %d and %d arg(s), received %d", min, max, len(args))
}
return nil
}
}

View file

@ -117,6 +117,8 @@ func TestBashCompletions(t *testing.T) {
// check for filename extension flags // check for filename extension flags
check(t, str, `flags_completion+=("_filedir")`) check(t, str, `flags_completion+=("_filedir")`)
// check for filename extension flags // check for filename extension flags
check(t, str, `must_have_one_noun+=("three")`)
// check for filename extention flags
check(t, str, `flags_completion+=("__handle_filename_extension_flag json|yaml|yml")`) check(t, str, `flags_completion+=("__handle_filename_extension_flag json|yaml|yml")`)
// check for custom flags // check for custom flags
check(t, str, `flags_completion+=("__complete_custom")`) check(t, str, `flags_completion+=("__complete_custom")`)

View file

@ -47,6 +47,15 @@ var EnablePrefixMatching = false
// To disable sorting, set it to false. // To disable sorting, set it to false.
var EnableCommandSorting = true var EnableCommandSorting = true
// MousetrapHelpText enables an information splash screen on Windows
// if the CLI is started from explorer.exe.
// To disable the mousetrap, just set this variable to blank string ("").
// Works only on Microsoft Windows.
var MousetrapHelpText string = `This is a command line tool.
You need to open cmd.exe and run it from there.
`
// AddTemplateFunc adds a template function that's available to Usage and Help // AddTemplateFunc adds a template function that's available to Usage and Help
// template generation. // template generation.
func AddTemplateFunc(name string, tmplFunc interface{}) { func AddTemplateFunc(name string, tmplFunc interface{}) {

View file

@ -121,7 +121,7 @@ func validateCmdName(source string) string {
func createCmdFile(license License, path, cmdName string) { func createCmdFile(license License, path, cmdName string) {
template := `{{comment .copyright}} template := `{{comment .copyright}}
{{comment .license}} {{if .license}}{{comment .license}}{{end}}
package {{.cmdPackage}} package {{.cmdPackage}}

View file

@ -6,6 +6,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/spf13/viper"
) )
// TestGoldenAddCmd initializes the project "github.com/spf13/testproject" // TestGoldenAddCmd initializes the project "github.com/spf13/testproject"
@ -20,6 +22,9 @@ func TestGoldenAddCmd(t *testing.T) {
// Initialize the project at first. // Initialize the project at first.
initializeProject(project) initializeProject(project)
defer os.RemoveAll(project.AbsPath()) defer os.RemoveAll(project.AbsPath())
defer viper.Set("year", nil)
viper.Set("year", 2017) // For reproducible builds
// Then add the "test" command. // Then add the "test" command.
cmdName := "test" cmdName := "test"
@ -48,7 +53,7 @@ func TestGoldenAddCmd(t *testing.T) {
goldenPath := filepath.Join("testdata", filepath.Base(path)+".golden") goldenPath := filepath.Join("testdata", filepath.Base(path)+".golden")
switch relPath { switch relPath {
// Know directories. // Known directories.
case ".": case ".":
return nil return nil
// Known files. // Known files.

View file

@ -39,11 +39,11 @@ func compareFiles(pathA, pathB string) error {
// Don't execute diff if it can't be found. // Don't execute diff if it can't be found.
return nil return nil
} }
diffCmd := exec.Command(diffPath, pathA, pathB) diffCmd := exec.Command(diffPath, "-u", pathA, pathB)
diffCmd.Stdout = output diffCmd.Stdout = output
diffCmd.Stderr = output diffCmd.Stderr = output
output.WriteString("$ diff " + pathA + " " + pathB + "\n") output.WriteString("$ diff -u " + pathA + " " + pathB + "\n")
if err := diffCmd.Run(); err != nil { if err := diffCmd.Run(); err != nil {
output.WriteString("\n" + err.Error()) output.WriteString("\n" + err.Error())
} }

View file

@ -45,24 +45,34 @@ func er(msg interface{}) {
} }
// isEmpty checks if a given path is empty. // isEmpty checks if a given path is empty.
// Hidden files in path are ignored.
func isEmpty(path string) bool { func isEmpty(path string) bool {
fi, err := os.Stat(path) fi, err := os.Stat(path)
if err != nil { if err != nil {
er(err) er(err)
} }
if fi.IsDir() {
f, err := os.Open(path) if !fi.IsDir() {
if err != nil { return fi.Size() == 0
er(err)
}
defer f.Close()
dirs, err := f.Readdirnames(1)
if err != nil && err != io.EOF {
er(err)
}
return len(dirs) == 0
} }
return fi.Size() == 0
f, err := os.Open(path)
if err != nil {
er(err)
}
defer f.Close()
names, err := f.Readdirnames(-1)
if err != nil && err != io.EOF {
er(err)
}
for _, name := range names {
if len(name) > 0 && name[0] != '.' {
return false
}
}
return true
} }
// exists checks if a file or directory exists. // exists checks if a file or directory exists.

View file

@ -6,6 +6,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/spf13/viper"
) )
// TestGoldenInitCmd initializes the project "github.com/spf13/testproject" // TestGoldenInitCmd initializes the project "github.com/spf13/testproject"
@ -16,6 +18,9 @@ func TestGoldenInitCmd(t *testing.T) {
projectName := "github.com/spf13/testproject" projectName := "github.com/spf13/testproject"
project := NewProject(projectName) project := NewProject(projectName)
defer os.RemoveAll(project.AbsPath()) defer os.RemoveAll(project.AbsPath())
defer viper.Set("year", nil)
viper.Set("year", 2017) // For reproducible builds
os.Args = []string{"cobra", "init", projectName} os.Args = []string{"cobra", "init", projectName}
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
@ -44,7 +49,7 @@ func TestGoldenInitCmd(t *testing.T) {
goldenPath := filepath.Join("testdata", filepath.Base(path)+".golden") goldenPath := filepath.Join("testdata", filepath.Base(path)+".golden")
switch relPath { switch relPath {
// Know directories. // Known directories.
case ".", "cmd": case ".", "cmd":
return nil return nil
// Known files. // Known files.

View file

@ -4,8 +4,7 @@ func initAgpl() {
Licenses["agpl"] = License{ Licenses["agpl"] = License{
Name: "GNU Affero General Public License", Name: "GNU Affero General Public License",
PossibleMatches: []string{"agpl", "affero gpl", "gnu agpl"}, PossibleMatches: []string{"agpl", "affero gpl", "gnu agpl"},
Header: `{{.copyright}} Header: `
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or

View file

@ -19,7 +19,8 @@ func initApache2() {
Licenses["apache"] = License{ Licenses["apache"] = License{
Name: "Apache 2.0", Name: "Apache 2.0",
PossibleMatches: []string{"apache", "apache20", "apache 2.0", "apache2.0", "apache-2.0"}, PossibleMatches: []string{"apache", "apache20", "apache 2.0", "apache2.0", "apache-2.0"},
Header: `Licensed under the Apache License, Version 2.0 (the "License"); Header: `
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at

View file

@ -20,8 +20,7 @@ func initBsdClause2() {
Name: "Simplified BSD License", Name: "Simplified BSD License",
PossibleMatches: []string{"freebsd", "simpbsd", "simple bsd", "2-clause bsd", PossibleMatches: []string{"freebsd", "simpbsd", "simple bsd", "2-clause bsd",
"2 clause bsd", "simplified bsd license"}, "2 clause bsd", "simplified bsd license"},
Header: ` Header: `All rights reserved.
All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:

View file

@ -19,8 +19,7 @@ func initBsdClause3() {
Licenses["bsd"] = License{ Licenses["bsd"] = License{
Name: "NewBSD", Name: "NewBSD",
PossibleMatches: []string{"bsd", "newbsd", "3 clause bsd", "3-clause bsd"}, PossibleMatches: []string{"bsd", "newbsd", "3 clause bsd", "3-clause bsd"},
Header: ` Header: `All rights reserved.
All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:

View file

@ -19,20 +19,19 @@ func initGpl2() {
Licenses["gpl2"] = License{ Licenses["gpl2"] = License{
Name: "GNU General Public License 2.0", Name: "GNU General Public License 2.0",
PossibleMatches: []string{"gpl2", "gnu gpl2", "gplv2"}, PossibleMatches: []string{"gpl2", "gnu gpl2", "gplv2"},
Header: `{{.copyright}} Header: `
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is free software; you can redistribute it and/or This program is distributed in the hope that it will be useful,
modify it under the terms of the GNU General Public License but WITHOUT ANY WARRANTY; without even the implied warranty of
as published by the Free Software Foundation; either version 2 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
of the License, or (at your option) any later version. GNU General Public License for more details.
This program is distributed in the hope that it will be useful, You should have received a copy of the GNU Lesser General Public License
but WITHOUT ANY WARRANTY; without even the implied warranty of along with this program. If not, see <http://www.gnu.org/licenses/>.`,
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.`,
Text: ` GNU GENERAL PUBLIC LICENSE Text: ` GNU GENERAL PUBLIC LICENSE
Version 2, June 1991 Version 2, June 1991

View file

@ -19,8 +19,7 @@ func initGpl3() {
Licenses["gpl3"] = License{ Licenses["gpl3"] = License{
Name: "GNU General Public License 3.0", Name: "GNU General Public License 3.0",
PossibleMatches: []string{"gpl3", "gplv3", "gpl", "gnu gpl3", "gnu gpl"}, PossibleMatches: []string{"gpl3", "gplv3", "gpl", "gnu gpl3", "gnu gpl"},
Header: `{{.copyright}} Header: `
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or

View file

@ -4,8 +4,7 @@ func initLgpl() {
Licenses["lgpl"] = License{ Licenses["lgpl"] = License{
Name: "GNU Lesser General Public License", Name: "GNU Lesser General Public License",
PossibleMatches: []string{"lgpl", "lesser gpl", "gnu lgpl"}, PossibleMatches: []string{"lgpl", "lesser gpl", "gnu lgpl"},
Header: `{{.copyright}} Header: `
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or

View file

@ -17,7 +17,7 @@ package cmd
func initMit() { func initMit() {
Licenses["mit"] = License{ Licenses["mit"] = License{
Name: "Mit", Name: "MIT License",
PossibleMatches: []string{"mit"}, PossibleMatches: []string{"mit"},
Header: ` Header: `
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy

View file

@ -77,7 +77,11 @@ func getLicense() License {
func copyrightLine() string { func copyrightLine() string {
author := viper.GetString("author") author := viper.GetString("author")
year := time.Now().Format("2006")
year := viper.GetString("year") // For reproducible builds
if year == "" {
year = time.Now().Format("2006")
}
return "Copyright © " + year + " " + author return "Copyright © " + year + " " + author
} }

View file

@ -40,7 +40,7 @@ func Execute() {
} }
func init() { func init() {
initViper() cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)") rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "author name for copyright attribution") rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "author name for copyright attribution")
@ -55,7 +55,7 @@ func init() {
rootCmd.AddCommand(initCmd) rootCmd.AddCommand(initCmd)
} }
func initViper() { func initConfig() {
if cfgFile != "" { if cfgFile != "" {
// Use config file from the flag. // Use config file from the flag.
viper.SetConfigFile(cfgFile) viper.SetConfigFile(cfgFile)

View file

@ -1,4 +1,5 @@
// Copyright © 2017 NAME HERE <EMAIL ADDRESS> // Copyright © 2017 NAME HERE <EMAIL ADDRESS>
//
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at

View file

@ -1,4 +1,5 @@
// Copyright © 2017 NAME HERE <EMAIL ADDRESS> // Copyright © 2017 NAME HERE <EMAIL ADDRESS>
//
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at

View file

@ -1,4 +1,5 @@
// Copyright © 2017 NAME HERE <EMAIL ADDRESS> // Copyright © 2017 NAME HERE <EMAIL ADDRESS>
//
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at

View file

@ -36,6 +36,7 @@ var cmdHidden = &Command{
var cmdPrint = &Command{ var cmdPrint = &Command{
Use: "print [string to print]", Use: "print [string to print]",
Args: MinimumNArgs(1),
Short: "Print anything to the screen", Short: "Print anything to the screen",
Long: `an absolutely utterly useless command for testing.`, Long: `an absolutely utterly useless command for testing.`,
Run: func(cmd *Command, args []string) { Run: func(cmd *Command, args []string) {
@ -75,6 +76,7 @@ var cmdDeprecated = &Command{
Deprecated: "Please use echo instead", Deprecated: "Please use echo instead",
Run: func(cmd *Command, args []string) { Run: func(cmd *Command, args []string) {
}, },
Args: NoArgs,
} }
var cmdTimes = &Command{ var cmdTimes = &Command{
@ -88,6 +90,8 @@ var cmdTimes = &Command{
Run: func(cmd *Command, args []string) { Run: func(cmd *Command, args []string) {
tt = args tt = args
}, },
Args: OnlyValidArgs,
ValidArgs: []string{"one", "two", "three", "four"},
} }
var cmdRootNoRun = &Command{ var cmdRootNoRun = &Command{
@ -105,6 +109,16 @@ var cmdRootSameName = &Command{
Long: "The root description for help", Long: "The root description for help",
} }
var cmdRootTakesArgs = &Command{
Use: "root-with-args [random args]",
Short: "The root can run it's own function and takes args!",
Long: "The root description for help, and some args",
Run: func(cmd *Command, args []string) {
tr = args
},
Args: ArbitraryArgs,
}
var cmdRootWithRun = &Command{ var cmdRootWithRun = &Command{
Use: "cobra-test", Use: "cobra-test",
Short: "The root can run its own function", Short: "The root can run its own function",
@ -458,6 +472,63 @@ func TestUsage(t *testing.T) {
checkResultOmits(t, x, cmdCustomFlags.Use+" [flags]") checkResultOmits(t, x, cmdCustomFlags.Use+" [flags]")
} }
func TestRootTakesNoArgs(t *testing.T) {
c := initializeWithSameName()
c.AddCommand(cmdPrint, cmdEcho)
result := simpleTester(c, "illegal")
if result.Error == nil {
t.Fatal("Expected an error")
}
expectedError := `unknown command "illegal" for "print"`
if !strings.Contains(result.Error.Error(), expectedError) {
t.Errorf("exptected %v, got %v", expectedError, result.Error.Error())
}
}
func TestRootTakesArgs(t *testing.T) {
c := cmdRootTakesArgs
result := simpleTester(c, "legal")
if result.Error != nil {
t.Errorf("expected no error, but got %v", result.Error)
}
}
func TestSubCmdTakesNoArgs(t *testing.T) {
result := fullSetupTest("deprecated", "illegal")
if result.Error == nil {
t.Fatal("Expected an error")
}
expectedError := `unknown command "illegal" for "cobra-test deprecated"`
if !strings.Contains(result.Error.Error(), expectedError) {
t.Errorf("expected %v, got %v", expectedError, result.Error.Error())
}
}
func TestSubCmdTakesArgs(t *testing.T) {
noRRSetupTest("echo", "times", "one", "two")
if strings.Join(tt, " ") != "one two" {
t.Error("Command didn't parse correctly")
}
}
func TestCmdOnlyValidArgs(t *testing.T) {
result := noRRSetupTest("echo", "times", "one", "two", "five")
if result.Error == nil {
t.Fatal("Expected an error")
}
expectedError := `invalid argument "five"`
if !strings.Contains(result.Error.Error(), expectedError) {
t.Errorf("expected %v, got %v", expectedError, result.Error.Error())
}
}
func TestFlagLong(t *testing.T) { func TestFlagLong(t *testing.T) {
noRRSetupTest("echo", "--intone=13", "something", "--", "here") noRRSetupTest("echo", "--intone=13", "something", "--", "here")
@ -672,9 +743,9 @@ func TestPersistentFlags(t *testing.T) {
} }
// persistentFlag should act like normal flag on its own command // persistentFlag should act like normal flag on its own command
fullSetupTest("echo", "times", "-s", "again", "-c", "-p", "test", "here") fullSetupTest("echo", "times", "-s", "again", "-c", "-p", "one", "two")
if strings.Join(tt, " ") != "test here" { if strings.Join(tt, " ") != "one two" {
t.Errorf("flags didn't leave proper args remaining. %s given", tt) t.Errorf("flags didn't leave proper args remaining. %s given", tt)
} }
@ -1095,7 +1166,7 @@ func TestGlobalNormFuncPropagation(t *testing.T) {
rootCmd := initialize() rootCmd := initialize()
rootCmd.SetGlobalNormalizationFunc(normFunc) rootCmd.SetGlobalNormalizationFunc(normFunc)
if reflect.ValueOf(normFunc) != reflect.ValueOf(rootCmd.GlobalNormalizationFunc()) { if reflect.ValueOf(normFunc).Pointer() != reflect.ValueOf(rootCmd.GlobalNormalizationFunc()).Pointer() {
t.Error("rootCmd seems to have a wrong normalization function") t.Error("rootCmd seems to have a wrong normalization function")
} }

View file

@ -59,6 +59,8 @@ type Command struct {
// but accepted if entered manually. // but accepted if entered manually.
ArgAliases []string ArgAliases []string
// Expected arguments
Args PositionalArgs
// BashCompletionFunction is custom functions used by the bash autocompletion generator. // BashCompletionFunction is custom functions used by the bash autocompletion generator.
BashCompletionFunction string BashCompletionFunction string
@ -513,33 +515,29 @@ func (c *Command) Find(args []string) (*Command, []string, error) {
} }
commandFound, a := innerfind(c, args) commandFound, a := innerfind(c, args)
argsWOflags := stripFlags(a, commandFound) if commandFound.Args == nil {
return commandFound, a, legacyArgs(commandFound, stripFlags(a, commandFound))
// no subcommand, always take args
if !commandFound.HasSubCommands() {
return commandFound, a, nil
} }
// root command with subcommands, do subcommand checking
if commandFound == c && len(argsWOflags) > 0 {
suggestionsString := ""
if !c.DisableSuggestions {
if c.SuggestionsMinimumDistance <= 0 {
c.SuggestionsMinimumDistance = 2
}
if suggestions := c.SuggestionsFor(argsWOflags[0]); len(suggestions) > 0 {
suggestionsString += "\n\nDid you mean this?\n"
for _, s := range suggestions {
suggestionsString += fmt.Sprintf("\t%v\n", s)
}
}
}
return commandFound, a, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), suggestionsString)
}
return commandFound, a, nil return commandFound, a, nil
} }
func (c *Command) findSuggestions(arg string) string {
if c.DisableSuggestions {
return ""
}
if c.SuggestionsMinimumDistance <= 0 {
c.SuggestionsMinimumDistance = 2
}
suggestionsString := ""
if suggestions := c.SuggestionsFor(arg); len(suggestions) > 0 {
suggestionsString += "\n\nDid you mean this?\n"
for _, s := range suggestions {
suggestionsString += fmt.Sprintf("\t%v\n", s)
}
}
return suggestionsString
}
// SuggestionsFor provides suggestions for the typedName. // SuggestionsFor provides suggestions for the typedName.
func (c *Command) SuggestionsFor(typedName string) []string { func (c *Command) SuggestionsFor(typedName string) []string {
suggestions := []string{} suggestions := []string{}
@ -624,6 +622,10 @@ func (c *Command) execute(a []string) (err error) {
argWoFlags = a argWoFlags = a
} }
if err := c.ValidateArgs(argWoFlags); err != nil {
return err
}
for p := c; p != nil; p = p.Parent() { for p := c; p != nil; p = p.Parent() {
if p.PersistentPreRunE != nil { if p.PersistentPreRunE != nil {
if err := p.PersistentPreRunE(c, argWoFlags); err != nil { if err := p.PersistentPreRunE(c, argWoFlags); err != nil {
@ -747,6 +749,13 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
return cmd, err return cmd, err
} }
func (c *Command) ValidateArgs(args []string) error {
if c.Args == nil {
return nil
}
return c.Args(c, args)
}
// InitDefaultHelpFlag adds default help flag to c. // InitDefaultHelpFlag adds default help flag to c.
// It is called automatically by executing the c or by calling help and usage. // It is called automatically by executing the c or by calling help and usage.
// If c already has help flag, it will do nothing. // If c already has help flag, it will do nothing.
@ -776,7 +785,7 @@ func (c *Command) InitDefaultHelpCmd() {
Use: "help [command]", Use: "help [command]",
Short: "Help about any command", Short: "Help about any command",
Long: `Help provides help for any command in the application. Long: `Help provides help for any command in the application.
Simply type ` + c.Name() + ` help [path to command] for full details.`, Simply type ` + c.Name() + ` help [path to command] for full details.`,
Run: func(c *Command, args []string) { Run: func(c *Command, args []string) {
cmd, _, e := c.Root().Find(args) cmd, _, e := c.Root().Find(args)

View file

@ -11,14 +11,8 @@ import (
var preExecHookFn = preExecHook var preExecHookFn = preExecHook
// enables an information splash screen on Windows if the CLI is started from explorer.exe.
var MousetrapHelpText string = `This is a command line tool
You need to open cmd.exe and run it from there.
`
func preExecHook(c *Command) { func preExecHook(c *Command) {
if mousetrap.StartedByExplorer() { if MousetrapHelpText != "" && mousetrap.StartedByExplorer() {
c.Print(MousetrapHelpText) c.Print(MousetrapHelpText)
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
os.Exit(1) os.Exit(1)

114
zsh_completions.go Normal file
View file

@ -0,0 +1,114 @@
package cobra
import (
"bytes"
"fmt"
"io"
"strings"
)
// GenZshCompletion generates a zsh completion file and writes to the passed writer.
func (cmd *Command) GenZshCompletion(w io.Writer) error {
buf := new(bytes.Buffer)
writeHeader(buf, cmd)
maxDepth := maxDepth(cmd)
writeLevelMapping(buf, maxDepth)
writeLevelCases(buf, maxDepth, cmd)
_, err := buf.WriteTo(w)
return err
}
func writeHeader(w io.Writer, cmd *Command) {
fmt.Fprintf(w, "#compdef %s\n\n", cmd.Name())
}
func maxDepth(c *Command) int {
if len(c.Commands()) == 0 {
return 0
}
maxDepthSub := 0
for _, s := range c.Commands() {
subDepth := maxDepth(s)
if subDepth > maxDepthSub {
maxDepthSub = subDepth
}
}
return 1 + maxDepthSub
}
func writeLevelMapping(w io.Writer, numLevels int) {
fmt.Fprintln(w, `_arguments \`)
for i := 1; i <= numLevels; i++ {
fmt.Fprintf(w, ` '%d: :->level%d' \`, i, i)
fmt.Fprintln(w)
}
fmt.Fprintf(w, ` '%d: :%s'`, numLevels+1, "_files")
fmt.Fprintln(w)
}
func writeLevelCases(w io.Writer, maxDepth int, root *Command) {
fmt.Fprintln(w, "case $state in")
defer fmt.Fprintln(w, "esac")
for i := 1; i <= maxDepth; i++ {
fmt.Fprintf(w, " level%d)\n", i)
writeLevel(w, root, i)
fmt.Fprintln(w, " ;;")
}
fmt.Fprintln(w, " *)")
fmt.Fprintln(w, " _arguments '*: :_files'")
fmt.Fprintln(w, " ;;")
}
func writeLevel(w io.Writer, root *Command, i int) {
fmt.Fprintf(w, " case $words[%d] in\n", i)
defer fmt.Fprintln(w, " esac")
commands := filterByLevel(root, i)
byParent := groupByParent(commands)
for p, c := range byParent {
names := names(c)
fmt.Fprintf(w, " %s)\n", p)
fmt.Fprintf(w, " _arguments '%d: :(%s)'\n", i, strings.Join(names, " "))
fmt.Fprintln(w, " ;;")
}
fmt.Fprintln(w, " *)")
fmt.Fprintln(w, " _arguments '*: :_files'")
fmt.Fprintln(w, " ;;")
}
func filterByLevel(c *Command, l int) []*Command {
cs := make([]*Command, 0)
if l == 0 {
cs = append(cs, c)
return cs
}
for _, s := range c.Commands() {
cs = append(cs, filterByLevel(s, l-1)...)
}
return cs
}
func groupByParent(commands []*Command) map[string][]*Command {
m := make(map[string][]*Command)
for _, c := range commands {
parent := c.Parent()
if parent == nil {
continue
}
m[parent.Name()] = append(m[parent.Name()], c)
}
return m
}
func names(commands []*Command) []string {
ns := make([]string, len(commands))
for i, c := range commands {
ns[i] = c.Name()
}
return ns
}

88
zsh_completions_test.go Normal file
View file

@ -0,0 +1,88 @@
package cobra
import (
"bytes"
"strings"
"testing"
)
func TestZshCompletion(t *testing.T) {
tcs := []struct {
name string
root *Command
expectedExpressions []string
}{
{
name: "trivial",
root: &Command{Use: "trivialapp"},
expectedExpressions: []string{"#compdef trivial"},
},
{
name: "linear",
root: func() *Command {
r := &Command{Use: "linear"}
sub1 := &Command{Use: "sub1"}
r.AddCommand(sub1)
sub2 := &Command{Use: "sub2"}
sub1.AddCommand(sub2)
sub3 := &Command{Use: "sub3"}
sub2.AddCommand(sub3)
return r
}(),
expectedExpressions: []string{"sub1", "sub2", "sub3"},
},
{
name: "flat",
root: func() *Command {
r := &Command{Use: "flat"}
r.AddCommand(&Command{Use: "c1"})
r.AddCommand(&Command{Use: "c2"})
return r
}(),
expectedExpressions: []string{"(c1 c2)"},
},
{
name: "tree",
root: func() *Command {
r := &Command{Use: "tree"}
sub1 := &Command{Use: "sub1"}
r.AddCommand(sub1)
sub11 := &Command{Use: "sub11"}
sub12 := &Command{Use: "sub12"}
sub1.AddCommand(sub11)
sub1.AddCommand(sub12)
sub2 := &Command{Use: "sub2"}
r.AddCommand(sub2)
sub21 := &Command{Use: "sub21"}
sub22 := &Command{Use: "sub22"}
sub2.AddCommand(sub21)
sub2.AddCommand(sub22)
return r
}(),
expectedExpressions: []string{"(sub11 sub12)", "(sub21 sub22)"},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
buf := new(bytes.Buffer)
tc.root.GenZshCompletion(buf)
completion := buf.String()
for _, expectedExpression := range tc.expectedExpressions {
if !strings.Contains(completion, expectedExpression) {
t.Errorf("expected completion to contain '%v' somewhere; got '%v'", expectedExpression, completion)
}
}
})
}
}