github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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, the corresponding values are returned:
    41  
    42      $ snap get snap-name username password
    43      Key       Value
    44      username  frank
    45      password  ...
    46  
    47  Nested values may be retrieved via a dotted path:
    48  
    49      $ snap get snap-name author.name
    50      frank
    51  `)
    52  
    53  type cmdGet struct {
    54  	clientMixin
    55  	Positional struct {
    56  		Snap installedSnapName `required:"yes"`
    57  		Keys []string
    58  	} `positional-args:"yes"`
    59  
    60  	Typed    bool `short:"t"`
    61  	Document bool `short:"d"`
    62  	List     bool `short:"l"`
    63  }
    64  
    65  func init() {
    66  	addCommand("get", shortGetHelp, longGetHelp, func() flags.Commander { return &cmdGet{} },
    67  		map[string]string{
    68  			// TRANSLATORS: This should not start with a lowercase letter.
    69  			"d": i18n.G("Always return document, even with single key"),
    70  			// TRANSLATORS: This should not start with a lowercase letter.
    71  			"l": i18n.G("Always return list, even with single key"),
    72  			// TRANSLATORS: This should not start with a lowercase letter.
    73  			"t": i18n.G("Strict typing with nulls and quoted strings"),
    74  		}, []argDesc{
    75  			{
    76  				name: "<snap>",
    77  				// TRANSLATORS: This should not start with a lowercase letter.
    78  				desc: i18n.G("The snap whose conf is being requested"),
    79  			},
    80  			{
    81  				// TRANSLATORS: This needs to begin with < and end with >
    82  				name: i18n.G("<key>"),
    83  				// TRANSLATORS: This should not start with a lowercase letter.
    84  				desc: i18n.G("Key of interest within the configuration"),
    85  			},
    86  		})
    87  }
    88  
    89  type ConfigValue struct {
    90  	Path  string
    91  	Value interface{}
    92  }
    93  
    94  type byConfigPath []ConfigValue
    95  
    96  func (s byConfigPath) Len() int      { return len(s) }
    97  func (s byConfigPath) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
    98  func (s byConfigPath) Less(i, j int) bool {
    99  	other := s[j].Path
   100  	for k, c := range s[i].Path {
   101  		if len(other) <= k {
   102  			return false
   103  		}
   104  
   105  		switch {
   106  		case c == rune(other[k]):
   107  			continue
   108  		case c == '.':
   109  			return true
   110  		case other[k] == '.' || c > rune(other[k]):
   111  			return false
   112  		}
   113  		return true
   114  	}
   115  	return true
   116  }
   117  
   118  func sortByPath(config []ConfigValue) {
   119  	sort.Sort(byConfigPath(config))
   120  }
   121  
   122  func flattenConfig(cfg map[string]interface{}, root bool) (values []ConfigValue) {
   123  	const docstr = "{...}"
   124  	for k, v := range cfg {
   125  		if input, ok := v.(map[string]interface{}); ok {
   126  			if root {
   127  				values = append(values, ConfigValue{k, docstr})
   128  			} else {
   129  				for kk, vv := range input {
   130  					p := k + "." + kk
   131  					if _, ok := vv.(map[string]interface{}); ok {
   132  						values = append(values, ConfigValue{p, docstr})
   133  					} else {
   134  						values = append(values, ConfigValue{p, vv})
   135  					}
   136  				}
   137  			}
   138  		} else {
   139  			values = append(values, ConfigValue{k, v})
   140  		}
   141  	}
   142  	sortByPath(values)
   143  	return values
   144  }
   145  
   146  func rootRequested(confKeys []string) bool {
   147  	return len(confKeys) == 0
   148  }
   149  
   150  // outputJson will be used when the user requested "document" output via
   151  // the "-d" commandline switch.
   152  func (c *cmdGet) outputJson(conf interface{}) error {
   153  	bytes, err := json.MarshalIndent(conf, "", "\t")
   154  	if err != nil {
   155  		return err
   156  	}
   157  
   158  	fmt.Fprintln(Stdout, string(bytes))
   159  	return nil
   160  }
   161  
   162  // outputList will be used when the user requested list output via the
   163  // "-l" commandline switch.
   164  func (x *cmdGet) outputList(conf map[string]interface{}) error {
   165  	if rootRequested(x.Positional.Keys) && len(conf) == 0 {
   166  		return fmt.Errorf("snap %q has no configuration", x.Positional.Snap)
   167  	}
   168  
   169  	w := tabWriter()
   170  	defer w.Flush()
   171  
   172  	fmt.Fprintf(w, "Key\tValue\n")
   173  	values := flattenConfig(conf, rootRequested(x.Positional.Keys))
   174  	for _, v := range values {
   175  		fmt.Fprintf(w, "%s\t%v\n", v.Path, v.Value)
   176  	}
   177  	return nil
   178  }
   179  
   180  // outputDefault will be used when no commandline switch to override the
   181  // output where used. The output follows the following rules:
   182  // - a single key with a string value is printed directly
   183  // - multiple keys are printed as a list to the terminal (if there is one)
   184  //   or as json if there is no terminal
   185  // - the option "typed" is honored
   186  func (x *cmdGet) outputDefault(conf map[string]interface{}, snapName string, confKeys []string) error {
   187  	if rootRequested(confKeys) && len(conf) == 0 {
   188  		return fmt.Errorf("snap %q has no configuration", snapName)
   189  	}
   190  
   191  	var confToPrint interface{} = conf
   192  
   193  	if len(confKeys) == 1 {
   194  		// if single key was requested, then just output the
   195  		// value unless it's a map, in which case it will be
   196  		// printed as a list below.
   197  		if _, ok := conf[confKeys[0]].(map[string]interface{}); !ok {
   198  			confToPrint = conf[confKeys[0]]
   199  		}
   200  	}
   201  
   202  	// conf looks like a map
   203  	if cfg, ok := confToPrint.(map[string]interface{}); ok {
   204  		if isStdinTTY {
   205  			return x.outputList(cfg)
   206  		}
   207  
   208  		// TODO: remove this conditional and the warning below
   209  		// after a transition period.
   210  		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`))
   211  		return x.outputJson(confToPrint)
   212  	}
   213  
   214  	if s, ok := confToPrint.(string); ok && !x.Typed {
   215  		fmt.Fprintln(Stdout, s)
   216  		return nil
   217  	}
   218  
   219  	if confToPrint != nil || x.Typed {
   220  		return x.outputJson(confToPrint)
   221  	}
   222  
   223  	fmt.Fprintln(Stdout, "")
   224  	return nil
   225  
   226  }
   227  
   228  func (x *cmdGet) Execute(args []string) error {
   229  	if len(args) > 0 {
   230  		// TRANSLATORS: the %s is the list of extra arguments
   231  		return fmt.Errorf(i18n.G("too many arguments: %s"), strings.Join(args, " "))
   232  	}
   233  
   234  	if x.Document && x.Typed {
   235  		return fmt.Errorf("cannot use -d and -t together")
   236  	}
   237  
   238  	if x.Document && x.List {
   239  		return fmt.Errorf("cannot use -d and -l together")
   240  	}
   241  
   242  	snapName := string(x.Positional.Snap)
   243  	confKeys := x.Positional.Keys
   244  
   245  	conf, err := x.client.Conf(snapName, confKeys)
   246  	if err != nil {
   247  		return err
   248  	}
   249  
   250  	switch {
   251  	case x.Document:
   252  		return x.outputJson(conf)
   253  	case x.List:
   254  		return x.outputList(conf)
   255  	default:
   256  		return x.outputDefault(conf, snapName, confKeys)
   257  	}
   258  }