Repository: spf13/cobra
Pull request #1530 (2021-11-08 19:22:12)

Prefer ReplaceAll instead of Replace(..., -1)
Minor change that gopls pointed out. I think it improves readability a bit.

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/spf13/cobra?pullRequest=1530) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/spf13/cobra?pullRequest=1530) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/spf13/cobra?pullRequest=1530) it.</sub>
umarcor: According to https://pkg.go.dev/strings#ReplaceAll, `ReplaceAll` was added in go 1.12. I guess that's why it was not used before.  Nevertheless, 1.12 was EOL in Feb 2020, so I guess it's ok to introduce this change.
WhyNotHugo: Signing the DCO requires me to grant read-write access to all my gists, commit statuses and webhooks.

I'm okay with [the DCO](https://developercertificate.org/), and agree to its terms for this contributions.

However, I can't grant you write access to all my unrelated projects, I'm sure you'll understand that this doesn't make sense for an external contributor.

The wording above is also rather confusing; @CLAassistant here says I need to sign a CLA, and it says CLA everywhere on public pages, but the text I'm shown when trying to complete the flow is the DCO. I find it a bit discomforting that public content specifies one document, but the signing procedure displays another.

Maybe you want to consider using the this integration instead? -> https://github.com/apps/dco
umarcor: @WhyNotHugo I understand your concerns, but unfortunately I cannot confirm.

On the one hand, I do not remember signing the CLA or DCO, however, in my PRs that check is shown green. Maybe it's the first contribution only and it then checks the list of previous contributors. Therefore, I'm not sure it is compulsory for your to sign it (use the app).

On the other hand, if I go to https://cla-assistant.io/spf13/cobra?pullRequest=1530 and I click "Sign in with Github to agree", I see the following:

![image](https://user-images.githubusercontent.com/38422348/141121916-a911321f-1a8a-4073-a101-aaef6d37b848.png)

I'm curious about why it's asking you read-write access to all the gists, commit statuses and webhooks.

> The wording above is also rather confusing; @CLAassistant here says I need to sign a CLA, and it says CLA everywhere on public pages, but the text I'm shown when trying to complete the flow is the DCO. I find it a bit discomforting that public content specifies one document, but the signing procedure displays another.
> 
> Maybe you want to consider using the this integration instead? -> https://github.com/apps/dco

I like this proposal. Maybe you can open an issue about it?

WhyNotHugo: ![image](https://user-images.githubusercontent.com/730811/141127292-a09d3336-d5b0-48c0-a6f6-4199ddc7af38.png)

Maybe it doesn't show up for you because you've already granted permissions? Yeah, I'll open up a separate issue on the topic :)
umarcor: > Maybe it doesn't show up for you because you've already granted permissions?

Interesting... I checked my profile and that app is not found anywhere...
jpmcb: Hmmmm @WhyNotHugo this is somewhat concerning. I had no idea the CLA / DCO required such wide permissions. Can you open a separate issue, include the screenshot, and a short description and we can ping Steve there? 
WhyNotHugo: See #1555.
github-actions[bot]: This PR is being marked as stale due to a long period of inactivity
WhyNotHugo: Not stale
marckhouzam: > According to https://pkg.go.dev/strings#ReplaceAll, `ReplaceAll` was added in go 1.12. I guess that's why it was not used before. Nevertheless, 1.12 was EOL in Feb 2020, so I guess it's ok to introduce this change.

What is the oldest version of Go that can be used with Cobra right now? Is it older than 1.12?


umarcor: @marckhouzam we are testing 1.14 to 1.18 in CI. go.mod declares 1.15: https://github.com/spf13/cobra/blob/master/go.mod#L3.
Deprecating <1.14 was announced in January 2021: #1322. See https://github.com/spf13/cobra/blob/master/CONDUCT.md#backward-compatibility. So, we might announce that <1.17 will be deprecated in the next release.
marckhouzam: > @marckhouzam we are testing 1.14 to 1.18 in CI. go.mod declares 1.15: https://github.com/spf13/cobra/blob/master/go.mod#L3. Deprecating <1.14 was announced in January 2021: #1322. 

Thanks @umarcor !

Ok, so using a construct from go 1.12 is perfectly fine.

@WhyNotHugo could you rebase this PR to the latest version of the master branch please?




Patch:
diff --git a/.github/workflows/size-labeler.yml b/.github/workflows/size-labeler.yml
index af081dc..f04024f 100644
--- a/.github/workflows/size-labeler.yml
+++ b/.github/workflows/size-labeler.yml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
     name: Label the PR size
     steps:
-      - uses: codelytv/pr-size-labeler@v1.8.0
+      - uses: codelytv/pr-size-labeler@v1.8.1
         with:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           xs_label: 'size/XS'
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index ce57c31..a63518f 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -13,9 +13,38 @@ jobs:
     - uses: actions/stale@v5
       with:
         repo-token: ${{ secrets.GITHUB_TOKEN }}
-        stale-issue-message: 'This issue is being marked as stale due to a long period of inactivity'
-        stale-pr-message: 'This PR is being marked as stale due to a long period of inactivity'
-        stale-issue-label: 'kind/stale'
-        stale-pr-label: 'kind/stale'
-        exempt-issue-label: 'kind/stale'
-        exempt-pr-label: 'kind/stale'
+        stale-issue-message: "The Cobra project currently lacks enough contributors to adequately respond to all issues.
+          This bot triages issues and PRs according to the following rules:
+
+          - After 60d of inactivity, lifecycle/stale is applied.
+          - After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied and the issue is closed.
+
+          You can:
+
+          - Make a comment to remove the stale label and show your support. The 60 days reset.
+          - If an issue has lifecycle/rotten and is closed, comment and ask maintainers if they'd be interseted in reopening"
+
+        stale-pr-message: "The Cobra project currently lacks enough contributors to adequately respond to all PRs.
+          This bot triages issues and PRs according to the following rules:
+
+          - After 60d of inactivity, lifecycle/stale is applied.
+          - After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied and the PR is closed.
+
+          You can:
+
+          - Make a comment to remove the stale label and show your support. The 60 days reset.
+          - If a PR has lifecycle/rotten and is closed, comment and ask maintainers if they'd be interseted in reopening."
+
+        days-before-stale: 60
+        days-before-close: 30
+        stale-issue-label: 'lifecycle/stale'
+        stale-pr-label: 'lifecycle/stale'
+        exempt-issue-label: 'lifecycle/frozen'
+        exempt-pr-label: 'lifecycle/frozen'
+        close-issue-label: 'lifecycle/rotten'
+        close-pr-label: 'lifecycle/rotten'
+
+        # Since cobra has so many legacy issues and PRs that need to be triaged,
+        # only label new PRs and issues.
+        start-date: '2022-02-01T00:00:00Z'
+
diff --git a/bash_completions.go b/bash_completions.go
index 6c360c5..3e1e528 100644
--- a/bash_completions.go
+++ b/bash_completions.go
@@ -99,7 +99,7 @@ __%[1]s_handle_go_custom_completion()
         directive=0
     fi
     __%[1]s_debug "${FUNCNAME[0]}: the completion directive is: ${directive}"
-    __%[1]s_debug "${FUNCNAME[0]}: the completions are: ${out[*]}"
+    __%[1]s_debug "${FUNCNAME[0]}: the completions are: ${out}"
 
     if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
         # Error code.  No completion.
@@ -125,7 +125,7 @@ __%[1]s_handle_go_custom_completion()
         local fullFilter filter filteringCmd
         # Do not use quotes around the $out variable or else newline
         # characters will be kept.
-        for filter in ${out[*]}; do
+        for filter in ${out}; do
             fullFilter+="$filter|"
         done
 
@@ -136,7 +136,7 @@ __%[1]s_handle_go_custom_completion()
         # File completion for directories only
         local subdir
         # Use printf to strip any trailing newline
-        subdir=$(printf "%%s" "${out[0]}")
+        subdir=$(printf "%%s" "${out}")
         if [ -n "$subdir" ]; then
             __%[1]s_debug "Listing directories in $subdir"
             __%[1]s_handle_subdirs_in_dir_flag "$subdir"
@@ -147,7 +147,7 @@ __%[1]s_handle_go_custom_completion()
     else
         while IFS='' read -r comp; do
             COMPREPLY+=("$comp")
-        done < <(compgen -W "${out[*]}" -- "$cur")
+        done < <(compgen -W "${out}" -- "$cur")
     fi
 }
 
@@ -387,7 +387,7 @@ __%[1]s_handle_word()
 }
 
 func writePostscript(buf io.StringWriter, name string) {
-	name = strings.Replace(name, ":", "__", -1)
+	name = strings.ReplaceAll(name, ":", "__")
 	WriteStringAndCheck(buf, fmt.Sprintf("__start_%s()\n", name))
 	WriteStringAndCheck(buf, fmt.Sprintf(`{
     local cur prev words cword split
@@ -645,8 +645,8 @@ func gen(buf io.StringWriter, cmd *Command) {
 		gen(buf, c)
 	}
 	commandName := cmd.CommandPath()
-	commandName = strings.Replace(commandName, " ", "_", -1)
-	commandName = strings.Replace(commandName, ":", "__", -1)
+	commandName = strings.ReplaceAll(commandName, " ", "_")
+	commandName = strings.ReplaceAll(commandName, ":", "__")
 
 	if cmd.Root() == cmd {
 		WriteStringAndCheck(buf, fmt.Sprintf("_%s_root_command()\n{\n", commandName))
diff --git a/bash_completionsV2.go b/bash_completionsV2.go
index 82d26c1..5e558a5 100644
--- a/bash_completionsV2.go
+++ b/bash_completionsV2.go
@@ -78,7 +78,7 @@ __%[1]s_get_completion_results() {
         directive=0
     fi
     __%[1]s_debug "The completion directive is: ${directive}"
-    __%[1]s_debug "The completions are: ${out[*]}"
+    __%[1]s_debug "The completions are: ${out}"
 }
 
 __%[1]s_process_completion_results() {
@@ -117,7 +117,7 @@ __%[1]s_process_completion_results() {
 
         # Do not use quotes around the $out variable or else newline
         # characters will be kept.
-        for filter in ${out[*]}; do
+        for filter in ${out}; do
             fullFilter+="$filter|"
         done
 
@@ -129,7 +129,7 @@ __%[1]s_process_completion_results() {
 
         # Use printf to strip any trailing newline
         local subdir
-        subdir=$(printf "%%s" "${out[0]}")
+        subdir=$(printf "%%s" "${out}")
         if [ -n "$subdir" ]; then
             __%[1]s_debug "Listing directories in $subdir"
             pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
@@ -154,17 +154,16 @@ __%[1]s_handle_completion_types() {
         # If the user requested inserting one completion at a time, or all
         # completions at once on the command-line we must remove the descriptions.
         # https://github.com/spf13/cobra/issues/1508
-        local tab comp
-        tab=$(printf '\t')
+        local tab=$'\t' comp
         while IFS='' read -r comp; do
+            [[ -z $comp ]] && continue
             # Strip any description
             comp=${comp%%%%$tab*}
             # Only consider the completions that match
-            comp=$(compgen -W "$comp" -- "$cur")
-            if [ -n "$comp" ]; then
+            if [[ $comp == "$cur"* ]]; then
                 COMPREPLY+=("$comp")
             fi
-        done < <(printf "%%s\n" "${out[@]}")
+        done < <(printf "%%s\n" "${out}")
         ;;
 
     *)
@@ -175,44 +174,38 @@ __%[1]s_handle_completion_types() {
 }
 
 __%[1]s_handle_standard_completion_case() {
-    local tab comp
-    tab=$(printf '\t')
+    local tab=$'\t' comp
+
+    # Short circuit to optimize if we don't have descriptions
+    if [[ $out != *$tab* ]]; then
+        while IFS='' read -r comp; do
+            COMPREPLY+=("$comp")
+        done < <(IFS=$'\n' compgen -W "$out" -- "$cur")
+        return 0
+    fi
 
     local longest=0
+    local compline
     # Look for the longest completion so that we can format things nicely
-    while IFS='' read -r comp; do
+    while IFS='' read -r compline; do
         # Strip any description before checking the length
-        comp=${comp%%%%$tab*}
+        comp=${compline%%%%$tab*}
         # Only consider the completions that match
-        comp=$(compgen -W "$comp" -- "$cur")
+        [[ $comp == "$cur"* ]] || continue
+        COMPREPLY+=("$compline")
         if ((${#comp}>longest)); then
             longest=${#comp}
         fi
-    done < <(printf "%%s\n" "${out[@]}")
-
-    local completions=()
-    while IFS='' read -r comp; do
-        if [ -z "$comp" ]; then
-            continue
-        fi
-
-        __%[1]s_debug "Original comp: $comp"
-        comp="$(__%[1]s_format_comp_descriptions "$comp" "$longest")"
-        __%[1]s_debug "Final comp: $comp"
-        completions+=("$comp")
-    done < <(printf "%%s\n" "${out[@]}")
-
-    while IFS='' read -r comp; do
-        COMPREPLY+=("$comp")
-    done < <(compgen -W "${completions[*]}" -- "$cur")
+    done < <(printf "%%s\n" "${out}")
 
     # If there is a single completion left, remove the description text
     if [ ${#COMPREPLY[*]} -eq 1 ]; then
         __%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}"
-        comp="${COMPREPLY[0]%%%% *}"
+        comp="${COMPREPLY[0]%%%%$tab*}"
         __%[1]s_debug "Removed description from single completion, which is now: ${comp}"
-        COMPREPLY=()
-        COMPREPLY+=("$comp")
+        COMPREPLY[0]=$comp
+    else # Format the descriptions
+        __%[1]s_format_comp_descriptions $longest
     fi
 }
 
@@ -231,45 +224,48 @@ __%[1]s_handle_special_char()
 
 __%[1]s_format_comp_descriptions()
 {
-    local tab
-    tab=$(printf '\t')
-    local comp="$1"
-    local longest=$2
-
-    # Properly format the description string which follows a tab character if there is one
-    if [[ "$comp" == *$tab* ]]; then
-        desc=${comp#*$tab}
-        comp=${comp%%%%$tab*}
-
-        # $COLUMNS stores the current shell width.
-        # Remove an extra 4 because we add 2 spaces and 2 parentheses.
-        maxdesclength=$(( COLUMNS - longest - 4 ))
-
-        # Make sure we can fit a description of at least 8 characters
-        # if we are to align the descriptions.
-        if [[ $maxdesclength -gt 8 ]]; then
-            # Add the proper number of spaces to align the descriptions
-            for ((i = ${#comp} ; i < longest ; i++)); do
-                comp+=" "
-            done
-        else
-            # Don't pad the descriptions so we can fit more text after the completion
-            maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
-        fi
+    local tab=$'\t'
+    local comp desc maxdesclength
+    local longest=$1
+
+    local i ci
+    for ci in ${!COMPREPLY[*]}; do
+        comp=${COMPREPLY[ci]}
+        # Properly format the description string which follows a tab character if there is one
+        if [[ "$comp" == *$tab* ]]; then
+            __%[1]s_debug "Original comp: $comp"
+            desc=${comp#*$tab}
+            comp=${comp%%%%$tab*}
+
+            # $COLUMNS stores the current shell width.
+            # Remove an extra 4 because we add 2 spaces and 2 parentheses.
+            maxdesclength=$(( COLUMNS - longest - 4 ))
+
+            # Make sure we can fit a description of at least 8 characters
+            # if we are to align the descriptions.
+            if [[ $maxdesclength -gt 8 ]]; then
+                # Add the proper number of spaces to align the descriptions
+                for ((i = ${#comp} ; i < longest ; i++)); do
+                    comp+=" "
+                done
+            else
+                # Don't pad the descriptions so we can fit more text after the completion
+                maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
+            fi
 
-        # If there is enough space for any description text,
-        # truncate the descriptions that are too long for the shell width
-        if [ $maxdesclength -gt 0 ]; then
-            if [ ${#desc} -gt $maxdesclength ]; then
-                desc=${desc:0:$(( maxdesclength - 1 ))}
-                desc+="…"
+            # If there is enough space for any description text,
+            # truncate the descriptions that are too long for the shell width
+            if [ $maxdesclength -gt 0 ]; then
+                if [ ${#desc} -gt $maxdesclength ]; then
+                    desc=${desc:0:$(( maxdesclength - 1 ))}
+                    desc+="…"
+                fi
+                comp+="  ($desc)"
             fi
-            comp+="  ($desc)"
+            COMPREPLY[ci]=$comp
+            __%[1]s_debug "Final comp: $comp"
         fi
-    fi
-
-    # Must use printf to escape all special characters
-    printf "%%q" "${comp}"
+    done
 }
 
 __start_%[1]s()
diff --git a/command.go b/command.go
index ee5365b..0f4511f 100644
--- a/command.go
+++ b/command.go
@@ -863,6 +863,10 @@ func (c *Command) execute(a []string) (err error) {
 	if err := c.validateRequiredFlags(); err != nil {
 		return err
 	}
+	if err := c.validateFlagGroups(); err != nil {
+		return err
+	}
+
 	if c.RunE != nil {
 		if err := c.RunE(c, argWoFlags); err != nil {
 			return err
diff --git a/doc/man_docs.go b/doc/man_docs.go
index 916e361..8dbce35 100644
--- a/doc/man_docs.go
+++ b/doc/man_docs.go
@@ -66,7 +66,7 @@ func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error {
 	if opts.CommandSeparator != "" {
 		separator = opts.CommandSeparator
 	}
-	basename := strings.Replace(cmd.CommandPath(), " ", separator, -1)
+	basename := strings.ReplaceAll(cmd.CommandPath(), " ", separator)
 	filename := filepath.Join(opts.Path, basename+"."+section)
 	f, err := os.Create(filename)
 	if err != nil {
@@ -116,7 +116,7 @@ func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error {
 
 func fillHeader(header *GenManHeader, name string, disableAutoGen bool) error {
 	if header.Title == "" {
-		header.Title = strings.ToUpper(strings.Replace(name, " ", "\\-", -1))
+		header.Title = strings.ToUpper(strings.ReplaceAll(name, " ", "\\-"))
 	}
 	if header.Section == "" {
 		header.Section = "1"
@@ -203,7 +203,7 @@ func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
 	cmd.InitDefaultHelpFlag()
 
 	// something like `rootcmd-subcmd1-subcmd2`
-	dashCommandName := strings.Replace(cmd.CommandPath(), " ", "-", -1)
+	dashCommandName := strings.ReplaceAll(cmd.CommandPath(), " ", "-")
 
 	buf := new(bytes.Buffer)
 
@@ -218,7 +218,7 @@ func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
 		seealsos := make([]string, 0)
 		if cmd.HasParent() {
 			parentPath := cmd.Parent().CommandPath()
-			dashParentPath := strings.Replace(parentPath, " ", "-", -1)
+			dashParentPath := strings.ReplaceAll(parentPath, " ", "-")
 			seealso := fmt.Sprintf("**%s(%s)**", dashParentPath, header.Section)
 			seealsos = append(seealsos, seealso)
 			cmd.VisitParents(func(c *cobra.Command) {
diff --git a/doc/md_docs.go b/doc/md_docs.go
index 5181af8..19d7e93 100644
--- a/doc/md_docs.go
+++ b/doc/md_docs.go
@@ -83,7 +83,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
 			parent := cmd.Parent()
 			pname := parent.CommandPath()
 			link := pname + ".md"
-			link = strings.Replace(link, " ", "_", -1)
+			link = strings.ReplaceAll(link, " ", "_")
 			buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short))
 			cmd.VisitParents(func(c *cobra.Command) {
 				if c.DisableAutoGenTag {
@@ -101,7 +101,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
 			}
 			cname := name + " " + child.Name()
 			link := cname + ".md"
-			link = strings.Replace(link, " ", "_", -1)
+			link = strings.ReplaceAll(link, " ", "_")
 			buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short))
 		}
 		buf.WriteString("\n")
@@ -137,7 +137,7 @@ func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHa
 		}
 	}
 
-	basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".md"
+	basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".md"
 	filename := filepath.Join(dir, basename)
 	f, err := os.Create(filename)
 	if err != nil {
diff --git a/doc/rest_docs.go b/doc/rest_docs.go
index 051d8dc..23dca16 100644
--- a/doc/rest_docs.go
+++ b/doc/rest_docs.go
@@ -70,7 +70,7 @@ func GenReSTCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string, str
 	if len(long) == 0 {
 		long = short
 	}
-	ref := strings.Replace(name, " ", "_", -1)
+	ref := strings.ReplaceAll(name, " ", "_")
 
 	buf.WriteString(".. _" + ref + ":\n\n")
 	buf.WriteString(name + "\n")
@@ -99,7 +99,7 @@ func GenReSTCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string, str
 		if cmd.HasParent() {
 			parent := cmd.Parent()
 			pname := parent.CommandPath()
-			ref = strings.Replace(pname, " ", "_", -1)
+			ref = strings.ReplaceAll(pname, " ", "_")
 			buf.WriteString(fmt.Sprintf("* %s \t - %s\n", linkHandler(pname, ref), parent.Short))
 			cmd.VisitParents(func(c *cobra.Command) {
 				if c.DisableAutoGenTag {
@@ -116,7 +116,7 @@ func GenReSTCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string, str
 				continue
 			}
 			cname := name + " " + child.Name()
-			ref = strings.Replace(cname, " ", "_", -1)
+			ref = strings.ReplaceAll(cname, " ", "_")
 			buf.WriteString(fmt.Sprintf("* %s \t - %s\n", linkHandler(cname, ref), child.Short))
 		}
 		buf.WriteString("\n")
@@ -151,7 +151,7 @@ func GenReSTTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string
 		}
 	}
 
-	basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".rst"
+	basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".rst"
 	filename := filepath.Join(dir, basename)
 	f, err := os.Create(filename)
 	if err != nil {
diff --git a/doc/yaml_docs.go b/doc/yaml_docs.go
index 96e6ad7..a79fa40 100644
--- a/doc/yaml_docs.go
+++ b/doc/yaml_docs.go
@@ -66,7 +66,7 @@ func GenYamlTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandle
 		}
 	}
 
-	basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".yaml"
+	basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".yaml"
 	filename := filepath.Join(dir, basename)
 	f, err := os.Create(filename)
 	if err != nil {
diff --git a/fish_completions.go b/fish_completions.go
index bb57fd5..e14a06e 100644
--- a/fish_completions.go
+++ b/fish_completions.go
@@ -11,8 +11,8 @@ import (
 func genFishComp(buf io.StringWriter, name string, includeDesc bool) {
 	// Variables should not contain a '-' or ':' character
 	nameForVar := name
-	nameForVar = strings.Replace(nameForVar, "-", "_", -1)
-	nameForVar = strings.Replace(nameForVar, ":", "_", -1)
+	nameForVar = strings.ReplaceAll(nameForVar, "-", "_")
+	nameForVar = strings.ReplaceAll(nameForVar, ":", "_")
 
 	compCmd := ShellCompRequestCmd
 	if !includeDesc {
diff --git a/flag_groups.go b/flag_groups.go
new file mode 100644
index 0000000..1dba424
--- /dev/null
+++ b/flag_groups.go
@@ -0,0 +1,174 @@
+// Copyright © 2022 Steve Francia <spf@spf13.com>.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cobra
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+
+	flag "github.com/spf13/pflag"
+)
+
+const (
+	requiredAsGroup   = "cobra_annotation_required_if_others_set"
+	mutuallyExclusive = "cobra_annotation_mutually_exclusive"
+)
+
+// MarkFlagsRequiredTogether marks the given flags with annotations so that Cobra errors
+// if the command is invoked with a subset (but not all) of the given flags.
+func (c *Command) MarkFlagsRequiredTogether(flagNames ...string) {
+	c.mergePersistentFlags()
+	for _, v := range flagNames {
+		f := c.Flags().Lookup(v)
+		if f == nil {
+			panic(fmt.Sprintf("Failed to find flag %q and mark it as being required in a flag group", v))
+		}
+		if err := c.Flags().SetAnnotation(v, requiredAsGroup, append(f.Annotations[requiredAsGroup], strings.Join(flagNames, " "))); err != nil {
+			// Only errs if the flag isn't found.
+			panic(err)
+		}
+	}
+}
+
+// MarkFlagsMutuallyExclusive marks the given flags with annotations so that Cobra errors
+// if the command is invoked with more than one flag from the given set of flags.
+func (c *Command) MarkFlagsMutuallyExclusive(flagNames ...string) {
+	c.mergePersistentFlags()
+	for _, v := range flagNames {
+		f := c.Flags().Lookup(v)
+		if f == nil {
+			panic(fmt.Sprintf("Failed to find flag %q and mark it as being in a mutually exclusive flag group", v))
+		}
+		// Each time this is called is a single new entry; this allows it to be a member of multiple groups if needed.
+		if err := c.Flags().SetAnnotation(v, mutuallyExclusive, append(f.Annotations[mutuallyExclusive], strings.Join(flagNames, " "))); err != nil {
+			panic(err)
+		}
+	}
+}
+
+// validateFlagGroups validates the mutuallyExclusive/requiredAsGroup logic and returns the
+// first error encountered.
+func (c *Command) validateFlagGroups() error {
+	if c.DisableFlagParsing {
+		return nil
+	}
+
+	flags := c.Flags()
+
+	// groupStatus format is the list of flags as a unique ID,
+	// then a map of each flag name and whether it is set or not.
+	groupStatus := map[string]map[string]bool{}
+	mutuallyExclusiveGroupStatus := map[string]map[string]bool{}
+	flags.VisitAll(func(pflag *flag.Flag) {
+		processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus)
+		processFlagForGroupAnnotation(flags, pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus)
+	})
+
+	if err := validateRequiredFlagGroups(groupStatus); err != nil {
+		return err
+	}
+	if err := validateExclusiveFlagGroups(mutuallyExclusiveGroupStatus); err != nil {
+		return err
+	}
+	return nil
+}
+
+func hasAllFlags(fs *flag.FlagSet, flagnames ...string) bool {
+	for _, fname := range flagnames {
+		f := fs.Lookup(fname)
+		if f == nil {
+			return false
+		}
+	}
+	return true
+}
+
+func processFlagForGroupAnnotation(flags *flag.FlagSet, pflag *flag.Flag, annotation string, groupStatus map[string]map[string]bool) {
+	groupInfo, found := pflag.Annotations[annotation]
+	if found {
+		for _, group := range groupInfo {
+			if groupStatus[group] == nil {
+				flagnames := strings.Split(group, " ")
+
+				// Only consider this flag group at all if all the flags are defined.
+				if !hasAllFlags(flags, flagnames...) {
+					continue
+				}
+
+				groupStatus[group] = map[string]bool{}
+				for _, name := range flagnames {
+					groupStatus[group][name] = false
+				}
+			}
+
+			groupStatus[group][pflag.Name] = pflag.Changed
+		}
+	}
+}
+
+func validateRequiredFlagGroups(data map[string]map[string]bool) error {
+	keys := sortedKeys(data)
+	for _, flagList := range keys {
+		flagnameAndStatus := data[flagList]
+
+		unset := []string{}
+		for flagname, isSet := range flagnameAndStatus {
+			if !isSet {
+				unset = append(unset, flagname)
+			}
+		}
+		if len(unset) == len(flagnameAndStatus) || len(unset) == 0 {
+			continue
+		}
+
+		// Sort values, so they can be tested/scripted against consistently.
+		sort.Strings(unset)
+		return fmt.Errorf("if any flags in the group [%v] are set they must all be set; missing %v", flagList, unset)
+	}
+
+	return nil
+}
+
+func validateExclusiveFlagGroups(data map[string]map[string]bool) error {
+	keys := sortedKeys(data)
+	for _, flagList := range keys {
+		flagnameAndStatus := data[flagList]
+		var set []string
+		for flagname, isSet := range flagnameAndStatus {
+			if isSet {
+				set = append(set, flagname)
+			}
+		}
+		if len(set) == 0 || len(set) == 1 {
+			continue
+		}
+
+		// Sort values, so they can be tested/scripted against consistently.
+		sort.Strings(set)
+		return fmt.Errorf("if any flags in the group [%v] are set none of the others can be; %v were all set", flagList, set)
+	}
+	return nil
+}
+
+func sortedKeys(m map[string]map[string]bool) []string {
+	keys := make([]string, len(m))
+	i := 0
+	for k := range m {
+		keys[i] = k
+		i++
+	}
+	sort.Strings(keys)
+	return keys
+}
diff --git a/go.sum b/go.sum
index 431058e..6d53459 100644
--- a/go.sum
+++ b/go.sum
@@ -2,17 +2,11 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKY
 github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
-github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/projects_using_cobra.md b/projects_using_cobra.md
index ccd02e1..1d2266d 100644
--- a/projects_using_cobra.md
+++ b/projects_using_cobra.md
@@ -37,6 +37,7 @@
 - [Ory Hydra](https://github.com/ory/hydra)
 - [Ory Kratos](https://github.com/ory/kratos)
 - [Pixie](https://github.com/pixie-io/pixie)
+- [Polygon Edge](https://github.com/0xPolygon/polygon-edge)
 - [Pouch](https://github.com/alibaba/pouch)
 - [ProjectAtomic (enterprise)](http://www.projectatomic.io/)
 - [Prototool](https://github.com/uber/prototool)
diff --git a/shell_completions.md b/shell_completions.md
index 33a4c65..be17bf8 100644
--- a/shell_completions.md
+++ b/shell_completions.md
@@ -122,7 +122,7 @@ For example, if you want `kubectl get [tab][tab]` to show a list of valid "nouns
 Some simplified code from `kubectl get` looks like:
 
 ```go
-validArgs []string = { "pod", "node", "service", "replicationcontroller" }
+validArgs = []string{ "pod", "node", "service", "replicationcontroller" }
 
 cmd := &cobra.Command{
 	Use:     "get [(-o|--output=)json|yaml|template|...] (RESOURCE [NAME] | RESOURCE/NAME ...)",
@@ -148,7 +148,7 @@ node   pod   replicationcontroller   service
 If your nouns have aliases, you can define them alongside `ValidArgs` using `ArgAliases`:
 
 ```go
-argAliases []string = { "pods", "nodes", "services", "svc", "replicationcontrollers", "rc" }
+argAliases = []string { "pods", "nodes", "services", "svc", "replicationcontrollers", "rc" }
 
 cmd := &cobra.Command{
     ...
diff --git a/user_guide.md b/user_guide.md
index cbf9a89..56a1e9c 100644
--- a/user_guide.md
+++ b/user_guide.md
@@ -300,6 +300,30 @@ rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (re
 rootCmd.MarkPersistentFlagRequired("region")
 ```
 
+### Flag Groups
+
+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:
+```go
+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.MarkFlagsRequiredTogether("username", "password")
+``` 
+
+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:
+```go
+rootCmd.Flags().BoolVar(&u, "json", false, "Output in JSON")
+rootCmd.Flags().BoolVar(&pw, "yaml", false, "Output in YAML")
+rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")
+```
+
+In both of these cases:
+  - both local and persistent flags can be used
+    - **NOTE:** the group is only enforced on commands where every flag is defined
+  - a flag may appear in multiple groups
+  - a group may contain any number of flags
+
 ## Positional and Custom Arguments
 
 Validation of positional arguments can be specified using the `Args` field
diff --git a/zsh_completions.go b/zsh_completions.go
index 624adab..c6048a9 100644
--- a/zsh_completions.go
+++ b/zsh_completions.go
@@ -171,7 +171,7 @@ _%[1]s()
             # We first need to escape any : as part of the completion itself.
             comp=${comp//:/\\:}
 
-            local tab=$(printf '\t')
+            local tab="$(printf '\t')"
             comp=${comp//$tab/:}
 
             __%[1]s_debug "Adding completion: ${comp}"


Test patch:
diff --git a/doc/man_docs_test.go b/doc/man_docs_test.go
index f3fe3d6..f7994f8 100644
--- a/doc/man_docs_test.go
+++ b/doc/man_docs_test.go
@@ -20,7 +20,7 @@ func assertNoErr(t *testing.T, e error) {
 }
 
 func translate(in string) string {
-	return strings.Replace(in, "-", "\\-", -1)
+	return strings.ReplaceAll(in, "-", "\\-")
 }
 
 func TestGenManDoc(t *testing.T) {
@@ -38,7 +38,7 @@ func TestGenManDoc(t *testing.T) {
 
 	// Make sure parent has - in CommandPath() in SEE ALSO:
 	parentPath := echoCmd.Parent().CommandPath()
-	dashParentPath := strings.Replace(parentPath, " ", "-", -1)
+	dashParentPath := strings.ReplaceAll(parentPath, " ", "-")
 	expected := translate(dashParentPath)
 	expected = expected + "(" + header.Section + ")"
 	checkStringContains(t, output, expected)
@@ -73,7 +73,7 @@ func TestGenManNoHiddenParents(t *testing.T) {
 
 	// Make sure parent has - in CommandPath() in SEE ALSO:
 	parentPath := echoCmd.Parent().CommandPath()
-	dashParentPath := strings.Replace(parentPath, " ", "-", -1)
+	dashParentPath := strings.ReplaceAll(parentPath, " ", "-")
 	expected := translate(dashParentPath)
 	expected = expected + "(" + header.Section + ")"
 	checkStringContains(t, output, expected)
diff --git a/flag_groups_test.go b/flag_groups_test.go
new file mode 100644
index 0000000..404ede5
--- /dev/null
+++ b/flag_groups_test.go
@@ -0,0 +1,151 @@
+// Copyright © 2022 Steve Francia <spf@spf13.com>.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cobra
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestValidateFlagGroups(t *testing.T) {
+	getCmd := func() *Command {
+		c := &Command{
+			Use: "testcmd",
+			Run: func(cmd *Command, args []string) {
+			}}
+		// Define lots of flags to utilize for testing.
+		for _, v := range []string{"a", "b", "c", "d"} {
+			c.Flags().String(v, "", "")
+		}
+		for _, v := range []string{"e", "f", "g"} {
+			c.PersistentFlags().String(v, "", "")
+		}
+		subC := &Command{
+			Use: "subcmd",
+			Run: func(cmd *Command, args []string) {
+			}}
+		subC.Flags().String("subonly", "", "")
+		c.AddCommand(subC)
+		return c
+	}
+
+	// Each test case uses a unique command from the function above.
+	testcases := []struct {
+		desc                      string
+		flagGroupsRequired        []string
+		flagGroupsExclusive       []string
+		subCmdFlagGroupsRequired  []string
+		subCmdFlagGroupsExclusive []string
+		args                      []string
+		expectErr                 string
+	}{
+		{
+			desc: "No flags no problem",
+		}, {
+			desc:                "No flags no problem even with conflicting groups",
+			flagGroupsRequired:  []string{"a b"},
+			flagGroupsExclusive: []string{"a b"},
+		}, {
+			desc:               "Required flag group not satisfied",
+			flagGroupsRequired: []string{"a b c"},
+			args:               []string{"--a=foo"},
+			expectErr:          "if any flags in the group [a b c] are set they must all be set; missing [b c]",
+		}, {
+			desc:                "Exclusive flag group not satisfied",
+			flagGroupsExclusive: []string{"a b c"},
+			args:                []string{"--a=foo", "--b=foo"},
+			expectErr:           "if any flags in the group [a b c] are set none of the others can be; [a b] were all set",
+		}, {
+			desc:               "Multiple required flag group not satisfied returns first error",
+			flagGroupsRequired: []string{"a b c", "a d"},
+			args:               []string{"--c=foo", "--d=foo"},
+			expectErr:          `if any flags in the group [a b c] are set they must all be set; missing [a b]`,
+		}, {
+			desc:                "Multiple exclusive flag group not satisfied returns first error",
+			flagGroupsExclusive: []string{"a b c", "a d"},
+			args:                []string{"--a=foo", "--c=foo", "--d=foo"},
+			expectErr:           `if any flags in the group [a b c] are set none of the others can be; [a c] were all set`,
+		}, {
+			desc:               "Validation of required groups occurs on groups in sorted order",
+			flagGroupsRequired: []string{"a d", "a b", "a c"},
+			args:               []string{"--a=foo"},
+			expectErr:          `if any flags in the group [a b] are set they must all be set; missing [b]`,
+		}, {
+			desc:                "Validation of exclusive groups occurs on groups in sorted order",
+			flagGroupsExclusive: []string{"a d", "a b", "a c"},
+			args:                []string{"--a=foo", "--b=foo", "--c=foo"},
+			expectErr:           `if any flags in the group [a b] are set none of the others can be; [a b] were all set`,
+		}, {
+			desc:                "Persistent flags utilize both features and can fail required groups",
+			flagGroupsRequired:  []string{"a e", "e f"},
+			flagGroupsExclusive: []string{"f g"},
+			args:                []string{"--a=foo", "--f=foo", "--g=foo"},
+			expectErr:           `if any flags in the group [a e] are set they must all be set; missing [e]`,
+		}, {
+			desc:                "Persistent flags utilize both features and can fail mutually exclusive groups",
+			flagGroupsRequired:  []string{"a e", "e f"},
+			flagGroupsExclusive: []string{"f g"},
+			args:                []string{"--a=foo", "--e=foo", "--f=foo", "--g=foo"},
+			expectErr:           `if any flags in the group [f g] are set none of the others can be; [f g] were all set`,
+		}, {
+			desc:                "Persistent flags utilize both features and can pass",
+			flagGroupsRequired:  []string{"a e", "e f"},
+			flagGroupsExclusive: []string{"f g"},
+			args:                []string{"--a=foo", "--e=foo", "--f=foo"},
+		}, {
+			desc:                     "Subcmds can use required groups using inherited flags",
+			subCmdFlagGroupsRequired: []string{"e subonly"},
+			args:                     []string{"subcmd", "--e=foo", "--subonly=foo"},
+		}, {
+			desc:                      "Subcmds can use exclusive groups using inherited flags",
+			subCmdFlagGroupsExclusive: []string{"e subonly"},
+			args:                      []string{"subcmd", "--e=foo", "--subonly=foo"},
+			expectErr:                 "if any flags in the group [e subonly] are set none of the others can be; [e subonly] were all set",
+		}, {
+			desc:                      "Subcmds can use exclusive groups using inherited flags and pass",
+			subCmdFlagGroupsExclusive: []string{"e subonly"},
+			args:                      []string{"subcmd", "--e=foo"},
+		}, {
+			desc:                     "Flag groups not applied if not found on invoked command",
+			subCmdFlagGroupsRequired: []string{"e subonly"},
+			args:                     []string{"--e=foo"},
+		},
+	}
+	for _, tc := range testcases {
+		t.Run(tc.desc, func(t *testing.T) {
+			c := getCmd()
+			sub := c.Commands()[0]
+			for _, flagGroup := range tc.flagGroupsRequired {
+				c.MarkFlagsRequiredTogether(strings.Split(flagGroup, " ")...)
+			}
+			for _, flagGroup := range tc.flagGroupsExclusive {
+				c.MarkFlagsMutuallyExclusive(strings.Split(flagGroup, " ")...)
+			}
+			for _, flagGroup := range tc.subCmdFlagGroupsRequired {
+				sub.MarkFlagsRequiredTogether(strings.Split(flagGroup, " ")...)
+			}
+			for _, flagGroup := range tc.subCmdFlagGroupsExclusive {
+				sub.MarkFlagsMutuallyExclusive(strings.Split(flagGroup, " ")...)
+			}
+			c.SetArgs(tc.args)
+			err := c.Execute()
+			switch {
+			case err == nil && len(tc.expectErr) > 0:
+				t.Errorf("Expected error %q but got nil", tc.expectErr)
+			case err != nil && err.Error() != tc.expectErr:
+				t.Errorf("Expected error %q but got %q", tc.expectErr, err)
+			}
+		})
+	}
+}