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

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2018 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  	"fmt"
    24  	"io"
    25  	"path/filepath"
    26  	"strconv"
    27  	"strings"
    28  	"text/tabwriter"
    29  	"time"
    30  	"unicode"
    31  	"unicode/utf8"
    32  
    33  	"github.com/jessevdk/go-flags"
    34  	"gopkg.in/yaml.v2"
    35  
    36  	"github.com/snapcore/snapd/asserts"
    37  	"github.com/snapcore/snapd/client"
    38  	"github.com/snapcore/snapd/cmd"
    39  	"github.com/snapcore/snapd/i18n"
    40  	"github.com/snapcore/snapd/osutil"
    41  	"github.com/snapcore/snapd/snap"
    42  	"github.com/snapcore/snapd/snap/squashfs"
    43  	"github.com/snapcore/snapd/strutil"
    44  )
    45  
    46  type infoCmd struct {
    47  	clientMixin
    48  	colorMixin
    49  	timeMixin
    50  
    51  	Verbose    bool `long:"verbose"`
    52  	Positional struct {
    53  		Snaps []anySnapName `positional-arg-name:"<snap>" required:"1"`
    54  	} `positional-args:"yes" required:"yes"`
    55  }
    56  
    57  var shortInfoHelp = i18n.G("Show detailed information about snaps")
    58  var longInfoHelp = i18n.G(`
    59  The info command shows detailed information about snaps.
    60  
    61  The snaps can be specified by name or by path; names are looked for both in the
    62  store and in the installed snaps; paths can refer to a .snap file, or to a
    63  directory that contains an unpacked snap suitable for 'snap try' (an example
    64  of this would be the 'prime' directory snapcraft produces).
    65  `)
    66  
    67  func init() {
    68  	addCommand("info",
    69  		shortInfoHelp,
    70  		longInfoHelp,
    71  		func() flags.Commander {
    72  			return &infoCmd{}
    73  		}, colorDescs.also(timeDescs).also(map[string]string{
    74  			// TRANSLATORS: This should not start with a lowercase letter.
    75  			"verbose": i18n.G("Include more details on the snap (expanded notes, base, etc.)"),
    76  		}), nil)
    77  }
    78  
    79  func (iw *infoWriter) maybePrintHealth() {
    80  	if iw.localSnap == nil {
    81  		return
    82  	}
    83  	health := iw.localSnap.Health
    84  	if health == nil {
    85  		if !iw.verbose {
    86  			return
    87  		}
    88  		health = &client.SnapHealth{
    89  			Status:  "unknown",
    90  			Message: "health has not been set",
    91  		}
    92  	}
    93  	if health.Status == "okay" && !iw.verbose {
    94  		return
    95  	}
    96  
    97  	fmt.Fprintln(iw, "health:")
    98  	fmt.Fprintf(iw, "  status:\t%s\n", health.Status)
    99  	if health.Message != "" {
   100  		wrapGeneric(iw, quotedIfNeeded(health.Message), "  message:\t", "    ", iw.termWidth)
   101  	}
   102  	if health.Code != "" {
   103  		fmt.Fprintf(iw, "  code:\t%s\n", health.Code)
   104  	}
   105  	if !health.Timestamp.IsZero() {
   106  		fmt.Fprintf(iw, "  checked:\t%s\n", iw.fmtTime(health.Timestamp))
   107  	}
   108  	if !health.Revision.Unset() {
   109  		fmt.Fprintf(iw, "  revision:\t%s\n", health.Revision)
   110  	}
   111  	iw.Flush()
   112  }
   113  
   114  func clientSnapFromPath(path string) (*client.Snap, error) {
   115  	snapf, err := snap.Open(path)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  	info, err := snap.ReadInfoFromSnapFile(snapf, nil)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	direct, err := cmd.ClientSnapFromSnapInfo(info)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	return direct, nil
   130  }
   131  
   132  func norm(path string) string {
   133  	path = filepath.Clean(path)
   134  	if osutil.IsDirectory(path) {
   135  		path = path + "/"
   136  	}
   137  
   138  	return path
   139  }
   140  
   141  // runesTrimRightSpace returns text, with any trailing whitespace dropped.
   142  func runesTrimRightSpace(text []rune) []rune {
   143  	j := len(text)
   144  	for j > 0 && unicode.IsSpace(text[j-1]) {
   145  		j--
   146  	}
   147  	return text[:j]
   148  }
   149  
   150  // runesLastIndexSpace returns the index of the last whitespace rune
   151  // in the text. If the text has no whitespace, returns -1.
   152  func runesLastIndexSpace(text []rune) int {
   153  	for i := len(text) - 1; i >= 0; i-- {
   154  		if unicode.IsSpace(text[i]) {
   155  			return i
   156  		}
   157  	}
   158  	return -1
   159  }
   160  
   161  // wrapLine wraps a line, assumed to be part of a block-style yaml
   162  // string, to fit into termWidth, preserving the line's indent, and
   163  // writes it out prepending padding to each line.
   164  func wrapLine(out io.Writer, text []rune, pad string, termWidth int) error {
   165  	// discard any trailing whitespace
   166  	text = runesTrimRightSpace(text)
   167  	// establish the indent of the whole block
   168  	idx := 0
   169  	for idx < len(text) && unicode.IsSpace(text[idx]) {
   170  		idx++
   171  	}
   172  	indent := pad + string(text[:idx])
   173  	text = text[idx:]
   174  	if len(indent) > termWidth/2 {
   175  		// If indent is too big there's not enough space for the actual
   176  		// text, in the pathological case the indent can even be bigger
   177  		// than the terminal which leads to lp:1828425.
   178  		// Rather than let that happen, give up.
   179  		indent = pad + "  "
   180  	}
   181  	return wrapGeneric(out, text, indent, indent, termWidth)
   182  }
   183  
   184  // wrapFlow wraps the text using yaml's flow style, allowing indent
   185  // characters for the first line.
   186  func wrapFlow(out io.Writer, text []rune, indent string, termWidth int) error {
   187  	return wrapGeneric(out, text, indent, "  ", termWidth)
   188  }
   189  
   190  // wrapGeneric wraps the given text to the given width, prefixing the
   191  // first line with indent and the remaining lines with indent2
   192  func wrapGeneric(out io.Writer, text []rune, indent, indent2 string, termWidth int) error {
   193  	// Note: this is _wrong_ for much of unicode (because the width of a rune on
   194  	//       the terminal is anything between 0 and 2, not always 1 as this code
   195  	//       assumes) but fixing that is Hard. Long story short, you can get close
   196  	//       using a couple of big unicode tables (which is what wcwidth
   197  	//       does). Getting it 100% requires a terminfo-alike of unicode behaviour.
   198  	//       However, before this we'd count bytes instead of runes, so we'd be
   199  	//       even more broken. Think of it as successive approximations... at least
   200  	//       with this work we share tabwriter's opinion on the width of things!
   201  
   202  	// This (and possibly printDescr below) should move to strutil once
   203  	// we're happy with it getting wider (heh heh) use.
   204  
   205  	indentWidth := utf8.RuneCountInString(indent)
   206  	delta := indentWidth - utf8.RuneCountInString(indent2)
   207  	width := termWidth - indentWidth
   208  
   209  	// establish the indent of the whole block
   210  	idx := 0
   211  	var err error
   212  	for len(text) > width && err == nil {
   213  		// find a good place to chop the text
   214  		idx = runesLastIndexSpace(text[:width+1])
   215  		if idx < 0 {
   216  			// there's no whitespace; just chop at line width
   217  			idx = width
   218  		}
   219  		_, err = fmt.Fprint(out, indent, string(text[:idx]), "\n")
   220  		// prune any remaining whitespace before the start of the next line
   221  		for idx < len(text) && unicode.IsSpace(text[idx]) {
   222  			idx++
   223  		}
   224  		text = text[idx:]
   225  		width += delta
   226  		indent = indent2
   227  		delta = 0
   228  	}
   229  	if err != nil {
   230  		return err
   231  	}
   232  	_, err = fmt.Fprint(out, indent, string(text), "\n")
   233  	return err
   234  }
   235  
   236  func quotedIfNeeded(raw string) []rune {
   237  	// simplest way of checking to see if it needs quoting is to try
   238  	raw = strings.TrimSpace(raw)
   239  	type T struct {
   240  		S string
   241  	}
   242  	if len(raw) == 0 {
   243  		raw = `""`
   244  	} else if err := yaml.UnmarshalStrict([]byte("s: "+raw), &T{}); err != nil {
   245  		raw = strconv.Quote(raw)
   246  	}
   247  	return []rune(raw)
   248  }
   249  
   250  // printDescr formats a given string (typically a snap description)
   251  // in a user friendly way.
   252  //
   253  // The rules are (intentionally) very simple:
   254  // - trim trailing whitespace
   255  // - word wrap at "max" chars preserving line indent
   256  // - keep \n intact and break there
   257  func printDescr(w io.Writer, descr string, termWidth int) error {
   258  	var err error
   259  	descr = strings.TrimRightFunc(descr, unicode.IsSpace)
   260  	for _, line := range strings.Split(descr, "\n") {
   261  		err = wrapLine(w, []rune(line), "  ", termWidth)
   262  		if err != nil {
   263  			break
   264  		}
   265  	}
   266  	return err
   267  }
   268  
   269  type writeflusher interface {
   270  	io.Writer
   271  	Flush() error
   272  }
   273  
   274  type infoWriter struct {
   275  	// fields that are set every iteration
   276  	theSnap    *client.Snap
   277  	diskSnap   *client.Snap
   278  	localSnap  *client.Snap
   279  	remoteSnap *client.Snap
   280  	resInfo    *client.ResultInfo
   281  	path       string
   282  	// fields that don't change and so can be set once
   283  	writeflusher
   284  	esc       *escapes
   285  	termWidth int
   286  	fmtTime   func(time.Time) string
   287  	absTime   bool
   288  	verbose   bool
   289  }
   290  
   291  func (iw *infoWriter) setupDiskSnap(path string, diskSnap *client.Snap) {
   292  	iw.localSnap, iw.remoteSnap, iw.resInfo = nil, nil, nil
   293  	iw.path = path
   294  	iw.diskSnap = diskSnap
   295  	iw.theSnap = diskSnap
   296  }
   297  
   298  func (iw *infoWriter) setupSnap(localSnap, remoteSnap *client.Snap, resInfo *client.ResultInfo) {
   299  	iw.path, iw.diskSnap = "", nil
   300  	iw.localSnap = localSnap
   301  	iw.remoteSnap = remoteSnap
   302  	iw.resInfo = resInfo
   303  	if localSnap != nil {
   304  		iw.theSnap = localSnap
   305  	} else {
   306  		iw.theSnap = remoteSnap
   307  	}
   308  }
   309  
   310  func (iw *infoWriter) maybePrintPrice() {
   311  	if iw.resInfo == nil {
   312  		return
   313  	}
   314  	price, currency, err := getPrice(iw.remoteSnap.Prices, iw.resInfo.SuggestedCurrency)
   315  	if err != nil {
   316  		return
   317  	}
   318  	fmt.Fprintf(iw, "price:\t%s\n", formatPrice(price, currency))
   319  }
   320  
   321  func (iw *infoWriter) maybePrintType() {
   322  	// XXX: using literals here until we reshuffle snap & client properly
   323  	// (and os->core rename happens, etc)
   324  	t := iw.theSnap.Type
   325  	switch t {
   326  	case "", "app", "application":
   327  		return
   328  	case "os":
   329  		t = "core"
   330  	}
   331  
   332  	fmt.Fprintf(iw, "type:\t%s\n", t)
   333  }
   334  
   335  func (iw *infoWriter) maybePrintID() {
   336  	if iw.theSnap.ID != "" {
   337  		fmt.Fprintf(iw, "snap-id:\t%s\n", iw.theSnap.ID)
   338  	}
   339  }
   340  
   341  func (iw *infoWriter) maybePrintTrackingChannel() {
   342  	if iw.localSnap == nil {
   343  		return
   344  	}
   345  	if iw.localSnap.TrackingChannel == "" {
   346  		return
   347  	}
   348  	fmt.Fprintf(iw, "tracking:\t%s\n", iw.localSnap.TrackingChannel)
   349  }
   350  
   351  func (iw *infoWriter) maybePrintInstallDate() {
   352  	if iw.localSnap == nil {
   353  		return
   354  	}
   355  	if iw.localSnap.InstallDate.IsZero() {
   356  		return
   357  	}
   358  	fmt.Fprintf(iw, "refresh-date:\t%s\n", iw.fmtTime(iw.localSnap.InstallDate))
   359  }
   360  
   361  func (iw *infoWriter) maybePrintChinfo() {
   362  	if iw.diskSnap != nil {
   363  		return
   364  	}
   365  	chInfos := channelInfos{
   366  		chantpl:     "%s%s:\t%s %s%*s %*s %s\n",
   367  		releasedfmt: "2006-01-02",
   368  		esc:         iw.esc,
   369  	}
   370  	if iw.absTime {
   371  		chInfos.releasedfmt = time.RFC3339
   372  	}
   373  	if iw.remoteSnap != nil && iw.remoteSnap.Channels != nil && iw.remoteSnap.Tracks != nil {
   374  		iw.Flush()
   375  		chInfos.chantpl = "%s%s:\t%s\t%s\t%*s\t%*s\t%s\n"
   376  		chInfos.addFromRemote(iw.remoteSnap)
   377  	}
   378  	if iw.localSnap != nil {
   379  		chInfos.addFromLocal(iw.localSnap)
   380  	}
   381  	chInfos.dump(iw)
   382  }
   383  
   384  func (iw *infoWriter) maybePrintBase() {
   385  	if iw.verbose && iw.theSnap.Base != "" {
   386  		fmt.Fprintf(iw, "base:\t%s\n", iw.theSnap.Base)
   387  	}
   388  }
   389  
   390  func (iw *infoWriter) maybePrintPath() {
   391  	if iw.path != "" {
   392  		fmt.Fprintf(iw, "path:\t%q\n", iw.path)
   393  	}
   394  }
   395  
   396  func (iw *infoWriter) printName() {
   397  	fmt.Fprintf(iw, "name:\t%s\n", iw.theSnap.Name)
   398  }
   399  
   400  func (iw *infoWriter) printSummary() {
   401  	wrapFlow(iw, quotedIfNeeded(iw.theSnap.Summary), "summary:\t", iw.termWidth)
   402  }
   403  
   404  func (iw *infoWriter) maybePrintPublisher() {
   405  	if iw.diskSnap != nil {
   406  		// snaps read from disk won't have a publisher
   407  		return
   408  	}
   409  	fmt.Fprintf(iw, "publisher:\t%s\n", longPublisher(iw.esc, iw.theSnap.Publisher))
   410  }
   411  
   412  func (iw *infoWriter) maybePrintStandaloneVersion() {
   413  	if iw.diskSnap == nil {
   414  		// snaps not read from disk will have version information shown elsewhere
   415  		return
   416  	}
   417  	version := iw.diskSnap.Version
   418  	if version == "" {
   419  		version = iw.esc.dash
   420  	}
   421  	// NotesFromRemote might be better called NotesFromNotInstalled but that's nasty
   422  	fmt.Fprintf(iw, "version:\t%s %s\n", version, NotesFromRemote(iw.diskSnap, nil))
   423  }
   424  
   425  func (iw *infoWriter) maybePrintBuildDate() {
   426  	if iw.diskSnap == nil {
   427  		return
   428  	}
   429  	if osutil.IsDirectory(iw.path) {
   430  		return
   431  	}
   432  	buildDate := squashfs.BuildDate(iw.path)
   433  	if buildDate.IsZero() {
   434  		return
   435  	}
   436  	fmt.Fprintf(iw, "build-date:\t%s\n", iw.fmtTime(buildDate))
   437  }
   438  
   439  func (iw *infoWriter) maybePrintContact() error {
   440  	contact := strings.TrimPrefix(iw.theSnap.Contact, "mailto:")
   441  	if contact == "" {
   442  		return nil
   443  	}
   444  	_, err := fmt.Fprintf(iw, "contact:\t%s\n", contact)
   445  	return err
   446  }
   447  
   448  func (iw *infoWriter) printLicense() {
   449  	license := iw.theSnap.License
   450  	if license == "" {
   451  		license = "unset"
   452  	}
   453  	fmt.Fprintf(iw, "license:\t%s\n", license)
   454  }
   455  
   456  func (iw *infoWriter) printDescr() {
   457  	fmt.Fprintln(iw, "description: |")
   458  	printDescr(iw, iw.theSnap.Description, iw.termWidth)
   459  }
   460  
   461  func (iw *infoWriter) maybePrintCommands() {
   462  	if len(iw.theSnap.Apps) == 0 {
   463  		return
   464  	}
   465  
   466  	commands := make([]string, 0, len(iw.theSnap.Apps))
   467  	for _, app := range iw.theSnap.Apps {
   468  		if app.IsService() {
   469  			continue
   470  		}
   471  
   472  		cmdStr := snap.JoinSnapApp(iw.theSnap.Name, app.Name)
   473  		commands = append(commands, cmdStr)
   474  	}
   475  	if len(commands) == 0 {
   476  		return
   477  	}
   478  
   479  	fmt.Fprintf(iw, "commands:\n")
   480  	for _, cmd := range commands {
   481  		fmt.Fprintf(iw, "  - %s\n", cmd)
   482  	}
   483  }
   484  
   485  func (iw *infoWriter) maybePrintServices() {
   486  	if len(iw.theSnap.Apps) == 0 {
   487  		return
   488  	}
   489  
   490  	services := make([]string, 0, len(iw.theSnap.Apps))
   491  	for _, app := range iw.theSnap.Apps {
   492  		if !app.IsService() {
   493  			continue
   494  		}
   495  
   496  		var active, enabled string
   497  		if app.Active {
   498  			active = "active"
   499  		} else {
   500  			active = "inactive"
   501  		}
   502  		if app.Enabled {
   503  			enabled = "enabled"
   504  		} else {
   505  			enabled = "disabled"
   506  		}
   507  		services = append(services, fmt.Sprintf("  %s:\t%s, %s, %s", snap.JoinSnapApp(iw.theSnap.Name, app.Name), app.Daemon, enabled, active))
   508  	}
   509  	if len(services) == 0 {
   510  		return
   511  	}
   512  
   513  	fmt.Fprintf(iw, "services:\n")
   514  	for _, svc := range services {
   515  		fmt.Fprintln(iw, svc)
   516  	}
   517  }
   518  
   519  func (iw *infoWriter) maybePrintNotes() {
   520  	if !iw.verbose {
   521  		return
   522  	}
   523  	fmt.Fprintln(iw, "notes:\t")
   524  	fmt.Fprintf(iw, "  private:\t%t\n", iw.theSnap.Private)
   525  	fmt.Fprintf(iw, "  confinement:\t%s\n", iw.theSnap.Confinement)
   526  	if iw.localSnap == nil {
   527  		return
   528  	}
   529  	jailMode := iw.localSnap.Confinement == client.DevModeConfinement && !iw.localSnap.DevMode
   530  	fmt.Fprintf(iw, "  devmode:\t%t\n", iw.localSnap.DevMode)
   531  	fmt.Fprintf(iw, "  jailmode:\t%t\n", jailMode)
   532  	fmt.Fprintf(iw, "  trymode:\t%t\n", iw.localSnap.TryMode)
   533  	fmt.Fprintf(iw, "  enabled:\t%t\n", iw.localSnap.Status == client.StatusActive)
   534  	if iw.localSnap.Broken == "" {
   535  		fmt.Fprintf(iw, "  broken:\t%t\n", false)
   536  	} else {
   537  		fmt.Fprintf(iw, "  broken:\t%t (%s)\n", true, iw.localSnap.Broken)
   538  	}
   539  
   540  	fmt.Fprintf(iw, "  ignore-validation:\t%t\n", iw.localSnap.IgnoreValidation)
   541  	return
   542  }
   543  
   544  func (iw *infoWriter) maybePrintCohortKey() {
   545  	if !iw.verbose {
   546  		return
   547  	}
   548  	if iw.localSnap == nil {
   549  		return
   550  	}
   551  	coh := iw.localSnap.CohortKey
   552  	if coh == "" {
   553  		return
   554  	}
   555  	if isStdoutTTY {
   556  		// 15 is 1 + the length of "refresh-date: "
   557  		coh = strutil.ElliptLeft(iw.localSnap.CohortKey, iw.termWidth-15)
   558  	}
   559  	fmt.Fprintf(iw, "cohort:\t%s\n", coh)
   560  }
   561  
   562  func (iw *infoWriter) maybePrintSum() {
   563  	if !iw.verbose {
   564  		return
   565  	}
   566  	if iw.diskSnap == nil {
   567  		// TODO: expose the sha via /v2/snaps and /v2/find
   568  		return
   569  	}
   570  	if osutil.IsDirectory(iw.path) {
   571  		// no sha3_384 of a directory :-)
   572  		return
   573  	}
   574  	sha3_384, _, _ := asserts.SnapFileSHA3_384(iw.path)
   575  	if sha3_384 == "" {
   576  		return
   577  	}
   578  	fmt.Fprintf(iw, "sha3-384:\t%s\n", sha3_384)
   579  }
   580  
   581  var channelRisks = []string{"stable", "candidate", "beta", "edge"}
   582  
   583  type channelInfo struct {
   584  	indent, name, version, released, revision, size, notes string
   585  }
   586  
   587  type channelInfos struct {
   588  	channels              []*channelInfo
   589  	maxRevLen, maxSizeLen int
   590  	releasedfmt, chantpl  string
   591  	needsHeader           bool
   592  	esc                   *escapes
   593  }
   594  
   595  func (chInfos *channelInfos) add(indent, name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) {
   596  	chInfo := &channelInfo{
   597  		indent:   indent,
   598  		name:     name,
   599  		version:  version,
   600  		revision: fmt.Sprintf("(%s)", revision),
   601  		size:     strutil.SizeToStr(size),
   602  		notes:    notes.String(),
   603  	}
   604  	if !released.IsZero() {
   605  		chInfo.released = released.Format(chInfos.releasedfmt)
   606  	}
   607  	if len(chInfo.revision) > chInfos.maxRevLen {
   608  		chInfos.maxRevLen = len(chInfo.revision)
   609  	}
   610  	if len(chInfo.size) > chInfos.maxSizeLen {
   611  		chInfos.maxSizeLen = len(chInfo.size)
   612  	}
   613  	chInfos.channels = append(chInfos.channels, chInfo)
   614  }
   615  
   616  func (chInfos *channelInfos) addFromLocal(local *client.Snap) {
   617  	chInfos.add("", "installed", local.Version, local.Revision, time.Time{}, local.InstalledSize, NotesFromLocal(local))
   618  }
   619  
   620  func (chInfos *channelInfos) addOpenChannel(name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) {
   621  	chInfos.add("  ", name, version, revision, released, size, notes)
   622  }
   623  
   624  func (chInfos *channelInfos) addClosedChannel(name string, trackHasOpenChannel bool) {
   625  	chInfo := &channelInfo{indent: "  ", name: name}
   626  	if trackHasOpenChannel {
   627  		chInfo.version = chInfos.esc.uparrow
   628  	} else {
   629  		chInfo.version = chInfos.esc.dash
   630  	}
   631  
   632  	chInfos.channels = append(chInfos.channels, chInfo)
   633  }
   634  
   635  func (chInfos *channelInfos) addFromRemote(remote *client.Snap) {
   636  	// order by tracks
   637  	for _, tr := range remote.Tracks {
   638  		trackHasOpenChannel := false
   639  		for _, risk := range channelRisks {
   640  			chName := fmt.Sprintf("%s/%s", tr, risk)
   641  			ch, ok := remote.Channels[chName]
   642  			if tr == "latest" {
   643  				chName = risk
   644  			}
   645  			if ok {
   646  				chInfos.addOpenChannel(chName, ch.Version, ch.Revision, ch.ReleasedAt, ch.Size, NotesFromChannelSnapInfo(ch))
   647  				trackHasOpenChannel = true
   648  			} else {
   649  				chInfos.addClosedChannel(chName, trackHasOpenChannel)
   650  			}
   651  		}
   652  	}
   653  	chInfos.needsHeader = len(chInfos.channels) > 0
   654  }
   655  
   656  func (chInfos *channelInfos) dump(w io.Writer) {
   657  	if chInfos.needsHeader {
   658  		fmt.Fprintln(w, "channels:")
   659  	}
   660  	for _, c := range chInfos.channels {
   661  		fmt.Fprintf(w, chInfos.chantpl, c.indent, c.name, c.version, c.released, chInfos.maxRevLen, c.revision, chInfos.maxSizeLen, c.size, c.notes)
   662  	}
   663  }
   664  
   665  func (x *infoCmd) Execute([]string) error {
   666  	termWidth, _ := termSize()
   667  	termWidth -= 3
   668  	if termWidth > 100 {
   669  		// any wider than this and it gets hard to read
   670  		termWidth = 100
   671  	}
   672  
   673  	esc := x.getEscapes()
   674  	w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0)
   675  	iw := &infoWriter{
   676  		writeflusher: w,
   677  		esc:          esc,
   678  		termWidth:    termWidth,
   679  		verbose:      x.Verbose,
   680  		fmtTime:      x.fmtTime,
   681  		absTime:      x.AbsTime,
   682  	}
   683  
   684  	noneOK := true
   685  	for i, snapName := range x.Positional.Snaps {
   686  		snapName := string(snapName)
   687  		if i > 0 {
   688  			fmt.Fprintln(w, "---")
   689  		}
   690  		if snapName == "system" {
   691  			fmt.Fprintln(w, "system: You can't have it.")
   692  			continue
   693  		}
   694  
   695  		if diskSnap, err := clientSnapFromPath(snapName); err == nil {
   696  			iw.setupDiskSnap(norm(snapName), diskSnap)
   697  		} else {
   698  			remoteSnap, resInfo, _ := x.client.FindOne(snap.InstanceSnap(snapName))
   699  			localSnap, _, _ := x.client.Snap(snapName)
   700  			iw.setupSnap(localSnap, remoteSnap, resInfo)
   701  		}
   702  		// note diskSnap == nil, or localSnap == nil and remoteSnap == nil
   703  
   704  		if iw.theSnap == nil {
   705  			if len(x.Positional.Snaps) == 1 {
   706  				w.Flush()
   707  				return fmt.Errorf("no snap found for %q", snapName)
   708  			}
   709  
   710  			fmt.Fprintf(w, fmt.Sprintf(i18n.G("warning:\tno snap found for %q\n"), snapName))
   711  			continue
   712  		}
   713  		noneOK = false
   714  
   715  		iw.maybePrintPath()
   716  		iw.printName()
   717  		iw.printSummary()
   718  		iw.maybePrintHealth()
   719  		iw.maybePrintPublisher()
   720  		iw.maybePrintStandaloneVersion()
   721  		iw.maybePrintBuildDate()
   722  		iw.maybePrintContact()
   723  		iw.printLicense()
   724  		iw.maybePrintPrice()
   725  		iw.printDescr()
   726  		iw.maybePrintCommands()
   727  		iw.maybePrintServices()
   728  		iw.maybePrintNotes()
   729  		// stops the notes etc trying to be aligned with channels
   730  		iw.Flush()
   731  		iw.maybePrintType()
   732  		iw.maybePrintBase()
   733  		iw.maybePrintSum()
   734  		iw.maybePrintID()
   735  		iw.maybePrintCohortKey()
   736  		iw.maybePrintTrackingChannel()
   737  		iw.maybePrintInstallDate()
   738  		iw.maybePrintChinfo()
   739  	}
   740  	w.Flush()
   741  
   742  	if noneOK {
   743  		return fmt.Errorf(i18n.G("no valid snaps given"))
   744  	}
   745  
   746  	return nil
   747  }