github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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 } 559 560 func (iw *infoWriter) maybePrintCohortKey() { 561 if !iw.verbose { 562 return 563 } 564 if iw.localSnap == nil { 565 return 566 } 567 coh := iw.localSnap.CohortKey 568 if coh == "" { 569 return 570 } 571 if isStdoutTTY { 572 // 15 is 1 + the length of "refresh-date: " 573 coh = strutil.ElliptLeft(iw.localSnap.CohortKey, iw.termWidth-15) 574 } 575 fmt.Fprintf(iw, "cohort:\t%s\n", coh) 576 } 577 578 func (iw *infoWriter) maybePrintSum() { 579 if !iw.verbose { 580 return 581 } 582 if iw.diskSnap == nil { 583 // TODO: expose the sha via /v2/snaps and /v2/find 584 return 585 } 586 if osutil.IsDirectory(iw.path) { 587 // no sha3_384 of a directory :-) 588 return 589 } 590 sha3_384, _, _ := asserts.SnapFileSHA3_384(iw.path) 591 if sha3_384 == "" { 592 return 593 } 594 fmt.Fprintf(iw, "sha3-384:\t%s\n", sha3_384) 595 } 596 597 var channelRisks = []string{"stable", "candidate", "beta", "edge"} 598 599 type channelInfo struct { 600 indent, name, version, released, revision, size, notes string 601 } 602 603 type channelInfos struct { 604 channels []*channelInfo 605 maxRevLen, maxSizeLen int 606 releasedfmt, chantpl string 607 needsHeader bool 608 esc *escapes 609 } 610 611 func (chInfos *channelInfos) add(indent, name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) { 612 chInfo := &channelInfo{ 613 indent: indent, 614 name: name, 615 version: version, 616 revision: fmt.Sprintf("(%s)", revision), 617 size: strutil.SizeToStr(size), 618 notes: notes.String(), 619 } 620 if !released.IsZero() { 621 chInfo.released = released.Format(chInfos.releasedfmt) 622 } 623 if len(chInfo.revision) > chInfos.maxRevLen { 624 chInfos.maxRevLen = len(chInfo.revision) 625 } 626 if len(chInfo.size) > chInfos.maxSizeLen { 627 chInfos.maxSizeLen = len(chInfo.size) 628 } 629 chInfos.channels = append(chInfos.channels, chInfo) 630 } 631 632 func (chInfos *channelInfos) addFromLocal(local *client.Snap) { 633 chInfos.add("", "installed", local.Version, local.Revision, time.Time{}, local.InstalledSize, NotesFromLocal(local)) 634 } 635 636 func (chInfos *channelInfos) addOpenChannel(name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) { 637 chInfos.add(" ", name, version, revision, released, size, notes) 638 } 639 640 func (chInfos *channelInfos) addClosedChannel(name string, trackHasOpenChannel bool) { 641 chInfo := &channelInfo{indent: " ", name: name} 642 if trackHasOpenChannel { 643 chInfo.version = chInfos.esc.uparrow 644 } else { 645 chInfo.version = chInfos.esc.dash 646 } 647 648 chInfos.channels = append(chInfos.channels, chInfo) 649 } 650 651 func (chInfos *channelInfos) addFromRemote(remote *client.Snap) { 652 // order by tracks 653 for _, tr := range remote.Tracks { 654 trackHasOpenChannel := false 655 for _, risk := range channelRisks { 656 chName := fmt.Sprintf("%s/%s", tr, risk) 657 ch, ok := remote.Channels[chName] 658 if ok { 659 chInfos.addOpenChannel(chName, ch.Version, ch.Revision, ch.ReleasedAt, ch.Size, NotesFromChannelSnapInfo(ch)) 660 trackHasOpenChannel = true 661 } else { 662 chInfos.addClosedChannel(chName, trackHasOpenChannel) 663 } 664 } 665 } 666 chInfos.needsHeader = len(chInfos.channels) > 0 667 } 668 669 func (chInfos *channelInfos) dump(w io.Writer) { 670 if chInfos.needsHeader { 671 fmt.Fprintln(w, "channels:") 672 } 673 for _, c := range chInfos.channels { 674 fmt.Fprintf(w, chInfos.chantpl, c.indent, c.name, c.version, c.released, chInfos.maxRevLen, c.revision, chInfos.maxSizeLen, c.size, c.notes) 675 } 676 } 677 678 func (x *infoCmd) Execute([]string) error { 679 termWidth, _ := termSize() 680 termWidth -= 3 681 if termWidth > 100 { 682 // any wider than this and it gets hard to read 683 termWidth = 100 684 } 685 686 esc := x.getEscapes() 687 w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) 688 iw := &infoWriter{ 689 writeflusher: w, 690 esc: esc, 691 termWidth: termWidth, 692 verbose: x.Verbose, 693 fmtTime: x.fmtTime, 694 absTime: x.AbsTime, 695 } 696 697 noneOK := true 698 for i, snapName := range x.Positional.Snaps { 699 snapName := string(snapName) 700 if i > 0 { 701 fmt.Fprintln(w, "---") 702 } 703 if snapName == "system" { 704 fmt.Fprintln(w, "system: You can't have it.") 705 continue 706 } 707 708 if diskSnap, err := clientSnapFromPath(snapName); err == nil { 709 iw.setupDiskSnap(norm(snapName), diskSnap) 710 } else { 711 remoteSnap, resInfo, _ := x.client.FindOne(snap.InstanceSnap(snapName)) 712 localSnap, _, _ := x.client.Snap(snapName) 713 iw.setupSnap(localSnap, remoteSnap, resInfo) 714 } 715 // note diskSnap == nil, or localSnap == nil and remoteSnap == nil 716 717 if iw.theSnap == nil { 718 if len(x.Positional.Snaps) == 1 { 719 w.Flush() 720 return fmt.Errorf("no snap found for %q", snapName) 721 } 722 723 fmt.Fprintf(w, fmt.Sprintf(i18n.G("warning:\tno snap found for %q\n"), snapName)) 724 continue 725 } 726 noneOK = false 727 728 iw.maybePrintPath() 729 iw.printName() 730 iw.printSummary() 731 iw.maybePrintHealth() 732 iw.maybePrintPublisher() 733 iw.maybePrintStoreURL() 734 iw.maybePrintStandaloneVersion() 735 iw.maybePrintBuildDate() 736 iw.maybePrintContact() 737 iw.printLicense() 738 iw.maybePrintPrice() 739 iw.printDescr() 740 iw.maybePrintCommands() 741 iw.maybePrintServices() 742 iw.maybePrintNotes() 743 // stops the notes etc trying to be aligned with channels 744 iw.Flush() 745 iw.maybePrintType() 746 iw.maybePrintBase() 747 iw.maybePrintSum() 748 iw.maybePrintID() 749 iw.maybePrintCohortKey() 750 iw.maybePrintTrackingChannel() 751 iw.maybePrintInstallDate() 752 iw.maybePrintChinfo() 753 } 754 w.Flush() 755 756 if noneOK { 757 return fmt.Errorf(i18n.G("no valid snaps given")) 758 } 759 760 return nil 761 }