Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ba5108a
Update README.md
ericdachen Apr 11, 2025
73f7846
Update README.md
ericdachen Apr 11, 2025
0846d16
Update README.md
ericdachen Apr 11, 2025
0ed70dc
Update README.md
ericdachen Apr 15, 2025
67171d6
putting sponsorship below header
spf13 May 6, 2025
db3ddb5
Adding sponsorship to README.md
spf13 May 6, 2025
75790e4
chore(golangci-lint): upgrade to v2
scop Mar 26, 2025
4af7b64
refactor: apply golangci-lint autofixes, work around false positives
scop Mar 26, 2025
c8289c1
chore(golangci-lint): add some exclusion presets
scop May 28, 2025
6dec1ae
The default ShellCompDirective can be customized for a command and it…
albers May 31, 2025
dcaf42e
Add Periscope to the list of projects using Cobra (#2299)
anishathalye Aug 4, 2025
3f3b818
Update README.md with new logo
spf13 Aug 11, 2025
51d6751
Bump pflag to 1.0.8 (#2303)
tomasaschan Aug 31, 2025
7da941c
chore: Bump pflag to v1.0.9 (#2305)
jpmcb Sep 1, 2025
0629892
Fix linter (#2327)
marckhouzam Nov 14, 2025
e2dd29d
Add documentation for repeated flags functionality (#2316)
rvergis Nov 17, 2025
117698a
refactor: replace several vars with consts (#2328)
htoyoda18 Nov 17, 2025
fc81d20
refactor: change minUsagePadding from var to const (#2325)
ssam18 Nov 18, 2025
346d408
fix: actions/setup-go v6 (#2337)
jpmcb Dec 3, 2025
88b30ab
chore: Migrate from gopkg.in/yaml.v3 to go.yaml.in/yaml/v3 (#2336)
dims Dec 3, 2025
10d4b48
perf: Loop over the bash variable directly instead of starting subpro…
JeffFaer Dec 6, 2025
61968e8
fix: quote args in fish shell completion (#2285)
n9v9 Dec 10, 2025
85c12c2
projects_using_cobra: add Ollama (#2389)
ArturAronov Apr 15, 2026
f2878ba
Ignore the linter error (#2390)
marckhouzam Apr 15, 2026
746ef07
fix: prevent completions from mutating os.Args via append side effect…
veeceey Apr 24, 2026
ad460ea
Add cobra unique args validator (#2397)
takanuva15 Apr 25, 2026
2bf318f
sync from upstream: resolve merge conflicts with xgo branch
Copilot May 2, 2026
66c25c4
.golangci.yml
xushiwei May 2, 2026
78f8f10
uses golangci-lint-action@v9.2.0
xushiwei May 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ jobs:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version: '^1.22'
check-latest: true
cache: true

- uses: golangci/golangci-lint-action@v4.0.0
- uses: golangci/golangci-lint-action@v9.2.0
with:
version: latest
args: --verbose
Expand All @@ -76,7 +76,7 @@ jobs:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version: 1.${{ matrix.go }}.x
cache: true
Expand Down
35 changes: 22 additions & 13 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,23 @@
# See the License for the specific language governing permissions and
# limitations under the License.

version: "2"

run:
deadline: 5m
timeout: 5m
skip-dirs:
- xcmd
- pflag

formatters:
enable:
- gofmt
- goimports

linters:
disable-all: true
default: none
enable:
#- bodyclose
# - deadcode ! deprecated since v1.49.0; replaced by 'unused'
#- depguard
#- dogsled
#- dupl
Expand All @@ -33,28 +39,31 @@ linters:
- goconst
- gocritic
#- gocyclo
- gofmt
- goimports
#- gomnd
#- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
#- lll
- misspell
#- mnd
#- nakedret
#- noctx
- nolintlint
#- rowserrcheck
#- scopelint
- staticcheck
#- structcheck ! deprecated since v1.49.0; replaced by 'unused'
- stylecheck
#- typecheck
- unconvert
#- unparam
- unused
# - varcheck ! deprecated since v1.49.0; replaced by 'unused'
#- whitespace
fast: false
exclusions:
presets:
- common-false-positives
- legacy
- std-error-handling
settings:
govet:
# Disable buildtag check to allow dual build tag syntax (both //go:build and // +build).
# This is necessary for Go 1.15 compatibility since //go:build was introduced in Go 1.17.
# This can be removed once Cobra requires Go 1.17 or higher.
disable:
- buildtag
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@

![cobra logo](https://github.com/user-attachments/assets/cbc3adf8-0dff-46e9-a88d-5e2d971c169e)
<div align="center">
<a href="https://cobra.dev">
<img width="512" height="535" alt="cobra-logo" src="https://github.com/user-attachments/assets/c8bf9aad-b5ae-41d3-8899-d83baec10af8" />
</a>
</div>

Cobra is a library for creating powerful modern CLI applications.

<a href="https://cobra.dev">Visit Cobra.dev for extensive documentation</a>


Cobra is used in many Go projects such as [Kubernetes](https://kubernetes.io/),
[Hugo](https://gohugo.io), and [GitHub CLI](https://github.com/cli/cli) to
name a few. [This list](site/content/projects_using_cobra.md) contains a more extensive list of projects using Cobra.
Expand All @@ -11,6 +17,20 @@ name a few. [This list](site/content/projects_using_cobra.md) contains a more ex
[![Go Reference](https://pkg.go.dev/badge/github.com/spf13/cobra.svg)](https://pkg.go.dev/github.com/spf13/cobra)
[![Go Report Card](https://goreportcard.com/badge/github.com/spf13/cobra)](https://goreportcard.com/report/github.com/spf13/cobra)
[![Slack](https://img.shields.io/badge/Slack-cobra-brightgreen)](https://gophers.slack.com/archives/CD3LP1199)
<hr>
<div align="center" markdown="1">
<sup>Supported by:</sup>
<br>
<br>
<a href="https://www.warp.dev/cobra">
<img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/ab8dd143-b0fd-4904-bdc5-dd7ecac94eae">
</a>

### [Warp, the AI terminal for devs](https://www.warp.dev/cobra)
[Try Cobra in Warp today](https://www.warp.dev/cobra)<br>

</div>
<hr>

# Overview

Expand Down
13 changes: 13 additions & 0 deletions args.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ func OnlyValidArgs(cmd *Command, args []string) error {
return nil
}

// NoDuplicateArgs returns an error if there are any duplicate positional args.
func NoDuplicateArgs(cmd *Command, args []string) error {
seen := make(map[string]struct{}, len(args))
for _, arg := range args {
if _, ok := seen[arg]; ok {
return fmt.Errorf("duplicate argument %q for %q", arg, cmd.CommandPath())
}
seen[arg] = struct{}{}
}

return nil
}

// ArbitraryArgs never returns an error.
func ArbitraryArgs(cmd *Command, args []string) error {
return nil
Expand Down
37 changes: 37 additions & 0 deletions args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ func rangeArgsWithInvalidCount(err error, t *testing.T) {
}
}

func noDuplicateArgsWithDuplicate(err error, t *testing.T, arg string) {
if err == nil {
t.Fatal("Expected an error")
}
got := err.Error()
expected := `duplicate argument "` + arg + `" for "c"`
if got != expected {
t.Errorf("Expected: %q, got: %q", expected, got)
}
}

// NoArgs

func TestNoArgs(t *testing.T) {
Expand Down Expand Up @@ -153,6 +164,32 @@ func TestOnlyValidArgs_WithInvalidArgs(t *testing.T) {
validOnlyWithInvalidArgs(err, t)
}

// NoDuplicateArgs

func TestNoDuplicateArgs(t *testing.T) {
c := getCommand(NoDuplicateArgs, false)
output, err := executeCommand(c, "one", "two")
expectSuccess(output, err, t)
}

func TestNoDuplicateArgs_WithDuplicateArgs(t *testing.T) {
c := getCommand(NoDuplicateArgs, false)
_, err := executeCommand(c, "one", "one")
noDuplicateArgsWithDuplicate(err, t, "one")
}

func TestNoDuplicateArgs_WithValid_WithDuplicateArgs(t *testing.T) {
c := getCommand(NoDuplicateArgs, true)
_, err := executeCommand(c, "one", "one")
noDuplicateArgsWithDuplicate(err, t, "one")
}

func TestNoDuplicateArgs_WithValidOnly_WithInvalidArgs(t *testing.T) {
c := getCommand(MatchAll(OnlyValidArgs, NoDuplicateArgs), true)
_, err := executeCommand(c, "a", "a")
validOnlyWithInvalidArgs(err, t)
}

// ArbitraryArgs

func TestArbitraryArgs(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions bash_completionsV2.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ __%[1]s_handle_standard_completion_case() {
local longest=0
local compline
# Look for the longest completion so that we can format things nicely
while IFS='' read -r compline; do
for compline in "${completions[@]}"; do
[[ -z $compline ]] && continue

# Before checking if the completion matches what the user typed,
Expand All @@ -352,7 +352,7 @@ __%[1]s_handle_standard_completion_case() {
if ((${#comp}>longest)); then
longest=${#comp}
fi
done < <(printf "%%s\n" "${completions[@]}")
done

# If there is a single completion left, remove the description text and escape any special characters
if ((${#COMPREPLY[*]} == 1)); then
Expand Down
12 changes: 6 additions & 6 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ func (c *Command) FlagErrorFunc() (f func(*Command, error) error) {
}
}

var minUsagePadding = 25
const minUsagePadding = 25

// UsagePadding return padding for the usage.
func (c *Command) UsagePadding() int {
Expand All @@ -567,7 +567,7 @@ func (c *Command) UsagePadding() int {
return c.parent.commandsMaxUseLen
}

var minCommandPathPadding = 11
const minCommandPathPadding = 11

// CommandPathPadding return padding for the command path.
func (c *Command) CommandPathPadding() int {
Expand All @@ -577,7 +577,7 @@ func (c *Command) CommandPathPadding() int {
return c.parent.commandsMaxCommandPathLen
}

var minNamePadding = 11
const minNamePadding = 11

// NamePadding returns padding for the name.
func (c *Command) NamePadding() int {
Expand Down Expand Up @@ -1939,7 +1939,7 @@ type tmplFunc struct {
fn func(io.Writer, interface{}) error
}

var defaultUsageTemplate = `Usage:{{if .Runnable}}
const defaultUsageTemplate = `Usage:{{if .Runnable}}
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}

Expand Down Expand Up @@ -2039,7 +2039,7 @@ func defaultUsageFunc(w io.Writer, in interface{}) error {
return nil
}

var defaultHelpTemplate = `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}}
const defaultHelpTemplate = `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}}

{{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`

Expand All @@ -2061,7 +2061,7 @@ func defaultHelpFunc(w io.Writer, in interface{}) error {
return nil
}

var defaultVersionTemplate = `{{with .DisplayName}}{{printf "%s " .}}{{end}}{{printf "version %s" .Version}}
const defaultVersionTemplate = `{{with .DisplayName}}{{printf "%s " .}}{{end}}{{printf "version %s" .Version}}
`

// defaultVersionFunc is equivalent to executing defaultVersionTemplate. The two should be changed in sync.
Expand Down
2 changes: 1 addition & 1 deletion command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2926,7 +2926,7 @@ func TestHelpFuncExecuted(t *testing.T) {
helpText := "Long description"

// Create a context that will be unique, not just the background context
//nolint:golint,staticcheck // We can safely use a basic type as key in tests.
//nolint:staticcheck // We can safely use a basic type as key in tests.
executionCtx := context.WithValue(context.Background(), "testKey", "123")

child := &Command{Use: "child", Run: emptyRun}
Expand Down
25 changes: 22 additions & 3 deletions completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ type CompletionOptions struct {
DisableDescriptions bool
// HiddenDefaultCmd makes the default 'completion' command hidden
HiddenDefaultCmd bool
// DefaultShellCompDirective sets the ShellCompDirective that is returned
// if no special directive can be determined
DefaultShellCompDirective *ShellCompDirective

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Code Quality] Doc comment doesn't explain pointer semantics or scope

The pointer is needed to distinguish nil ("not set, inherit from parent") from 0 (ShellCompDirectiveDefault). This non-obvious design should be documented on the field:

// DefaultShellCompDirective overrides the directive used when no completion
// function is registered. It is a pointer to distinguish "not set" (nil, inherits
// from the nearest ancestor) from "explicitly set to ShellCompDirectiveDefault (0)".
// Applies only to argument/noun completion; flag-name and flag-value completion
// paths use their own fixed directives.
DefaultShellCompDirective *ShellCompDirective

}

func (receiver *CompletionOptions) SetDefaultShellCompDirective(directive ShellCompDirective) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Code Quality] Non-idiomatic receiver name

receiver is not a conventional Go receiver name. The rest of the codebase uses short names (c *Command, etc.). Per Go conventions (and the enabled gocritic linter), this should be o or co:

func (o *CompletionOptions) SetDefaultShellCompDirective(directive ShellCompDirective) {

receiver.DefaultShellCompDirective = &directive
}
Comment on lines +123 to 125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The receiver name receiver is not idiomatic in Go. Receiver names should be short (usually one or two letters) and reflect the type name (e.g., o or opt for CompletionOptions).

Suggested change
func (receiver *CompletionOptions) SetDefaultShellCompDirective(directive ShellCompDirective) {
receiver.DefaultShellCompDirective = &directive
}
func (o *CompletionOptions) SetDefaultShellCompDirective(directive ShellCompDirective) {
o.DefaultShellCompDirective = &directive
}


// Completion is a string that can be used for completions
Expand Down Expand Up @@ -310,7 +317,10 @@ func (c *Command) getCompletions(args []string) (*Command, []Completion, ShellCo
// The last argument, which is not completely typed by the user,
// should not be part of the list of arguments
toComplete := args[len(args)-1]
trimmedArgs := args[:len(args)-1]
// Copy trimmedArgs to a new slice to avoid mutating the caller's
// backing array (which may be os.Args) when later appending "--".
trimmedArgs := make([]string, len(args)-1)
copy(trimmedArgs, args[:len(args)-1])

var finalCmd *Command
var finalArgs []string
Expand Down Expand Up @@ -375,7 +385,7 @@ func (c *Command) getCompletions(args []string) (*Command, []Completion, ShellCo
// Error while attempting to parse flags
if flagErr != nil {
// If error type is flagCompError and we don't want flagCompletion we should ignore the error
if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
if _, ok := flagErr.(*flagCompError); !ok || flagCompletion {
return finalCmd, []Completion{}, ShellCompDirectiveDefault, flagErr
}
}
Expand Down Expand Up @@ -480,6 +490,14 @@ func (c *Command) getCompletions(args []string) (*Command, []Completion, ShellCo
}
} else {
directive = ShellCompDirectiveDefault
// check current and parent commands for a custom DefaultShellCompDirective
for cmd := finalCmd; cmd != nil; cmd = cmd.parent {
if cmd.CompletionOptions.DefaultShellCompDirective != nil {
directive = *cmd.CompletionOptions.DefaultShellCompDirective
break
}
}

if flag == nil {
foundLocalNonPersistentFlag := false
// If TraverseChildren is true on the root command we don't check for
Expand Down Expand Up @@ -773,7 +791,7 @@ See each sub-command's help for details on how to use the generated script.
// shell completion for it (prog __complete completion '')
subCmd, cmdArgs, err := c.Find(args)
if err != nil || subCmd.Name() != compCmdName &&
!(subCmd.Name() == ShellCompRequestCmd && len(cmdArgs) > 1 && cmdArgs[0] == compCmdName) {
(subCmd.Name() != ShellCompRequestCmd || len(cmdArgs) <= 1 || cmdArgs[0] != compCmdName) {
// The completion command is not being called or being completed so we remove it.
c.RemoveCommand(completionCmd)
return
Expand Down Expand Up @@ -940,6 +958,7 @@ func CompDebug(msg string, printToStdErr bool) {
// Such logs are only printed when the user has set the environment
// variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" {
//nolint:gosec // G703:BASH_COMP_DEBUG_FILE intentionally user-controlled for completion debug logging.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Security - Low] Wrong gosec rule ID in nolint annotation

The comment cites G703, which is not a valid gosec rule. The actual rule that fires here is G304 ("Potential file inclusion via variable"), since path is derived from an environment variable. The suppression intent is reasonable, but the incorrect rule ID undermines auditability and may not suppress the actual finding in all gosec versions:

//nolint:gosec // G304: BASH_COMP_DEBUG_FILE is intentionally user-controlled for completion debug logging.

f, err := os.OpenFile(path,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err == nil {
Expand Down
Loading
Loading