Announcing GoReleaser v2.4
New release coming in hot! new: create macOS app bundles. Initially they are only usable with …
Everyone likes command line completions, so much that some even install extra tools just to have them.
But you don’t need to install anything just for completions: Bash, Fish and ZSH all support it out of the box!
In this post I’ll show you how to ship completions for your Go tools using Cobra and GoReleaser.
Cobra is a tool that helps you write command line applications. It is used by a lot of big Go projects, such as Kubernetes, Hugo and many others.
You can follow its README on how to get started, and, get this, it
enables a completion
command by default, and it works for Bash, ZSH, Fish and
PowerShell!
You can also change its default behaviors, just follow this guide.
If everything works, you should now be able to run:
your-cli completion --help
The help for each shell shows you how to enable the completions.
Shipping it this way works already, but users would have to enable completions manually. I think it’s nicer to enable the completions upon installing the package, and since I release my Go projects with GoReleaser, I can leverage it to handle this for me.
First thing we’ll want to do is creating the completion files.
I usually do this in a scripts/completions.sh
file.
It looks like this:
#!/bin/sh
# scripts/completions.sh
set -e
rm -rf completions
mkdir completions
# TODO: replace your-cli with your binary name
for sh in bash zsh fish; do
go run main.go completion "$sh" >"completions/your-cli.$sh"
done
And then I call it in my .goreleaser.yml
global before hooks:
# .goreleaser.yml
# ...
before:
hooks:
- ./scripts/completions.sh
# ...
It’s your choice to commit these completion files or not.
I usually don’t, so I also add the completions
folder to my .gitignore
:
chmod +x ./script/completions.sh
echo completions >>.gitignore
Now, we need to package all this up. Let’s see how it looks like for each packager option.
The first step is adding it to the archives, as we’ll use them in Homebrew, for example.
You can do so by adding the files to the archives
section of your
.goreleaser.yml
:
# .goreleaser.yml
# ...
archives:
- files:
- README.md
- LICENSE.md
- completions/*
# ...
The Homebrew tap uses the archive to install, so we just need to instruct it to copy the files to the right places:
# .goreleaser.yml
# ...
brews:
# TODO: change your-cli with your binary name.
- install: |-
bin.install "your-cli"
bash_completion.install "completions/your-cli.bash" => "your-cli"
zsh_completion.install "completions/your-cli.zsh" => "_your-cli"
fish_completion.install "completions/your-cli.fish"
# ...
The Arch User Repository recipes will also use the archive, so, we just need to put them in the right places:
# .goreleaser.yml
# ...
aurs:
# TODO: change your-cli with your binary name.
- package: |-
# bin
install -Dm755 "./your-cli" "${pkgdir}/usr/bin/your-cli"
# license
install -Dm644 "./LICENSE.md" "${pkgdir}/usr/share/licenses/your-cli/LICENSE"
# completions
mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
install -Dm644 "./completions/your-cli.bash" "${pkgdir}/usr/share/bash-completion/completions/your-cli"
install -Dm644 "./completions/your-cli.zsh" "${pkgdir}/usr/share/zsh/site-functions/_your-cli"
install -Dm644 "./completions/your-cli.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/your-cli.fish"
# ...
Also known as not-FPM — creates .deb
, .rpm
and .apk
files.
It does not use the archives, so we copy the files from the root of our project:
# .goreleaser.yml
# ...
nfpms:
# TODO: change your-cli with your binary name.
- contents:
- src: ./completions/your-cli.bash
dst: /usr/share/bash-completion/completions/your-cli
file_info:
mode: 0644
- src: ./completions/your-cli.fish
dst: /usr/share/fish/vendor_completions.d/your-cli.fish
file_info:
mode: 0644
- src: ./completions/your-cli.zsh
dst: /usr/share/zsh/vendor-completions/_your-cli
file_info:
mode: 0644
# ...
You may now run:
goreleaser releaser --clean --snapshot
And inspecting the archives et al.
They should all have the completions there. When you’re ready, you can tag and release, and users might just install the packages and have completions in their shells automatically!
On Unix-like systems, it is expected that packages provide man pages as well.
We can do this as well, using Cobra, Mango and GoReleaser.
We’re going to use the same approach:
Let’s start with the man
command:
// main.go
import (
mcobra "github.com/muesli/mango-cobra"
"github.com/muesli/roff"
)
// ...
rootCmd.AddCommand(&cobra.Command{
Use: "man",
Short: "Generates manpages",
SilenceUsage: true,
DisableFlagsInUseLine: true,
Hidden: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
manPage, err := mcobra.NewManPage(1, root.cmd.Root())
if err != nil {
return err
}
_, err = fmt.Fprint(os.Stdout, manPage.Build(roff.NewDocument()))
return err
},
})
// ...
Then we can test it:
go mod tidy
go run . man | less
If that works, the next step is to create our script:
#!/bin/sh
# script/manpages.sh
set -e
rm -rf manpages
mkdir manpages
go run . man | gzip -c -9 >manpages/your-cli.1.gz
The main difference here is that we also need to gzip
it.
As you can see, it is straightforward to do.
Same as before, we add the manpages
folder to .gitignore
:
chmod +x ./script/manpages.sh
echo manpages >>.gitignore
The, we add it to our before hooks:
# .goreleaser.yml
# ...
before:
hooks:
# keep the other lines here
- ./scripts/manpages.sh
# keep the other lines here
# ...
And to the archives:
# .goreleaser.yml
# ...
archives:
- files:
# keep the other lines here
- manpages/*
# keep the other lines here
# ...
And to Homebrew:
# .goreleaser.yml
# ...
brews:
- install: |-
# keep the other lines here
# TODO: replace your-cli with your binary name
man1.install "manpages/your-cli.1.gz"
# keep the other lines here
# ...
And AUR:
# .goreleaser.yml
# ...
aurs:
- package: |-
# keep the other lines here
# TODO: replace your-cli with your binary name
install -Dm644 "./manpages/your-cli.1.gz" "${pkgdir}/usr/share/man/man1/your-cli.1.gz"
# keep the other lines here
# ...
And finally, nFPM:
# .goreleaser.yml
# ...
nfpms:
- contents:
# keep the other lines here
# TODO: replace your-cli with your binary name
- src: ./manpages/your-cli.1.gz
dst: /usr/share/man/man1/your-cli.1.gz
file_info:
mode: 0644
# keep the other lines here
# ...
You can now run a test release like before, and everything should work!
You should now be able to ship your command with both shell completions and man pages, all automatically generated for you — without having to ask your users to install any third party software.
Another advantage of this approach is that you, as a developer, and your users, don’t need to worry about versioning of the completions: the binary ships with its own completions, and on upgrade, completions are upgraded as well.
See you in the next one!
You can also look into how GoReleaser release itself here.