diff --git a/cmd/cli/cli/bucket_hdlr.go b/cmd/cli/cli/bucket_hdlr.go index 5a34fa5b3de..35d90892b1b 100644 --- a/cmd/cli/cli/bucket_hdlr.go +++ b/cmd/cli/cli/bucket_hdlr.go @@ -171,7 +171,7 @@ var ( } bucketCmdCopy = cli.Command{ Name: commandCopy, - Usage: "copy entire bucket or selected objects (to select multiple, use '--list' or '--template')", + Usage: "copy entire bucket or selected objects (to select multiple, use '--list', '--template', or '--prefix')", ArgsUsage: bucketObjectSrcArgument + " " + bucketDstArgument, Flags: bucketCmdsFlags[commandCopy], Action: copyBucketHandler, diff --git a/cmd/cli/cli/err.go b/cmd/cli/cli/err.go index a140276cf67..382562c40a7 100644 --- a/cmd/cli/cli/err.go +++ b/cmd/cli/cli/err.go @@ -147,7 +147,7 @@ func commandNotFoundError(c *cli.Context, cmd string) *errUsage { context: c, message: msg, helpData: c.App, - helpTemplate: teb.ShortUsageTmpl, + helpTemplate: teb.ExtendedUsageTmpl, bottomMessage: didYouMeanMessage(c, cmd, similar, closestCommand, distance, trailingShow), } } @@ -275,7 +275,7 @@ func _errUsage(c *cli.Context, msg string) *errUsage { context: c, message: msg, helpData: c.Command, - helpTemplate: teb.UsageOnlyTmpl, // NOTE: `cli.CommandHelpTemplate` is often way too long + helpTemplate: teb.ShortUsageTmpl, } } diff --git a/cmd/cli/cli/etl.go b/cmd/cli/cli/etl.go index cb4bff149fd..49d879d6979 100644 --- a/cmd/cli/cli/etl.go +++ b/cmd/cli/cli/etl.go @@ -121,7 +121,7 @@ var ( } bckCmdETL = cli.Command{ Name: cmdBucket, - Usage: "transform entire bucket or selected objects (to select multiple, use '--list' or '--template')", + Usage: "transform entire bucket or selected objects (to select multiple, use '--list', '--template', or '--prefix')", ArgsUsage: etlNameArgument + " " + bucketObjectSrcArgument + " " + bucketDstArgument, Action: etlBucketHandler, Flags: etlSubFlags[cmdBucket], diff --git a/cmd/cli/cli/parse_uri.go b/cmd/cli/cli/parse_uri.go index 602dc944f7e..5abd1da0c95 100644 --- a/cmd/cli/cli/parse_uri.go +++ b/cmd/cli/cli/parse_uri.go @@ -154,8 +154,11 @@ func parseBckObjURI(c *cli.Context, uri string, emptyObjnameOK bool) (bck cmn.Bc } bck, objName, err = cmn.ParseBckObjectURI(uri, opts) if err != nil { - if len(uri) > 1 && uri[:2] == "--" { // FIXME: needed smth like c.LooksLikeFlag - return bck, objName, incorrectUsageMsg(c, "misplaced flag %q", uri) + if errV := errMisplacedFlag(c, uri); errV != nil { + return bck, objName, errV + } + if strings.Contains(err.Error(), cos.OnlyPlus) && strings.Contains(err.Error(), "bucket name") { + return bck, objName, fmt.Errorf("bucket name in %q is invalid: "+cos.OnlyPlus, uri) } var msg string if emptyObjnameOK { diff --git a/cmd/cli/cli/tcbtco.go b/cmd/cli/cli/tcbtco.go index 7e5a5e7f1d2..60af9822848 100644 --- a/cmd/cli/cli/tcbtco.go +++ b/cmd/cli/cli/tcbtco.go @@ -26,9 +26,10 @@ import ( // via (I) x-copy-bucket ("full bucket") _or_ (II) x-copy-listrange ("multi-object") // Notice a certain usable redundancy: -// (I) `ais cp from to --prefix abc" -// is the same as: -// (II) `ais cp from to --template abc" +// +// (I) `ais cp from to --prefix abc" is the same as (II) `ais cp from to --template abc" +// +// Also, note [CONVENTIONS] below. func copyBucketHandler(c *cli.Context) (err error) { var ( bckFrom, bckTo cmn.Bck @@ -39,11 +40,6 @@ func copyBucketHandler(c *cli.Context) (err error) { err = missingArgumentsError(c, c.Command.ArgsUsage) case c.NArg() == 1: bckFrom, objFrom, err = parseBckObjURI(c, c.Args().Get(0), true /*emptyObjnameOK*/) - if err != nil { - if strings.Contains(err.Error(), cos.OnlyPlus) && strings.Contains(err.Error(), "bucket name") { - err = fmt.Errorf("bucket name in %q is invalid: "+cos.OnlyPlus, c.Args().Get(0)) - } - } default: bckFrom, bckTo, objFrom, err = parseBcks(c, bucketSrcArgument, bucketDstArgument, 0 /*shift*/, true /*optionalSrcObjname*/) } @@ -51,11 +47,26 @@ func copyBucketHandler(c *cli.Context) (err error) { return err } - if flagIsSet(c, syncFlag) && bckTo.IsEmpty() { + // [CONVENTIONS] + // 1. '--sync' and '--latest' both require aistore to reach out for remote metadata and, therefore, + // if destination is omitted both options imply namesake in-cluster destination (ie., bckTo = bckFrom) + // 2. in addition, '--sync' also implies '--all' (will traverse non-present) + + if bckTo.IsEmpty() { + if !flagIsSet(c, syncFlag) && !flagIsSet(c, latestVerFlag) { + var hint string + if bckFrom.IsRemote() { + hint = fmt.Sprintf(" (or, did you mean 'ais cp %s %s' or 'ais cp %s %s'?)", + c.Args().Get(0), flprn(syncFlag), c.Args().Get(0), flprn(latestVerFlag)) + } + return incorrectUsageMsg(c, "missing destination bucket%s", hint) + } bckTo = bckFrom } - return copyTransform(c, "" /*etlName*/, objFrom, bckFrom, bckTo, flagIsSet(c, copyAllObjsFlag)) + allIncludingRemote := flagIsSet(c, copyAllObjsFlag) || flagIsSet(c, syncFlag) + + return copyTransform(c, "" /*etlName*/, objFrom, bckFrom, bckTo, allIncludingRemote) } // diff --git a/cmd/cli/cli/utils.go b/cmd/cli/cli/utils.go index 4dc3c01353c..b2a08535927 100644 --- a/cmd/cli/cli/utils.go +++ b/cmd/cli/cli/utils.go @@ -96,6 +96,13 @@ func arg0Node(c *cli.Context) (node *meta.Snode, sname string, err error) { return } +func errMisplacedFlag(c *cli.Context, arg string) (err error) { + if len(arg) > 1 && arg[0] == '-' { + err = incorrectUsageMsg(c, "missing command line argument (hint: flag '%s' misplaced?)", arg) + } + return err +} + func isWebURL(url string) bool { return cos.IsHTTP(url) || cos.IsHTTPS(url) } func jsonMarshalIndent(v any) ([]byte, error) { return jsoniter.MarshalIndent(v, "", " ") } diff --git a/cmd/cli/teb/templates.go b/cmd/cli/teb/templates.go index 70f8082e806..92409fd16a1 100644 --- a/cmd/cli/teb/templates.go +++ b/cmd/cli/teb/templates.go @@ -262,7 +262,7 @@ const ( "{{end}}" + "TOTAL\t " - ShortUsageTmpl = "{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} - {{.Usage}}\n" + + ExtendedUsageTmpl = "{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} - {{.Usage}}\n" + "\n\tCOMMANDS:\t" + "{{range .VisibleCategories}}" + "{{ range $index, $element := .VisibleCommands}}" + @@ -277,8 +277,11 @@ const ( "--{{FlagName $flag }}" + "{{end}}{{end}}\n" - UsageOnlyTmpl = `{{.HelpName}} - {{.Usage}} + ShortUsageTmpl = `{{.HelpName}} - {{.Usage}} {{.UsageText}} +USAGE: + {{.HelpName}} {{.ArgsUsage}} + See '--help' and docs/cli for details.` AuthNClusterTmpl = "CLUSTER ID\tALIAS\tURLs\n" +