Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 52 additions & 12 deletions cmd/snapd/cli/cmd_find.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,40 @@ func getPrice(prices map[string]float64, currency string) (float64, string, erro
return val, currency, nil
}

const (
// sectionShowAllValue is used when the user specified --section
// without any argument.
sectionShowAllValue = "show-all-sections-please"
// sectionDefaultValue is used when "--section" was not specified on
// the commandline at all.
sectionDefaultValue = "no-section-specified"
)

type SectionName string

// UnmarshalFlag accumulates repeated --section flags into a comma-separated
// list instead of overwriting, since the store already matches against any
// of them.
func (s *SectionName) UnmarshalFlag(value string) error {
switch string(*s) {
case "", sectionDefaultValue:
*s = SectionName(value)
case sectionShowAllValue:
// a bare --section (show all sections) was already given; an
// explicit value after that doesn't combine sensibly with "show
// all", so let the explicit value take over instead of joining
*s = SectionName(value)
default:
if value == sectionShowAllValue {
// bare --section after one or more explicit values: keep
// filtering rather than switching to "show all sections"
return nil
}
*s += "," + SectionName(value)
}
return nil
}

func (s SectionName) Complete(match string) []flags.Completion {
if ret, err := completeFromSortedFile(dirs.SnapSectionsFile, match); err == nil {
return ret
Expand All @@ -100,6 +132,16 @@ func (s SectionName) Complete(match string) []flags.Completion {
return ret
}

func unknownSections(requested, known []string) []string {
var unknown []string
for _, r := range requested {
if !strutil.ListContains(known, r) {
unknown = append(unknown, r)
}
}
return unknown
}

func cachedSections() (sections []string, err error) {
cachedSections, err := os.Open(dirs.SnapSectionsFile)
if err != nil {
Expand Down Expand Up @@ -188,15 +230,10 @@ func (x *cmdFind) Execute(args []string) error {
query = ""
}

// section will be:
// - "show-all-sections-please" if the user specified --section
// without any argument
// - "no-section-specified" if "--section" was not specified on
// the commandline at all
switch x.Section {
case "show-all-sections-please":
case sectionShowAllValue:
return showSections(x.client)
case "no-section-specified":
case sectionDefaultValue:
x.Section = ""
}

Expand All @@ -207,19 +244,22 @@ func (x *cmdFind) Execute(args []string) error {
}

if x.Section != "" && x.Section != "featured" {
requested := strutil.CommaSeparatedList(string(x.Section))
sections, err := cachedSections()
if err != nil {
return err
}
if !strutil.ListContains(sections, string(x.Section)) {
// try the store just in case it was added in the last 24 hours
unknown := unknownSections(requested, sections)
if len(unknown) > 0 {
// try the store just in case they were added in the last 24 hours
sections, err = x.client.Sections()
if err != nil {
return err
}
if !strutil.ListContains(sections, string(x.Section)) {
// TRANSLATORS: the %q is the (quoted) name of the section the user entered
return fmt.Errorf(i18n.G("No matching section %q, use --section to list existing sections"), x.Section)
unknown = unknownSections(requested, sections)
if len(unknown) > 0 {
// TRANSLATORS: %s is a comma-separated list of one or more (quoted) section names the user entered
return fmt.Errorf(i18n.G("No matching section(s) %s, use --section to list existing sections"), strutil.Quoted(unknown))
}
}
}
Expand Down
86 changes: 84 additions & 2 deletions cmd/snapd/cli/cmd_find_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ func (s *SnapSuite) TestFindSnapInvalidSection(c *check.C) {
n++
})
_, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section=foobar", "hello"})
c.Assert(err, check.ErrorMatches, `No matching section "foobar", use --section to list existing sections`)
c.Assert(err, check.ErrorMatches, `No matching section\(s\) "foobar", use --section to list existing sections`)
}

func (s *SnapSuite) TestFindSnapNotFoundInSection(c *check.C) {
Expand Down Expand Up @@ -679,7 +679,7 @@ func (s *SnapSuite) TestFindSnapCachedSection(c *check.C) {

_, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section=foobar", "hello"})
c.Logf("stdout: %s", s.Stdout())
c.Assert(err, check.ErrorMatches, `No matching section "foobar", use --section to list existing sections`)
c.Assert(err, check.ErrorMatches, `No matching section\(s\) "foobar", use --section to list existing sections`)

s.ResetStdStreams()

Expand All @@ -698,3 +698,85 @@ Please try 'snap find --section=<selected section>'
s.ResetStdStreams()
c.Check(numHits, check.Equals, 1)
}

func (s *SnapSuite) TestFindRepeatedSectionFlagSearchesBoth(c *check.C) {
n := 0
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch n {
case 0:
c.Check(r.Method, check.Equals, "GET")
c.Check(r.URL.Path, check.Equals, "/v2/sections")
EncodeResponseBody(c, w, map[string]any{
"type": "sync",
"result": []string{"games", "social"},
})
case 1:
c.Check(r.Method, check.Equals, "GET")
c.Check(r.URL.Path, check.Equals, "/v2/find")
v, ok := r.URL.Query()["section"]
c.Check(ok, check.Equals, true)
c.Check(v, check.DeepEquals, []string{"games,social"})
EncodeResponseBody(c, w, map[string]any{
"type": "sync",
"result": []string{},
})
default:
c.Fatalf("expected to get 2 requests, now on #%d", n+1)
}
n++
})

_, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section=games", "--section=social", "steam"})
c.Assert(err, check.IsNil)
}

func (s *SnapSuite) TestFindSectionFlagCommaAndRepeatCombo(c *check.C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v2/sections":
EncodeResponseBody(c, w, map[string]any{
"type": "sync",
"result": []string{"x", "y", "z", "a", "b", "c"},
})
case "/v2/find":
v, ok := r.URL.Query()["section"]
c.Check(ok, check.Equals, true)
c.Check(v, check.DeepEquals, []string{"x,y,z,a,b,c"})
EncodeResponseBody(c, w, map[string]any{
"type": "sync",
"result": []string{},
})
default:
c.Fatalf("unexpected request to %s", r.URL.Path)
}
})

_, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section=x,y", "--section=z", "--section=a,b,c", "steam"})
c.Assert(err, check.IsNil)
}

func (s *SnapSuite) TestFindRepeatedSectionFlagUnknownSection(c *check.C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.URL.Path, check.Equals, "/v2/sections")
EncodeResponseBody(c, w, map[string]any{
"type": "sync",
"result": []string{"games"},
})
})

_, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section=games", "--section=bogus", "steam"})
c.Assert(err, check.ErrorMatches, `No matching section\(s\) "bogus", use --section to list existing sections`)
}

func (s *SnapSuite) TestFindRepeatedSectionFlagAllUnknownSections(c *check.C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.URL.Path, check.Equals, "/v2/sections")
EncodeResponseBody(c, w, map[string]any{
"type": "sync",
"result": []string{"games", "social"},
})
})

_, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "--section=bogus1", "--section=bogus2", "steam"})
c.Assert(err, check.ErrorMatches, `No matching section\(s\) "bogus1", "bogus2", use --section to list existing sections`)
}