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 }