github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/auto-complete.go (about) 1 // Copyright (c) 2015-2022 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 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 Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "os" 22 "path/filepath" 23 "sort" 24 "strings" 25 26 "github.com/minio/cli" 27 "github.com/posener/complete" 28 ) 29 30 // fsComplete knows how to complete file/dir names by the given path 31 type fsComplete struct{} 32 33 // predictPathWithTilde completes an FS path which starts with a `~/` 34 func (fs fsComplete) predictPathWithTilde(a complete.Args) []string { 35 homeDir, e := os.UserHomeDir() 36 if e != nil || homeDir == "" { 37 return nil 38 } 39 // Clean the home directory path 40 homeDir = strings.TrimRight(homeDir, "/") 41 42 // Replace the first occurrence of ~ with the real path and complete 43 a.Last = strings.Replace(a.Last, "~", homeDir, 1) 44 predictions := complete.PredictFiles("*").Predict(a) 45 46 // Restore ~ to avoid disturbing the completion user experience 47 for i := range predictions { 48 predictions[i] = strings.Replace(predictions[i], homeDir, "~", 1) 49 } 50 51 return predictions 52 } 53 54 func (fs fsComplete) Predict(a complete.Args) []string { 55 if strings.HasPrefix(a.Last, "~/") { 56 return fs.predictPathWithTilde(a) 57 } 58 return complete.PredictFiles("*").Predict(a) 59 } 60 61 func completeAdminConfigKeys(aliasPath, keyPrefix string) (prediction []string) { 62 // Convert alias/bucket/incompl to alias/bucket/ to list its contents 63 parentDirPath := filepath.Dir(aliasPath) + "/" 64 clnt, err := newAdminClient(parentDirPath) 65 if err != nil { 66 return nil 67 } 68 69 h, e := clnt.HelpConfigKV(globalContext, "", "", false) 70 if e != nil { 71 return nil 72 } 73 74 for _, hkv := range h.KeysHelp { 75 if strings.HasPrefix(hkv.Key, keyPrefix) { 76 prediction = append(prediction, hkv.Key) 77 } 78 } 79 80 return prediction 81 } 82 83 // Complete S3 path. If the prediction result is only one directory, 84 // then recursively scans it. This is needed to satisfy posener/complete 85 // (look at posener/complete.PredictFiles) 86 func completeS3Path(s3Path string) (prediction []string) { 87 // Convert alias/bucket/incompl to alias/bucket/ to list its contents 88 parentDirPath := filepath.Dir(s3Path) + "/" 89 clnt, err := newClient(parentDirPath) 90 if err != nil { 91 return nil 92 } 93 94 // Calculate alias from the path 95 alias := splitStr(s3Path, "/", 3)[0] 96 97 // List dirPath content and only pick elements that corresponds 98 // to the path that we want to complete 99 for content := range clnt.List(globalContext, ListOptions{Recursive: false, ShowDir: DirFirst}) { 100 cmplS3Path := alias + getKey(content) 101 if content.Type.IsDir() { 102 if !strings.HasSuffix(cmplS3Path, "/") { 103 cmplS3Path += "/" 104 } 105 } 106 if strings.HasPrefix(cmplS3Path, s3Path) { 107 prediction = append(prediction, cmplS3Path) 108 } 109 } 110 111 // If completion found only one directory, recursively scan it. 112 if len(prediction) == 1 && strings.HasSuffix(prediction[0], "/") { 113 prediction = append(prediction, completeS3Path(prediction[0])...) 114 } 115 116 return 117 } 118 119 type adminConfigComplete struct{} 120 121 func (adm adminConfigComplete) Predict(a complete.Args) (prediction []string) { 122 defer func() { 123 sort.Strings(prediction) 124 }() 125 126 loadMcConfig = loadMcConfigFactory() 127 conf, err := loadMcConfig() 128 if err != nil { 129 return 130 } 131 132 // We have already predicted the keys, we are done. 133 if len(a.Completed) == 3 { 134 return 135 } 136 137 arg := a.Last 138 lastArg := a.LastCompleted 139 if _, ok := conf.Aliases[filepath.Clean(a.LastCompleted)]; !ok { 140 if strings.IndexByte(arg, '/') == -1 { 141 // Only predict alias since '/' is not found 142 for alias := range conf.Aliases { 143 if strings.HasPrefix(alias, arg) { 144 prediction = append(prediction, alias+"/") 145 } 146 } 147 } else { 148 prediction = completeAdminConfigKeys(arg, "") 149 } 150 } else { 151 prediction = completeAdminConfigKeys(lastArg, arg) 152 } 153 return 154 } 155 156 // s3Complete knows how to complete an mc s3 path 157 type s3Complete struct { 158 deepLevel int 159 } 160 161 func (s3 s3Complete) Predict(a complete.Args) (prediction []string) { 162 defer func() { 163 sort.Strings(prediction) 164 }() 165 166 loadMcConfig = loadMcConfigFactory() 167 conf, err := loadMcConfig() 168 if err != nil { 169 return nil 170 } 171 172 arg := a.Last 173 174 if strings.IndexByte(arg, '/') == -1 { 175 // Only predict alias since '/' is not found 176 for alias := range conf.Aliases { 177 if strings.HasPrefix(alias, arg) { 178 prediction = append(prediction, alias+"/") 179 } 180 } 181 if len(prediction) == 1 && strings.HasSuffix(prediction[0], "/") { 182 prediction = append(prediction, completeS3Path(prediction[0])...) 183 } 184 } else { 185 // Complete S3 path until the specified path deep level 186 if s3.deepLevel > 0 { 187 if strings.Count(arg, "/") >= s3.deepLevel { 188 return []string{arg} 189 } 190 } 191 // Predict S3 path 192 prediction = completeS3Path(arg) 193 } 194 195 return 196 } 197 198 // aliasComplete only completes aliases 199 type aliasComplete struct{} 200 201 func (al aliasComplete) Predict(a complete.Args) (prediction []string) { 202 defer func() { 203 sort.Strings(prediction) 204 }() 205 206 loadMcConfig = loadMcConfigFactory() 207 conf, err := loadMcConfig() 208 if err != nil { 209 return nil 210 } 211 212 arg := a.Last 213 for alias := range conf.Aliases { 214 if strings.HasPrefix(alias, arg) { 215 prediction = append(prediction, alias+"/") 216 } 217 } 218 219 return 220 } 221 222 var ( 223 adminConfigCompleter = adminConfigComplete{} 224 s3Completer = s3Complete{} 225 aliasCompleter = aliasComplete{} 226 fsCompleter = fsComplete{} 227 ) 228 229 // The list of all commands supported by mc with their mapping 230 // with their bash completer function 231 var completeCmds = map[string]complete.Predictor{ 232 // S3 API level commands 233 "/ls": complete.PredictOr(s3Completer, fsCompleter), 234 "/cp": complete.PredictOr(s3Completer, fsCompleter), 235 "/mv": complete.PredictOr(s3Completer, fsCompleter), 236 "/rm": complete.PredictOr(s3Completer, fsCompleter), 237 "/rb": complete.PredictOr(s3Complete{deepLevel: 2}, fsCompleter), 238 "/cat": complete.PredictOr(s3Completer, fsCompleter), 239 "/head": complete.PredictOr(s3Completer, fsCompleter), 240 "/diff": complete.PredictOr(s3Completer, fsCompleter), 241 "/find": complete.PredictOr(s3Completer, fsCompleter), 242 "/mirror": complete.PredictOr(s3Completer, fsCompleter), 243 "/pipe": complete.PredictOr(s3Completer, fsCompleter), 244 "/stat": complete.PredictOr(s3Completer, fsCompleter), 245 "/watch": complete.PredictOr(s3Completer, fsCompleter), 246 "/anonymous": complete.PredictOr(s3Completer, fsCompleter), 247 "/tree": complete.PredictOr(s3Complete{deepLevel: 2}, fsCompleter), 248 "/du": complete.PredictOr(s3Complete{deepLevel: 2}, fsCompleter), 249 250 "/retention/set": s3Completer, 251 "/retention/clear": s3Completer, 252 "/retention/info": s3Completer, 253 254 "/legalhold/set": s3Completer, 255 "/legalhold/clear": s3Completer, 256 "/legalhold/info": s3Completer, 257 258 "/sql": s3Completer, 259 "/mb": aliasCompleter, 260 261 "/event/add": s3Complete{deepLevel: 2}, 262 "/event/list": s3Complete{deepLevel: 2}, 263 "/event/remove": s3Complete{deepLevel: 2}, 264 265 "/encrypt/set": s3Complete{deepLevel: 2}, 266 "/encrypt/info": s3Complete{deepLevel: 2}, 267 "/encrypt/clear": s3Complete{deepLevel: 2}, 268 269 "/replicate/add": s3Complete{deepLevel: 2}, 270 "/replicate/edit": s3Complete{deepLevel: 2}, 271 "/replicate/update": s3Complete{deepLevel: 2}, 272 "/replicate/list": s3Complete{deepLevel: 2}, 273 "/replicate/remove": s3Complete{deepLevel: 2}, 274 "/replicate/backlog": s3Complete{deepLevel: 2}, 275 276 "/replicate/export": s3Complete{deepLevel: 2}, 277 "/replicate/import": s3Complete{deepLevel: 2}, 278 "/replicate/status": s3Complete{deepLevel: 2}, 279 "/replicate/resync/start": s3Complete{deepLevel: 3}, 280 "/replicate/resync/status": s3Complete{deepLevel: 3}, 281 282 "/tag/list": s3Completer, 283 "/tag/remove": s3Completer, 284 "/tag/set": s3Completer, 285 286 "/version/info": s3Complete{deepLevel: 2}, 287 "/version/enable": s3Complete{deepLevel: 2}, 288 "/version/suspend": s3Complete{deepLevel: 2}, 289 290 "/lock/compliance": s3Completer, 291 "/lock/governance": s3Completer, 292 "/lock/clear": s3Completer, 293 "/lock/info": s3Completer, 294 295 "/share/download": s3Completer, 296 "/share/list": nil, 297 "/share/upload": s3Completer, 298 299 "/ilm/list": s3Complete{deepLevel: 2}, 300 "/ilm/add": s3Complete{deepLevel: 2}, 301 "/ilm/edit": s3Complete{deepLevel: 2}, 302 "/ilm/remove": s3Complete{deepLevel: 2}, 303 "/ilm/export": s3Complete{deepLevel: 2}, 304 "/ilm/import": s3Complete{deepLevel: 2}, 305 "/ilm/restore": s3Completer, 306 307 "/ilm/rule/list": s3Complete{deepLevel: 2}, 308 "/ilm/rule/add": s3Complete{deepLevel: 2}, 309 "/ilm/rule/edit": s3Complete{deepLevel: 2}, 310 "/ilm/rule/remove": s3Complete{deepLevel: 2}, 311 "/ilm/rule/export": s3Complete{deepLevel: 2}, 312 "/ilm/rule/import": s3Complete{deepLevel: 2}, 313 "/ilm/rule/restore": s3Completer, 314 315 "/undo": s3Completer, 316 317 // Admin API commands MinIO only. 318 "/admin/heal": s3Completer, 319 320 "/admin/info": aliasCompleter, 321 "/admin/logs": aliasCompleter, 322 323 "/admin/config/get": adminConfigCompleter, 324 "/admin/config/set": adminConfigCompleter, 325 "/admin/config/reset": adminConfigCompleter, 326 "/admin/config/import": aliasCompleter, 327 "/admin/config/export": aliasCompleter, 328 "/admin/config/history": aliasCompleter, 329 "/admin/config/restore": aliasCompleter, 330 331 "/admin/decom/start": aliasCompleter, 332 "/admin/decom/status": aliasCompleter, 333 "/admin/decom/cancel": aliasCompleter, 334 "/admin/decommission/start": aliasCompleter, 335 "/admin/decommission/status": aliasCompleter, 336 "/admin/decommission/cancel": aliasCompleter, 337 338 "/admin/rebalance/start": aliasCompleter, 339 "/admin/rebalance/status": aliasCompleter, 340 "/admin/rebalance/stop": aliasCompleter, 341 342 "/admin/trace": aliasCompleter, 343 "/admin/speedtest": aliasCompleter, 344 "/admin/console": aliasCompleter, 345 "/admin/update": aliasCompleter, 346 "/admin/inspect": s3Completer, 347 "/admin/top/locks": aliasCompleter, 348 "/admin/top/api": aliasCompleter, 349 350 "/admin/scanner/status": aliasCompleter, 351 "/admin/scanner/trace": aliasCompleter, 352 353 "/admin/service/stop": aliasCompleter, 354 "/admin/service/restart": aliasCompleter, 355 "/admin/service/freeze": aliasCompleter, 356 "/admin/service/unfreeze": aliasCompleter, 357 358 "/admin/prometheus/generate": aliasCompleter, 359 "/admin/prometheus/metrics": aliasCompleter, 360 361 "/admin/profile/start": aliasCompleter, 362 "/admin/profile/stop": aliasCompleter, 363 364 "/idp/openid/add": aliasCompleter, 365 "/idp/openid/update": aliasCompleter, 366 "/idp/openid/remove": aliasCompleter, 367 "/idp/openid/list": aliasCompleter, 368 "/idp/openid/info": aliasCompleter, 369 "/idp/openid/enable": aliasCompleter, 370 "/idp/openid/disable": aliasCompleter, 371 372 "/idp/ldap/add": aliasCompleter, 373 "/idp/ldap/update": aliasCompleter, 374 "/idp/ldap/remove": aliasCompleter, 375 "/idp/ldap/list": aliasCompleter, 376 "/idp/ldap/info": aliasCompleter, 377 "/idp/ldap/enable": aliasCompleter, 378 "/idp/ldap/disable": aliasCompleter, 379 380 "/idp/ldap/policy/entities": aliasCompleter, 381 "/idp/ldap/policy/attach": aliasCompleter, 382 "/idp/ldap/policy/detach": aliasCompleter, 383 384 "/idp/ldap/accesskey/create": aliasCompleter, 385 "/idp/ldap/accesskey/create-with-login": aliasCompleter, 386 "/idp/ldap/accesskey/list": aliasCompleter, 387 "/idp/ldap/accesskey/ls": aliasCompleter, 388 "/idp/ldap/accesskey/remove": aliasCompleter, 389 "/idp/ldap/accesskey/rm": aliasCompleter, 390 "/idp/ldap/accesskey/info": aliasCompleter, 391 392 "/admin/policy/info": aliasCompleter, 393 "/admin/policy/update": aliasCompleter, 394 "/admin/policy/add": aliasCompleter, 395 "/admin/policy/remove": aliasCompleter, 396 "/admin/policy/create": aliasCompleter, 397 "/admin/policy/list": aliasCompleter, 398 "/admin/policy/attach": aliasCompleter, 399 "/admin/policy/detach": aliasCompleter, 400 "/admin/policy/entities": aliasCompleter, 401 402 "/admin/user/add": aliasCompleter, 403 "/admin/user/disable": aliasCompleter, 404 "/admin/user/enable": aliasCompleter, 405 "/admin/user/list": aliasCompleter, 406 "/admin/user/remove": aliasCompleter, 407 "/admin/user/info": aliasCompleter, 408 "/admin/user/policy": aliasCompleter, 409 410 "/admin/user/svcacct/add": aliasCompleter, 411 "/admin/user/svcacct/list": aliasCompleter, 412 "/admin/user/svcacct/remove": aliasCompleter, 413 "/admin/user/svcacct/info": aliasCompleter, 414 "/admin/user/svcacct/edit": aliasCompleter, 415 "/admin/user/svcacct/set": aliasCompleter, 416 "/admin/user/svcacct/enable": aliasCompleter, 417 "/admin/user/svcacct/disable": aliasCompleter, 418 419 "/admin/user/sts/info": aliasCompleter, 420 421 "/admin/group/add": aliasCompleter, 422 "/admin/group/disable": aliasCompleter, 423 "/admin/group/enable": aliasCompleter, 424 "/admin/group/list": aliasCompleter, 425 "/admin/group/remove": aliasCompleter, 426 "/admin/group/info": aliasCompleter, 427 428 "/admin/bucket/remote/add": aliasCompleter, 429 "/admin/bucket/remote/edit": aliasCompleter, 430 "/admin/bucket/remote/remove": aliasCompleter, 431 "/admin/bucket/quota": aliasCompleter, 432 "/admin/bucket/info": s3Complete{deepLevel: 2}, 433 434 "/admin/kms/key/create": aliasCompleter, 435 "/admin/kms/key/status": aliasCompleter, 436 "/admin/kms/key/list": aliasCompleter, 437 438 "/admin/subnet/health": aliasCompleter, 439 "/admin/subnet/register": aliasCompleter, 440 441 "/admin/tier/add": nil, 442 "/admin/tier/edit": nil, 443 "/admin/tier/list": nil, 444 "/admin/tier/info": nil, 445 "/admin/tier/remove": nil, 446 "/admin/tier/verify": nil, 447 448 "/ilm/tier/info": nil, 449 "/ilm/tier/list": nil, 450 "/ilm/tier/add": nil, 451 "/ilm/tier/update": nil, 452 "/ilm/tier/check": nil, 453 "/ilm/tier/remove": nil, 454 455 "/admin/replicate/add": aliasCompleter, 456 "/admin/replicate/update": aliasCompleter, 457 "/admin/replicate/edit": aliasCompleter, 458 "/admin/replicate/info": aliasCompleter, 459 "/admin/replicate/status": aliasCompleter, 460 "/admin/replicate/remove": aliasCompleter, 461 "/admin/replicate/resync/start": aliasCompleter, 462 "/admin/replicate/resync/cancel": aliasCompleter, 463 "/admin/replicate/resync/status": aliasCompleter, 464 465 "/admin/cluster/bucket/export": aliasCompleter, 466 "/admin/cluster/bucket/import": aliasCompleter, 467 "/admin/cluster/iam/export": aliasCompleter, 468 "/admin/cluster/iam/import": aliasCompleter, 469 470 "/alias/set": nil, 471 "/alias/list": aliasCompleter, 472 "/alias/remove": aliasCompleter, 473 "/alias/import": nil, 474 "/alias/export": aliasCompleter, 475 476 "/support/callhome": aliasCompleter, 477 "/support/register": aliasCompleter, 478 "/support/diag": aliasCompleter, 479 "/support/profile": aliasCompleter, 480 "/support/proxy/set": aliasCompleter, 481 "/support/proxy/show": aliasCompleter, 482 "/support/proxy/remove": aliasCompleter, 483 "/support/inspect": aliasCompleter, 484 "/support/perf": aliasCompleter, 485 "/support/metrics": aliasCompleter, 486 "/support/status": aliasCompleter, 487 "/support/top/locks": aliasCompleter, 488 "/support/top/api": aliasCompleter, 489 "/support/top/drive": aliasCompleter, 490 "/support/top/disk": aliasCompleter, 491 "/support/top/net": aliasCompleter, 492 "/support/upload": aliasCompleter, 493 494 "/license/register": aliasCompleter, 495 "/license/info": aliasCompleter, 496 "/license/update": aliasCompleter, 497 498 "/update": nil, 499 "/ready": aliasCompleter, 500 "/ping": aliasCompleter, 501 "/od": nil, 502 "/batch/generate": aliasCompleter, 503 "/batch/start": aliasCompleter, 504 "/batch/list": aliasCompleter, 505 "/batch/status": aliasCompleter, 506 "/batch/describe": aliasCompleter, 507 "/batch/cancel": aliasCompleter, 508 509 "/quota/set": aliasCompleter, 510 "/quota/info": aliasCompleter, 511 "/quota/clear": aliasCompleter, 512 "/put": complete.PredictOr(s3Completer, fsCompleter), 513 "/get": complete.PredictOr(s3Completer, fsCompleter), 514 } 515 516 // flagsToCompleteFlags transforms a cli.Flag to complete.Flags 517 // understood by posener/complete library. 518 func flagsToCompleteFlags(flags []cli.Flag) complete.Flags { 519 complFlags := make(complete.Flags) 520 for _, f := range flags { 521 for _, s := range strings.Split(f.GetName(), ",") { 522 var flagName string 523 s = strings.TrimSpace(s) 524 if len(s) == 1 { 525 flagName = "-" + s 526 } else { 527 flagName = "--" + s 528 } 529 complFlags[flagName] = complete.PredictNothing 530 } 531 } 532 return complFlags 533 } 534 535 // This function recursively transforms cli.Command to complete.Command 536 // understood by posener/complete library. 537 func cmdToCompleteCmd(cmd cli.Command, parentPath string) complete.Command { 538 var complCmd complete.Command 539 complCmd.Sub = make(complete.Commands) 540 541 for _, subCmd := range cmd.Subcommands { 542 if subCmd.Hidden { 543 continue 544 } 545 complCmd.Sub[subCmd.Name] = cmdToCompleteCmd(subCmd, parentPath+"/"+cmd.Name) 546 for _, alias := range subCmd.Aliases { 547 complCmd.Sub[alias] = cmdToCompleteCmd(subCmd, parentPath+"/"+cmd.Name) 548 } 549 } 550 551 complCmd.Flags = flagsToCompleteFlags(cmd.Flags) 552 complCmd.Args = completeCmds[parentPath+"/"+cmd.Name] 553 return complCmd 554 } 555 556 // Main function to answer to bash completion calls 557 func mainComplete() error { 558 // Recursively register all commands and subcommands 559 // along with global and local flags 560 complCmds := make(complete.Commands) 561 for _, cmd := range appCmds { 562 if cmd.Hidden { 563 continue 564 } 565 complCmds[cmd.Name] = cmdToCompleteCmd(cmd, "") 566 for _, alias := range cmd.Aliases { 567 complCmds[alias] = cmdToCompleteCmd(cmd, "") 568 } 569 } 570 complFlags := flagsToCompleteFlags(globalFlags) 571 mcComplete := complete.Command{ 572 Sub: complCmds, 573 GlobalFlags: complFlags, 574 } 575 // Answer to bash completion call 576 complete.New(filepath.Base(os.Args[0]), mcComplete).Run() 577 return nil 578 }