github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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 in the stable channel.
    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"`
   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  }