Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autocomplete improvements #1268

Merged
merged 5 commits into from
Apr 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion base/component/field_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package component

import (
"regexp"
"strings"
)

// DatasetFields is a list of valid dataset field identifiers
var DatasetFields = []string{"commit", "cm", "structure", "st", "body", "bd", "meta", "md", "readme", "rm", "viz", "vz", "transform", "tf", "rendered", "rd", "stats"}

// IsDatasetField can be used to check if a string is a dataset field identifier
var IsDatasetField = regexp.MustCompile("(?i)^(commit|cm|structure|st|body|bd|meta|md|readme|rm|viz|vz|transform|tf|rendered|rd|stats)($|\\.)")
var IsDatasetField = regexp.MustCompile("(?i)^(" + strings.Join(DatasetFields, "|") + ")($|\\.)")
151 changes: 136 additions & 15 deletions cmd/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import (
"io"

"github.com/qri-io/ioes"
"github.com/qri-io/qri/base/component"
"github.com/qri-io/qri/lib"
"github.com/spf13/cobra"
)

// NewAutocompleteCommand creates a new `qri complete` cobra command that prints autocomplete scripts
func NewAutocompleteCommand(_ Factory, ioStreams ioes.IOStreams) *cobra.Command {
func NewAutocompleteCommand(f Factory, ioStreams ioes.IOStreams) *cobra.Command {
o := &AutocompleteOptions{IOStreams: ioStreams}
cfgOpt := ConfigOptions{IOStreams: ioStreams}
cmd := &cobra.Command{
Use: "completion [bash|zsh]",
Short: "generate shell auto-completion scripts",
Expand Down Expand Up @@ -39,9 +42,70 @@ run on each terminal session.`,
ValidArgs: []string{"bash", "zsh"},
}

configCompletion := &cobra.Command{
Use: "config [FIELD]",
Hidden: true,
Short: "get configuration keys",
Long: `'qri completion config' is a util function for auto-completion of config keys, ignores private data`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if err := cfgOpt.CompleteConfig(f); err != nil {
return err
}
return cfgOpt.GetConfig(args)
},
}

structureCompletion := &cobra.Command{
Use: "structure [FIELD]",
Hidden: true,
Short: "get structure keys",
Long: `'qri completion structure' is a util function for auto-completion of structure keys`,
Args: cobra.MaximumNArgs(1),
ValidArgs: component.DatasetFields,
RunE: func(cmd *cobra.Command, args []string) error {
for _, structureArg := range component.DatasetFields {
fmt.Fprintln(ioStreams.Out, structureArg)
}
return nil
},
}

cmd.AddCommand(configCompletion)
cmd.AddCommand(structureCompletion)

return cmd
}

// CompleteConfig adds any missing configuration that can only be added just before calling GetConfig
func (o *ConfigOptions) CompleteConfig(f Factory) (err error) {
o.inst = f.Instance()
o.ConfigMethods, err = f.ConfigMethods()
if err != nil {
return
}

o.ProfileMethods, err = f.ProfileMethods()
return
}

// GetConfig gets configuration keys based on a partial key supplied
func (o *ConfigOptions) GetConfig(args []string) (err error) {
params := &lib.GetConfigParams{}
if len(args) == 1 {
params.Field = args[0]
}

var data []byte

if err = o.ConfigMethods.GetConfigKeys(params, &data); err != nil {
return err
}

fmt.Fprintln(o.Out, string(data))
return
}

// AutocompleteOptions encapsulates completion options
type AutocompleteOptions struct {
ioes.IOStreams
Expand Down Expand Up @@ -70,54 +134,111 @@ func (o *AutocompleteOptions) Run(cmd *cobra.Command, args []string) (err error)

const (
bashCompletionFunc = `
# arg completions

__qri_parse_list()
{
local qri_output out
if qri_output=$(qri list --format=simple --no-prompt --no-color 2>/dev/null); then
out=($(echo "${qri_output}"))
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
echo "${qri_output}"
return 1
fi
return 0
}

__qri_parse_search()
{
local qri_output out
if qri_output=$(qri search $cur --format=simple --no-prompt --no-color 2>/dev/null); then
out=($(echo "${qri_output}"))
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
echo "${qri_output}"
return 1
fi
return 0
}

__qri_get_datasets()
__qri_parse_config()
{
__qri_parse_list
if [[ $? -eq 0 ]]; then
return 0
local qri_output out
if qri_output=$(qri completion config $cur --no-prompt --no-color 2>/dev/null); then
echo "${qri_output}"
return 1
fi
return 0
}

__qri_get_search()
__qri_parse_structure_args()
{
__qri_parse_search
if [[ $? -eq 0 ]]; then
local qri_output out
if qri_output=$(qri completion structure --no-prompt --no-color 2>/dev/null); then
echo "${qri_output}"
return 1
fi
return 0
}

__qri_parse_peers()
{
local qri_output out
if qri_output=$(qri peers list --format=simple --no-prompt --no-color 2>/dev/null); then
echo "${qri_output}"
return 1
fi
return 0
}

__qri_join_completions()
{
local q1=($1)
local q2=($2)
echo "${q1[*]} ${q2[*]}"
return 1
}

__qri_suggest_completion()
{
local res=($1)
if test -z "${res}"; then
return 0
fi
COMPREPLY=( $( compgen -W "${res[*]}" -- "$cur" ) )
return 1
}

__qri_custom_func() {
local out
case ${last_command} in
qri_checkout | qri_export | qri_get | qri_log | qri_logbook | qri_publish | qri_remove | qri_rename | qri_render | qri_save | qri_stats | qri_use | qri_validate | qri_whatchanged)
__qri_get_datasets
qri_checkout | qri_export | qri_log | qri_logbook | qri_publish | qri_remove | qri_rename | qri_render | qri_save | qri_stats | qri_use | qri_validate | qri_whatchanged | qri_workdir_link | qri_workdir_unlink)
__qri_suggest_completion "$(__qri_parse_list)"
return
;;
qri_add | qri_fetch | qri_search)
__qri_get_search
__qri_suggest_completion "$(__qri_parse_search)"
return
;;
qri_config_get | qri_config_set)
__qri_suggest_completion "$(__qri_parse_config)"
return
;;
qri_get)
local completions=$(__qri_join_completions "$(__qri_parse_structure_args)" "$(__qri_parse_list)")
__qri_suggest_completion "${completions}"
return
;;
qri_peers_info | qri_peers_connect | qri_peers_disconnect)
__qri_suggest_completion "$(__qri_parse_peers)"
return
;;
*)
;;
esac
}

# flag completions

__qri_get_peer_flag_suggestions()
{
__qri_suggest_completion "$(__qri_parse_peers)"
}
`

zshHead = `# reference kubectl completion zsh
Expand Down
1 change: 1 addition & 0 deletions cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ must have ` + "`qri connect`" + ` running in a separate terminal window.`,
cmd.Flags().BoolVarP(&o.Published, "published", "p", false, "list only published datasets")
cmd.Flags().BoolVarP(&o.ShowNumVersions, "num-versions", "n", false, "show number of versions")
cmd.Flags().StringVar(&o.Peername, "peer", "", "peer whose datasets to list")
cmd.MarkFlagCustom("peer", "__qri_get_peer_flag_suggestions")
cmd.Flags().BoolVarP(&o.Raw, "raw", "r", false, "to show raw references")
cmd.Flags().BoolVarP(&o.UseDscache, "use-dscache", "", false, "experimental: build and use dscache to list")

Expand Down
30 changes: 15 additions & 15 deletions cmd/peers.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ connected, use the ` + "`--cached`" + ` flag.`,

list.Flags().BoolVarP(&o.Cached, "cached", "c", false, "show peers that aren't online, but previously seen")
list.Flags().StringVarP(&o.Network, "network", "n", "", "specify network to show peers from (qri|ipfs) (defaults to qri)")
list.Flags().StringVarP(&o.Format, "format", "", "", "output format. formats: simple")
// TODO (ramfox): when we determine the best way to order and paginate peers, restore!
// list.Flags().IntVar(&o.PageSize, "page-size", 200, "max page size number of peers to show, default 200")
// list.Flags().IntVar(&o.Page, "page", 1, "page number of peers, default 1")
Expand Down Expand Up @@ -210,23 +211,17 @@ func (o *PeersOptions) List() (err error) {
// convert Page and PageSize to Limit and Offset
page := util.NewPage(o.Page, o.PageSize)

var items []fmt.Stringer
res := []*config.ProfilePod{}

if o.Network == "ipfs" {
res := []string{}
limit := page.Limit()
if err := o.PeerRequests.ConnectedIPFSPeers(&limit, &res); err != nil {
if err := o.PeerRequests.ConnectedQriProfiles(&limit, &res); err != nil {
return err
}

items = make([]fmt.Stringer, len(res))
for i, p := range res {
items[i] = stringer(p)
}
} else {
// if we don't have an RPC client, assume we're not connected
if !o.UsingRPC && !o.Cached {
printInfo(o.Out, "qri not connected, listing cached peers")
printInfo(o.ErrOut, "qri not connected, listing cached peers")
o.Cached = true
}

Expand All @@ -235,18 +230,23 @@ func (o *PeersOptions) List() (err error) {
Offset: page.Offset(),
Cached: o.Cached,
}
res := []*config.ProfilePod{}
if err = o.PeerRequests.List(p, &res); err != nil {
return err
}
}

items = make([]fmt.Stringer, len(res))
for i, p := range res {
items[i] = peerStringer(*p)
}
items := make([]fmt.Stringer, len(res))
peerNames := make([]string, len(res))
for i, p := range res {
items[i] = peerStringer(*p)
peerNames[i] = p.Peername
}

printItems(o.Out, items, page.Offset())
if o.Format == "simple" {
printlnStringItems(o.Out, peerNames)
} else {
printItems(o.Out, items, page.Offset())
}
return
}

Expand Down
77 changes: 77 additions & 0 deletions lib/config.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package lib

import (
"bytes"
"encoding/json"
"fmt"
"strings"

"github.com/ghodss/yaml"
"github.com/qri-io/qri/config"
Expand Down Expand Up @@ -74,6 +76,81 @@ func (m *ConfigMethods) GetConfig(p *GetConfigParams, res *[]byte) (err error) {
return nil
}

// GetConfigKeys returns the Config key fields, or sub keys of the specified
// fields of the Config, as a slice of bytes to be used for auto completion
func (m *ConfigMethods) GetConfigKeys(p *GetConfigParams, res *[]byte) (err error) {
if m.inst.rpc != nil {
return checkRPCError(m.inst.rpc.Call("ConfigMethods.GetConfigKeys", p, res))
}

var (
cfg = m.inst.cfg
encode interface{}
)

cfg = cfg.WithoutPrivateValues()

encode = cfg
keyPrefix := ""

if len(p.Field) > 0 && p.Field[len(p.Field)-1] == '.' {
p.Field = p.Field[:len(p.Field)-1]
}
parentKey := p.Field

if p.Field != "" {
fieldArgs := strings.Split(p.Field, ".")
encode, err = cfg.Get(p.Field)
if err != nil {
keyPrefix = fieldArgs[len(fieldArgs)-1]
if len(fieldArgs) == 1 {
encode = cfg
parentKey = ""
} else {
parentKey = strings.Join(fieldArgs[:len(fieldArgs)-1], ".")
newEncode, fieldErr := cfg.Get(parentKey)
if fieldErr != nil {
return fmt.Errorf("error getting %s from config: %s", p.Field, err)
}
encode = newEncode
}
}
}

*res, err = parseKeys(encode, keyPrefix, parentKey)
return err
}

func parseKeys(cfg interface{}, prefix, parentKey string) ([]byte, error) {
cfgBytes, parseErr := json.Marshal(cfg)
if parseErr != nil {
return nil, parseErr
}

cfgMap := map[string]interface{}{}
parseErr = json.Unmarshal(cfgBytes, &cfgMap)
if parseErr != nil {
return nil, parseErr
}

buff := bytes.Buffer{}
for s := range cfgMap {
if prefix != "" && !strings.HasPrefix(s, prefix) {
continue
}
if parentKey != "" {
buff.WriteString(parentKey)
buff.WriteString(".")
}
buff.WriteString(s)
buff.WriteString("\n")
}
if len(buff.Bytes()) > 0 {
return buff.Bytes(), nil
}
return nil, fmt.Errorf("error getting %s from config", prefix)
}

// SetConfig validates, updates and saves the config
func (m *ConfigMethods) SetConfig(update *config.Config, set *bool) (err error) {
if m.inst.rpc != nil {
Expand Down