github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/cmd/snap/complete.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 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  	"fmt"
    25  	"os"
    26  	"strconv"
    27  	"strings"
    28  
    29  	"github.com/jessevdk/go-flags"
    30  
    31  	"github.com/snapcore/snapd/asserts"
    32  	"github.com/snapcore/snapd/client"
    33  	"github.com/snapcore/snapd/dirs"
    34  	"github.com/snapcore/snapd/i18n"
    35  	"github.com/snapcore/snapd/snap"
    36  )
    37  
    38  type installedSnapName string
    39  
    40  func (s installedSnapName) Complete(match string) []flags.Completion {
    41  	snaps, err := mkClient().List(nil, nil)
    42  	if err != nil {
    43  		return nil
    44  	}
    45  
    46  	ret := make([]flags.Completion, 0, len(snaps))
    47  	for _, snap := range snaps {
    48  		if strings.HasPrefix(snap.Name, match) {
    49  			ret = append(ret, flags.Completion{Item: snap.Name})
    50  		}
    51  	}
    52  
    53  	return ret
    54  }
    55  
    56  func installedSnapNames(snaps []installedSnapName) []string {
    57  	names := make([]string, len(snaps))
    58  	for i, name := range snaps {
    59  		names[i] = string(name)
    60  	}
    61  
    62  	return names
    63  }
    64  
    65  func completeFromSortedFile(filename, match string) ([]flags.Completion, error) {
    66  	file, err := os.Open(filename)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	defer file.Close()
    71  
    72  	var ret []flags.Completion
    73  
    74  	// TODO: look into implementing binary search
    75  	//       e.g. https://github.com/pts/pts-line-bisect/
    76  	scanner := bufio.NewScanner(file)
    77  	for scanner.Scan() {
    78  		line := scanner.Text()
    79  		if line < match {
    80  			continue
    81  		}
    82  		if !strings.HasPrefix(line, match) {
    83  			break
    84  		}
    85  		ret = append(ret, flags.Completion{Item: line})
    86  		if len(ret) > 10000 {
    87  			// too many matches; slow machines could take too long to process this
    88  			// e.g. the bbb takes ~1s to process ~2M entries (i.e. to reach the
    89  			// point of asking the user if they actually want to see that many
    90  			// results). 10k ought to be enough for anybody.
    91  			break
    92  		}
    93  	}
    94  
    95  	return ret, nil
    96  }
    97  
    98  type remoteSnapName string
    99  
   100  func (s remoteSnapName) Complete(match string) []flags.Completion {
   101  	if ret, err := completeFromSortedFile(dirs.SnapNamesFile, match); err == nil {
   102  		return ret
   103  	}
   104  
   105  	if len(match) < 3 {
   106  		return nil
   107  	}
   108  	snaps, _, err := mkClient().Find(&client.FindOptions{
   109  		Query:  match,
   110  		Prefix: true,
   111  	})
   112  	if err != nil {
   113  		return nil
   114  	}
   115  	ret := make([]flags.Completion, len(snaps))
   116  	for i, snap := range snaps {
   117  		ret[i] = flags.Completion{Item: snap.Name}
   118  	}
   119  	return ret
   120  }
   121  
   122  func remoteSnapNames(snaps []remoteSnapName) []string {
   123  	names := make([]string, len(snaps))
   124  	for i, name := range snaps {
   125  		names[i] = string(name)
   126  	}
   127  
   128  	return names
   129  }
   130  
   131  type anySnapName string
   132  
   133  func (s anySnapName) Complete(match string) []flags.Completion {
   134  	res := installedSnapName(s).Complete(match)
   135  	seen := make(map[string]bool)
   136  	for _, x := range res {
   137  		seen[x.Item] = true
   138  	}
   139  
   140  	for _, x := range remoteSnapName(s).Complete(match) {
   141  		if !seen[x.Item] {
   142  			res = append(res, x)
   143  		}
   144  	}
   145  
   146  	return res
   147  }
   148  
   149  type changeID string
   150  
   151  func (s changeID) Complete(match string) []flags.Completion {
   152  	changes, err := mkClient().Changes(&client.ChangesOptions{Selector: client.ChangesAll})
   153  	if err != nil {
   154  		return nil
   155  	}
   156  
   157  	ret := make([]flags.Completion, 0, len(changes))
   158  	for _, change := range changes {
   159  		if strings.HasPrefix(change.ID, match) {
   160  			ret = append(ret, flags.Completion{Item: change.ID})
   161  		}
   162  	}
   163  
   164  	return ret
   165  }
   166  
   167  type assertTypeName string
   168  
   169  func (n assertTypeName) Complete(match string) []flags.Completion {
   170  	cli := mkClient()
   171  	names, err := cli.AssertionTypes()
   172  	if err != nil {
   173  		return nil
   174  	}
   175  	ret := make([]flags.Completion, 0, len(names))
   176  	for _, name := range names {
   177  		if strings.HasPrefix(name, match) {
   178  			ret = append(ret, flags.Completion{Item: name})
   179  		}
   180  	}
   181  
   182  	return ret
   183  }
   184  
   185  type keyName string
   186  
   187  func (s keyName) Complete(match string) []flags.Completion {
   188  	var res []flags.Completion
   189  	asserts.NewGPGKeypairManager().Walk(func(_ asserts.PrivateKey, _ string, uid string) error {
   190  		if strings.HasPrefix(uid, match) {
   191  			res = append(res, flags.Completion{Item: uid})
   192  		}
   193  		return nil
   194  	})
   195  	return res
   196  }
   197  
   198  type disconnectSlotOrPlugSpec struct {
   199  	SnapAndName
   200  }
   201  
   202  func (dps disconnectSlotOrPlugSpec) Complete(match string) []flags.Completion {
   203  	spec := &interfaceSpec{
   204  		SnapAndName:  dps.SnapAndName,
   205  		slots:        true,
   206  		plugs:        true,
   207  		connected:    true,
   208  		disconnected: false,
   209  	}
   210  	return spec.Complete(match)
   211  }
   212  
   213  type disconnectSlotSpec struct {
   214  	SnapAndName
   215  }
   216  
   217  // TODO: look at what the previous arg is, and filter accordingly
   218  func (dss disconnectSlotSpec) Complete(match string) []flags.Completion {
   219  	spec := &interfaceSpec{
   220  		SnapAndName:  dss.SnapAndName,
   221  		slots:        true,
   222  		plugs:        false,
   223  		connected:    true,
   224  		disconnected: false,
   225  	}
   226  	return spec.Complete(match)
   227  }
   228  
   229  type connectPlugSpec struct {
   230  	SnapAndName
   231  }
   232  
   233  func (cps connectPlugSpec) Complete(match string) []flags.Completion {
   234  	spec := &interfaceSpec{
   235  		SnapAndName:  cps.SnapAndName,
   236  		slots:        false,
   237  		plugs:        true,
   238  		connected:    false,
   239  		disconnected: true,
   240  	}
   241  	return spec.Complete(match)
   242  }
   243  
   244  type connectSlotSpec struct {
   245  	SnapAndName
   246  }
   247  
   248  // TODO: look at what the previous arg is, and filter accordingly
   249  func (css connectSlotSpec) Complete(match string) []flags.Completion {
   250  	spec := &interfaceSpec{
   251  		SnapAndName:  css.SnapAndName,
   252  		slots:        true,
   253  		plugs:        false,
   254  		connected:    false,
   255  		disconnected: true,
   256  	}
   257  	return spec.Complete(match)
   258  }
   259  
   260  type interfacesSlotOrPlugSpec struct {
   261  	SnapAndName
   262  }
   263  
   264  func (is interfacesSlotOrPlugSpec) Complete(match string) []flags.Completion {
   265  	spec := &interfaceSpec{
   266  		SnapAndName:  is.SnapAndName,
   267  		slots:        true,
   268  		plugs:        true,
   269  		connected:    true,
   270  		disconnected: true,
   271  	}
   272  	return spec.Complete(match)
   273  }
   274  
   275  type interfaceSpec struct {
   276  	SnapAndName
   277  	slots        bool
   278  	plugs        bool
   279  	connected    bool
   280  	disconnected bool
   281  }
   282  
   283  func (spec *interfaceSpec) connFilter(numConns int) bool {
   284  	if spec.connected && numConns > 0 {
   285  		return true
   286  	}
   287  	if spec.disconnected && numConns == 0 {
   288  		return true
   289  	}
   290  
   291  	return false
   292  }
   293  
   294  func (spec *interfaceSpec) Complete(match string) []flags.Completion {
   295  	// Parse what the user typed so far, it can be either
   296  	// nothing (""), a "snap", a "snap:" or a "snap:name".
   297  	parts := strings.SplitN(match, ":", 2)
   298  
   299  	// Ask snapd about available interfaces.
   300  	opts := client.ConnectionOptions{
   301  		All: true,
   302  	}
   303  	ifaces, err := mkClient().Connections(&opts)
   304  	if err != nil {
   305  		return nil
   306  	}
   307  
   308  	snaps := make(map[string]bool)
   309  
   310  	var ret []flags.Completion
   311  
   312  	var prefix string
   313  	if len(parts) == 2 {
   314  		// The user typed the colon, means they know the snap they want;
   315  		// go with that.
   316  		prefix = parts[1]
   317  		snaps[parts[0]] = true
   318  	} else {
   319  		// The user is about to or has started typing a snap name but didn't
   320  		// reach the colon yet. Offer plugs for snaps with names that start
   321  		// like that.
   322  		snapPrefix := parts[0]
   323  		if spec.plugs {
   324  			for _, plug := range ifaces.Plugs {
   325  				if strings.HasPrefix(plug.Snap, snapPrefix) && spec.connFilter(len(plug.Connections)) {
   326  					snaps[plug.Snap] = true
   327  				}
   328  			}
   329  		}
   330  		if spec.slots {
   331  			for _, slot := range ifaces.Slots {
   332  				if strings.HasPrefix(slot.Snap, snapPrefix) && spec.connFilter(len(slot.Connections)) {
   333  					snaps[slot.Snap] = true
   334  				}
   335  			}
   336  		}
   337  	}
   338  
   339  	if len(snaps) == 1 {
   340  		for snapName := range snaps {
   341  			actualName := snapName
   342  			if spec.plugs {
   343  				if spec.connected && snapName == "" {
   344  					actualName = "core"
   345  				}
   346  				for _, plug := range ifaces.Plugs {
   347  					if plug.Snap == actualName && strings.HasPrefix(plug.Name, prefix) && spec.connFilter(len(plug.Connections)) {
   348  						// TODO: in the future annotate plugs that can take
   349  						// multiple connection sensibly and don't skip those even
   350  						// if they have connections already.
   351  						ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:%s", snapName, plug.Name), Description: "plug"})
   352  					}
   353  				}
   354  			}
   355  			if spec.slots {
   356  				if actualName == "" {
   357  					actualName = "core"
   358  				}
   359  				for _, slot := range ifaces.Slots {
   360  					if slot.Snap == actualName && strings.HasPrefix(slot.Name, prefix) && spec.connFilter(len(slot.Connections)) {
   361  						ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:%s", snapName, slot.Name), Description: "slot"})
   362  					}
   363  				}
   364  			}
   365  		}
   366  	} else {
   367  	snaps:
   368  		for snapName := range snaps {
   369  			if spec.plugs {
   370  				for _, plug := range ifaces.Plugs {
   371  					if plug.Snap == snapName && spec.connFilter(len(plug.Connections)) {
   372  						ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:", snapName)})
   373  						continue snaps
   374  					}
   375  				}
   376  			}
   377  			if spec.slots {
   378  				for _, slot := range ifaces.Slots {
   379  					if slot.Snap == snapName && spec.connFilter(len(slot.Connections)) {
   380  						ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:", snapName)})
   381  						continue snaps
   382  					}
   383  				}
   384  			}
   385  		}
   386  	}
   387  
   388  	return ret
   389  }
   390  
   391  type interfaceName string
   392  
   393  func (s interfaceName) Complete(match string) []flags.Completion {
   394  	ifaces, err := mkClient().Interfaces(nil)
   395  	if err != nil {
   396  		return nil
   397  	}
   398  
   399  	ret := make([]flags.Completion, 0, len(ifaces))
   400  	for _, iface := range ifaces {
   401  		if strings.HasPrefix(iface.Name, match) {
   402  			ret = append(ret, flags.Completion{Item: iface.Name, Description: iface.Summary})
   403  		}
   404  	}
   405  
   406  	return ret
   407  }
   408  
   409  type appName string
   410  
   411  func (s appName) Complete(match string) []flags.Completion {
   412  	cli := mkClient()
   413  	apps, err := cli.Apps(nil, client.AppOptions{})
   414  	if err != nil {
   415  		return nil
   416  	}
   417  
   418  	var ret []flags.Completion
   419  	for _, app := range apps {
   420  		if app.IsService() {
   421  			continue
   422  		}
   423  		name := snap.JoinSnapApp(app.Snap, app.Name)
   424  		if !strings.HasPrefix(name, match) {
   425  			continue
   426  		}
   427  		ret = append(ret, flags.Completion{Item: name})
   428  	}
   429  
   430  	return ret
   431  }
   432  
   433  type serviceName string
   434  
   435  func (s serviceName) Complete(match string) []flags.Completion {
   436  	cli := mkClient()
   437  	apps, err := cli.Apps(nil, client.AppOptions{Service: true})
   438  	if err != nil {
   439  		return nil
   440  	}
   441  
   442  	snaps := map[string]int{}
   443  	var ret []flags.Completion
   444  	for _, app := range apps {
   445  		if !app.IsService() {
   446  			continue
   447  		}
   448  		name := snap.JoinSnapApp(app.Snap, app.Name)
   449  		if !strings.HasPrefix(name, match) {
   450  			continue
   451  		}
   452  		ret = append(ret, flags.Completion{Item: name})
   453  		if len(match) <= len(app.Snap) {
   454  			snaps[app.Snap]++
   455  		}
   456  	}
   457  	for snap, n := range snaps {
   458  		if n > 1 {
   459  			ret = append(ret, flags.Completion{Item: snap})
   460  		}
   461  	}
   462  
   463  	return ret
   464  }
   465  
   466  type aliasOrSnap string
   467  
   468  func (s aliasOrSnap) Complete(match string) []flags.Completion {
   469  	aliases, err := mkClient().Aliases()
   470  	if err != nil {
   471  		return nil
   472  	}
   473  	var ret []flags.Completion
   474  	for snap, aliases := range aliases {
   475  		if strings.HasPrefix(snap, match) {
   476  			ret = append(ret, flags.Completion{Item: snap})
   477  		}
   478  		for alias, status := range aliases {
   479  			if status.Status == "disabled" {
   480  				continue
   481  			}
   482  			if strings.HasPrefix(alias, match) {
   483  				ret = append(ret, flags.Completion{Item: alias})
   484  			}
   485  		}
   486  	}
   487  	return ret
   488  }
   489  
   490  type snapshotID string
   491  
   492  func (snapshotID) Complete(match string) []flags.Completion {
   493  	shots, err := mkClient().SnapshotSets(0, nil)
   494  	if err != nil {
   495  		return nil
   496  	}
   497  	var ret []flags.Completion
   498  	for _, sg := range shots {
   499  		sid := strconv.FormatUint(sg.ID, 10)
   500  		if strings.HasPrefix(sid, match) {
   501  			ret = append(ret, flags.Completion{Item: sid})
   502  		}
   503  	}
   504  
   505  	return ret
   506  }
   507  
   508  func (s snapshotID) ToUint() (uint64, error) {
   509  	setID, err := strconv.ParseUint((string)(s), 10, 64)
   510  	if err != nil {
   511  		return 0, fmt.Errorf(i18n.G("invalid argument for snapshot set id: expected a non-negative integer argument (see 'snap help saved')"))
   512  	}
   513  	return setID, nil
   514  }