-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix zsh completion not working on the first time in a shell session #1237
Conversation
Hi @midchildan
Actually, the completion script is the one that generates the completions. It does not replace itself with anything.
Could you clarify the steps to cause this problem? The script generated by Cobra should work right away if you put it in a directory that is on $fpath |
AFAIK the completion script provided by cobra is an autoloaded function, meaning that it's a lazy-loaded function whose contents are read from the corresponding file in Line 90 in 0bc8bfb
This redefined completion function would be called when the user invokes the completion a second time, but it won't work on initial invocation because this redefined function isn't called anywhere in the initial function. |
This problem can be seen with the GitHub CLI, which uses cobra. Although it uses cobra |
Just for reference, Example completion script from zsh-completions which does something similar: Relevant documentation on autoloaded functions: |
@midchildan thank you for the explanation. I see you are correct. I learn new things every day 😄 On a fresh zsh shell where the script is part of my
Which does cause the need to press With your PR, the first tab press will trigger the above |
Ah but this causes a limitation in the fact that the Cobra script can no longer be sourced. Some programs choose to append the (adapted) following line when generating the Cobra script,
to allow users to do something like:
With this PR, it won't work anymore and complain that the
|
I vaguely recall there was a way to detect if the running code is in completion context, but I'm not certain. |
The zsh completion script output by cobra is a stub completion function which replaces itself with the actual completion function. This technique enables cobra to define helper functions without splitting the completion script into multiple files. However, the current implementation forgets to call the actual completion function at the end of the stub function, meaning that completion won't work the first time it's invoked in a shell session. This commit is a fix for this problem.
I couldn't find it, so I might've been confusing it with something else. I took an alternative approach and decided to check the function call stack instead. Here's a quick demonstration to show that it works: % f() { f() { echo "real f is called" }; if [ "$funcstack[1]" = "f" ]; then f; fi }; f
real f is called
% source <(echo 'f() { echo "real f is called" }; if [ "$funcstack[1]" = "f" ]; then f; fi') |
@midchildan I thought you had it! But this case fails:
Is there a simple way to include this case? If not, maybe it's still ok... |
I was able to source this script without problems, though. #compdef _gh gh
function _gh {
local -a commands
_arguments -C \
'--help[Show help for command]' \
'--version[Show gh version]' \
"1: :->cmnds" \
"*::arg:->args"
case $state in
cmnds)
commands=(
"alias:Create command shortcuts"
"api:Make an authenticated GitHub API request"
"auth:Login, logout, and refresh your authentication"
"completion:Generate shell completion scripts"
"config:Manage configuration for gh"
"gist:Create gists"
"help:Help about any command"
"issue:Manage issues"
"pr:Manage pull requests"
"release:Manage GitHub releases"
"repo:Create, clone, fork, and view repositories"
)
_describe "command" commands
;;
esac
case "$words[1]" in
alias)
_gh_alias
;;
api)
_gh_api
;;
auth)
_gh_auth
;;
completion)
_gh_completion
;;
config)
_gh_config
;;
gist)
_gh_gist
;;
help)
_gh_help
;;
issue)
_gh_issue
;;
pr)
_gh_pr
;;
release)
_gh_release
;;
repo)
_gh_repo
;;
esac
}
function _gh_alias {
local -a commands
_arguments -C \
'--help[Show help for command]' \
"1: :->cmnds" \
"*::arg:->args"
case $state in
cmnds)
commands=(
"delete:Delete an alias"
"list:List your aliases"
"set:Create a shortcut for a gh command"
)
_describe "command" commands
;;
esac
case "$words[1]" in
delete)
_gh_alias_delete
;;
list)
_gh_alias_list
;;
set)
_gh_alias_set
;;
esac
}
function _gh_alias_delete {
_arguments \
'--help[Show help for command]'
}
function _gh_alias_list {
_arguments \
'--help[Show help for command]'
}
function _gh_alias_set {
_arguments \
'(-s --shell)'{-s,--shell}'[Declare an alias to be passed through a shell interpreter]' \
'--help[Show help for command]'
}
function _gh_api {
_arguments \
'(*-F *--field)'{\*-F,\*--field}'[Add a parameter of inferred type]:' \
'(*-H *--header)'{\*-H,\*--header}'[Add an additional HTTP request header]:' \
'(-i --include)'{-i,--include}'[Include HTTP response headers in the output]' \
'--input[The file to use as body for the HTTP request]:' \
'(-X --method)'{-X,--method}'[The HTTP method for the request]:' \
'--paginate[Make additional HTTP requests to fetch all pages of results]' \
'(*-f *--raw-field)'{\*-f,\*--raw-field}'[Add a string parameter]:' \
'--silent[Do not print the response body]' \
'--help[Show help for command]'
}
function _gh_auth {
local -a commands
_arguments -C \
'--help[Show help for command]' \
"1: :->cmnds" \
"*::arg:->args"
case $state in
cmnds)
commands=(
"login:Authenticate with a GitHub host"
"logout:Log out of a GitHub host"
"refresh:Refresh stored authentication credentials"
"status:View authentication status"
)
_describe "command" commands
;;
esac
case "$words[1]" in
login)
_gh_auth_login
;;
logout)
_gh_auth_logout
;;
refresh)
_gh_auth_refresh
;;
status)
_gh_auth_status
;;
esac
}
function _gh_auth_login {
_arguments \
'(-h --hostname)'{-h,--hostname}'[The hostname of the GitHub instance to authenticate with]:' \
'(-w --web)'{-w,--web}'[Open a browser to authenticate]' \
'--with-token[Read token from standard input]' \
'--help[Show help for command]'
}
function _gh_auth_logout {
_arguments \
'(-h --hostname)'{-h,--hostname}'[The hostname of the GitHub instance to log out of]:' \
'--help[Show help for command]'
}
function _gh_auth_refresh {
_arguments \
'(-h --hostname)'{-h,--hostname}'[The GitHub host to use for authentication]:' \
'(*-s *--scopes)'{\*-s,\*--scopes}'[Additional authentication scopes for gh to have]:' \
'--help[Show help for command]'
}
function _gh_auth_status {
_arguments \
'(-h --hostname)'{-h,--hostname}'[Check a specific hostname'\''s auth status]:' \
'--help[Show help for command]'
}
function _gh_completion {
_arguments \
'(-s --shell)'{-s,--shell}'[Shell type: {bash|zsh|fish|powershell}]:' \
'--help[Show help for command]'
}
function _gh_config {
local -a commands
_arguments -C \
'--help[Show help for command]' \
"1: :->cmnds" \
"*::arg:->args"
case $state in
cmnds)
commands=(
"get:Print the value of a given configuration key"
"set:Update configuration with a value for the given key"
)
_describe "command" commands
;;
esac
case "$words[1]" in
get)
_gh_config_get
;;
set)
_gh_config_set
;;
esac
}
function _gh_config_get {
_arguments \
'(-h --host)'{-h,--host}'[Get per-host setting]:' \
'--help[Show help for command]'
}
function _gh_config_set {
_arguments \
'(-h --host)'{-h,--host}'[Set per-host setting]:' \
'--help[Show help for command]'
}
function _gh_gist {
local -a commands
_arguments -C \
'--help[Show help for command]' \
"1: :->cmnds" \
"*::arg:->args"
case $state in
cmnds)
commands=(
"create:Create a new gist"
"edit:Edit one of your gists"
"list:List your gists"
"view:View a gist"
)
_describe "command" commands
;;
esac
case "$words[1]" in
create)
_gh_gist_create
;;
edit)
_gh_gist_edit
;;
list)
_gh_gist_list
;;
view)
_gh_gist_view
;;
esac
}
function _gh_gist_create {
_arguments \
'(-d --desc)'{-d,--desc}'[A description for this gist]:' \
'(-f --filename)'{-f,--filename}'[Provide a filename to be used when reading from STDIN]:' \
'(-p --public)'{-p,--public}'[List the gist publicly (default: private)]' \
'--help[Show help for command]'
}
function _gh_gist_edit {
_arguments \
'(-f --filename)'{-f,--filename}'[a specific file to edit]:' \
'--help[Show help for command]'
}
function _gh_gist_list {
_arguments \
'(-L --limit)'{-L,--limit}'[Maximum number of gists to fetch]:' \
'--public[Show only public gists]' \
'--secret[Show only secret gists]' \
'--help[Show help for command]'
}
function _gh_gist_view {
_arguments \
'(-f --filename)'{-f,--filename}'[display a single file of the gist]:' \
'(-r --raw)'{-r,--raw}'[do not try and render markdown]' \
'(-w --web)'{-w,--web}'[open gist in browser]' \
'--help[Show help for command]'
}
function _gh_help {
_arguments \
'--help[Show help for command]'
}
function _gh_issue {
local -a commands
_arguments -C \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:' \
'--help[Show help for command]' \
"1: :->cmnds" \
"*::arg:->args"
case $state in
cmnds)
commands=(
"close:Close issue"
"create:Create a new issue"
"list:List and filter issues in this repository"
"reopen:Reopen issue"
"status:Show status of relevant issues"
"view:View an issue"
)
_describe "command" commands
;;
esac
case "$words[1]" in
close)
_gh_issue_close
;;
create)
_gh_issue_create
;;
list)
_gh_issue_list
;;
reopen)
_gh_issue_reopen
;;
status)
_gh_issue_status
;;
view)
_gh_issue_view
;;
esac
}
function _gh_issue_close {
_arguments \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_issue_create {
_arguments \
'(*-a *--assignee)'{\*-a,\*--assignee}'[Assign people by their `login`]:' \
'(-b --body)'{-b,--body}'[Supply a body. Will prompt for one otherwise.]:' \
'(*-l *--label)'{\*-l,\*--label}'[Add labels by `name`]:' \
'(-m --milestone)'{-m,--milestone}'[Add the issue to a milestone by `name`]:' \
'(*-p *--project)'{\*-p,\*--project}'[Add the issue to projects by `name`]:' \
'(-t --title)'{-t,--title}'[Supply a title. Will prompt for one otherwise.]:' \
'(-w --web)'{-w,--web}'[Open the browser to create an issue]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_issue_list {
_arguments \
'(-a --assignee)'{-a,--assignee}'[Filter by assignee]:' \
'(-A --author)'{-A,--author}'[Filter by author]:' \
'(*-l *--label)'{\*-l,\*--label}'[Filter by labels]:' \
'(-L --limit)'{-L,--limit}'[Maximum number of issues to fetch]:' \
'--mention[Filter by mention]:' \
'(-m --milestone)'{-m,--milestone}'[Filter by milestone `number` or `title`]:' \
'(-s --state)'{-s,--state}'[Filter by state: {open|closed|all}]:' \
'(-w --web)'{-w,--web}'[Open the browser to list the issue(s)]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_issue_reopen {
_arguments \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_issue_status {
_arguments \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_issue_view {
_arguments \
'(-w --web)'{-w,--web}'[Open an issue in the browser]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_pr {
local -a commands
_arguments -C \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:' \
'--help[Show help for command]' \
"1: :->cmnds" \
"*::arg:->args"
case $state in
cmnds)
commands=(
"checkout:Check out a pull request in git"
"checks:Show CI status for a single pull request"
"close:Close a pull request"
"create:Create a pull request"
"diff:View changes in a pull request"
"list:List and filter pull requests in this repository"
"merge:Merge a pull request"
"ready:Mark a pull request as ready for review"
"reopen:Reopen a pull request"
"review:Add a review to a pull request"
"status:Show status of relevant pull requests"
"view:View a pull request"
)
_describe "command" commands
;;
esac
case "$words[1]" in
checkout)
_gh_pr_checkout
;;
checks)
_gh_pr_checks
;;
close)
_gh_pr_close
;;
create)
_gh_pr_create
;;
diff)
_gh_pr_diff
;;
list)
_gh_pr_list
;;
merge)
_gh_pr_merge
;;
ready)
_gh_pr_ready
;;
reopen)
_gh_pr_reopen
;;
review)
_gh_pr_review
;;
status)
_gh_pr_status
;;
view)
_gh_pr_view
;;
esac
}
function _gh_pr_checkout {
_arguments \
'--recurse-submodules[Update all active submodules (recursively)]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_pr_checks {
_arguments \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_pr_close {
_arguments \
'(-d --delete-branch)'{-d,--delete-branch}'[Delete the local and remote branch after close]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_pr_create {
_arguments \
'(*-a *--assignee)'{\*-a,\*--assignee}'[Assign people by their `login`]:' \
'(-B --base)'{-B,--base}'[The `branch` into which you want your code merged]:' \
'(-b --body)'{-b,--body}'[Body for the pull request]:' \
'(-d --draft)'{-d,--draft}'[Mark pull request as a draft]' \
'(-f --fill)'{-f,--fill}'[Do not prompt for title/body and just use commit info]' \
'(-H --head)'{-H,--head}'[The `branch` that contains commits for your pull request (default: current branch)]:' \
'(*-l *--label)'{\*-l,\*--label}'[Add labels by `name`]:' \
'(-m --milestone)'{-m,--milestone}'[Add the pull request to a milestone by `name`]:' \
'(*-p *--project)'{\*-p,\*--project}'[Add the pull request to projects by `name`]:' \
'(*-r *--reviewer)'{\*-r,\*--reviewer}'[Request reviews from people by their `login`]:' \
'(-t --title)'{-t,--title}'[Title for the pull request]:' \
'(-w --web)'{-w,--web}'[Open the web browser to create a pull request]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_pr_diff {
_arguments \
'--color[Use color in diff output: {always|never|auto}]:' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_pr_list {
_arguments \
'(-a --assignee)'{-a,--assignee}'[Filter by assignee]:' \
'(-B --base)'{-B,--base}'[Filter by base branch]:' \
'(*-l *--label)'{\*-l,\*--label}'[Filter by labels]:' \
'(-L --limit)'{-L,--limit}'[Maximum number of items to fetch]:' \
'(-s --state)'{-s,--state}'[Filter by state: {open|closed|merged|all}]:' \
'(-w --web)'{-w,--web}'[Open the browser to list the pull requests]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_pr_merge {
_arguments \
'(-d --delete-branch)'{-d,--delete-branch}'[Delete the local and remote branch after merge]' \
'(-m --merge)'{-m,--merge}'[Merge the commits with the base branch]' \
'(-r --rebase)'{-r,--rebase}'[Rebase the commits onto the base branch]' \
'(-s --squash)'{-s,--squash}'[Squash the commits into one commit and merge it into the base branch]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_pr_ready {
_arguments \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_pr_reopen {
_arguments \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_pr_review {
_arguments \
'(-a --approve)'{-a,--approve}'[Approve pull request]' \
'(-b --body)'{-b,--body}'[Specify the body of a review]:' \
'(-c --comment)'{-c,--comment}'[Comment on a pull request]' \
'(-r --request-changes)'{-r,--request-changes}'[Request changes on a pull request]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_pr_status {
_arguments \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_pr_view {
_arguments \
'(-w --web)'{-w,--web}'[Open a pull request in the browser]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_release {
local -a commands
_arguments -C \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:' \
'--help[Show help for command]' \
"1: :->cmnds" \
"*::arg:->args"
case $state in
cmnds)
commands=(
"create:Create a new release"
"delete:Delete a release"
"download:Download release assets"
"list:List releases in a repository"
"upload:Upload assets to a release"
"view:View information about a release"
)
_describe "command" commands
;;
esac
case "$words[1]" in
create)
_gh_release_create
;;
delete)
_gh_release_delete
;;
download)
_gh_release_download
;;
list)
_gh_release_list
;;
upload)
_gh_release_upload
;;
view)
_gh_release_view
;;
esac
}
function _gh_release_create {
_arguments \
'(-d --draft)'{-d,--draft}'[Save the release as a draft instead of publishing it]' \
'(-n --notes)'{-n,--notes}'[Release notes]:' \
'(-F --notes-file)'{-F,--notes-file}'[Read release notes from `file`]:' \
'(-p --prerelease)'{-p,--prerelease}'[Mark the release as a prerelease]' \
'--target[Target `branch` or commit SHA (default: main branch)]:' \
'(-t --title)'{-t,--title}'[Release title]:' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_release_delete {
_arguments \
'(-y --yes)'{-y,--yes}'[Skip the confirmation prompt]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_release_download {
_arguments \
'(-D --dir)'{-D,--dir}'[The directory to download files into]:' \
'(*-p *--pattern)'{\*-p,\*--pattern}'[Download only assets that match a glob pattern]:' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_release_list {
_arguments \
'(-L --limit)'{-L,--limit}'[Maximum number of items to fetch]:' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_release_upload {
_arguments \
'--clobber[Overwrite existing assets of the same name]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_release_view {
_arguments \
'(-w --web)'{-w,--web}'[Open the release in the browser]' \
'--help[Show help for command]' \
'(-R --repo)'{-R,--repo}'[Select another repository using the `OWNER/REPO` format]:'
}
function _gh_repo {
local -a commands
_arguments -C \
'--help[Show help for command]' \
"1: :->cmnds" \
"*::arg:->args"
case $state in
cmnds)
commands=(
"clone:Clone a repository locally"
"create:Create a new repository"
"fork:Create a fork of a repository"
"view:View a repository"
)
_describe "command" commands
;;
esac
case "$words[1]" in
clone)
_gh_repo_clone
;;
create)
_gh_repo_create
;;
fork)
_gh_repo_fork
;;
view)
_gh_repo_view
;;
esac
}
function _gh_repo_clone {
_arguments \
'--help[Show help for command]'
}
function _gh_repo_create {
_arguments \
'(-y --confirm)'{-y,--confirm}'[Confirm the submission directly]' \
'(-d --description)'{-d,--description}'[Description of repository]:' \
'--enable-issues[Enable issues in the new repository]' \
'--enable-wiki[Enable wiki in the new repository]' \
'(-h --homepage)'{-h,--homepage}'[Repository home page URL]:' \
'--internal[Make the new repository internal]' \
'--private[Make the new repository private]' \
'--public[Make the new repository public]' \
'(-t --team)'{-t,--team}'[The name of the organization team to be granted access]:' \
'(-p --template)'{-p,--template}'[Make the new repository based on a template repository]:' \
'--help[Show help for command]'
}
function _gh_repo_fork {
_arguments \
'--clone[Clone the fork {true|false}]' \
'--remote[Add remote for fork {true|false}]' \
'--help[Show help for command]'
}
function _gh_repo_view {
_arguments \
'(-w --web)'{-w,--web}'[Open a repository in the browser]' \
'--help[Show help for command]'
}
if [ "$funcstack[1]" = "_gh" ]; then
_gh
fi |
I looked into |
If you dump the As for helm, it's waiting for the next Cobra release so it can use the new zsh completion. I'm currently testing with a local branch pointing to your PR and calling the new code. |
It seems that I wasn't able to replicate the failure because I was using an older version of zsh. I got the same error when I tested using a newer version.
I personally think it's fine to not support this use case, because the only time you'd want to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally feel this follows the proper zsh completion approach, while still protecting users that want to use source
as best we can.
Thanks @midchildan.
@jharshman what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @midchildan and @marckhouzam.
This change LGTM.
The zsh completion script output by cobra is a stub completion function which replaces itself with the actual completion function. This technique enables cobra to define helper functions without splitting the completion script into multiple files. However, the current implementation forgets to call the actual completion function at the end of the stub function, meaning that completion won't work the first time it's invoked in a shell session. This PR is a fix for this problem.