github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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 }