Skip to content

Instantly share code, notes, and snippets.

@colecrouter
Created June 19, 2021 06:10
Show Gist options
  • Save colecrouter/32a25ce147d5eacb974231a462c43b90 to your computer and use it in GitHub Desktop.
Save colecrouter/32a25ce147d5eacb974231a462c43b90 to your computer and use it in GitHub Desktop.
Bash autocomplete with static and dynamic suggestions in Go
/*
There's very little info on how to programmatically do bash autocomplete online, much less how to do it outside of
the bash language. I've used libraries, such as posener/complete, but they don't let you build autocomplete
behaviour how you want to. In my case, I'm using gRPC to get data from servers across the globe, so it doesn't make
sense to fetch *all* the info you *could* need, every time for both dynamic & static suggestions. Here I've made a
program that has static subcommands (predefined), but uses functions to get dynamic suggestion after that, such as
"'stop serverX' as long as serverX is online", where "stop" is static, but "serverX" is dynamic. To use this in
practice, you'll have to compile like this:
go build autocomplete.go
then add this command to your .bashrc or .zshrc:
complete -o nospace -C /path/to/compiled/autocomplete commmand_you_want_to_autocomplete
If you're using zsh, you'll have to add this before that though:
autoload bashcompinit && bashcompinit
to allow autocomplete to work. Happy autocompleting.
With this code, typing `yourcommand sta<tab>` will turn into `yourcommand start`, and adding to the `getOffline()`
function will let you extend it `yourcommand start whatever_you_want
*/
package main
import (
"context"
"fmt"
"log"
"os"
"strconv"
"strings"
)
func main() {
// Get env variables
comp := Completion{}
comp.Init()
// Interpret bash env variables. WordIndex means which word the user is typing.
switch comp.WordIndex {
case 0: // Typing the base command, no autocomplete needed.
break
case 1: // 1 meaning get the static subcommand from map.
res := []string{}
// Suggest all subcommands. Can't spread-append because map.
for k := range commands {
res = append(res, k)
}
comp.Complete(&res)
return
default: // 2 or more, meaning get dynamic subcommand from a function.
comp.Complete(commands[comp.Words[1]](&comp.Words))
}
}
// List of each subcommand, and the function to call to get the right options. Note how the map's key type is a function. Could substitute for `interface{}`.
var commands = map[string]func(args *[]string) (list *[]string){
"start": getOffline,
"stop": getOnline,
"restart": getOnline,
}
func getOffline(args *[]string) *[]string {
list := make([]string, 0)
// Your own logic, `list = append(list, yourstring)`
return &list
}
func getOnline(args *[]string) *[]string {
list := make([]string, 0)
// Your own logic, `list = append(list, yourstring)`
return &list
}
// Completion represents the arguments the shell passes to us.
type Completion struct {
Words []string // All arguments
WordIndex int // Which word the cursor is on
Line string // Current command line
CursorIndex int // Position of the cursor
DefaultReplies []string // Replies returned by the OS
}
// Init gets the environment variables and loads them into itself.
func (c *Completion) Init() {
c.Words = strings.Split(os.Getenv("COMP_WORDS"), "\n")
c.WordIndex, _ = strconv.Atoi(os.Getenv("COMP_CWORD"))
c.Line = os.Getenv("COMP_LINE")
c.CursorIndex, _ = strconv.Atoi(os.Getenv("COMP_POINT"))
c.DefaultReplies = strings.Split(os.Getenv("COMPREPLY"), "\n")
// "Words" wasn't working for me (I'm using zsh?), this replicates "Words" behaviour
if len(c.Words[0]) == 0 {
c.Words = strings.Split(c.Line, " ")
}
}
// Complete returns the completion options to the shell.
func (c *Completion) Complete(args *[]string) {
fmt.Println(strings.Join(*args, "\n")) // Outputting to stdout with \n is what tells the shell what the autocomplete options are.
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment