Compare commits

..

1 commit

Author SHA1 Message Date
Johannes Altmanninger eabe4e512a fish completions: fix double-evaluation of commandline
We capture the commandline tokens using fish's "commandline --tokenize" (-o).
That function turns

	echo 'some argument $(123)'

into two arguments while removing the quotes

	echo
	some argument $(123)

Later we pass "some argument $(123)" without quotes to the shell's
"eval". This is wrong and causes spurious evaluation of the parenthesis
as command substitution.

Fix this by escaping the arguments.

The downside of this change is that things like "$HOME" or "~" will
no longer be escaped. Changing this requires changes in fish, which
I'm working on.

Reproduce the issue by pasting the completion script at
https://github.com/fish-shell/fish-shell/issues/10194#issuecomment-1879563545
to a file "grafana-manager.fish" and running

	function grafana-manager; end
	source grafana-manager.fish

Then type (without pressing Enter)

	grafana-manager public-dashboards delete --organization-id 3 --dashboard-name "k8s (public)" <TAB>

Fixes https://github.com/fish-shell/fish-shell/issues/10194
2024-01-06 10:47:36 +01:00
15 changed files with 35 additions and 49 deletions

View file

@ -43,11 +43,11 @@ jobs:
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: '^1.22' go-version: '^1.21'
check-latest: true check-latest: true
cache: true cache: true
- uses: golangci/golangci-lint-action@v4.0.0 - uses: golangci/golangci-lint-action@v3.7.0
with: with:
version: latest version: latest
args: --verbose args: --verbose
@ -66,7 +66,6 @@ jobs:
- 19 - 19
- 20 - 20
- 21 - 21
- 22
name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x' name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x'
runs-on: ${{ matrix.platform }}-latest runs-on: ${{ matrix.platform }}-latest
steps: steps:
@ -109,7 +108,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/cache@v4 - uses: actions/cache@v3
with: with:
path: ~/go/pkg/mod path: ~/go/pkg/mod
key: ${{ runner.os }}-${{ matrix.go }}-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-${{ matrix.go }}-${{ hashFiles('**/go.sum') }}

View file

@ -29,7 +29,7 @@ linters:
- gas - gas
#- gochecknoinits #- gochecknoinits
- goconst - goconst
- gocritic #- gocritic
#- gocyclo #- gocyclo
#- gofmt #- gofmt
- goimports - goimports
@ -37,7 +37,7 @@ linters:
#- gomnd #- gomnd
#- goprintffuncname #- goprintffuncname
#- gosec #- gosec
- gosimple #- gosimple
- govet - govet
- ineffassign - ineffassign
- interfacer - interfacer

View file

@ -597,16 +597,19 @@ func writeRequiredFlag(buf io.StringWriter, cmd *Command) {
if nonCompletableFlag(flag) { if nonCompletableFlag(flag) {
return return
} }
if _, ok := flag.Annotations[BashCompOneRequiredFlag]; ok { for key := range flag.Annotations {
format := " must_have_one_flag+=(\"--%s" switch key {
if flag.Value.Type() != "bool" { case BashCompOneRequiredFlag:
format += "=" format := " must_have_one_flag+=(\"--%s"
} if flag.Value.Type() != "bool" {
format += cbn format += "="
WriteStringAndCheck(buf, fmt.Sprintf(format, flag.Name)) }
format += cbn
WriteStringAndCheck(buf, fmt.Sprintf(format, flag.Name))
if len(flag.Shorthand) > 0 { if len(flag.Shorthand) > 0 {
WriteStringAndCheck(buf, fmt.Sprintf(" must_have_one_flag+=(\"-%s"+cbn, flag.Shorthand)) WriteStringAndCheck(buf, fmt.Sprintf(" must_have_one_flag+=(\"-%s"+cbn, flag.Shorthand))
}
} }
} }
}) })

View file

@ -154,10 +154,8 @@ type Command struct {
// pflags contains persistent flags. // pflags contains persistent flags.
pflags *flag.FlagSet pflags *flag.FlagSet
// lflags contains local flags. // lflags contains local flags.
// This field does not represent internal state, it's used as a cache to optimise LocalFlags function call
lflags *flag.FlagSet lflags *flag.FlagSet
// iflags contains inherited flags. // iflags contains inherited flags.
// This field does not represent internal state, it's used as a cache to optimise InheritedFlags function call
iflags *flag.FlagSet iflags *flag.FlagSet
// parentsPflags is all persistent flags of cmd's parents. // parentsPflags is all persistent flags of cmd's parents.
parentsPflags *flag.FlagSet parentsPflags *flag.FlagSet
@ -1661,7 +1659,6 @@ func (c *Command) Flags() *flag.FlagSet {
} }
// LocalNonPersistentFlags are flags specific to this command which will NOT persist to subcommands. // LocalNonPersistentFlags are flags specific to this command which will NOT persist to subcommands.
// This function does not modify the flags of the current command, it's purpose is to return the current state.
func (c *Command) LocalNonPersistentFlags() *flag.FlagSet { func (c *Command) LocalNonPersistentFlags() *flag.FlagSet {
persistentFlags := c.PersistentFlags() persistentFlags := c.PersistentFlags()
@ -1675,7 +1672,6 @@ func (c *Command) LocalNonPersistentFlags() *flag.FlagSet {
} }
// LocalFlags returns the local FlagSet specifically set in the current command. // LocalFlags returns the local FlagSet specifically set in the current command.
// This function does not modify the flags of the current command, it's purpose is to return the current state.
func (c *Command) LocalFlags() *flag.FlagSet { func (c *Command) LocalFlags() *flag.FlagSet {
c.mergePersistentFlags() c.mergePersistentFlags()
@ -1703,7 +1699,6 @@ func (c *Command) LocalFlags() *flag.FlagSet {
} }
// InheritedFlags returns all flags which were inherited from parent commands. // InheritedFlags returns all flags which were inherited from parent commands.
// This function does not modify the flags of the current command, it's purpose is to return the current state.
func (c *Command) InheritedFlags() *flag.FlagSet { func (c *Command) InheritedFlags() *flag.FlagSet {
c.mergePersistentFlags() c.mergePersistentFlags()
@ -1729,7 +1724,6 @@ func (c *Command) InheritedFlags() *flag.FlagSet {
} }
// NonInheritedFlags returns all flags which were not inherited from parent commands. // NonInheritedFlags returns all flags which were not inherited from parent commands.
// This function does not modify the flags of the current command, it's purpose is to return the current state.
func (c *Command) NonInheritedFlags() *flag.FlagSet { func (c *Command) NonInheritedFlags() *flag.FlagSet {
return c.LocalFlags() return c.LocalFlags()
} }

View file

@ -18,7 +18,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io" "io/ioutil"
"os" "os"
"reflect" "reflect"
"strings" "strings"
@ -2092,12 +2092,12 @@ func TestCommandPrintRedirection(t *testing.T) {
t.Error(err) t.Error(err)
} }
gotErrBytes, err := io.ReadAll(errBuff) gotErrBytes, err := ioutil.ReadAll(errBuff)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
gotOutBytes, err := io.ReadAll(outBuff) gotOutBytes, err := ioutil.ReadAll(outBuff)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -2777,7 +2777,7 @@ func TestFind(t *testing.T) {
func TestUnknownFlagShouldReturnSameErrorRegardlessOfArgPosition(t *testing.T) { func TestUnknownFlagShouldReturnSameErrorRegardlessOfArgPosition(t *testing.T) {
testCases := [][]string{ testCases := [][]string{
// {"--unknown", "--namespace", "foo", "child", "--bar"}, // FIXME: This test case fails, returning the error `unknown command "foo" for "root"` instead of the expected error `unknown flag: --unknown` //{"--unknown", "--namespace", "foo", "child", "--bar"}, // FIXME: This test case fails, returning the error `unknown command "foo" for "root"` instead of the expected error `unknown flag: --unknown`
{"--namespace", "foo", "--unknown", "child", "--bar"}, {"--namespace", "foo", "--unknown", "child", "--bar"},
{"--namespace", "foo", "child", "--unknown", "--bar"}, {"--namespace", "foo", "child", "--unknown", "--bar"},
{"--namespace", "foo", "child", "--bar", "--unknown"}, {"--namespace", "foo", "child", "--bar", "--unknown"},

View file

@ -133,7 +133,7 @@ func fillHeader(header *GenManHeader, name string, disableAutoGen bool) error {
} }
header.Date = &now header.Date = &now
} }
header.date = header.Date.Format("Jan 2006") header.date = (*header.Date).Format("Jan 2006")
if header.Source == "" && !disableAutoGen { if header.Source == "" && !disableAutoGen {
header.Source = "Auto generated by spf13/cobra" header.Source = "Auto generated by spf13/cobra"
} }

View file

@ -128,7 +128,7 @@ func GenMarkdownTree(cmd *cobra.Command, dir string) error {
return GenMarkdownTreeCustom(cmd, dir, emptyStr, identity) return GenMarkdownTreeCustom(cmd, dir, emptyStr, identity)
} }
// GenMarkdownTreeCustom is the same as GenMarkdownTree, but // GenMarkdownTreeCustom is the the same as GenMarkdownTree, but
// with custom filePrepender and linkHandler. // with custom filePrepender and linkHandler.
func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error { func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error {
for _, c := range cmd.Commands() { for _, c := range cmd.Commands() {

View file

@ -140,7 +140,7 @@ func GenReSTTree(cmd *cobra.Command, dir string) error {
return GenReSTTreeCustom(cmd, dir, emptyStr, defaultLinkHandler) return GenReSTTreeCustom(cmd, dir, emptyStr, defaultLinkHandler)
} }
// GenReSTTreeCustom is the same as GenReSTTree, but // GenReSTTreeCustom is the the same as GenReSTTree, but
// with custom filePrepender and linkHandler. // with custom filePrepender and linkHandler.
func GenReSTTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string) string, linkHandler func(string, string) string) error { func GenReSTTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string) string, linkHandler func(string, string) string) error {
for _, c := range cmd.Commands() { for _, c := range cmd.Commands() {

View file

@ -40,7 +40,7 @@ func hasSeeAlso(cmd *cobra.Command) bool {
// that do not contain \n. // that do not contain \n.
func forceMultiLine(s string) string { func forceMultiLine(s string) string {
if len(s) > 60 && !strings.Contains(s, "\n") { if len(s) > 60 && !strings.Contains(s, "\n") {
s += "\n" s = s + "\n"
} }
return s return s
} }

View file

@ -45,9 +45,7 @@ function __%[1]s_perform_completion
__%[1]s_debug "Starting __%[1]s_perform_completion" __%[1]s_debug "Starting __%[1]s_perform_completion"
# Extract all args except the last one # Extract all args except the last one
set -l args ( set -l args (commandline -opc | string escape)
commandline -xpc 2>/dev/null ||
commandline -opc | string escape)
# Extract the last arg and escape it in case it is a space # Extract the last arg and escape it in case it is a space
set -l lastArg (string escape -- (commandline -ct)) set -l lastArg (string escape -- (commandline -ct))

2
go.mod
View file

@ -3,7 +3,7 @@ module github.com/spf13/cobra
go 1.15 go 1.15
require ( require (
github.com/cpuguy83/go-md2man/v2 v2.0.4 github.com/cpuguy83/go-md2man/v2 v2.0.3
github.com/inconshreveable/mousetrap v1.1.0 github.com/inconshreveable/mousetrap v1.1.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1

4
go.sum
View file

@ -1,5 +1,5 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=

View file

@ -28,8 +28,8 @@ import (
func genPowerShellComp(buf io.StringWriter, name string, includeDesc bool) { func genPowerShellComp(buf io.StringWriter, name string, includeDesc bool) {
// Variables should not contain a '-' or ':' character // Variables should not contain a '-' or ':' character
nameForVar := name nameForVar := name
nameForVar = strings.ReplaceAll(nameForVar, "-", "_") nameForVar = strings.Replace(nameForVar, "-", "_", -1)
nameForVar = strings.ReplaceAll(nameForVar, ":", "_") nameForVar = strings.Replace(nameForVar, ":", "_", -1)
compCmd := ShellCompRequestCmd compCmd := ShellCompRequestCmd
if !includeDesc { if !includeDesc {

View file

@ -24,7 +24,6 @@
- [GoReleaser](https://goreleaser.com) - [GoReleaser](https://goreleaser.com)
- [Helm](https://helm.sh) - [Helm](https://helm.sh)
- [Hugo](https://gohugo.io) - [Hugo](https://gohugo.io)
- [Incus](https://linuxcontainers.org/incus/)
- [Infracost](https://github.com/infracost/infracost) - [Infracost](https://github.com/infracost/infracost)
- [Istio](https://istio.io) - [Istio](https://istio.io)
- [Kool](https://github.com/kool-dev/kool) - [Kool](https://github.com/kool-dev/kool)
@ -58,7 +57,6 @@
- [Scaleway CLI](https://github.com/scaleway/scaleway-cli) - [Scaleway CLI](https://github.com/scaleway/scaleway-cli)
- [Sia](https://github.com/SiaFoundation/siad) - [Sia](https://github.com/SiaFoundation/siad)
- [Skaffold](https://skaffold.dev/) - [Skaffold](https://skaffold.dev/)
- [Taikun](https://taikun.cloud/)
- [Tendermint](https://github.com/tendermint/tendermint) - [Tendermint](https://github.com/tendermint/tendermint)
- [Twitch CLI](https://github.com/twitchdev/twitch-cli) - [Twitch CLI](https://github.com/twitchdev/twitch-cli)
- [UpCloud CLI (`upctl`)](https://github.com/UpCloudLtd/upcloud-cli) - [UpCloud CLI (`upctl`)](https://github.com/UpCloudLtd/upcloud-cli)

View file

@ -3,7 +3,7 @@
While you are welcome to provide your own organization, typically a Cobra-based While you are welcome to provide your own organization, typically a Cobra-based
application will follow the following organizational structure: application will follow the following organizational structure:
```test ```
▾ appName/ ▾ appName/
▾ cmd/ ▾ cmd/
add.go add.go
@ -301,7 +301,6 @@ command := cobra.Command{
### Bind Flags with Config ### Bind Flags with Config
You can also bind your flags with [viper](https://github.com/spf13/viper): You can also bind your flags with [viper](https://github.com/spf13/viper):
```go ```go
var author string var author string
@ -321,14 +320,12 @@ More in [viper documentation](https://github.com/spf13/viper#working-with-flags)
Flags are optional by default. If instead you wish your command to report an error Flags are optional by default. If instead you wish your command to report an error
when a flag has not been set, mark it as required: when a flag has not been set, mark it as required:
```go ```go
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)") rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region") rootCmd.MarkFlagRequired("region")
``` ```
Or, for persistent flags: Or, for persistent flags:
```go ```go
rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (required)") rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkPersistentFlagRequired("region") rootCmd.MarkPersistentFlagRequired("region")
@ -338,7 +335,6 @@ rootCmd.MarkPersistentFlagRequired("region")
If you have different flags that must be provided together (e.g. if they provide the `--username` flag they MUST provide the `--password` flag as well) then If you have different flags that must be provided together (e.g. if they provide the `--username` flag they MUST provide the `--password` flag as well) then
Cobra can enforce that requirement: Cobra can enforce that requirement:
```go ```go
rootCmd.Flags().StringVarP(&u, "username", "u", "", "Username (required if password is set)") rootCmd.Flags().StringVarP(&u, "username", "u", "", "Username (required if password is set)")
rootCmd.Flags().StringVarP(&pw, "password", "p", "", "Password (required if username is set)") rootCmd.Flags().StringVarP(&pw, "password", "p", "", "Password (required if username is set)")
@ -347,7 +343,6 @@ rootCmd.MarkFlagsRequiredTogether("username", "password")
You can also prevent different flags from being provided together if they represent mutually You can also prevent different flags from being provided together if they represent mutually
exclusive options such as specifying an output format as either `--json` or `--yaml` but never both: exclusive options such as specifying an output format as either `--json` or `--yaml` but never both:
```go ```go
rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON") rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON")
rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML") rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML")
@ -356,7 +351,6 @@ rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")
If you want to require at least one flag from a group to be present, you can use `MarkFlagsOneRequired`. If you want to require at least one flag from a group to be present, you can use `MarkFlagsOneRequired`.
This can be combined with `MarkFlagsMutuallyExclusive` to enforce exactly one flag from a given group: This can be combined with `MarkFlagsMutuallyExclusive` to enforce exactly one flag from a given group:
```go ```go
rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON") rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON")
rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML") rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML")
@ -434,7 +428,7 @@ by not providing a 'Run' for the 'rootCmd'.
We have only defined one flag for a single command. We have only defined one flag for a single command.
More documentation about flags is available at https://github.com/spf13/pflag. More documentation about flags is available at https://github.com/spf13/pflag
```go ```go
package main package main
@ -728,7 +722,7 @@ command.SuggestionsMinimumDistance = 1
You can also explicitly set names for which a given command will be suggested using the `SuggestFor` attribute. This allows suggestions for strings that are not close in terms of string distance, but make sense in your set of commands but for which You can also explicitly set names for which a given command will be suggested using the `SuggestFor` attribute. This allows suggestions for strings that are not close in terms of string distance, but make sense in your set of commands but for which
you don't want aliases. Example: you don't want aliases. Example:
```bash ```
$ kubectl remove $ kubectl remove
Error: unknown command "remove" for "kubectl" Error: unknown command "remove" for "kubectl"
@ -793,7 +787,7 @@ func main() {
Example run as a kubectl plugin: Example run as a kubectl plugin:
```bash ```
$ kubectl myplugin $ kubectl myplugin
Usage: Usage:
kubectl myplugin [command] kubectl myplugin [command]