10

There are many resources available (1, 2, 3) that explain how to make use of bash's ability to auto-complete commands and arguments, but all of these resources require the addition of code to a user's ~/.bash_profile or /etc/bash_completion.d/* but is there a way to make a script and its available completions self-contained? As a crude and incomplete example:

~/bin/script-with-integrated-autocomplete.sh:

#!/usr/bin/env bash

function _completions {
  complete ...
}

if [ "$1" == "completions" ]; then
  _completions
  exit 0
fi

# (Whatever the script really does goes here.)
# ...
# ...

The deal-breaker (in the context of this question) is that the above example still requires you to add something like ~/bin/script-with-integrated-autocomplete.sh completions to your .profile to engage the completions.

Is there a way for a single bash script (file) to declare its own completions and have bash recognize them at the time of invocation (ideally with no additional system or environment setup)?

beporter
  • 203
  • 2
  • 7
  • You're asking if there is somewhere you can put a file so that it will be automatically and transparently included in the user's session? No, there isn't. – roaima Jan 29 '20 at 16:37
  • 3
    No, I'm asking if there's an approach (crafty or otherwise) that would cause bash to know about the completions available for a script without having to explicitly configure that script in each environment it runs in. In other words: Can a bash script be "self-contained" with its own completions and without modifying the surrounding system/env. – beporter Jan 29 '20 at 16:42
  • 1
    Bash would need some code already run for auto-completion to work. This is typically done by modifying the environment. A self-containing script would only be executed after entering it on the command-line followed by pressing “Return”. So, essentially you’d need to “dry-run” your script first with the auto-complete section executed in such a manner that it is thereafter known to the environment. Then you actually need to run it again to use the auto-completion. So, in theory it could possibly be done, but the effort would be the same or higher compared to modifying the environment. – Phoenix Jan 29 '20 at 21:05
  • 1
    @Phoenix That's my understanding as well, but I'm inclined to let the question stand because the challenge is still interesting. For example: Is there a (reasonable/secure) way to modify or improve bash itself to include this functionality out of the box in some theoretical future version? That particular suggestion may cross over into a code-golf style challenge, but experience tells me not to underestimate the people that answer Stack Exchange questions either. – beporter Jan 30 '20 at 18:37
  • 1
    I'd suggest you to explicitly add to your question that your aim is to be able to autocomplete _the invocation of_ `script` (without having changed anything in the environment or sourced the script). Maybe it's only me, but it took me more than one read to get it. I think I was initially confused because you mention `.bash_profile` and other separate files, but the location of completion specifications is not a real issue. The hard part is how to change the environment without even sourcing anything. – fra-san Jan 30 '20 at 21:54
  • @fra-san A good point; I've edited that in. _Technically_ the ideal answer would work regardless of the nature of the executable. It would be cool if binary/compiled programs or anything with an execute bit set had a way to advertise its completions to bash. Sticking to text-based bash scripts just seems like a good proof-of-concept limit though. – beporter Jan 31 '20 at 19:36

3 Answers3

4

As stated in comments to your question, Bash cannot complete the command line for a command without any previous configuration and without changing the current environment. Bash does not peek into files, nor it tries to interpret them unless you explicitly execute or source them.

My read on the subject is that such a feature is unlikely to make its way into Bash anytime soon, because of compatibility and resources concerns—as a shell, it is meant to reliably run on a variety of different systems, with a variety of purposes and adapting to the preferences of a variety of users; and it has only one person as developer and maintainer1.

While Bash provides the tools to implement autocompletion, completion generators seem to be by design intended as external facilities. And indeed what you are looking for can be easily implemented with the existing tools and a small amount of work.

Given the sample script foo:

#!/bin/bash

# Setting up completion for foo. Only if the script is sourced
if [ "$0" != "$BASH_SOURCE" ]; then
    _foo() {
        local cur
        COMPREPLY=()
        cur="${COMP_WORDS[COMP_CWORD]}"
        COMPREPLY=( $(compgen -W 'bar baz' -- "$cur") )
        return 0
    }
    complete -F "_foo" "foo"
    return
fi

# The body of foo -- what it does when executed
echo foo "$*"
# ...

using bash-completion, self-contained autocompletion can be enabled by adding a symbolic link to foo to the directory that stores dynamically loaded user completions:

ln -s /path/to/foo ~/.local/share/bash-completion/completions/

The exact path may vary, you can refer to bash-completion's FAQ for how to check its value on your system.

Without bash-completion, one approach could be to define a default completion function that sources (files corresponding to) commands if they meet certain conditions; here we are using an associative array as a whitelist (to avoid blindly sourcing every command on the system). This code has to be added to your .bashrc (or equivalent, i.e. a file that is sourced in the current environment every time an interactive shell is started):

_default_completion () {
    # Do nothing when completion is invoked for an empty command
    if [ "$1" = "" ]; then
        return 1
    fi
    # Get the command path, exit with failure if it can't be found
    cmd=$(type -p "$1") || return 1
    if [ "${_compwhitelist["${cmd##*/}"]}" = y ]; then
        # Source the file corresponding to the command; on success,
        # return 124 to trigger a new autocompletion attempt
        source "$cmd" && return 124
    fi
}
complete -D -F _default_completion -o bashdefault -o default

# The list of commands with embedded completion
declare -A _compwhitelist
_compwhitelist[foo]=y

I know, you asked for a solution that didn't require to edit configuration files, but please note that a certain amount of configuration is always required. A substantial part of it is done by distribution/package maintainers, even if users may never come to realize it.

1 Reference: the Bash home page on Chet Ramey's website and point "A1" in the related Bash FAQ.

fra-san
  • 9,931
  • 2
  • 21
  • 42
  • _"please note that a certain amount of configuration is always required."_ Indeed, although for the purposes of this question, having that configuration done by a distribution or maintainer is totally acceptable. – beporter Feb 03 '20 at 17:43
  • @beporter Well, I was actually inclined to think that you were already aware of (possibly) everything that is in my answer; so, despite its wording, my final remark is more of a general consideration than directly aimed at you. – fra-san Feb 03 '20 at 18:24
2

Yes, it can. All you need to do is to check if the environment variable COMP_LINE is set and then simply echo out all the auto-complete options that should be available. One line per option and then exit the script. The script does not even need to be a shell script. Can be anything, Python, PHP, Golang whatever.

Pseudo-code:

if COMP_LINE {
  echo "Foo"
  echo "Bar"
  Exit 0
}

//Rest of the script here

Then simply add your script to the shells auto-complete rules with:

complete -C /usr/bin/the_script.sh the_script.sh

Now, after typing "the_script.sh <tab> <tab>", it will show the "foo, bar" options.

Daniele Testa
  • 203
  • 1
  • 7
  • 1
    You might have missed the `at the time of invocation` part. My hope was to be able to skip the `complete -C ...` part of your answer and jump **immediately** to the `the_script.sh ` part. There's no way for bash to "automatically" detect the completions IN the script during the very first execution OF the script. Maybe some $PROMPT trickery? – beporter Oct 17 '22 at 19:16
  • No. Only way would be to "activate" the autocomplete first time you run the script by having it run "complete". The shell would simply not know that the script has autocomplete before you tell it (by using complete). – Daniele Testa Oct 18 '22 at 12:06
0

Open your shell and type asdfasdf Tab you will see that it autocompletes with a list of files in the $CWD.

If you can find the "default completion" that is called for that and make a wrapper for it, a single modification would work for all scripts.

The wrapper could do a check for embedded completion, then fallback to the original completion. All scripts would have to follow whatever standard you come up with. I think responding to COMP_LINE or COMP_WORDS env vars would be a pretty good choice.

Seems like this should have already been done by someone. I never assume my ideas are groundbreaking.

Bruno Bronosky
  • 4,434
  • 2
  • 27
  • 24