github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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/client/clientutil" 39 "github.com/snapcore/snapd/i18n" 40 "github.com/snapcore/snapd/osutil" 41 "github.com/snapcore/snapd/snap" 42 "github.com/snapcore/snapd/snap/snapfile" 43 "github.com/snapcore/snapd/snap/squashfs" 44 "github.com/snapcore/snapd/strutil" 45 ) 46 47 type infoCmd struct { 48 clientMixin 49 colorMixin 50 timeMixin 51 52 Verbose bool `long:"verbose"` 53 Positional struct { 54 Snaps []anySnapName `positional-arg-name:"<snap>" required:"1"` 55 } `positional-args:"yes" required:"yes"` 56 } 57 58 var shortInfoHelp = i18n.G("Show detailed information about snaps") 59 var longInfoHelp = i18n.G(` 60 The info command shows detailed information about snaps. 61 62 The snaps can be specified by name or by path; names are looked for both in the 63 store and in the installed snaps; paths can refer to a .snap file, or to a 64 directory that contains an unpacked snap suitable for 'snap try' (an example 65 of this would be the 'prime' directory snapcraft produces). 66 `) 67 68 func init() { 69 addCommand("info", 70 shortInfoHelp, 71 longInfoHelp, 72 func() flags.Commander { 73 return &infoCmd{} 74 }, colorDescs.also(timeDescs).also(map[string]string{ 75 // TRANSLATORS: This should not start with a lowercase letter. 76 "verbose": i18n.G("Include more details on the snap (expanded notes, base, etc.)"), 77 }), nil) 78 } 79 80 func (iw *infoWriter) maybePrintHealth() { 81 if iw.localSnap == nil { 82 return 83 } 84 health := iw.localSnap.Health 85 if health == nil { 86 if !iw.verbose { 87 return 88 } 89 health = &client.SnapHealth{ 90 Status: "unknown", 91 Message: "health has not been set", 92 } 93 } 94 if health.Status == "okay" && !iw.verbose { 95 return 96 } 97 98 fmt.Fprintln(iw, "health:") 99 fmt.Fprintf(iw, " status:\t%s\n", health.Status) 100 if health.Message != "" { 101 wrapGeneric(iw, quotedIfNeeded(health.Message), " message:\t", " ", iw.termWidth) 102 } 103 if health.Code != "" { 104 fmt.Fprintf(iw, " code:\t%s\n", health.Code) 105 } 106 if !health.Timestamp.IsZero() { 107 fmt.Fprintf(iw, " checked:\t%s\n", iw.fmtTime(health.Timestamp)) 108 } 109 if !health.Revision.Unset() { 110 fmt.Fprintf(iw, " revision:\t%s\n", health.Revision) 111 } 112 iw.Flush() 113 } 114 115 func clientSnapFromPath(path string) (*client.Snap, error) { 116 snapf, err := snapfile.Open(path) 117 if err != nil { 118 return nil, err 119 } 120 info, err := snap.ReadInfoFromSnapFile(snapf, nil) 121 if err != nil { 122 return nil, err 123 } 124 125 direct, err := clientutil.ClientSnapFromSnapInfo(info, nil) 126 if err != nil { 127 return nil, err 128 } 129 130 return direct, nil 131 } 132 133 func norm(path string) string { 134 path = filepath.Clean(path) 135 if osutil.IsDirectory(path) { 136 path = path + "/" 137 } 138 139 return path 140 } 141 142 // runesTrimRightSpace returns text, with any trailing whitespace dropped. 143 func runesTrimRightSpace(text []rune) []rune { 144 j := len(text) 145 for j > 0 && unicode.IsSpace(text[j-1]) { 146 j-- 147 } 148 return text[:j] 149 } 150 151 // runesLastIndexSpace returns the index of the last whitespace rune 152 // in the text. If the text has no whitespace, returns -1. 153 func runesLastIndexSpace(text []rune) int { 154 for i := len(text) - 1; i >= 0; i-- { 155 if unicode.IsSpace(text[i]) { 156 return i 157 } 158 } 159 return -1 160 } 161 162 // wrapLine wraps a line, assumed to be part of a block-style yaml 163 // string, to fit into termWidth, preserving the line's indent, and 164 // writes it out prepending padding to each line. 165 func wrapLine(out io.Writer, text []rune, pad string, termWidth int) error { 166 // discard any trailing whitespace 167 text = runesTrimRightSpace(text) 168 // establish the indent of the whole block 169 idx := 0 170 for idx < len(text) && unicode.IsSpace(text[idx]) { 171 idx++ 172 } 173 indent := pad + string(text[:idx]) 174 text = text[idx:] 175 if len(indent) > termWidth/2 { 176 // If indent is too big there's not enough space for the actual 177 // text, in the pathological case the indent can even be bigger 178 // than the terminal which leads to lp:1828425. 179 // Rather than let that happen, give up. 180 indent = pad + " " 181 } 182 return wrapGeneric(out, text, indent, indent, termWidth) 183 } 184 185 // wrapFlow wraps the text using yaml's flow style, allowing indent 186 // characters for the first line. 187 func wrapFlow(out io.Writer, text []rune, indent string, termWidth int) error { 188 return wrapGeneric(out, text, indent, " ", termWidth) 189 } 190 191 // wrapGeneric wraps the given text to the given width, prefixing the 192 // first line with indent and the remaining lines with indent2 193 func wrapGeneric(out io.Writer, text []rune, indent, indent2 string, termWidth int) error { 194 // Note: this is _wrong_ for much of unicode (because the width of a rune on 195 // the terminal is anything between 0 and 2, not always 1 as this code 196 // assumes) but fixing that is Hard. Long story short, you can get close 197 // using a couple of big unicode tables (which is what wcwidth 198 // does). Getting it 100% requires a terminfo-alike of unicode behaviour. 199 // However, before this we'd count bytes instead of runes, so we'd be 200 // even more broken. Think of it as successive approximations... at least 201 // with this work we share tabwriter's opinion on the width of things! 202 203 // This (and possibly printDescr below) should move to strutil once 204 // we're happy with it getting wider (heh heh) use. 205 206 indentWidth := utf8.RuneCountInString(indent) 207 delta := indentWidth - utf8.RuneCountInString(indent2) 208 width := termWidth - indentWidth 209 210 // establish the indent of the whole block 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) maybePrintStoreURL() { 405 storeURL := "" 406 // XXX: store-url for local snaps comes from aux data, but that gets 407 // updated only when the snap is refreshed, be smart and poke remote 408 // snap info if available 409 switch { 410 case iw.theSnap.StoreURL != "": 411 storeURL = iw.theSnap.StoreURL 412 case iw.remoteSnap != nil && iw.remoteSnap.StoreURL != "": 413 storeURL = iw.remoteSnap.StoreURL 414 } 415 if storeURL == "" { 416 return 417 } 418 fmt.Fprintf(iw, "store-url:\t%s\n", storeURL) 419 } 420 421 func (iw *infoWriter) maybePrintPublisher() { 422 if iw.diskSnap != nil { 423 // snaps read from disk won't have a publisher 424 return 425 } 426 fmt.Fprintf(iw, "publisher:\t%s\n", longPublisher(iw.esc, iw.theSnap.Publisher)) 427 } 428 429 func (iw *infoWriter) maybePrintStandaloneVersion() { 430 if iw.diskSnap == nil { 431 // snaps not read from disk will have version information shown elsewhere 432 return 433 } 434 version := iw.diskSnap.Version 435 if version == "" { 436 version = iw.esc.dash 437 } 438 // NotesFromRemote might be better called NotesFromNotInstalled but that's nasty 439 fmt.Fprintf(iw, "version:\t%s %s\n", version, NotesFromRemote(iw.diskSnap, nil)) 440 } 441 442 func (iw *infoWriter) maybePrintBuildDate() { 443 if iw.diskSnap == nil { 444 return 445 } 446 if osutil.IsDirectory(iw.path) { 447 return 448 } 449 buildDate := squashfs.BuildDate(iw.path) 450 if buildDate.IsZero() { 451 return 452 } 453 fmt.Fprintf(iw, "build-date:\t%s\n", iw.fmtTime(buildDate)) 454 } 455 456 func (iw *infoWriter) maybePrintContact() error { 457 contact := strings.TrimPrefix(iw.theSnap.Contact, "mailto:") 458 if contact == "" { 459 return nil 460 } 461 _, err := fmt.Fprintf(iw, "contact:\t%s\n", contact) 462 return err 463 } 464 465 func (iw *infoWriter) printLicense() { 466 license := iw.theSnap.License 467 if license == "" { 468 license = "unset" 469 } 470 fmt.Fprintf(iw, "license:\t%s\n", license) 471 } 472 473 func (iw *infoWriter) printDescr() { 474 fmt.Fprintln(iw, "description: |") 475 printDescr(iw, iw.theSnap.Description, iw.termWidth) 476 } 477 478 func (iw *infoWriter) maybePrintCommands() { 479 if len(iw.theSnap.Apps) == 0 { 480 return 481 } 482 483 commands := make([]string, 0, len(iw.theSnap.Apps)) 484 for _, app := range iw.theSnap.Apps { 485 if app.IsService() { 486 continue 487 } 488 489 cmdStr := snap.JoinSnapApp(iw.theSnap.Name, app.Name) 490 commands = append(commands, cmdStr) 491 } 492 if len(commands) == 0 { 493 return 494 } 495 496 fmt.Fprintf(iw, "commands:\n") 497 for _, cmd := range commands { 498 fmt.Fprintf(iw, " - %s\n", cmd) 499 } 500 } 501 502 func (iw *infoWriter) maybePrintServices() { 503 if len(iw.theSnap.Apps) == 0 { 504 return 505 } 506 507 services := make([]string, 0, len(iw.theSnap.Apps)) 508 for _, app := range iw.theSnap.Apps { 509 if !app.IsService() { 510 continue 511 } 512 513 var active, enabled string 514 if app.Active { 515 active = "active" 516 } else { 517 active = "inactive" 518 } 519 if app.Enabled { 520 enabled = "enabled" 521 } else { 522 enabled = "disabled" 523 } 524 services = append(services, fmt.Sprintf(" %s:\t%s, %s, %s", snap.JoinSnapApp(iw.theSnap.Name, app.Name), app.Daemon, enabled, active)) 525 } 526 if len(services) == 0 { 527 return 528 } 529 530 fmt.Fprintf(iw, "services:\n") 531 for _, svc := range services { 532 fmt.Fprintln(iw, svc) 533 } 534 } 535 536 func (iw *infoWriter) maybePrintNotes() { 537 if !iw.verbose { 538 return 539 } 540 fmt.Fprintln(iw, "notes:\t") 541 fmt.Fprintf(iw, " private:\t%t\n", iw.theSnap.Private) 542 fmt.Fprintf(iw, " confinement:\t%s\n", iw.theSnap.Confinement) 543 if iw.localSnap == nil { 544 return 545 } 546 jailMode := iw.localSnap.Confinement == client.DevModeConfinement && !iw.localSnap.DevMode 547 fmt.Fprintf(iw, " devmode:\t%t\n", iw.localSnap.DevMode) 548 fmt.Fprintf(iw, " jailmode:\t%t\n", jailMode) 549 fmt.Fprintf(iw, " trymode:\t%t\n", iw.localSnap.TryMode) 550 fmt.Fprintf(iw, " enabled:\t%t\n", iw.localSnap.Status == client.StatusActive) 551 if iw.localSnap.Broken == "" { 552 fmt.Fprintf(iw, " broken:\t%t\n", false) 553 } else { 554 fmt.Fprintf(iw, " broken:\t%t (%s)\n", true, iw.localSnap.Broken) 555 } 556 557 fmt.Fprintf(iw, " ignore-validation:\t%t\n", iw.localSnap.IgnoreValidation) 558 return 559 } 560 561 func (iw *infoWriter) maybePrintCohortKey() { 562 if !iw.verbose { 563 return 564 } 565 if iw.localSnap == nil { 566 return 567 } 568 coh := iw.localSnap.CohortKey 569 if coh == "" { 570 return 571 } 572 if isStdoutTTY { 573 // 15 is 1 + the length of "refresh-date: " 574 coh = strutil.ElliptLeft(iw.localSnap.CohortKey, iw.termWidth-15) 575 } 576 fmt.Fprintf(iw, "cohort:\t%s\n", coh) 577 } 578 579 func (iw *infoWriter) maybePrintSum() { 580 if !iw.verbose { 581 return 582 } 583 if iw.diskSnap == nil { 584 // TODO: expose the sha via /v2/snaps and /v2/find 585 return 586 } 587 if osutil.IsDirectory(iw.path) { 588 // no sha3_384 of a directory :-) 589 return 590 } 591 sha3_384, _, _ := asserts.SnapFileSHA3_384(iw.path) 592 if sha3_384 == "" { 593 return 594 } 595 fmt.Fprintf(iw, "sha3-384:\t%s\n", sha3_384) 596 } 597 598 var channelRisks = []string{"stable", "candidate", "beta", "edge"} 599 600 type channelInfo struct { 601 indent, name, version, released, revision, size, notes string 602 } 603 604 type channelInfos struct { 605 channels []*channelInfo 606 maxRevLen, maxSizeLen int 607 releasedfmt, chantpl string 608 needsHeader bool 609 esc *escapes 610 } 611 612 func (chInfos *channelInfos) add(indent, name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) { 613 chInfo := &channelInfo{ 614 indent: indent, 615 name: name, 616 version: version, 617 revision: fmt.Sprintf("(%s)", revision), 618 size: strutil.SizeToStr(size), 619 notes: notes.String(), 620 } 621 if !released.IsZero() { 622 chInfo.released = released.Format(chInfos.releasedfmt) 623 } 624 if len(chInfo.revision) > chInfos.maxRevLen { 625 chInfos.maxRevLen = len(chInfo.revision) 626 } 627 if len(chInfo.size) > chInfos.maxSizeLen { 628 chInfos.maxSizeLen = len(chInfo.size) 629 } 630 chInfos.channels = append(chInfos.channels, chInfo) 631 } 632 633 func (chInfos *channelInfos) addFromLocal(local *client.Snap) { 634 chInfos.add("", "installed", local.Version, local.Revision, time.Time{}, local.InstalledSize, NotesFromLocal(local)) 635 } 636 637 func (chInfos *channelInfos) addOpenChannel(name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) { 638 chInfos.add(" ", name, version, revision, released, size, notes) 639 } 640 641 func (chInfos *channelInfos) addClosedChannel(name string, trackHasOpenChannel bool) { 642 chInfo := &channelInfo{indent: " ", name: name} 643 if trackHasOpenChannel { 644 chInfo.version = chInfos.esc.uparrow 645 } else { 646 chInfo.version = chInfos.esc.dash 647 } 648 649 chInfos.channels = append(chInfos.channels, chInfo) 650 } 651 652 func (chInfos *channelInfos) addFromRemote(remote *client.Snap) { 653 // order by tracks 654 for _, tr := range remote.Tracks { 655 trackHasOpenChannel := false 656 for _, risk := range channelRisks { 657 chName := fmt.Sprintf("%s/%s", tr, risk) 658 ch, ok := remote.Channels[chName] 659 if ok { 660 chInfos.addOpenChannel(chName, ch.Version, ch.Revision, ch.ReleasedAt, ch.Size, NotesFromChannelSnapInfo(ch)) 661 trackHasOpenChannel = true 662 } else { 663 chInfos.addClosedChannel(chName, trackHasOpenChannel) 664 } 665 } 666 } 667 chInfos.needsHeader = len(chInfos.channels) > 0 668 } 669 670 func (chInfos *channelInfos) dump(w io.Writer) { 671 if chInfos.needsHeader { 672 fmt.Fprintln(w, "channels:") 673 } 674 for _, c := range chInfos.channels { 675 fmt.Fprintf(w, chInfos.chantpl, c.indent, c.name, c.version, c.released, chInfos.maxRevLen, c.revision, chInfos.maxSizeLen, c.size, c.notes) 676 } 677 } 678 679 func (x *infoCmd) Execute([]string) error { 680 termWidth, _ := termSize() 681 termWidth -= 3 682 if termWidth > 100 { 683 // any wider than this and it gets hard to read 684 termWidth = 100 685 } 686 687 esc := x.getEscapes() 688 w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) 689 iw := &infoWriter{ 690 writeflusher: w, 691 esc: esc, 692 termWidth: termWidth, 693 verbose: x.Verbose, 694 fmtTime: x.fmtTime, 695 absTime: x.AbsTime, 696 } 697 698 noneOK := true 699 for i, snapName := range x.Positional.Snaps { 700 snapName := string(snapName) 701 if i > 0 { 702 fmt.Fprintln(w, "---") 703 } 704 if snapName == "system" { 705 fmt.Fprintln(w, "system: You can't have it.") 706 continue 707 } 708 709 if diskSnap, err := clientSnapFromPath(snapName); err == nil { 710 iw.setupDiskSnap(norm(snapName), diskSnap) 711 } else { 712 remoteSnap, resInfo, _ := x.client.FindOne(snap.InstanceSnap(snapName)) 713 localSnap, _, _ := x.client.Snap(snapName) 714 iw.setupSnap(localSnap, remoteSnap, resInfo) 715 } 716 // note diskSnap == nil, or localSnap == nil and remoteSnap == nil 717 718 if iw.theSnap == nil { 719 if len(x.Positional.Snaps) == 1 { 720 w.Flush() 721 return fmt.Errorf("no snap found for %q", snapName) 722 } 723 724 fmt.Fprintf(w, fmt.Sprintf(i18n.G("warning:\tno snap found for %q\n"), snapName)) 725 continue 726 } 727 noneOK = false 728 729 iw.maybePrintPath() 730 iw.printName() 731 iw.printSummary() 732 iw.maybePrintHealth() 733 iw.maybePrintPublisher() 734 iw.maybePrintStoreURL() 735 iw.maybePrintStandaloneVersion() 736 iw.maybePrintBuildDate() 737 iw.maybePrintContact() 738 iw.printLicense() 739 iw.maybePrintPrice() 740 iw.printDescr() 741 iw.maybePrintCommands() 742 iw.maybePrintServices() 743 iw.maybePrintNotes() 744 // stops the notes etc trying to be aligned with channels 745 iw.Flush() 746 iw.maybePrintType() 747 iw.maybePrintBase() 748 iw.maybePrintSum() 749 iw.maybePrintID() 750 iw.maybePrintCohortKey() 751 iw.maybePrintTrackingChannel() 752 iw.maybePrintInstallDate() 753 iw.maybePrintChinfo() 754 } 755 w.Flush() 756 757 if noneOK { 758 return fmt.Errorf(i18n.G("no valid snaps given")) 759 } 760 761 return nil 762 }