github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/cmd/snap/cmd_find.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 "bufio" 24 "errors" 25 "fmt" 26 "os" 27 "sort" 28 "strings" 29 30 "github.com/jessevdk/go-flags" 31 32 "github.com/snapcore/snapd/client" 33 "github.com/snapcore/snapd/dirs" 34 "github.com/snapcore/snapd/i18n" 35 "github.com/snapcore/snapd/logger" 36 "github.com/snapcore/snapd/strutil" 37 ) 38 39 var shortFindHelp = i18n.G("Find packages to install") 40 var longFindHelp = i18n.G(` 41 The find command queries the store for available packages. 42 43 With the --private flag, which requires the user to be logged-in to the store 44 (see 'snap help login'), it instead searches for private snaps that the user 45 has developer access to, either directly or through the store's collaboration 46 feature. 47 48 A green check mark (given color and unicode support) after a publisher name 49 indicates that the publisher has been verified. 50 `) 51 52 func getPrice(prices map[string]float64, currency string) (float64, string, error) { 53 // If there are no prices, then the snap is free 54 if len(prices) == 0 { 55 // TRANSLATORS: free as in gratis 56 return 0, "", errors.New(i18n.G("snap is free")) 57 } 58 59 // Look up the price by currency code 60 val, ok := prices[currency] 61 62 // Fall back to dollars 63 if !ok { 64 currency = "USD" 65 val, ok = prices["USD"] 66 } 67 68 // If there aren't even dollars, grab the first currency, 69 // ordered alphabetically by currency code 70 if !ok { 71 currency = "ZZZ" 72 for c, v := range prices { 73 if c < currency { 74 currency, val = c, v 75 } 76 } 77 } 78 79 return val, currency, nil 80 } 81 82 type SectionName string 83 84 func (s SectionName) Complete(match string) []flags.Completion { 85 if ret, err := completeFromSortedFile(dirs.SnapSectionsFile, match); err == nil { 86 return ret 87 } 88 89 cli := mkClient() 90 sections, err := cli.Sections() 91 if err != nil { 92 return nil 93 } 94 ret := make([]flags.Completion, 0, len(sections)) 95 for _, s := range sections { 96 if strings.HasPrefix(s, match) { 97 ret = append(ret, flags.Completion{Item: s}) 98 } 99 } 100 return ret 101 } 102 103 func cachedSections() (sections []string, err error) { 104 cachedSections, err := os.Open(dirs.SnapSectionsFile) 105 if err != nil { 106 if os.IsNotExist(err) { 107 return nil, nil 108 } 109 return nil, err 110 } 111 defer cachedSections.Close() 112 113 r := bufio.NewScanner(cachedSections) 114 for r.Scan() { 115 sections = append(sections, r.Text()) 116 } 117 if r.Err() != nil { 118 return nil, r.Err() 119 } 120 121 return sections, nil 122 } 123 124 func getSections(cli *client.Client) (sections []string, err error) { 125 // try loading from cached sections file 126 sections, err = cachedSections() 127 if err != nil { 128 return nil, err 129 } 130 if sections != nil { 131 return sections, nil 132 } 133 // fallback to listing from the daemon 134 return cli.Sections() 135 } 136 137 func showSections(cli *client.Client) error { 138 sections, err := getSections(cli) 139 if err != nil { 140 return err 141 } 142 sort.Strings(sections) 143 144 fmt.Fprintf(Stdout, i18n.G("No section specified. Available sections:\n")) 145 for _, sec := range sections { 146 fmt.Fprintf(Stdout, " * %s\n", sec) 147 } 148 fmt.Fprintf(Stdout, i18n.G("Please try 'snap find --section=<selected section>'\n")) 149 return nil 150 } 151 152 type cmdFind struct { 153 clientMixin 154 Private bool `long:"private"` 155 Narrow bool `long:"narrow"` 156 Section SectionName `long:"section" optional:"true" optional-value:"show-all-sections-please" default:"no-section-specified" default-mask:"-"` 157 Positional struct { 158 Query string 159 } `positional-args:"yes"` 160 colorMixin 161 } 162 163 func init() { 164 addCommand("find", shortFindHelp, longFindHelp, func() flags.Commander { 165 return &cmdFind{} 166 }, colorDescs.also(map[string]string{ 167 // TRANSLATORS: This should not start with a lowercase letter. 168 "private": i18n.G("Search private snaps."), 169 // TRANSLATORS: This should not start with a lowercase letter. 170 "narrow": i18n.G("Only search for snaps in “stable”."), 171 // TRANSLATORS: This should not start with a lowercase letter. 172 "section": i18n.G("Restrict the search to a given section."), 173 }), []argDesc{{ 174 // TRANSLATORS: This needs to begin with < and end with > 175 name: i18n.G("<query>"), 176 }}).alias = "search" 177 178 } 179 180 func (x *cmdFind) Execute(args []string) error { 181 if len(args) > 0 { 182 return ErrExtraArgs 183 } 184 185 // LP: 1740605 186 if strings.TrimSpace(x.Positional.Query) == "" { 187 x.Positional.Query = "" 188 } 189 190 // section will be: 191 // - "show-all-sections-please" if the user specified --section 192 // without any argument 193 // - "no-section-specified" if "--section" was not specified on 194 // the commandline at all 195 switch x.Section { 196 case "show-all-sections-please": 197 return showSections(x.client) 198 case "no-section-specified": 199 x.Section = "" 200 } 201 202 // magic! `snap find` returns the featured snaps 203 showFeatured := (x.Positional.Query == "" && x.Section == "") 204 if showFeatured { 205 x.Section = "featured" 206 } 207 208 if x.Section != "" && x.Section != "featured" { 209 sections, err := cachedSections() 210 if err != nil { 211 return err 212 } 213 if !strutil.ListContains(sections, string(x.Section)) { 214 // try the store just in case it was added in the last 24 hours 215 sections, err = x.client.Sections() 216 if err != nil { 217 return err 218 } 219 if !strutil.ListContains(sections, string(x.Section)) { 220 // TRANSLATORS: the %q is the (quoted) name of the section the user entered 221 return fmt.Errorf(i18n.G("No matching section %q, use --section to list existing sections"), x.Section) 222 } 223 } 224 } 225 226 opts := &client.FindOptions{ 227 Query: x.Positional.Query, 228 Section: string(x.Section), 229 Private: x.Private, 230 } 231 232 if !x.Narrow { 233 opts.Scope = "wide" 234 } 235 236 snaps, resInfo, err := x.client.Find(opts) 237 if e, ok := err.(*client.Error); ok && (e.Kind == client.ErrorKindNetworkTimeout || e.Kind == client.ErrorKindDNSFailure) { 238 logger.Debugf("cannot list snaps: %v", e) 239 return fmt.Errorf("unable to contact snap store") 240 } 241 if err != nil { 242 return err 243 } 244 if len(snaps) == 0 { 245 if x.Section == "" { 246 // TRANSLATORS: the %q is the (quoted) query the user entered 247 fmt.Fprintf(Stderr, i18n.G("No matching snaps for %q\n"), opts.Query) 248 } else { 249 // TRANSLATORS: the first %q is the (quoted) query, the 250 // second %q is the (quoted) name of the section the 251 // user entered 252 fmt.Fprintf(Stderr, i18n.G("No matching snaps for %q in section %q\n"), opts.Query, x.Section) 253 } 254 return nil 255 } 256 257 // show featured header *after* we checked for errors from the find 258 if showFeatured { 259 fmt.Fprint(Stdout, i18n.G("No search term specified. Here are some interesting snaps:\n\n")) 260 } 261 262 esc := x.getEscapes() 263 w := tabWriter() 264 // TRANSLATORS: the %s is to insert a filler escape sequence (please keep it flush to the column header, with no extra spaces) 265 fmt.Fprintf(w, i18n.G("Name\tVersion\tPublisher%s\tNotes\tSummary\n"), fillerPublisher(esc)) 266 for _, snap := range snaps { 267 fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, shortPublisher(esc, snap.Publisher), NotesFromRemote(snap, resInfo), snap.Summary) 268 } 269 w.Flush() 270 if showFeatured { 271 fmt.Fprint(Stdout, i18n.G("\nProvide a search term for more specific results.\n")) 272 } 273 return nil 274 }