github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/cmd/snap/complete.go (about)

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