Add shell completion
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
diple
|
||||||
23
README.md
23
README.md
@@ -59,6 +59,29 @@ diple --repo owner/repository \
|
|||||||
--endpoint https://github.example.com/api/graphql
|
--endpoint https://github.example.com/api/graphql
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Shell completion
|
||||||
|
|
||||||
|
`diple` generates completion scripts without contacting GitHub or loading the
|
||||||
|
configuration. Choose the command for your shell:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Bash: current session
|
||||||
|
source <(diple completion bash)
|
||||||
|
|
||||||
|
# Zsh: current session
|
||||||
|
source <(diple completion zsh)
|
||||||
|
|
||||||
|
# Fish: install for the current user
|
||||||
|
diple completion fish > ~/.config/fish/completions/diple.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
For persistent Bash completion, write the generated output to a directory
|
||||||
|
loaded by your distribution's `bash-completion` package. For persistent Zsh
|
||||||
|
completion, write it to a file named `_diple` in a directory on `$fpath`, then
|
||||||
|
run `compinit`. `diple completion --help` lists the supported shells, while
|
||||||
|
`diple --help` shows grouped command-line options, defaults, configuration
|
||||||
|
precedence, and authentication behavior.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The optional TOML configuration is loaded from
|
The optional TOML configuration is loaded from
|
||||||
|
|||||||
238
cli.go
Normal file
238
cli.go
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var completionShells = []string{"bash", "zsh", "fish"}
|
||||||
|
|
||||||
|
func handleCompletionCommand(args []string, output io.Writer) (bool, error) {
|
||||||
|
if len(args) == 0 || args[0] != "completion" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if len(args) == 2 && (args[1] == "-h" || args[1] == "--help") {
|
||||||
|
_, err := fmt.Fprintln(output, completionHelp)
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
if len(args) != 2 {
|
||||||
|
return true, fmt.Errorf("usage: diple completion <bash|zsh|fish>")
|
||||||
|
}
|
||||||
|
var script string
|
||||||
|
switch args[1] {
|
||||||
|
case "bash":
|
||||||
|
script = bashCompletion
|
||||||
|
case "zsh":
|
||||||
|
script = zshCompletion
|
||||||
|
case "fish":
|
||||||
|
script = fishCompletion
|
||||||
|
default:
|
||||||
|
return true, fmt.Errorf(
|
||||||
|
"unsupported shell %q; choose %s",
|
||||||
|
args[1], strings.Join(completionShells, ", "),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_, err := io.WriteString(output, script)
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const completionHelp = `Generate a shell completion script.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
diple completion <shell>
|
||||||
|
|
||||||
|
Available shells:
|
||||||
|
bash
|
||||||
|
zsh
|
||||||
|
fish
|
||||||
|
|
||||||
|
Run 'diple completion <shell>' and source or install the generated script.
|
||||||
|
See the README for shell-specific installation paths.`
|
||||||
|
|
||||||
|
func writeCLIHelp(output io.Writer, defaults Config, configPath string) {
|
||||||
|
fmt.Fprintf(output, `diple — review and manage GitHub pull requests from the terminal
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
diple [options]
|
||||||
|
diple completion <bash|zsh|fish>
|
||||||
|
diple help
|
||||||
|
|
||||||
|
Pull-request selection:
|
||||||
|
--repo OWNER/REPOSITORY Limit results to one repository (or use GH_REPO).
|
||||||
|
--all[=BOOL] Include every open PR in --repo. Default: %t.
|
||||||
|
--limit NUMBER Maximum PRs to load, from 1 to 1000. Default: %d.
|
||||||
|
|
||||||
|
GitHub and refresh:
|
||||||
|
--poll DURATION Base refresh interval, at least 2s. Default: %s.
|
||||||
|
--endpoint URL GitHub GraphQL endpoint.
|
||||||
|
Default: %s.
|
||||||
|
|
||||||
|
Appearance and navigation:
|
||||||
|
--theme NAME dark, light, catppuccin, catppuccin-latte,
|
||||||
|
gruvbox, gruvbox-light, one-dark-pro, github,
|
||||||
|
github-light, high-contrast, no-color, or custom.
|
||||||
|
Default: %s.
|
||||||
|
--dashboard-mode MODE hotkey or intermediate. Default: %s.
|
||||||
|
--thread-list-width N List width in percent, from 20 to 60. Default: %d.
|
||||||
|
--fold-resolved[=BOOL] Start resolved threads folded. Default: %t.
|
||||||
|
--compact-reviews[=BOOL] Aggregate submitted-review history. Default: %t.
|
||||||
|
--path-scroll[=BOOL] Scroll truncated file paths. Default: %t.
|
||||||
|
--path-scroll-interval D Path scroll step interval. Default: %s.
|
||||||
|
--editor-mode MODE vim or standard. Default: %s.
|
||||||
|
|
||||||
|
Local state:
|
||||||
|
--config FILE TOML configuration file.
|
||||||
|
Default: %s.
|
||||||
|
--cache[=BOOL] Enable instant cached startup and offline fallback.
|
||||||
|
Default: %t.
|
||||||
|
--cache-max-age DURATION Maximum offline cache age; 0 disables expiry.
|
||||||
|
Default: %s.
|
||||||
|
--cache-dir DIRECTORY Override the operating-system cache directory.
|
||||||
|
|
||||||
|
Other:
|
||||||
|
-h, --help Show this help and exit.
|
||||||
|
|
||||||
|
Boolean options accept explicit values, for example --cache=false.
|
||||||
|
Command-line options override TOML settings. GH_REPO is used only when
|
||||||
|
--repo is absent. DIPLE_CONFIG selects a configuration file; GH_THREADS_CONFIG
|
||||||
|
is retained as a migration fallback.
|
||||||
|
|
||||||
|
Authentication uses GH_TOKEN or GITHUB_TOKEN when set, otherwise the active
|
||||||
|
credential from 'gh auth login'. Run 'diple completion --help' for completion
|
||||||
|
installation guidance.
|
||||||
|
`,
|
||||||
|
defaults.ShowAll,
|
||||||
|
defaults.Limit,
|
||||||
|
defaults.RefreshInterval.Duration,
|
||||||
|
defaults.Endpoint,
|
||||||
|
defaults.Theme,
|
||||||
|
defaults.Display.DashboardMode,
|
||||||
|
defaults.Display.ThreadListWidthPercent,
|
||||||
|
defaults.Display.FoldResolved,
|
||||||
|
defaults.Display.CompactReviews,
|
||||||
|
defaults.Paths.Scroll,
|
||||||
|
defaults.Paths.ScrollInterval.Duration,
|
||||||
|
defaults.Editing.Mode,
|
||||||
|
configPath,
|
||||||
|
defaults.Cache.Enabled,
|
||||||
|
defaults.Cache.MaxAge.Duration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bashCompletion = `# bash completion for diple
|
||||||
|
_diple_completion() {
|
||||||
|
local current previous
|
||||||
|
current="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
previous="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
|
|
||||||
|
if [[ ${COMP_CWORD} -eq 1 && ${current} != -* ]]; then
|
||||||
|
COMPREPLY=($(compgen -W "completion help" -- "${current}"))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ ${COMP_WORDS[1]} == completion ]]; then
|
||||||
|
COMPREPLY=($(compgen -W "bash zsh fish" -- "${current}"))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ ${COMP_WORDS[1]} == help ]]; then
|
||||||
|
COMPREPLY=()
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${previous}" in
|
||||||
|
--theme)
|
||||||
|
COMPREPLY=($(compgen -W "dark light catppuccin catppuccin-mocha catppuccin-latte gruvbox gruvbox-dark gruvbox-light one-dark-pro github github-dark github-light high-contrast no-color custom" -- "${current}"))
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
--dashboard-mode)
|
||||||
|
COMPREPLY=($(compgen -W "hotkey intermediate" -- "${current}"))
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
--editor-mode)
|
||||||
|
COMPREPLY=($(compgen -W "vim standard" -- "${current}"))
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
--config|--cache-dir)
|
||||||
|
COMPREPLY=($(compgen -f -- "${current}"))
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local options="--repo --all --limit --poll --endpoint --theme --dashboard-mode --thread-list-width --fold-resolved --compact-reviews --path-scroll --path-scroll-interval --editor-mode --config --cache --cache-max-age --cache-dir --help -h"
|
||||||
|
COMPREPLY=($(compgen -W "${options}" -- "${current}"))
|
||||||
|
}
|
||||||
|
complete -F _diple_completion diple
|
||||||
|
`
|
||||||
|
|
||||||
|
const zshCompletion = `#compdef diple
|
||||||
|
|
||||||
|
_diple() {
|
||||||
|
local -a themes
|
||||||
|
themes=(dark light catppuccin catppuccin-mocha catppuccin-latte gruvbox gruvbox-dark gruvbox-light one-dark-pro github github-dark github-light high-contrast no-color custom)
|
||||||
|
|
||||||
|
if (( CURRENT == 2 )) && [[ ${PREFIX} != -* ]]; then
|
||||||
|
_values 'command' \
|
||||||
|
'completion[generate shell completion]' \
|
||||||
|
'help[show command-line help]'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ ${words[2]} == completion ]]; then
|
||||||
|
_values 'shell' bash zsh fish
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ ${words[2]} == help ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
_arguments -s \
|
||||||
|
'--repo[limit results to one repository]:owner/repository:' \
|
||||||
|
'--all=[include every open PR in the repository]:boolean:(true false)' \
|
||||||
|
'--limit[maximum pull requests to load]:number:' \
|
||||||
|
'--poll[base GitHub refresh interval]:duration:' \
|
||||||
|
'--endpoint[GitHub GraphQL endpoint]:url:' \
|
||||||
|
'--theme[UI and syntax color theme]:theme:($themes)' \
|
||||||
|
'--dashboard-mode[dashboard navigation mode]:mode:(hotkey intermediate)' \
|
||||||
|
'--thread-list-width[thread-list width percentage]:percent:' \
|
||||||
|
'--fold-resolved=[start resolved threads folded]:boolean:(true false)' \
|
||||||
|
'--compact-reviews=[aggregate submitted reviews]:boolean:(true false)' \
|
||||||
|
'--path-scroll=[scroll truncated paths]:boolean:(true false)' \
|
||||||
|
'--path-scroll-interval[path scrolling interval]:duration:' \
|
||||||
|
'--editor-mode[description editor mode]:mode:(vim standard)' \
|
||||||
|
'--config[TOML configuration file]:file:_files' \
|
||||||
|
'--cache=[enable local read cache]:boolean:(true false)' \
|
||||||
|
'--cache-max-age[maximum offline cache age]:duration:' \
|
||||||
|
'--cache-dir[local read-cache directory]:directory:_directories' \
|
||||||
|
'(-h --help)'{-h,--help}'[show help]'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( ! ${+functions[compdef]} )); then
|
||||||
|
autoload -Uz compinit
|
||||||
|
compinit -D
|
||||||
|
fi
|
||||||
|
compdef _diple diple
|
||||||
|
`
|
||||||
|
|
||||||
|
const fishCompletion = `# fish completion for diple
|
||||||
|
complete -c diple -f
|
||||||
|
complete -c diple -n '__fish_use_subcommand' -a completion -d 'Generate shell completion'
|
||||||
|
complete -c diple -n '__fish_use_subcommand' -a help -d 'Show help'
|
||||||
|
complete -c diple -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'
|
||||||
|
complete -c diple -l repo -d 'Limit results to owner/repository'
|
||||||
|
complete -c diple -l all -d 'Include every open PR in --repo'
|
||||||
|
complete -c diple -l limit -x -d 'Maximum pull requests to load'
|
||||||
|
complete -c diple -l poll -x -d 'Base GitHub refresh interval'
|
||||||
|
complete -c diple -l endpoint -x -d 'GitHub GraphQL endpoint'
|
||||||
|
complete -c diple -l theme -x -a 'dark light catppuccin catppuccin-mocha catppuccin-latte gruvbox gruvbox-dark gruvbox-light one-dark-pro github github-dark github-light high-contrast no-color custom' -d 'UI and syntax color theme'
|
||||||
|
complete -c diple -l dashboard-mode -x -a 'hotkey intermediate' -d 'Dashboard navigation mode'
|
||||||
|
complete -c diple -l thread-list-width -x -d 'Thread-list width percentage'
|
||||||
|
complete -c diple -l fold-resolved -d 'Start resolved threads folded'
|
||||||
|
complete -c diple -l compact-reviews -d 'Aggregate submitted reviews'
|
||||||
|
complete -c diple -l path-scroll -d 'Scroll truncated paths'
|
||||||
|
complete -c diple -l path-scroll-interval -x -d 'Path scrolling interval'
|
||||||
|
complete -c diple -l editor-mode -x -a 'vim standard' -d 'Description editor mode'
|
||||||
|
complete -c diple -l config -r -F -d 'TOML configuration file'
|
||||||
|
complete -c diple -l cache -d 'Enable local read cache'
|
||||||
|
complete -c diple -l cache-max-age -x -d 'Maximum offline cache age'
|
||||||
|
complete -c diple -l cache-dir -r -a '(__fish_complete_directories)' -d 'Local read-cache directory'
|
||||||
|
complete -c diple -s h -l help -d 'Show help'
|
||||||
|
`
|
||||||
77
cli_test.go
Normal file
77
cli_test.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCompletionCommandGeneratesSupportedShells(t *testing.T) {
|
||||||
|
for _, shell := range completionShells {
|
||||||
|
t.Run(shell, func(t *testing.T) {
|
||||||
|
var output bytes.Buffer
|
||||||
|
handled, err := handleCompletionCommand([]string{"completion", shell}, &output)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !handled || output.Len() == 0 ||
|
||||||
|
!strings.Contains(output.String(), "diple") {
|
||||||
|
t.Fatalf("completion output = %q, handled=%t", output.String(), handled)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZshCompletionRegistersWithoutCallingCompletionFunction(t *testing.T) {
|
||||||
|
var output bytes.Buffer
|
||||||
|
handled, err := handleCompletionCommand([]string{"completion", "zsh"}, &output)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
script := output.String()
|
||||||
|
if !handled || !strings.Contains(script, "compdef _diple diple") {
|
||||||
|
t.Fatalf("Zsh completion does not register _diple:\n%s", script)
|
||||||
|
}
|
||||||
|
if strings.Contains(script, `_diple "$@"`) {
|
||||||
|
t.Fatalf("Zsh completion invokes _diple while being sourced:\n%s", script)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompletionCommandRejectsUnknownShell(t *testing.T) {
|
||||||
|
var output bytes.Buffer
|
||||||
|
handled, err := handleCompletionCommand(
|
||||||
|
[]string{"completion", "powershell"}, &output,
|
||||||
|
)
|
||||||
|
if !handled || err == nil || !strings.Contains(err.Error(), "unsupported shell") {
|
||||||
|
t.Fatalf("handled=%t error=%v", handled, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompletionCommandDoesNotClaimNormalInvocation(t *testing.T) {
|
||||||
|
handled, err := handleCompletionCommand([]string{"--repo", "owner/repo"}, &bytes.Buffer{})
|
||||||
|
if handled || err != nil {
|
||||||
|
t.Fatalf("handled=%t error=%v", handled, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLIHelpIsGroupedAndActionable(t *testing.T) {
|
||||||
|
var output bytes.Buffer
|
||||||
|
writeCLIHelp(&output, defaultConfig(), "/tmp/diple/config.toml")
|
||||||
|
help := output.String()
|
||||||
|
for _, expected := range []string{
|
||||||
|
"Usage:",
|
||||||
|
"Pull-request selection:",
|
||||||
|
"GitHub and refresh:",
|
||||||
|
"Appearance and navigation:",
|
||||||
|
"Local state:",
|
||||||
|
"diple completion <bash|zsh|fish>",
|
||||||
|
"--repo OWNER/REPOSITORY",
|
||||||
|
"--cache=false",
|
||||||
|
"gh auth login",
|
||||||
|
"/tmp/diple/config.toml",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(help, expected) {
|
||||||
|
t.Fatalf("help does not contain %q:\n%s", expected, help)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
main.go
48
main.go
@@ -11,31 +11,47 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if handled, err := handleCompletionCommand(os.Args[1:], os.Stdout); handled {
|
||||||
|
if err != nil {
|
||||||
|
exitf("%v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
defaults := defaultConfig()
|
defaults := defaultConfig()
|
||||||
defaultConfigPath, err := configPath()
|
defaultConfigPath, err := configPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exitf("configuration: %v", err)
|
exitf("configuration: %v", err)
|
||||||
}
|
}
|
||||||
|
flag.Usage = func() {
|
||||||
|
writeCLIHelp(flag.CommandLine.Output(), defaults, defaultConfigPath)
|
||||||
|
}
|
||||||
|
if len(os.Args) == 2 && os.Args[1] == "help" {
|
||||||
|
flag.Usage()
|
||||||
|
return
|
||||||
|
}
|
||||||
var (
|
var (
|
||||||
configFile = flag.String("config", defaultConfigPath, "TOML configuration file")
|
configFile = flag.String("config", defaultConfigPath, "TOML configuration file")
|
||||||
repo = flag.String("repo", "", "optional GitHub repository filter as owner/name (or GH_REPO)")
|
repo = flag.String("repo", "", "limit pull requests to an owner/repository")
|
||||||
poll = flag.Duration("poll", defaults.RefreshInterval.Duration, "refresh interval")
|
poll = flag.Duration("poll", defaults.RefreshInterval.Duration, "base interval between GitHub refreshes")
|
||||||
showAll = flag.Bool("all", defaults.ShowAll, "show all open PRs in --repo, not only PRs assigned to you")
|
showAll = flag.Bool("all", defaults.ShowAll, "show every open PR in --repo instead of only assigned PRs")
|
||||||
limit = flag.Int("limit", defaults.Limit, "maximum open PRs to load (1-1000)")
|
limit = flag.Int("limit", defaults.Limit, "maximum number of open pull requests to load")
|
||||||
endpoint = flag.String("endpoint", defaults.Endpoint, "GitHub GraphQL endpoint")
|
endpoint = flag.String("endpoint", defaults.Endpoint, "GitHub GraphQL API endpoint")
|
||||||
theme = flag.String("theme", defaults.Theme, "color theme name (including custom, catppuccin, gruvbox, one-dark-pro, and github)")
|
theme = flag.String("theme", defaults.Theme, "built-in or custom color theme")
|
||||||
foldResolved = flag.Bool("fold-resolved", defaults.Display.FoldResolved, "start resolved threads folded")
|
foldResolved = flag.Bool("fold-resolved", defaults.Display.FoldResolved, "start resolved review threads folded")
|
||||||
listWidth = flag.Int("thread-list-width", defaults.Display.ThreadListWidthPercent, "thread list width as terminal percentage (20-60)")
|
listWidth = flag.Int("thread-list-width", defaults.Display.ThreadListWidthPercent, "thread-list width as a percentage of the terminal")
|
||||||
dashboardMode = flag.String("dashboard-mode", defaults.Display.DashboardMode, "dashboard navigation: intermediate or hotkey")
|
dashboardMode = flag.String("dashboard-mode", defaults.Display.DashboardMode, "open the dashboard by hotkey or as an intermediate screen")
|
||||||
compactReviews = flag.Bool("compact-reviews", defaults.Display.CompactReviews, "aggregate submitted review history")
|
compactReviews = flag.Bool("compact-reviews", defaults.Display.CompactReviews, "aggregate repeated submitted-review entries")
|
||||||
pathScroll = flag.Bool("path-scroll", defaults.Paths.Scroll, "scroll truncated paths")
|
pathScroll = flag.Bool("path-scroll", defaults.Paths.Scroll, "scroll file paths that do not fit")
|
||||||
pathScrollRate = flag.Duration("path-scroll-interval", defaults.Paths.ScrollInterval.Duration, "path scrolling interval")
|
pathScrollRate = flag.Duration("path-scroll-interval", defaults.Paths.ScrollInterval.Duration, "interval between file-path scroll steps")
|
||||||
cacheEnabled = flag.Bool("cache", defaults.Cache.Enabled, "enable local read cache fallback")
|
cacheEnabled = flag.Bool("cache", defaults.Cache.Enabled, "enable the local read cache and offline fallback")
|
||||||
cacheMaxAge = flag.Duration("cache-max-age", defaults.Cache.MaxAge.Duration, "maximum offline cache age (0 disables expiry)")
|
cacheMaxAge = flag.Duration("cache-max-age", defaults.Cache.MaxAge.Duration, "oldest cache entry accepted for offline fallback")
|
||||||
cacheDir = flag.String("cache-dir", defaults.Cache.Directory, "local read cache directory")
|
cacheDir = flag.String("cache-dir", defaults.Cache.Directory, "local read-cache directory")
|
||||||
editorMode = flag.String("editor-mode", defaults.Editing.Mode, "description editor mode: standard or vim")
|
editorMode = flag.String("editor-mode", defaults.Editing.Mode, "description editor mode")
|
||||||
)
|
)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
if flag.NArg() != 0 {
|
||||||
|
exitf("unexpected argument %q; run 'diple --help' for usage", flag.Arg(0))
|
||||||
|
}
|
||||||
|
|
||||||
visited := map[string]bool{}
|
visited := map[string]bool{}
|
||||||
flag.Visit(func(item *flag.Flag) { visited[item.Name] = true })
|
flag.Visit(func(item *flag.Flag) { visited[item.Name] = true })
|
||||||
|
|||||||
Reference in New Issue
Block a user