diff --git a/base/component/field_id.go b/base/component/field_id.go index 0976277ca..ec1c8bea3 100644 --- a/base/component/field_id.go +++ b/base/component/field_id.go @@ -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, "|") + ")($|\\.)") diff --git a/cmd/completion.go b/cmd/completion.go index b8bdb3995..dc5b71127 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -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", @@ -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 @@ -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 diff --git a/cmd/list.go b/cmd/list.go index 0fad0937d..5abf706ac 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -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") diff --git a/cmd/peers.go b/cmd/peers.go index 83ef3ceed..ef7491598 100644 --- a/cmd/peers.go +++ b/cmd/peers.go @@ -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") @@ -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 } @@ -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 } diff --git a/lib/config.go b/lib/config.go index 86910fdcb..e9e7c50b0 100644 --- a/lib/config.go +++ b/lib/config.go @@ -1,8 +1,10 @@ package lib import ( + "bytes" "encoding/json" "fmt" + "strings" "github.com/ghodss/yaml" "github.com/qri-io/qri/config" @@ -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 {