Created
June 19, 2021 06:10
-
-
Save colecrouter/32a25ce147d5eacb974231a462c43b90 to your computer and use it in GitHub Desktop.
Bash autocomplete with static and dynamic suggestions in Go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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