Skip to content

usage g completion-init zsh breaks default completion with _files #692

Closed

Description

@dhanak

After installing generic completion for usage scripts, zsh completion complains:

$ emacs <TAB>
(eval):1: no matches found: *:globbed-files

Below is Codex's analysis of the issue. It might be overcomplicating things quite a bit, but the crux is valid: if I remove emulate -L zsh from the completion-init script, the problem goes away.

The bug is here:

_usage_default_complete() {
    emulate -L zsh
    ...
    _files
}

emulate -L zsh resets shell options for the function. That is dangerous inside zsh completion functions because the completion system relies on a particular option environment when helpers like _files / _path_files run.

The fallback:

_files

is then running under the wrong local option state, so zsh eventually evaluates an internal file-pattern tag such as:

*:globbed-files

as a real glob. With nomatch active, this becomes:

no matches found: *:globbed-files

Minimal patch

Change:

emulate -L zsh

to:

emulate -L zsh
setopt localoptions nonomatch extendedglob

or more defensively:

emulate -L zsh -o extendedglob
setopt localoptions nonomatch

Better patch

Avoid forcing emulation around the fallback:

_usage_default_complete() {
    emulate -L zsh -o extendedglob
    setopt localoptions nonomatch

    local cmd cmdpath
    ...
    # Not a usage shebang script — fall back to file completion
    _files
}

Even better design

Do not replace the global default completer with a wrapper that calls _files directly:

compdef _usage_default_complete -default-

That is broad and fragile. It intercepts completion for every command without a specific completion.

A safer fallback would be:

_default

instead of:

_files

So the end becomes:

# Not a usage shebang script — fall back to normal default completion
_default

But then you must avoid infinite recursion, because this function itself is installed as -default-. So the robust fix is more involved: it should save the previous default completion function before replacing it, then call that previous handler as fallback.

Practical local workaround

Edit the generated script locally and patch the top:

_usage_default_complete() {
    emulate -L zsh -o extendedglob
    setopt localoptions nonomatch

Then restart zsh or reload the file.

The core issue is not globbed-files; that is a normal zsh completion tag. The problem is the generated completion wrapper changes the option environment and then delegates to _files.

Activity

  1. jdx commented on Jun 23, 2026

    @jdx
    Owner

    Opened #693 to address this.

    The PR keeps emulate -L zsh but restores a completion-friendly option state before falling back to _files:

    setopt localoptions nonomatch extendedglob

    It also extends the zsh init integration test to exercise the non-usage fallback path and assert that _files sees nomatch=off and extendedglob=on.

    Validation run:

    • cargo test -p usage-lib test_complete_zsh_init
    • cargo test -p usage-cli test_zsh_completion_init_integration --test shell_completions_integration

    This comment was generated by Claude Code.

  2. dhanak commented on Jun 24, 2026

    @dhanak
    Author

    Something still isn't quite right. With the fix, I don't get the error message anymore, for sure. But:

    $ source <(usage g completion-init zsh)
    $ emacs --<TAB>[completion inserts "\*" here]
    
    $ source <(usage g completion-init zsh | sed -e 's/emulate -L zsh//')
    $ emacs --<TAB>[bell, nothing is inserted]
    

    I asked the AI again:

    That confirms the diagnosis: emulate -L zsh is clobbering the completion system’s expected option state.

    The * insertion is a second symptom of the same thing. Adding nonomatch only suppresses the error; it lets the bad fallback proceed, so _files treats the internal * pattern as a literal candidate and inserts it.

    Better fix

    Do not call _files after emulate -L zsh.

    Instead, preserve the caller’s completion options around the generated code, or only use emulate in the branch that calls usage.

    Try this structure:

    _usage_default_complete() {
        local cmd cmdpath
        cmd="${words[1]}"
    
        if [[ "$cmd" == */* ]]; then
            cmdpath="$cmd"
        elif (( ${+commands[$cmd]} )); then
            cmdpath="${commands[$cmd]}"
        fi
    
        if [[ -n "$cmdpath" && -f "$cmdpath" ]]; then
            local first
            if IFS= read -r first < "$cmdpath" 2>/dev/null && [[ "$first" == "#!"*"usage"* ]]; then
                if (( ${+commands[usage]} )); then
                    emulate -L zsh
    
                    local -a values=() descs=() inserts=()
                    local needs_menu=0 line
                    while IFS= read -r line; do
                      local -a parts=("${(@ps:\t:)line}")
                      values+=("${parts[1]}")
                      descs+=("${parts[2]}")
                      inserts+=("${parts[3]}")
                      [[ "${parts[3]}" == "'"* ]] && needs_menu=1
                    done < <(command usage complete-word --shell zsh -f "$cmdpath" --cword=$((CURRENT - 1)) -- "${(Q)words[@]}")
    
                    (( needs_menu )) && compstate[insert]=menu
    
                    if (( ${#inserts[@]} )); then
                      local -a _usage_display=()
                      local _usage_i _usage_max=0 _usage_v _usage_pad
                      for _usage_v in "${values[@]}"; do
                        (( ${#_usage_v} > _usage_max )) && _usage_max=${#_usage_v}
                      done
                      for ((_usage_i=1; _usage_i<=${#values[@]}; _usage_i++)); do
                        if [[ -n "${descs[_usage_i]}" ]]; then
                          _usage_pad=$(( _usage_max - ${#values[_usage_i]} ))
                          _usage_display+=("${values[_usage_i]}${(l:_usage_pad:: :)}  -- ${descs[_usage_i]}")
                        else
                          _usage_display+=("${values[_usage_i]}")
                        fi
                      done
                      compadd -l -d _usage_display -U -Q -S '' -a inserts
                    fi
                    return $?
                fi
            fi
        fi
    
        _files
    }

    Smaller patch

    Move:

    emulate -L zsh

    from the top of the function to inside this branch:

    if (( ${+commands[usage]} )); then
        emulate -L zsh
        ...
    fi

    That way _files runs in the original completion environment.

    Why deleting it works

    Completion functions are already called in a special context. _files expects that context. Top-level emulate -L zsh resets options too broadly, so fallback completion breaks.

    setopt nonomatch masks the exception but leaves the completion semantics wrong, hence literal * insertion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions