github.com/rigado/snapd@v2.42.5-go-mod+incompatible/cmd/snap/cmd_get.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package main
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"sort"
    26  	"strings"
    27  
    28  	"github.com/jessevdk/go-flags"
    29  
    30  	"github.com/snapcore/snapd/i18n"
    31  )
    32  
    33  var shortGetHelp = i18n.G("Print configuration options")
    34  var longGetHelp = i18n.G(`
    35  The get command prints configuration options for the provided snap.
    36  
    37      $ snap get snap-name username
    38      frank
    39  
    40  If multiple option names are provided, a document is returned:
    41  
    42      $ snap get snap-name username password
    43      {
    44          "username": "frank",
    45          "password": "..."
    46      }
    47  
    48  Nested values may be retrieved via a dotted path:
    49  
    50      $ snap get snap-name author.name
    51      frank
    52  `)
    53  
    54  type cmdGet struct {
    55  	clientMixin
    56  	Positional struct {
    57  		Snap installedSnapName `required:"yes"`
    58  		Keys []string
    59  	} `positional-args:"yes"`
    60  
    61  	Typed    bool `short:"t"`
    62  	Document bool `short:"d"`
    63  	List     bool `short:"l"`
    64  }
    65  
    66  func init() {
    67  	addCommand("get", shortGetHelp, longGetHelp, func() flags.Commander { return &cmdGet{} },
    68  		map[string]string{
    69  			// TRANSLATORS: This should not start with a lowercase letter.
    70  			"d": i18n.G("Always return document, even with single key"),
    71  			// TRANSLATORS: This should not start with a lowercase letter.
    72  			"l": i18n.G("Always return list, even with single key"),
    73  			// TRANSLATORS: This should not start with a lowercase letter.
    74  			"t": i18n.G("Strict typing with nulls and quoted strings"),
    75  		}, []argDesc{
    76  			{
    77  				name: "<snap>",
    78  				// TRANSLATORS: This should not start with a lowercase letter.
    79  				desc: i18n.G("The snap whose conf is being requested"),
    80  			},
    81  			{
    82  				// TRANSLATORS: This needs to begin with < and end with >
    83  				name: i18n.G("<key>"),
    84  				// TRANSLATORS: This should not start with a lowercase letter.
    85  				desc: i18n.G("Key of interest within the configuration"),
    86  			},
    87  		})
    88  }
    89  
    90  type ConfigValue struct {
    91  	Path  string
    92  	Value interface{}
    93  }
    94  
    95  type byConfigPath []ConfigValue
    96  
    97  func (s byConfigPath) Len() int      { return len(s) }
    98  func (s byConfigPath) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
    99  func (s byConfigPath) Less(i, j int) bool {
   100  	other := s[j].Path
   101  	for k, c := range s[i].Path {
   102  		if len(other) <= k {
   103  			return false
   104  		}
   105  
   106  		switch {
   107  		case c == rune(other[k]):
   108  			continue
   109  		case c == '.':
   110  			return true
   111  		case other[k] == '.' || c > rune(other[k]):
   112  			return false
   113  		}
   114  		return true
   115  	}
   116  	return true
   117  }
   118  
   119  func sortByPath(config []ConfigValue) {
   120  	sort.Sort(byConfigPath(config))
   121  }
   122  
   123  func flattenConfig(cfg map[string]interface{}, root bool) (values []ConfigValue) {
   124  	const docstr = "{...}"
   125  	for k, v := range cfg {
   126  		if input, ok := v.(map[string]interface{}); ok {
   127  			if root {
   128  				values = append(values, ConfigValue{k, docstr})
   129  			} else {
   130  				for kk, vv := range input {
   131  					p := k + "." + kk
   132  					if _, ok := vv.(map[string]interface{}); ok {
   133  						values = append(values, ConfigValue{p, docstr})
   134  					} else {
   135  						values = append(values, ConfigValue{p, vv})
   136  					}
   137  				}
   138  			}
   139  		} else {
   140  			values = append(values, ConfigValue{k, v})
   141  		}
   142  	}
   143  	sortByPath(values)
   144  	return values
   145  }
   146  
   147  func rootRequested(confKeys []string) bool {
   148  	return len(confKeys) == 0
   149  }
   150  
   151  // outputJson will be used when the user requested "document" output via
   152  // the "-d" commandline switch.
   153  func (c *cmdGet) outputJson(conf interface{}) error {
   154  	bytes, err := json.MarshalIndent(conf, "", "\t")
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	fmt.Fprintln(Stdout, string(bytes))
   160  	return nil
   161  }
   162  
   163  // outputList will be used when the user requested list output via the
   164  // "-l" commandline switch.
   165  func (x *cmdGet) outputList(conf map[string]interface{}) error {
   166  	if rootRequested(x.Positional.Keys) && len(conf) == 0 {
   167  		return fmt.Errorf("snap %q has no configuration", x.Positional.Snap)
   168  	}
   169  
   170  	w := tabWriter()
   171  	defer w.Flush()
   172  
   173  	fmt.Fprintf(w, "Key\tValue\n")
   174  	values := flattenConfig(conf, rootRequested(x.Positional.Keys))
   175  	for _, v := range values {
   176  		fmt.Fprintf(w, "%s\t%v\n", v.Path, v.Value)
   177  	}
   178  	return nil
   179  }
   180  
   181  // outputDefault will be used when no commandline switch to override the
   182  // output where used. The output follows the following rules:
   183  // - a single key with a string value is printed directly
   184  // - multiple keys are printed as a list to the terminal (if there is one)
   185  //   or as json if there is no terminal
   186  // - the option "typed" is honored
   187  func (x *cmdGet) outputDefault(conf map[string]interface{}, snapName string, confKeys []string) error {
   188  	if rootRequested(confKeys) && len(conf) == 0 {
   189  		return fmt.Errorf("snap %q has no configuration", snapName)
   190  	}
   191  
   192  	var confToPrint interface{} = conf
   193  
   194  	if len(confKeys) == 1 {
   195  		// if single key was requested, then just output the
   196  		// value unless it's a map, in which case it will be
   197  		// printed as a list below.
   198  		if _, ok := conf[confKeys[0]].(map[string]interface{}); !ok {
   199  			confToPrint = conf[confKeys[0]]
   200  		}
   201  	}
   202  
   203  	// conf looks like a map
   204  	if cfg, ok := confToPrint.(map[string]interface{}); ok {
   205  		if isStdinTTY {
   206  			return x.outputList(cfg)
   207  		}
   208  
   209  		// TODO: remove this conditional and the warning below
   210  		// after a transition period.
   211  		fmt.Fprintf(Stderr, i18n.G(`WARNING: The output of 'snap get' will become a list with columns - use -d or -l to force the output format.\n`))
   212  		return x.outputJson(confToPrint)
   213  	}
   214  
   215  	if s, ok := confToPrint.(string); ok && !x.Typed {
   216  		fmt.Fprintln(Stdout, s)
   217  		return nil
   218  	}
   219  
   220  	if confToPrint != nil || x.Typed {
   221  		return x.outputJson(confToPrint)
   222  	}
   223  
   224  	fmt.Fprintln(Stdout, "")
   225  	return nil
   226  
   227  }
   228  
   229  func (x *cmdGet) Execute(args []string) error {
   230  	if len(args) > 0 {
   231  		// TRANSLATORS: the %s is the list of extra arguments
   232  		return fmt.Errorf(i18n.G("too many arguments: %s"), strings.Join(args, " "))
   233  	}
   234  
   235  	if x.Document && x.Typed {
   236  		return fmt.Errorf("cannot use -d and -t together")
   237  	}
   238  
   239  	if x.Document && x.List {
   240  		return fmt.Errorf("cannot use -d and -l together")
   241  	}
   242  
   243  	snapName := string(x.Positional.Snap)
   244  	confKeys := x.Positional.Keys
   245  
   246  	conf, err := x.client.Conf(snapName, confKeys)
   247  	if err != nil {
   248  		return err
   249  	}
   250  
   251  	switch {
   252  	case x.Document:
   253  		return x.outputJson(conf)
   254  	case x.List:
   255  		return x.outputList(conf)
   256  	default:
   257  		return x.outputDefault(conf, snapName, confKeys)
   258  	}
   259  }