// 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 ( "bytes" "errors" "fmt" "os" "os/exec" "path/filepath" "reflect" "regexp" "runtime" "sort" "strconv" "strings" "syscall" "time" "github.com/inconshreveable/mousetrap" "github.com/minio/cli" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/pkg/v2/console" "github.com/minio/pkg/v2/env" "github.com/minio/pkg/v2/trie" "github.com/minio/pkg/v2/words" "golang.org/x/term" completeinstall "github.com/posener/complete/cmd/install" ) // global flags for mc. var mcFlags = []cli.Flag{ cli.BoolFlag{ Name: "autocompletion", Usage: "install auto-completion for your shell", }, } // Help template for mc var mcHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: {{.Name}} {{if .VisibleFlags}}[FLAGS] {{end}}COMMAND{{if .VisibleFlags}} [COMMAND FLAGS | -h]{{end}} [ARGUMENTS...] COMMANDS: {{range .VisibleCommands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} {{end}}{{if .VisibleFlags}} GLOBAL FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} TIP: Use '{{.Name}} --autocompletion' to enable shell autocompletion COPYRIGHT: Copyright (c) 2015-` + CopyrightYear + ` MinIO, Inc. LICENSE: GNU AGPLv3 ` func init() { if env.IsSet(mcEnvConfigFile) { configFile := env.Get(mcEnvConfigFile, "") fatalIf(readAliasesFromFile(configFile).Trace(configFile), "Unable to parse "+configFile) } if runtime.GOOS == "windows" { if mousetrap.StartedByExplorer() { fmt.Printf("Don't double-click %s\n", os.Args[0]) fmt.Println("You need to open cmd.exe/PowerShell and run it from the command line") fmt.Println("Press the Enter Key to Exit") fmt.Scanln() os.Exit(1) } } } // Main starts mc application func Main(args []string) error { if len(args) > 1 { switch args[1] { case "mc", filepath.Base(args[0]): mainComplete() return nil } } // ``MC_PROFILER`` supported options are [cpu, mem, block, goroutine]. if p := os.Getenv("MC_PROFILER"); p != "" { profilers := strings.Split(p, ",") if e := enableProfilers(mustGetProfileDir(), profilers); e != nil { console.Fatal(e) } } probe.Init() // Set project's root source path. probe.SetAppInfo("Release-Tag", ReleaseTag) probe.SetAppInfo("Commit", ShortCommitID) // Fetch terminal size, if not available, automatically // set globalQuiet to true on non-window. if w, h, e := term.GetSize(int(os.Stdin.Fd())); e != nil { globalQuiet = runtime.GOOS != "windows" } else { globalTermWidth, globalTermHeight = w, h } // Set the mc app name. appName := filepath.Base(args[0]) if runtime.GOOS == "windows" && strings.HasSuffix(strings.ToLower(appName), ".exe") { // Trim ".exe" from Windows executable. appName = appName[:strings.LastIndex(appName, ".")] } // Monitor OS exit signals and cancel the global context in such case go trapSignals(os.Interrupt, syscall.SIGTERM, syscall.SIGKILL) globalHelpPager = newTermPager() // Wait until the user quits the pager defer globalHelpPager.WaitForExit() parsePagerDisableFlag(args) // Run the app return registerApp(appName).Run(args) } func flagValue(f cli.Flag) reflect.Value { fv := reflect.ValueOf(f) for fv.Kind() == reflect.Ptr { fv = reflect.Indirect(fv) } return fv } func visibleFlags(fl []cli.Flag) []cli.Flag { visible := []cli.Flag{} for _, flag := range fl { field := flagValue(flag).FieldByName("Hidden") if !field.IsValid() || !field.Bool() { visible = append(visible, flag) } } return visible } // Function invoked when invalid flag is passed func onUsageError(ctx *cli.Context, err error, _ bool) error { type subCommandHelp struct { flagName string usage string } // Calculate the maximum width of the flag name field // for a good looking printing vflags := visibleFlags(ctx.Command.Flags) help := make([]subCommandHelp, len(vflags)) maxWidth := 0 for i, f := range vflags { s := strings.Split(f.String(), "\t") if len(s[0]) > maxWidth { maxWidth = len(s[0]) } help[i] = subCommandHelp{flagName: s[0], usage: s[1]} } maxWidth += 2 var errMsg strings.Builder // Do the good-looking printing now fmt.Fprintln(&errMsg, "Invalid command usage,", err.Error()) if len(help) > 0 { fmt.Fprintln(&errMsg, "\nSUPPORTED FLAGS:") for _, h := range help { spaces := string(bytes.Repeat([]byte{' '}, maxWidth-len(h.flagName))) fmt.Fprintf(&errMsg, " %s%s%s\n", h.flagName, spaces, h.usage) } } console.Fatal(errMsg.String()) return err } // Function invoked when invalid command is passed. func commandNotFound(ctx *cli.Context, cmds []cli.Command) { command := ctx.Args().First() if command == "" { cli.ShowCommandHelp(ctx, command) return } msg := fmt.Sprintf("`%s` is not a recognized command. Get help using `--help` flag.", command) commandsTree := trie.NewTrie() for _, cmd := range cmds { commandsTree.Insert(cmd.Name) } closestCommands := findClosestCommands(commandsTree, command) if len(closestCommands) > 0 { msg += "\n\nDid you mean one of these?\n" if len(closestCommands) == 1 { cmd := closestCommands[0] msg += fmt.Sprintf(" `%s`", cmd) } else { for _, cmd := range closestCommands { msg += fmt.Sprintf(" `%s`\n", cmd) } } } fatalIf(errDummy().Trace(), msg) } // Check for sane config environment early on and gracefully report. func checkConfig() { // Refresh the config once. loadMcConfig = loadMcConfigFactory() // Ensures config file is sane. config, err := loadMcConfig() // Verify if the path is accesible before validating the config fatalIf(err.Trace(mustGetMcConfigPath()), "Unable to access configuration file.") // Validate and print error messges ok, errMsgs := validateConfigFile(config) if !ok { var errorMsg bytes.Buffer for index, errMsg := range errMsgs { // Print atmost 10 errors if index > 10 { break } errorMsg.WriteString(errMsg + "\n") } console.Fatal(errorMsg.String()) } } func migrate() { // Fix broken config files if any. fixConfig() // Migrate config files if any. migrateConfig() // Migrate shared urls if any. migrateShare() } // initMC - initialize 'mc'. func initMC() { // Check if mc config exists. if !isMcConfigExists() { err := saveMcConfig(newMcConfig()) fatalIf(err.Trace(), "Unable to save new mc config.") if !globalQuiet && !globalJSON { console.Infoln("Configuration written to `" + mustGetMcConfigPath() + "`. Please update your access credentials.") } } // Check if mc share directory exists. if !isShareDirExists() { initShareConfig() } // Check if certs dir exists if !isCertsDirExists() { fatalIf(createCertsDir().Trace(), "Unable to create `CAs` directory.") } // Check if CAs dir exists if !isCAsDirExists() { fatalIf(createCAsDir().Trace(), "Unable to create `CAs` directory.") } // Load all authority certificates present in CAs dir loadRootCAs() } func getShellName() (string, bool) { shellName := os.Getenv("SHELL") if shellName != "" || runtime.GOOS == "windows" { return strings.ToLower(filepath.Base(shellName)), true } ppid := os.Getppid() cmd := exec.Command("ps", "-p", strconv.Itoa(ppid), "-o", "comm=") ppName, err := cmd.Output() if err != nil { fatalIf(probe.NewError(err), "Failed to enable autocompletion. Cannot determine shell type and "+ "no SHELL environment variable found") } shellName = strings.TrimSpace(string(ppName)) return strings.ToLower(filepath.Base(shellName)), false } func installAutoCompletion() { if runtime.GOOS == "windows" { console.Infoln("autocompletion feature is not available for this operating system") return } shellName, ok := getShellName() if !ok { console.Infoln("No 'SHELL' env var. Your shell is auto determined as '" + shellName + "'.") } else { console.Infoln("Your shell is set to '" + shellName + "', by env var 'SHELL'.") } supportedShellsSet := set.CreateStringSet("bash", "zsh", "fish") if !supportedShellsSet.Contains(shellName) { fatalIf(probe.NewError(errors.New("")), "'"+shellName+"' is not a supported shell. "+ "Supported shells are: bash, zsh, fish") } e := completeinstall.Install(filepath.Base(os.Args[0])) var printMsg string if e != nil && strings.Contains(e.Error(), "* already installed") { errStr := e.Error()[strings.Index(e.Error(), "\n")+1:] re := regexp.MustCompile(`[::space::]*\*.*` + shellName + `.*`) relatedMsg := re.FindStringSubmatch(errStr) if len(relatedMsg) > 0 { printMsg = "\n" + relatedMsg[0] } else { printMsg = "" } } if printMsg != "" { if completeinstall.IsInstalled(filepath.Base(os.Args[0])) || completeinstall.IsInstalled("mc") { console.Infoln("autocompletion is enabled.", printMsg) } else { fatalIf(probe.NewError(e), "Unable to install auto-completion.") } } else { console.Infoln("enabled autocompletion in your '" + shellName + "' rc file. Please restart your shell.") } } func registerBefore(ctx *cli.Context) error { deprecatedFlagsWarning(ctx) if ctx.IsSet("config-dir") { // Set the config directory. setMcConfigDir(ctx.String("config-dir")) } else if ctx.GlobalIsSet("config-dir") { // Set the config directory. setMcConfigDir(ctx.GlobalString("config-dir")) } // Set global flags. setGlobalsFromContext(ctx) // Migrate any old version of config / state files to newer format. migrate() // Initialize default config files. initMC() // Check if config can be read. checkConfig() return nil } // findClosestCommands to match a given string with commands trie tree. func findClosestCommands(commandsTree *trie.Trie, command string) []string { closestCommands := commandsTree.PrefixMatch(command) sort.Strings(closestCommands) // Suggest other close commands - allow missed, wrongly added and even transposed characters for _, value := range commandsTree.Walk(commandsTree.Root()) { if sort.SearchStrings(closestCommands, value) < len(closestCommands) { continue } // 2 is arbitrary and represents the max allowed number of typed errors if words.DamerauLevenshteinDistance(command, value) < 2 { closestCommands = append(closestCommands, value) } } return closestCommands } // Check for updates and print a notification message func checkUpdate(ctx *cli.Context) { // Do not print update messages, if quiet flag is set. if ctx.Bool("quiet") || ctx.GlobalBool("quiet") { // Its OK to ignore any errors during doUpdate() here. if updateMsg, _, currentReleaseTime, latestReleaseTime, _, err := getUpdateInfo("", 2*time.Second); err == nil { printMsg(updateMessage{ Status: "success", Message: updateMsg, }) } else { printMsg(updateMessage{ Status: "success", Message: prepareUpdateMessage("Run `mc update`", latestReleaseTime.Sub(currentReleaseTime)), }) } } } var appCmds = []cli.Command{ aliasCmd, adminCmd, anonymousCmd, batchCmd, cpCmd, catCmd, configCmd, diffCmd, duCmd, encryptCmd, eventCmd, findCmd, getCmd, headCmd, ilmCmd, idpCmd, licenseCmd, legalHoldCmd, lsCmd, mbCmd, mvCmd, mirrorCmd, odCmd, pingCmd, policyCmd, pipeCmd, putCmd, quotaCmd, rmCmd, retentionCmd, rbCmd, replicateCmd, readyCmd, sqlCmd, statCmd, supportCmd, shareCmd, treeCmd, tagCmd, undoCmd, updateCmd, versionCmd, watchCmd, } func printMCVersion(c *cli.Context) { fmt.Fprintf(c.App.Writer, "%s version %s (commit-id=%s)\n", c.App.Name, c.App.Version, CommitID) fmt.Fprintf(c.App.Writer, "Runtime: %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH) fmt.Fprintf(c.App.Writer, "Copyright (c) 2015-%s MinIO, Inc.\n", CopyrightYear) fmt.Fprintf(c.App.Writer, "License GNU AGPLv3 \n") } func registerApp(name string) *cli.App { cli.HelpFlag = cli.BoolFlag{ Name: "help, h", Usage: "show help", } // Override default cli version printer cli.VersionPrinter = printMCVersion app := cli.NewApp() app.Name = name app.Action = func(ctx *cli.Context) error { if strings.HasPrefix(ReleaseTag, "RELEASE.") { // Check for new updates from dl.min.io. checkUpdate(ctx) } if ctx.Bool("autocompletion") || ctx.GlobalBool("autocompletion") { // Install shell completions installAutoCompletion() return nil } if ctx.Args().First() == "" { showAppHelpAndExit(ctx) } commandNotFound(ctx, app.Commands) return exitStatus(globalErrorExitStatus) } app.Before = registerBefore app.HideHelpCommand = true app.Usage = "MinIO Client for object storage and filesystems." app.Commands = appCmds app.Author = "MinIO, Inc." app.Version = ReleaseTag app.Flags = append(mcFlags, globalFlags...) app.CustomAppHelpTemplate = mcHelpTemplate app.EnableBashCompletion = true app.OnUsageError = onUsageError if isTerminal() && !globalPagerDisabled { app.HelpWriter = globalHelpPager } else { app.HelpWriter = os.Stdout } return app } // mustGetProfilePath must get location that the profile will be written to. func mustGetProfileDir() string { return filepath.Join(mustGetMcConfigDir(), globalProfileDir) } func showCommandHelpAndExit(cliCtx *cli.Context, code int) { cli.ShowCommandHelp(cliCtx, cliCtx.Command.Name) // Wait until the user quits the pager globalHelpPager.WaitForExit() os.Exit(code) } func showAppHelpAndExit(cliCtx *cli.Context) { cli.ShowAppHelp(cliCtx) // Wait until the user quits the pager globalHelpPager.WaitForExit() os.Exit(globalErrorExitStatus) }