From 3d976562fae4f511db7520b4ba8d43a7d9b6e69f Mon Sep 17 00:00:00 2001 From: Roman Inflianskas Date: Fri, 13 Sep 2024 15:33:49 +0300 Subject: [PATCH] generate: allow passing `-` for stdout output Since generating completions to stdout for multiple shells does not make sense, enforce `-` is supplied only once. --- changelog/unreleased/issue-2511 | 6 ++ cmd/restic/cmd_generate.go | 75 +++++++++++++-------- cmd/restic/cmd_generate_integration_test.go | 40 +++++++++++ 3 files changed, 93 insertions(+), 28 deletions(-) create mode 100644 changelog/unreleased/issue-2511 create mode 100644 cmd/restic/cmd_generate_integration_test.go diff --git a/changelog/unreleased/issue-2511 b/changelog/unreleased/issue-2511 new file mode 100644 index 000000000..97b01ccde --- /dev/null +++ b/changelog/unreleased/issue-2511 @@ -0,0 +1,6 @@ +Enhancement: Allow generating shell completions to stdout + +Restic `generate` now supports passing `-` passed as file name to `--[shell]-completion` option. + +https://github.com/restic/restic/issues/2511 +https://github.com/restic/restic/pull/5053 diff --git a/cmd/restic/cmd_generate.go b/cmd/restic/cmd_generate.go index b5c7cecb5..66b3fa7c5 100644 --- a/cmd/restic/cmd_generate.go +++ b/cmd/restic/cmd_generate.go @@ -1,6 +1,8 @@ package main import ( + "io" + "os" "time" "github.com/restic/restic/internal/errors" @@ -41,10 +43,10 @@ func init() { cmdRoot.AddCommand(cmdGenerate) fs := cmdGenerate.Flags() fs.StringVar(&genOpts.ManDir, "man", "", "write man pages to `directory`") - fs.StringVar(&genOpts.BashCompletionFile, "bash-completion", "", "write bash completion `file`") - fs.StringVar(&genOpts.FishCompletionFile, "fish-completion", "", "write fish completion `file`") - fs.StringVar(&genOpts.ZSHCompletionFile, "zsh-completion", "", "write zsh completion `file`") - fs.StringVar(&genOpts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file`") + fs.StringVar(&genOpts.BashCompletionFile, "bash-completion", "", "write bash completion `file` (`-` for stdout)") + fs.StringVar(&genOpts.FishCompletionFile, "fish-completion", "", "write fish completion `file` (`-` for stdout)") + fs.StringVar(&genOpts.ZSHCompletionFile, "zsh-completion", "", "write zsh completion `file` (`-` for stdout)") + fs.StringVar(&genOpts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file` (`-` for stdout)") } func writeManpages(dir string) error { @@ -65,32 +67,44 @@ func writeManpages(dir string) error { return doc.GenManTree(cmdRoot, header, dir) } -func writeBashCompletion(file string) error { +func writeCompletion(filename string, shell string, generate func(w io.Writer) error) (err error) { if stdoutIsTerminal() { - Verbosef("writing bash completion file to %v\n", file) + Verbosef("writing %s completion file to %v\n", shell, filename) } - return cmdRoot.GenBashCompletionFile(file) + var outWriter io.Writer + if filename != "-" { + var outFile *os.File + outFile, err = os.Create(filename) + if err != nil { + return + } + defer func() { err = outFile.Close() }() + outWriter = outFile + } else { + outWriter = globalOptions.stdout + } + + err = generate(outWriter) + return } -func writeFishCompletion(file string) error { - if stdoutIsTerminal() { - Verbosef("writing fish completion file to %v\n", file) +func checkStdoutForSingleShell(opts generateOptions) error { + completionFileOpts := []string{ + opts.BashCompletionFile, + opts.FishCompletionFile, + opts.ZSHCompletionFile, + opts.PowerShellCompletionFile, } - return cmdRoot.GenFishCompletionFile(file, true) -} - -func writeZSHCompletion(file string) error { - if stdoutIsTerminal() { - Verbosef("writing zsh completion file to %v\n", file) + seenIsStdout := false + for _, completionFileOpt := range completionFileOpts { + if completionFileOpt == "-" { + if seenIsStdout { + return errors.Fatal("the generate command can generate shell completions to stdout for single shell only") + } + seenIsStdout = true + } } - return cmdRoot.GenZshCompletionFile(file) -} - -func writePowerShellCompletion(file string) error { - if stdoutIsTerminal() { - Verbosef("writing powershell completion file to %v\n", file) - } - return cmdRoot.GenPowerShellCompletionFile(file) + return nil } func runGenerate(opts generateOptions, args []string) error { @@ -105,29 +119,34 @@ func runGenerate(opts generateOptions, args []string) error { } } + err := checkStdoutForSingleShell(opts) + if err != nil { + return err + } + if opts.BashCompletionFile != "" { - err := writeBashCompletion(opts.BashCompletionFile) + err := writeCompletion(opts.BashCompletionFile, "bash", cmdRoot.GenBashCompletion) if err != nil { return err } } if opts.FishCompletionFile != "" { - err := writeFishCompletion(opts.FishCompletionFile) + err := writeCompletion(opts.FishCompletionFile, "fish", func(w io.Writer) error { return cmdRoot.GenFishCompletion(w, true) }) if err != nil { return err } } if opts.ZSHCompletionFile != "" { - err := writeZSHCompletion(opts.ZSHCompletionFile) + err := writeCompletion(opts.ZSHCompletionFile, "zsh", cmdRoot.GenZshCompletion) if err != nil { return err } } if opts.PowerShellCompletionFile != "" { - err := writePowerShellCompletion(opts.PowerShellCompletionFile) + err := writeCompletion(opts.PowerShellCompletionFile, "powershell", cmdRoot.GenPowerShellCompletion) if err != nil { return err } diff --git a/cmd/restic/cmd_generate_integration_test.go b/cmd/restic/cmd_generate_integration_test.go new file mode 100644 index 000000000..0480abc04 --- /dev/null +++ b/cmd/restic/cmd_generate_integration_test.go @@ -0,0 +1,40 @@ +package main + +import ( + "bytes" + "strings" + "testing" + + rtest "github.com/restic/restic/internal/test" +) + +func TestGenerateStdout(t *testing.T) { + testCases := []struct { + name string + opts generateOptions + }{ + {"bash", generateOptions{BashCompletionFile: "-"}}, + {"fish", generateOptions{FishCompletionFile: "-"}}, + {"zsh", generateOptions{ZSHCompletionFile: "-"}}, + {"powershell", generateOptions{PowerShellCompletionFile: "-"}}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := bytes.NewBuffer(nil) + globalOptions.stdout = buf + err := runGenerate(tc.opts, []string{}) + rtest.OK(t, err) + completionString := buf.String() + rtest.Assert(t, strings.Contains(completionString, "# "+tc.name+" completion for restic"), "has no expected completion header") + }) + } + + t.Run("Generate shell completions to stdout for two shells", func(t *testing.T) { + buf := bytes.NewBuffer(nil) + globalOptions.stdout = buf + opts := generateOptions{BashCompletionFile: "-", FishCompletionFile: "-"} + err := runGenerate(opts, []string{}) + rtest.Assert(t, err != nil, "generate shell completions to stdout for two shells fails") + }) +}