complete

package module
v0.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2024 License: MIT Imports: 10 Imported by: 0

README

complete

Go Reference

This package provides a library for writing shell-agnostic tab completion. (bash, zsh, fish)

While completion for any program can be written with the library, it's targeted for self-completing Go programs. This is a program that doesn't need to distribute, or maintain, any complex bash/zsh/fish script alongside their normal CLI.

It was originally forked from posener/complete, which has went in a different direction. See "Motivation for Forking".

Features

  • Describe the skeleton of your CLI and get tab-completion for free
  • Write custom predictors to enrich suggestions using your existing business logic
  • Dynamic, contextual suggestions based on what the user has typed.
    • A flag may need different values depending on a positional argument's value.
  • Helpers to generate skeletons from well-known CLI frameworks.
    • cobra is automatic
    • Seamless helpers for flags and urfavecli is planned.

Self Completion

Shells can query external binaries to get tab suggestions.

They provide a few environment variables to the program, and this package parses them. It notes what has been typed in the prompt, the cursor position where the user has pressed TAB, and returns relevant suggestions.

By writing custom predictors, tools can hook into this and enrich the user's experience. Use existing logic in your code without duplicating it to a shell script.

Installation

While developers don't need to distribute complicated shell programs for completion, a bit of configuration is still needed.

Bash is one command and zsh is two, fish is a bit more. Any program that uses this package can be installed by setting COMP_INSTALL=1 and running the program.

# Detects the user's shell and operating system, configuring them appropriately.
COMP_INSTALL=1 mycli
COMP_INSTALL=1 COMP_YES=1 mycli # To auto confirm

# Uninstall
COMP_UNINSTALL=1 mycli

If you prefer the manual way:

# Bash
# The last argument is what the user is typing as argv[0] - not a path
complete -C /path/to/mycli mycli

# Zsh
autoload -U +X bashcompinit && bashcompinit
complete -C /path/to/mycli mycli

Examples

If you want to jump into an example, here they are:

Predictors

A Predictor is any type that implements Predict(args.Args) []string.

args.Args contains a few fields:

type Args struct {
    // Arguments in typed by the user so far, up until they pressed TAB.
    //   - At some point this will be all arguments, even if TAB was pressed in the
    //     middle of the line.
    All []string
    // Same as above, excluding the one currently being typed.
    Completed []string
    // The word currently being typed, or empty if there's a space before where
    // TAB was pressed.
    Last string
    // Last fully-typed word
    LastCompleted string
    // Domain-specific value that was emitted by `args.Parser(all []string)`
    ParsedRoot any
}

Each Predictor is mapped to a flag or command to generate suggestions depending on where the user presses TAB. If no predictor is set for a command, it's sub-commands are used. Otherwise it defaults to predict.Files.

There are a few canonical predictors to help you get started:

predict.Anything
predict.Cached
predict.Dirs
predict.Files
predict.Func
predict.Nothing
predict.Or
predict.ScopedCache
predict.Set

Testing

To make testing easy, the cmptest package provides two functions:

// Suggestions returns the options returned by the parser with a given prompt
//
// The prompt should look like it would on the command-line. If '<TAB>' is included,
// that is where we will assume the user pressed the tab key. The end of the prompt is
// used otherwise.
//
// Example: "mycli sub --<TAB> --other"
func Suggestions(t testing.TB, cp complete.CommandParser, prompt string) []string

// Assert suggestions from [Suggestions]
func Assert(t *testing.T, cp complete.CommandParser, prompt string, want []string)

A basic example using cobra would look like:

func TestBasic(t *testing.T) {
    cmd := &cobra.Command{
        Use:   "count",
        ValidArgs: []string{"one", "two", "three"},
        CompletionOptions: cobra.CompletionOptions{
            DisableDefaultCmd: true,
        },
    }

    cmptest.Assert(t, cmpcobra.New(cmd), "count <TAB>", []string{"one", "two", "three"})
}

Troubleshooting

Running your program with COMP_DEBUG=1 will output any logs written with cmplog.Log("some msg: %v", val).

The internal functions use this quite a bit, and you can include your own diagnostic messages for live troubleshooting.

export COMP_DEBUG=1

mycli <TAB>
complete 2024/09/04 17:19:30 Completing phrase: mycli
complete 2024/09/04 17:19:30 Completing last field:
complete 2024/09/04 17:19:30 Options: [one two three]
complete 2024/09/04 17:19:30 Matches: [one two three]

Motivation for Forking

First, much thanks and credit to posener/complete. It was the first library that demonstrated self-completion to me many years ago.

I've been using it ever sincce until recently, mostly due to it's v2 version making decisions that make dynamic, contextual completion hard.

  • Assumptions around certain flags always existing (--help, and -h)

    • While I agree every CLI should define these, it's not the completion engine's place to assert.
  • Predictors only having access to their token

    • Tab completion should enrich a user's experience, and sometimes this means making different decisions depending on other parts of the prompt.

As such, this is a fork from v1. I've had to change a few core ways in how the library works, and decided to publish the results going forward. You should be able to use this library by simply changing the import - all of the exported symbols have been aliased.

Changes

  • Removal of cmd install in favor of v2's COMP_INSTALL semantics
  • New2 and New2F functions as primary entrypoints
  • The concept of a Parser that can give wider context to each Predictor
  • The concept of a Commander that can return a Command
    • Enables framework-aware helpers to generate a completion skeleton
  • New packages args and predict for scoping and import cycle issues
  • args.Args.ParsedRoot contains the result of Parser.Parse()
  • predict.Cached to re-use values that may need a network or expensive call to generate
  • New packages cmptest and cmplog for easier testing
  • New package cmpcobra for generating completion from cobra programs

Documentation

Overview

Package complete provides a library for writing shell-agnostic tab completion.

Full documentation is located in the README. Otherwise, follow the below examples and API docs to get started!

Example (Barebones)

Barebones tab completion for a very naive 'cp' program

package main

import (
	"github.com/coxley/complete"
	"github.com/coxley/complete/predict"
)

func main() {
	// Barebones 'cp' completion
	comp := complete.New2(complete.NopParser(complete.Command{
		Sub: nil,
		Flags: complete.Flags{
			"--force":            predict.Nothing,
			"--help":             predict.Nothing,
			"--version":          predict.Nothing,
			"--target-directory": predict.Dirs("*"),
		},
		// This is the default when there are no sub-commands set
		Args: predict.Files("*"),
	}))

	if comp.Complete() {
		return
	}
}
Output:

Example (Barebones_SubCommands)

Barebones tab completion for a very naive 'git' program

package main

import (
	"github.com/coxley/complete"
	"github.com/coxley/complete/args"
	"github.com/coxley/complete/predict"
)

func main() {
	comp := complete.New2(complete.NopParser(complete.Command{
		Sub: complete.Commands{
			"switch": complete.Command{
				Flags: complete.Flags{
					"--quiet": predict.Nothing,
				},
				Args: predict.Func(func(a args.Args) []string {
					return []string{"branch1", "branch2", "master"}
				}),
			},
			"commit": complete.Command{
				Flags: complete.Flags{
					"--message": predict.Nothing,
					"--author": predict.Func(func(a args.Args) []string {
						// Maybe this looks up authors that have previously committed
						// and suggests them
						return nil
					}),
				},
				Args: predict.Func(func(a args.Args) []string {
					// This could return valid pathspecs
					return nil
				}),
			},
		},
		GlobalFlags: complete.Flags{
			"--help":    predict.Nothing,
			"--version": predict.Nothing,
		},
		// Args will default to sub-commands if not set
	}))

	if comp.Complete() {
		return
	}
}
Output:

Example (Cobra)

Cobra commands can be given directly into [cmpcobra.New] to generate a skeleton for you.

Individual flags and commmands can have their prediction logic overriden.

package main

import (
	"github.com/spf13/cobra"

	"github.com/coxley/complete"
	"github.com/coxley/complete/args"
	"github.com/coxley/complete/cmpcobra"
	"github.com/coxley/complete/predict"
)

func main() {
	cmd := &cobra.Command{
		Use:   "toggle-log-levels",
		Short: "Toggle log levels on a remote service for a period of time",
		// These will be included in suggestions
		ValidArgs: []string{"debug", "info", "warn", "error"},
		// We're not using the cobra completions so don't suggest it in help output
		CompletionOptions: cobra.CompletionOptions{
			DisableDefaultCmd: true,
		},
	}
	flags := cmd.Flags()
	flags.StringP("service", "s", "all", "Service to toggle logs on")

	// Override the default prediction value for string flags
	cmpcobra.RegisterFlag(cmd, "service", predict.Func(func(a args.Args) []string {
		return []string{"service1", "service2", "service3"}
	}))

	comp := complete.New2(cmpcobra.New(cmd))
	if comp.Complete() {
		return
	}
}
Output:

Example (CobraDynamic)

CLI that returns information about a service, but the information available changes depending on the type of service.

Dynamic tab completion can take into account other arguments to influence suggestions. In this case, we change the --field recommendations based on the service positional argument.

package main

import (
	"fmt"
	"maps"
	"os"
	"slices"

	"github.com/spf13/cobra"

	"github.com/coxley/complete"
	"github.com/coxley/complete/args"
	"github.com/coxley/complete/cmpcobra"
	"github.com/coxley/complete/cmplog"
	"github.com/coxley/complete/predict"
)

// These services are either "request-oriented" or "message-broker-oriented" and have
// different metadata.
var services = map[string]map[string]string{
	"server1": {
		"grpc_addr": "some.host:50051",
	},
	"server2": {
		"grpc_addr": "other.host:50051",
	},
	"consumer1": {
		"pubsub_topic":        "some_topic",
		"pubsub_subscription": "some_topic/some_subscription",
	},
	"consumer2": {
		"pubsub_topic":        "other_topic",
		"pubsub_subscription": "other_topic/other_subscription",
	},
}

// CLI that returns information about a service, but the information available
// changes depending on the type of service.
//
// Dynamic tab completion can take into account other arguments to influence
// suggestions. In this case, we change the --field recommendations based on the
// service positional argument.
func main() {
	cmd := &cobra.Command{
		Use:       "svc_registry",
		Args:      cobra.ExactArgs(1),
		ValidArgs: slices.Collect(maps.Keys(services)),
		RunE:      run,
	}

	cmd.Flags().StringArrayP("field", "f", nil, "service fields to print")
	cmpcobra.RegisterFlag(cmd, "field", predict.Func(predictFields))

	// If tab-completion takes place, exit
	if complete.New2(cmpcobra.New(cmd)).Complete() {
		return
	}

	// Otherwise proceed as usual
	if err := cmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func run(cmd *cobra.Command, args []string) error {
	// Print each requested field from the service registry
	svc := args[0]
	fields := services[svc]
	wanted, err := cmd.Flags().GetStringArray("field")
	if err != nil {
		return err
	}

	for _, f := range wanted {
		fmt.Printf("%s:\t%q\n", f, fields[f])
	}
	return nil
}

// predictFields returns the available fields for a given service if it's been
// specified on the command-line
func predictFields(args args.Args) []string {
	root, ok := args.ParsedRoot.(*cobra.Command)
	if !ok {
		cmplog.Log("root cobra command not parsed")
	}

	posArgs := root.Flags().Args()
	if len(posArgs) == 0 {
		// No suggestions to give if a service hasn't been specified
		return nil
	}

	validFields := services[posArgs[0]]
	return slices.Collect(maps.Keys(validFields))
}
Output:

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// Deprecated: See [predict.Or]
	PredictOr = predict.Or
	// Deprecated: See [predict.Nothing]
	PredictNothing = predict.Nothing
	// Deprecated: See [predict.Anything]
	PredictAnything = predict.Anything
	// Deprecated: See [predict.Dirs]
	PredictDirs = predict.Dirs
	// Deprecated: See [predict.Files]
	PredictFiles = predict.Files
	// Deprecated: See [predict.Set]
	PredictSet = predict.Set
)
View Source
var Log = cmplog.Log

Functions

This section is empty.

Types

type Args deprecated

type Args = args.Args

Deprecated: see args.Args

type Command

type Command = command.Command

Alias to command.Command for import ergonomics

type CommandParser

type CommandParser interface {
	Commander
	args.Parser
}

CommandParser should generate a fully-structured Command and parse arguments into an object that predictors can use.

func NopParser

func NopParser(command command.Command) CommandParser

NopParser returns a CommandParser that returns nil when parsing.

type Commander

type Commander interface {
	Command() command.Command
}

Commander returns a structured Command

type Commands

type Commands = command.Commands

Alias to command.Commands for import ergonomics

type Complete

type Complete struct {
	Command Command
	Out     io.Writer
	Parser  args.Parser
}

Complete structs define completion for a command with CLI options

func New

func New(name string, command command.Command) *Complete

New creates a new complete command.

'name' is unused, but is kept for backward-compatibility with posener/complete. It used to be used for installation of the completion script, but we prefer using os.Args[0] to allow the user to control what they name their binaries.

func New2

func New2(cp CommandParser) *Complete

New2 returns a completer structured by the CommandParser

By accepting an args.Parser, predictors can gain extra insight to the command at large to influence their suggestions. The result of args.Parser.Parse is stored in args.Args.ParsedRoot before any predictors run.

Suggestions are printed to os.Stdout.

func New2F

func New2F(w io.Writer, cp CommandParser) *Complete

New2F returns a completer that writes suggestions to 'w'

func (*Complete) Complete

func (c *Complete) Complete() bool

Complete determines if the user needs suggestions, and returns true if so. Programs should exit when true.

Environment variables that control our logic:

  • COMP_LINE: prompt of the user
  • COMP_POINT: cursor position wher tab was pressed
  • COMP_INSTALL=1: install completion script into the user's shell
  • COMP_UNINSTALL=1: uninstall completion script from the user's shell
  • COMP_YES=1: don't prompt when installing or uninstall

type Flags

type Flags = command.Flags

Alias to command.Flags for import ergonomics

type Parser deprecated

type Parser = args.Parser

Deprecated: See args.Parser

type PredictFunc deprecated

type PredictFunc = oldFunc

Deprecated: See predict.Func

type Predictor deprecated

type Predictor = predict.Predictor

Deprecated: See predict.Predictor

Directories

Path Synopsis
Package cmptest includes helper functions to validate tab completion
Package cmptest includes helper functions to validate tab completion
examples
install
Package install provide installation functions of command completion.
Package install provide installation functions of command completion.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL