diff --git a/pkg/auxents/auxents.go b/pkg/auxents/auxents.go index ec4e1299ae..7624572c9a 100644 --- a/pkg/auxents/auxents.go +++ b/pkg/auxents/auxents.go @@ -31,6 +31,7 @@ func init() { {"lecat", lecatMain}, {"termcvt", termcvtMain}, {"unhex", unhexMain}, + {"completion", genCompletion}, } } diff --git a/pkg/auxents/completion.go b/pkg/auxents/completion.go new file mode 100644 index 0000000000..d5a8f6573f --- /dev/null +++ b/pkg/auxents/completion.go @@ -0,0 +1,35 @@ +package auxents + +import "fmt" + +func genCompletion(args []string) int { + verb := args[1] + args = args[2:] + + printUsage := func() { + fmt.Printf("Usage: mlr %s SHELL\n", verb) + fmt.Println("Supported shells: bash") + fmt.Println() + fmt.Println("Add below to your bashrc to enable completion") + fmt.Println("source <(mlr completion bash)") + } + + if len(args) != 1 { + printUsage() + return 1 + } + + if args[0] == "-h" || args[0] == "--help" { + printUsage() + return 0 + } + + if args[0] != "bash" { + fmt.Printlf("Unsupported shell: %s\n", args[0]) + printUsage() + return 1 + } + + fmt.Println(`complete -o nospace -o nosort -C "mlr _complete_bash" mlr`) + return 0 +} diff --git a/pkg/completion/completion.go b/pkg/completion/completion.go new file mode 100644 index 0000000000..55a3429e8a --- /dev/null +++ b/pkg/completion/completion.go @@ -0,0 +1,65 @@ +// Package completion handles Shell completion +package completion + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/johnkerl/miller/v6/pkg/transformers" +) + +func DoCompletion() { + if os.Args[1] != "_complete_bash" { + return + } + if len(os.Args) < 5 { + Debug() + return + } + // See: https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html#index-complete + // Bash completion calls with three arguments: $1 is the name of the + // command whose arguments are being completed, $2 is the word being + // completed, and $3 is the word preceding the word being completed. Since + // we already set one argument, the rest of them are shifted by one + // position i.e. `mlr _complete_bash `, + last := os.Args[3] + prev := os.Args[4] + if prev == "then" { + matches := GetMatchingVerbs(last) + // See: https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html#index-COMP_005fTYPE + // When tab is hit two times, bash sets COMP_TYPE to ascii value of `?` i.e. 63 + if len(matches) == 1 && matches[0].Verb == last && os.Getenv("COMP_TYPE") == "63" { + v := matches[0] + v.UsageFunc(os.Stdout) + } else { + sort.Slice(matches, func(i, j int) bool { return matches[i].Verb < matches[j].Verb }) + for _, verb := range matches { + fmt.Println(verb.Verb) + } + } + } +} + +func GetMatchingVerbs(partVerb string) []*transformers.TransformerSetup { + var matches []*transformers.TransformerSetup + for _, verb := range transformers.TRANSFORMER_LOOKUP_TABLE { + localv := verb + if strings.HasPrefix(verb.Verb, partVerb) { + matches = append(matches, &localv) + } + } + return matches +} + +func Debug() { + for i, arg := range os.Args { + fmt.Fprintln(os.Stderr, i, arg) + } + for _, val := range os.Environ() { + if strings.HasPrefix(val, "COMP") { + fmt.Fprintln(os.Stderr, val) + } + } +} diff --git a/pkg/entrypoint/entrypoint.go b/pkg/entrypoint/entrypoint.go index 7426c726d1..fbb0937f7a 100644 --- a/pkg/entrypoint/entrypoint.go +++ b/pkg/entrypoint/entrypoint.go @@ -9,10 +9,12 @@ import ( "fmt" "os" "path" + "strings" "github.com/johnkerl/miller/v6/pkg/auxents" "github.com/johnkerl/miller/v6/pkg/cli" "github.com/johnkerl/miller/v6/pkg/climain" + "github.com/johnkerl/miller/v6/pkg/completion" "github.com/johnkerl/miller/v6/pkg/lib" "github.com/johnkerl/miller/v6/pkg/platform" "github.com/johnkerl/miller/v6/pkg/stream" @@ -24,6 +26,13 @@ type MainReturn struct { } func Main() MainReturn { + + if len(os.Args) > 1 { + if strings.HasPrefix(os.Args[1], "_complete") { + completion.DoCompletion() + return MainReturn{PrintElapsedTime: false} + } + } // Special handling for Windows so we can do things like: // // mlr put '$a = $b . "cd \"efg\" hi"' foo.dat