// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "os" "path/filepath" "sort" "strings" "github.com/minio/cli" "github.com/posener/complete" ) // fsComplete knows how to complete file/dir names by the given path type fsComplete struct{} // predictPathWithTilde completes an FS path which starts with a `~/` func (fs fsComplete) predictPathWithTilde(a complete.Args) []string { homeDir, e := os.UserHomeDir() if e != nil || homeDir == "" { return nil } // Clean the home directory path homeDir = strings.TrimRight(homeDir, "/") // Replace the first occurrence of ~ with the real path and complete a.Last = strings.Replace(a.Last, "~", homeDir, 1) predictions := complete.PredictFiles("*").Predict(a) // Restore ~ to avoid disturbing the completion user experience for i := range predictions { predictions[i] = strings.Replace(predictions[i], homeDir, "~", 1) } return predictions } func (fs fsComplete) Predict(a complete.Args) []string { if strings.HasPrefix(a.Last, "~/") { return fs.predictPathWithTilde(a) } return complete.PredictFiles("*").Predict(a) } func completeAdminConfigKeys(aliasPath, keyPrefix string) (prediction []string) { // Convert alias/bucket/incompl to alias/bucket/ to list its contents parentDirPath := filepath.Dir(aliasPath) + "/" clnt, err := newAdminClient(parentDirPath) if err != nil { return nil } h, e := clnt.HelpConfigKV(globalContext, "", "", false) if e != nil { return nil } for _, hkv := range h.KeysHelp { if strings.HasPrefix(hkv.Key, keyPrefix) { prediction = append(prediction, hkv.Key) } } return prediction } // Complete S3 path. If the prediction result is only one directory, // then recursively scans it. This is needed to satisfy posener/complete // (look at posener/complete.PredictFiles) func completeS3Path(s3Path string) (prediction []string) { // Convert alias/bucket/incompl to alias/bucket/ to list its contents parentDirPath := filepath.Dir(s3Path) + "/" clnt, err := newClient(parentDirPath) if err != nil { return nil } // Calculate alias from the path alias := splitStr(s3Path, "/", 3)[0] // List dirPath content and only pick elements that corresponds // to the path that we want to complete for content := range clnt.List(globalContext, ListOptions{Recursive: false, ShowDir: DirFirst}) { cmplS3Path := alias + getKey(content) if content.Type.IsDir() { if !strings.HasSuffix(cmplS3Path, "/") { cmplS3Path += "/" } } if strings.HasPrefix(cmplS3Path, s3Path) { prediction = append(prediction, cmplS3Path) } } // If completion found only one directory, recursively scan it. if len(prediction) == 1 && strings.HasSuffix(prediction[0], "/") { prediction = append(prediction, completeS3Path(prediction[0])...) } return } type adminConfigComplete struct{} func (adm adminConfigComplete) Predict(a complete.Args) (prediction []string) { defer func() { sort.Strings(prediction) }() loadMcConfig = loadMcConfigFactory() conf, err := loadMcConfig() if err != nil { return } // We have already predicted the keys, we are done. if len(a.Completed) == 3 { return } arg := a.Last lastArg := a.LastCompleted if _, ok := conf.Aliases[filepath.Clean(a.LastCompleted)]; !ok { if strings.IndexByte(arg, '/') == -1 { // Only predict alias since '/' is not found for alias := range conf.Aliases { if strings.HasPrefix(alias, arg) { prediction = append(prediction, alias+"/") } } } else { prediction = completeAdminConfigKeys(arg, "") } } else { prediction = completeAdminConfigKeys(lastArg, arg) } return } // s3Complete knows how to complete an mc s3 path type s3Complete struct { deepLevel int } func (s3 s3Complete) Predict(a complete.Args) (prediction []string) { defer func() { sort.Strings(prediction) }() loadMcConfig = loadMcConfigFactory() conf, err := loadMcConfig() if err != nil { return nil } arg := a.Last if strings.IndexByte(arg, '/') == -1 { // Only predict alias since '/' is not found for alias := range conf.Aliases { if strings.HasPrefix(alias, arg) { prediction = append(prediction, alias+"/") } } if len(prediction) == 1 && strings.HasSuffix(prediction[0], "/") { prediction = append(prediction, completeS3Path(prediction[0])...) } } else { // Complete S3 path until the specified path deep level if s3.deepLevel > 0 { if strings.Count(arg, "/") >= s3.deepLevel { return []string{arg} } } // Predict S3 path prediction = completeS3Path(arg) } return } // aliasComplete only completes aliases type aliasComplete struct{} func (al aliasComplete) Predict(a complete.Args) (prediction []string) { defer func() { sort.Strings(prediction) }() loadMcConfig = loadMcConfigFactory() conf, err := loadMcConfig() if err != nil { return nil } arg := a.Last for alias := range conf.Aliases { if strings.HasPrefix(alias, arg) { prediction = append(prediction, alias+"/") } } return } var ( adminConfigCompleter = adminConfigComplete{} s3Completer = s3Complete{} aliasCompleter = aliasComplete{} fsCompleter = fsComplete{} ) // The list of all commands supported by mc with their mapping // with their bash completer function var completeCmds = map[string]complete.Predictor{ // S3 API level commands "/ls": complete.PredictOr(s3Completer, fsCompleter), "/cp": complete.PredictOr(s3Completer, fsCompleter), "/mv": complete.PredictOr(s3Completer, fsCompleter), "/rm": complete.PredictOr(s3Completer, fsCompleter), "/rb": complete.PredictOr(s3Complete{deepLevel: 2}, fsCompleter), "/cat": complete.PredictOr(s3Completer, fsCompleter), "/head": complete.PredictOr(s3Completer, fsCompleter), "/diff": complete.PredictOr(s3Completer, fsCompleter), "/find": complete.PredictOr(s3Completer, fsCompleter), "/mirror": complete.PredictOr(s3Completer, fsCompleter), "/pipe": complete.PredictOr(s3Completer, fsCompleter), "/stat": complete.PredictOr(s3Completer, fsCompleter), "/watch": complete.PredictOr(s3Completer, fsCompleter), "/anonymous": complete.PredictOr(s3Completer, fsCompleter), "/tree": complete.PredictOr(s3Complete{deepLevel: 2}, fsCompleter), "/du": complete.PredictOr(s3Complete{deepLevel: 2}, fsCompleter), "/retention/set": s3Completer, "/retention/clear": s3Completer, "/retention/info": s3Completer, "/legalhold/set": s3Completer, "/legalhold/clear": s3Completer, "/legalhold/info": s3Completer, "/sql": s3Completer, "/mb": aliasCompleter, "/event/add": s3Complete{deepLevel: 2}, "/event/list": s3Complete{deepLevel: 2}, "/event/remove": s3Complete{deepLevel: 2}, "/encrypt/set": s3Complete{deepLevel: 2}, "/encrypt/info": s3Complete{deepLevel: 2}, "/encrypt/clear": s3Complete{deepLevel: 2}, "/replicate/add": s3Complete{deepLevel: 2}, "/replicate/edit": s3Complete{deepLevel: 2}, "/replicate/update": s3Complete{deepLevel: 2}, "/replicate/list": s3Complete{deepLevel: 2}, "/replicate/remove": s3Complete{deepLevel: 2}, "/replicate/backlog": s3Complete{deepLevel: 2}, "/replicate/export": s3Complete{deepLevel: 2}, "/replicate/import": s3Complete{deepLevel: 2}, "/replicate/status": s3Complete{deepLevel: 2}, "/replicate/resync/start": s3Complete{deepLevel: 3}, "/replicate/resync/status": s3Complete{deepLevel: 3}, "/tag/list": s3Completer, "/tag/remove": s3Completer, "/tag/set": s3Completer, "/version/info": s3Complete{deepLevel: 2}, "/version/enable": s3Complete{deepLevel: 2}, "/version/suspend": s3Complete{deepLevel: 2}, "/lock/compliance": s3Completer, "/lock/governance": s3Completer, "/lock/clear": s3Completer, "/lock/info": s3Completer, "/share/download": s3Completer, "/share/list": nil, "/share/upload": s3Completer, "/ilm/list": s3Complete{deepLevel: 2}, "/ilm/add": s3Complete{deepLevel: 2}, "/ilm/edit": s3Complete{deepLevel: 2}, "/ilm/remove": s3Complete{deepLevel: 2}, "/ilm/export": s3Complete{deepLevel: 2}, "/ilm/import": s3Complete{deepLevel: 2}, "/ilm/restore": s3Completer, "/ilm/rule/list": s3Complete{deepLevel: 2}, "/ilm/rule/add": s3Complete{deepLevel: 2}, "/ilm/rule/edit": s3Complete{deepLevel: 2}, "/ilm/rule/remove": s3Complete{deepLevel: 2}, "/ilm/rule/export": s3Complete{deepLevel: 2}, "/ilm/rule/import": s3Complete{deepLevel: 2}, "/ilm/rule/restore": s3Completer, "/undo": s3Completer, // Admin API commands MinIO only. "/admin/heal": s3Completer, "/admin/info": aliasCompleter, "/admin/logs": aliasCompleter, "/admin/config/get": adminConfigCompleter, "/admin/config/set": adminConfigCompleter, "/admin/config/reset": adminConfigCompleter, "/admin/config/import": aliasCompleter, "/admin/config/export": aliasCompleter, "/admin/config/history": aliasCompleter, "/admin/config/restore": aliasCompleter, "/admin/decom/start": aliasCompleter, "/admin/decom/status": aliasCompleter, "/admin/decom/cancel": aliasCompleter, "/admin/decommission/start": aliasCompleter, "/admin/decommission/status": aliasCompleter, "/admin/decommission/cancel": aliasCompleter, "/admin/rebalance/start": aliasCompleter, "/admin/rebalance/status": aliasCompleter, "/admin/rebalance/stop": aliasCompleter, "/admin/trace": aliasCompleter, "/admin/speedtest": aliasCompleter, "/admin/console": aliasCompleter, "/admin/update": aliasCompleter, "/admin/inspect": s3Completer, "/admin/top/locks": aliasCompleter, "/admin/top/api": aliasCompleter, "/admin/scanner/status": aliasCompleter, "/admin/scanner/trace": aliasCompleter, "/admin/service/stop": aliasCompleter, "/admin/service/restart": aliasCompleter, "/admin/service/freeze": aliasCompleter, "/admin/service/unfreeze": aliasCompleter, "/admin/prometheus/generate": aliasCompleter, "/admin/prometheus/metrics": aliasCompleter, "/admin/profile/start": aliasCompleter, "/admin/profile/stop": aliasCompleter, "/idp/openid/add": aliasCompleter, "/idp/openid/update": aliasCompleter, "/idp/openid/remove": aliasCompleter, "/idp/openid/list": aliasCompleter, "/idp/openid/info": aliasCompleter, "/idp/openid/enable": aliasCompleter, "/idp/openid/disable": aliasCompleter, "/idp/ldap/add": aliasCompleter, "/idp/ldap/update": aliasCompleter, "/idp/ldap/remove": aliasCompleter, "/idp/ldap/list": aliasCompleter, "/idp/ldap/info": aliasCompleter, "/idp/ldap/enable": aliasCompleter, "/idp/ldap/disable": aliasCompleter, "/idp/ldap/policy/entities": aliasCompleter, "/idp/ldap/policy/attach": aliasCompleter, "/idp/ldap/policy/detach": aliasCompleter, "/idp/ldap/accesskey/create": aliasCompleter, "/idp/ldap/accesskey/create-with-login": aliasCompleter, "/idp/ldap/accesskey/list": aliasCompleter, "/idp/ldap/accesskey/ls": aliasCompleter, "/idp/ldap/accesskey/remove": aliasCompleter, "/idp/ldap/accesskey/rm": aliasCompleter, "/idp/ldap/accesskey/info": aliasCompleter, "/idp/ldap/accesskey/edit": aliasCompleter, "/idp/ldap/accesskey/enable": aliasCompleter, "/idp/ldap/accesskey/disable": aliasCompleter, "/idp/ldap/accesskey/sts-revoke": aliasCompleter, "/admin/accesskey/create": aliasCompleter, "/admin/accesskey/list": aliasCompleter, "/admin/accesskey/ls": aliasCompleter, "/admin/accesskey/remove": aliasCompleter, "/admin/accesskey/rm": aliasCompleter, "/admin/accesskey/info": aliasCompleter, "/admin/accesskey/edit": aliasCompleter, "/admin/accesskey/enable": aliasCompleter, "/admin/accesskey/disable": aliasCompleter, "/admin/accesskey/sts-revoke": aliasCompleter, "/admin/policy/info": aliasCompleter, "/admin/policy/update": aliasCompleter, "/admin/policy/add": aliasCompleter, "/admin/policy/remove": aliasCompleter, "/admin/policy/create": aliasCompleter, "/admin/policy/list": aliasCompleter, "/admin/policy/attach": aliasCompleter, "/admin/policy/detach": aliasCompleter, "/admin/policy/entities": aliasCompleter, "/admin/user/add": aliasCompleter, "/admin/user/disable": aliasCompleter, "/admin/user/enable": aliasCompleter, "/admin/user/list": aliasCompleter, "/admin/user/remove": aliasCompleter, "/admin/user/info": aliasCompleter, "/admin/user/policy": aliasCompleter, "/admin/user/svcacct/add": aliasCompleter, "/admin/user/svcacct/list": aliasCompleter, "/admin/user/svcacct/remove": aliasCompleter, "/admin/user/svcacct/info": aliasCompleter, "/admin/user/svcacct/edit": aliasCompleter, "/admin/user/svcacct/set": aliasCompleter, "/admin/user/svcacct/enable": aliasCompleter, "/admin/user/svcacct/disable": aliasCompleter, "/admin/user/sts/info": aliasCompleter, "/admin/group/add": aliasCompleter, "/admin/group/disable": aliasCompleter, "/admin/group/enable": aliasCompleter, "/admin/group/list": aliasCompleter, "/admin/group/remove": aliasCompleter, "/admin/group/info": aliasCompleter, "/admin/bucket/remote/add": aliasCompleter, "/admin/bucket/remote/edit": aliasCompleter, "/admin/bucket/remote/remove": aliasCompleter, "/admin/bucket/quota": aliasCompleter, "/admin/bucket/info": s3Complete{deepLevel: 2}, "/admin/kms/key/create": aliasCompleter, "/admin/kms/key/status": aliasCompleter, "/admin/kms/key/list": aliasCompleter, "/admin/subnet/health": aliasCompleter, "/admin/subnet/register": aliasCompleter, "/admin/tier/add": nil, "/admin/tier/edit": nil, "/admin/tier/list": nil, "/admin/tier/info": nil, "/admin/tier/remove": nil, "/admin/tier/verify": nil, "/ilm/tier/info": nil, "/ilm/tier/list": nil, "/ilm/tier/add": nil, "/ilm/tier/update": nil, "/ilm/tier/check": nil, "/ilm/tier/remove": nil, "/admin/replicate/add": aliasCompleter, "/admin/replicate/update": aliasCompleter, "/admin/replicate/edit": aliasCompleter, "/admin/replicate/info": aliasCompleter, "/admin/replicate/status": aliasCompleter, "/admin/replicate/remove": aliasCompleter, "/admin/replicate/resync/start": aliasCompleter, "/admin/replicate/resync/cancel": aliasCompleter, "/admin/replicate/resync/status": aliasCompleter, "/admin/cluster/bucket/export": aliasCompleter, "/admin/cluster/bucket/import": aliasCompleter, "/admin/cluster/iam/export": aliasCompleter, "/admin/cluster/iam/import": aliasCompleter, "/alias/set": nil, "/alias/list": aliasCompleter, "/alias/remove": aliasCompleter, "/alias/import": nil, "/alias/export": aliasCompleter, "/support/callhome": aliasCompleter, "/support/register": aliasCompleter, "/support/diag": aliasCompleter, "/support/profile": aliasCompleter, "/support/proxy/set": aliasCompleter, "/support/proxy/show": aliasCompleter, "/support/proxy/remove": aliasCompleter, "/support/inspect": aliasCompleter, "/support/perf": aliasCompleter, "/support/metrics": aliasCompleter, "/support/status": aliasCompleter, "/support/top/locks": aliasCompleter, "/support/top/api": aliasCompleter, "/support/top/drive": aliasCompleter, "/support/top/disk": aliasCompleter, "/support/top/net": aliasCompleter, "/support/top/rpc": aliasCompleter, "/support/upload": aliasCompleter, "/license/register": aliasCompleter, "/license/info": aliasCompleter, "/license/update": aliasCompleter, "/update": nil, "/ready": aliasCompleter, "/ping": aliasCompleter, "/od": nil, "/batch/generate": aliasCompleter, "/batch/start": aliasCompleter, "/batch/list": aliasCompleter, "/batch/status": aliasCompleter, "/batch/describe": aliasCompleter, "/batch/cancel": aliasCompleter, "/quota/set": aliasCompleter, "/quota/info": aliasCompleter, "/quota/clear": aliasCompleter, "/put": complete.PredictOr(s3Completer, fsCompleter), "/get": complete.PredictOr(s3Completer, fsCompleter), "/cors/set": s3Complete{deepLevel: 2}, "/cors/get": s3Complete{deepLevel: 2}, "/cors/remove": s3Complete{deepLevel: 2}, } // flagsToCompleteFlags transforms a cli.Flag to complete.Flags // understood by posener/complete library. func flagsToCompleteFlags(flags []cli.Flag) complete.Flags { complFlags := make(complete.Flags) for _, f := range flags { for _, s := range strings.Split(f.GetName(), ",") { var flagName string s = strings.TrimSpace(s) if len(s) == 1 { flagName = "-" + s } else { flagName = "--" + s } complFlags[flagName] = complete.PredictNothing } } return complFlags } // This function recursively transforms cli.Command to complete.Command // understood by posener/complete library. func cmdToCompleteCmd(cmd cli.Command, parentPath string) complete.Command { var complCmd complete.Command complCmd.Sub = make(complete.Commands) for _, subCmd := range cmd.Subcommands { if subCmd.Hidden { continue } complCmd.Sub[subCmd.Name] = cmdToCompleteCmd(subCmd, parentPath+"/"+cmd.Name) for _, alias := range subCmd.Aliases { complCmd.Sub[alias] = cmdToCompleteCmd(subCmd, parentPath+"/"+cmd.Name) } } complCmd.Flags = flagsToCompleteFlags(cmd.Flags) complCmd.Args = completeCmds[parentPath+"/"+cmd.Name] return complCmd } // Main function to answer to bash completion calls func mainComplete() error { // Recursively register all commands and subcommands // along with global and local flags complCmds := make(complete.Commands) for _, cmd := range appCmds { if cmd.Hidden { continue } complCmds[cmd.Name] = cmdToCompleteCmd(cmd, "") for _, alias := range cmd.Aliases { complCmds[alias] = cmdToCompleteCmd(cmd, "") } } complFlags := flagsToCompleteFlags(globalFlags) mcComplete := complete.Command{ Sub: complCmds, GlobalFlags: complFlags, } // Answer to bash completion call complete.New(filepath.Base(os.Args[0]), mcComplete).Run() return nil }