Skip to content

Commit

Permalink
fix(cmd): completion supports config, structure and peer args, added …
Browse files Browse the repository at this point in the history
…workdir (#1268)

* fix(cmd): added config get/set autocompletion

* fix(cmd): added 'qri get' structure autocompletion

* fix(cmd): added peers autocompletion

* fix(cmd): added workdir autocompletion

* fix(cmd): cleanup
  • Loading branch information
Arqu authored Apr 9, 2020
1 parent 83b8850 commit ef62c71
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 31 deletions.
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

0 comments on commit ef62c71

Please sign in to comment.