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 }