diff --git a/cmd/uplink/cmd/config.go b/cmd/uplink/cmd/config.go index e6751fc00..0fe234b9c 100644 --- a/cmd/uplink/cmd/config.go +++ b/cmd/uplink/cmd/config.go @@ -57,7 +57,7 @@ type Config struct { // AccessConfig holds information about which accesses exist and are selected. type AccessConfig struct { Accesses map[string]string `internal:"true"` - Access string `help:"the serialized access, or name of the access to use" default:""` + Access string `help:"the serialized access, or name of the access to use" default:"" basic-help:"true"` // used for backward compatibility Scopes map[string]string `internal:"true"` // deprecated diff --git a/cmd/uplink/cmd/cp.go b/cmd/uplink/cmd/cp.go index 05f52c027..90ccb7ad3 100644 --- a/cmd/uplink/cmd/cp.go +++ b/cmd/uplink/cmd/cp.go @@ -38,6 +38,8 @@ func init() { progress = cpCmd.Flags().Bool("progress", true, "if true, show progress") expires = cpCmd.Flags().String("expires", "", "optional expiration date of an object. Please use format (yyyy-mm-ddThh:mm:ssZhh:mm)") metadata = cpCmd.Flags().String("metadata", "", "optional metadata for the object. Please use a single level JSON object of string to string only") + + setBasicFlags(cpCmd.Flags(), "progress", "expires", "metadata") } // upload transfers src from local machine to s3 compatible object dst diff --git a/cmd/uplink/cmd/import.go b/cmd/uplink/cmd/import.go index 29a435d3b..b69eb13a8 100644 --- a/cmd/uplink/cmd/import.go +++ b/cmd/uplink/cmd/import.go @@ -39,6 +39,9 @@ func init() { // flags. // TODO: revisit after the configuration/flag code is refactored. process.Bind(importCmd, &importCfg, defaults, cfgstruct.ConfDir(confDir)) + + // NB: access is not supported by `setup` or `import` + cfgstruct.SetBoolAnnotation(importCmd.Flags(), "access", cfgstruct.BasicHelpAnnotationName, false) } // importMain is the function executed when importCmd is called diff --git a/cmd/uplink/cmd/ls.go b/cmd/uplink/cmd/ls.go index 81f543bf6..3b73d1a3a 100644 --- a/cmd/uplink/cmd/ls.go +++ b/cmd/uplink/cmd/ls.go @@ -29,6 +29,8 @@ func init() { }, RootCmd) lsRecursiveFlag = lsCmd.Flags().Bool("recursive", false, "if true, list recursively") lsEncryptedFlag = lsCmd.Flags().Bool("encrypted", false, "if true, show paths as base64-encoded encrypted paths") + + setBasicFlags(lsCmd.Flags(), "recursive", "encrypted") } func list(cmd *cobra.Command, args []string) error { diff --git a/cmd/uplink/cmd/rm.go b/cmd/uplink/cmd/rm.go index 644c11a02..88a8cb508 100644 --- a/cmd/uplink/cmd/rm.go +++ b/cmd/uplink/cmd/rm.go @@ -25,6 +25,7 @@ func init() { RunE: deleteObject, }, RootCmd) rmEncryptedFlag = rmCmd.Flags().Bool("encrypted", false, "if true, treat paths as base64-encoded encrypted paths") + setBasicFlags(rmCmd.Flags(), "encrypted") } func deleteObject(cmd *cobra.Command, args []string) error { diff --git a/cmd/uplink/cmd/root.go b/cmd/uplink/cmd/root.go index 9197e86df..68e39ac23 100644 --- a/cmd/uplink/cmd/root.go +++ b/cmd/uplink/cmd/root.go @@ -4,15 +4,19 @@ package cmd import ( + "bufio" + "bytes" "context" "flag" "fmt" "os" "runtime" "runtime/pprof" + "strings" "github.com/spf13/cast" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/zeebo/errs" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -26,6 +30,8 @@ import ( "storj.io/storj/private/version/checker" ) +const advancedFlagName = "advanced" + // UplinkFlags configuration flags type UplinkFlags struct { NonInteractive bool `help:"disable interactive mode" default:"false" setup:"true"` @@ -51,6 +57,12 @@ var ( func init() { defaultConfDir := fpath.ApplicationDir("storj", "uplink") cfgstruct.SetupFlag(zap.L(), RootCmd, &confDir, "config-dir", defaultConfDir, "main directory for uplink configuration") + + // NB: more-help flag is always retrieved using `findBoolFlagEarly()` + RootCmd.PersistentFlags().BoolVar(new(bool), advancedFlagName, false, "if used in with -h, print advanced flags help") + + setBasicFlags(RootCmd.PersistentFlags(), "config-dir", advancedFlagName) + setUsageFunc(RootCmd) } var cpuProfile = flag.String("profile.cpu", "", "file path of the cpu profile to be created") @@ -244,3 +256,98 @@ func combineCobraFuncs(funcs ...func(*cobra.Command, []string) error) func(*cobr return err } } + +/* `setUsageFunc` is a bit unconventional but cobra didn't leave much room for + extensibility here. `cmd.SetUsageTemplate` is fairly useless for our case without + the ability to add to the template's function map (see: https://golang.org/pkg/text/template/#hdr-Functions). + + Because we can't alter what `cmd.Usage` generates, we have to edit it afterwards. + In order to hook this function *and* get the usage string, we have to juggle the + `cmd.usageFunc` between our hook and `nil`, so that we can get the usage string + from the default usage func. +*/ +func setUsageFunc(cmd *cobra.Command) { + if findBoolFlagEarly(advancedFlagName) { + return + } + + reset := func() (set func()) { + original := cmd.UsageFunc() + cmd.SetUsageFunc(nil) + + return func() { + cmd.SetUsageFunc(original) + } + } + + cmd.SetUsageFunc(func(cmd *cobra.Command) error { + set := reset() + usageStr := cmd.UsageString() + defer set() + + usageScanner := bufio.NewScanner(bytes.NewBufferString(usageStr)) + + var basicFlags []string + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + basic, ok := flag.Annotations[cfgstruct.BasicHelpAnnotationName] + if ok && len(basic) == 1 && basic[0] == "true" { + basicFlags = append(basicFlags, flag.Name) + } + }) + + for usageScanner.Scan() { + line := usageScanner.Text() + trimmedLine := strings.TrimSpace(line) + + var flagName string + if _, err := fmt.Sscanf(trimmedLine, "--%s", &flagName); err != nil { + fmt.Println(line) + continue + } + + // TODO: properly filter flags with short names + if !strings.HasPrefix(trimmedLine, "--") { + fmt.Println(line) + } + + for _, basicFlag := range basicFlags { + if basicFlag == flagName { + fmt.Println(line) + } + } + } + return nil + }) +} + +func findBoolFlagEarly(flagName string) bool { + for i, arg := range os.Args { + arg := arg + argHasPrefix := func(format string, args ...interface{}) bool { + return strings.HasPrefix(arg, fmt.Sprintf(format, args...)) + } + + if !argHasPrefix("--%s", flagName) { + continue + } + + // NB: covers `-- false` usage + if i+1 != len(os.Args) { + next := os.Args[i+1] + if next == "false" { + return false + } + } + + if !argHasPrefix("--%s=false", flagName) { + return true + } + } + return false +} + +func setBasicFlags(flagset interface{}, flagNames ...string) { + for _, name := range flagNames { + cfgstruct.SetBoolAnnotation(flagset, name, cfgstruct.BasicHelpAnnotationName, true) + } +} diff --git a/cmd/uplink/cmd/setup.go b/cmd/uplink/cmd/setup.go index ca9c9bcd4..960b3c3e3 100644 --- a/cmd/uplink/cmd/setup.go +++ b/cmd/uplink/cmd/setup.go @@ -32,6 +32,9 @@ var ( func init() { RootCmd.AddCommand(setupCmd) process.Bind(setupCmd, &setupCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.SetupMode()) + + // NB: access is not supported by `setup` or `import` + cfgstruct.SetBoolAnnotation(setupCmd.Flags(), "access", cfgstruct.BasicHelpAnnotationName, false) } func cmdSetup(cmd *cobra.Command, args []string) (err error) { diff --git a/cmd/uplink/cmd/share.go b/cmd/uplink/cmd/share.go index 5985b6eff..8c70a004a 100644 --- a/cmd/uplink/cmd/share.go +++ b/cmd/uplink/cmd/share.go @@ -21,16 +21,16 @@ import ( ) var shareCfg struct { - DisallowReads bool `default:"false" help:"if true, disallow reads"` - DisallowWrites bool `default:"false" help:"if true, disallow writes"` - DisallowLists bool `default:"false" help:"if true, disallow lists"` - DisallowDeletes bool `default:"false" help:"if true, disallow deletes"` - Readonly bool `default:"false" help:"implies disallow_writes and disallow_deletes"` - Writeonly bool `default:"false" help:"implies disallow_reads and disallow_lists"` - NotBefore string `help:"disallow access before this time"` - NotAfter string `help:"disallow access after this time"` + DisallowReads bool `default:"false" help:"if true, disallow reads" basic-help:"true"` + DisallowWrites bool `default:"false" help:"if true, disallow writes" basic-help:"true"` + DisallowLists bool `default:"false" help:"if true, disallow lists" basic-help:"true"` + DisallowDeletes bool `default:"false" help:"if true, disallow deletes" basic-help:"true"` + Readonly bool `default:"false" help:"implies disallow_writes and disallow_deletes" basic-help:"true"` + Writeonly bool `default:"false" help:"implies disallow_reads and disallow_lists" basic-help:"true"` + NotBefore string `help:"disallow access before this time" basic-help:"true"` + NotAfter string `help:"disallow access after this time" basic-help:"true"` AllowedPathPrefix []string `help:"whitelist of path prefixes to require, overrides the [allowed-path-prefix] arguments"` - ExportTo string `default:"" help:"path to export the shared access to"` + ExportTo string `default:"" help:"path to export the shared access to" basic-help:"true"` // Share requires information about the current access AccessConfig diff --git a/pkg/cfgstruct/bind.go b/pkg/cfgstruct/bind.go index 3cf29e47c..c26acda08 100644 --- a/pkg/cfgstruct/bind.go +++ b/pkg/cfgstruct/bind.go @@ -27,6 +27,10 @@ const ( // FlagSource is a source annotation for config values that just come from // flags (i.e. are never persisted to file) FlagSource = "flag" + + // BasicHelpAnnotationName is the name of the annotation used to indicate + // a flag should be included in basic usage/help. + BasicHelpAnnotationName = "basic-help" ) var ( @@ -182,18 +186,18 @@ func bindConfig(flags FlagSet, prefix string, val reflect.Value, vars map[string markHidden := false if onlyForSetup { - setBoolAnnotation(flags, flagname, "setup") + SetBoolAnnotation(flags, flagname, "setup", true) } if field.Tag.Get("user") == "true" { - setBoolAnnotation(flags, flagname, "user") + SetBoolAnnotation(flags, flagname, "user", true) } if field.Tag.Get("hidden") == "true" { markHidden = true - setBoolAnnotation(flags, flagname, "hidden") + SetBoolAnnotation(flags, flagname, "hidden", true) } if field.Tag.Get("deprecated") == "true" { markHidden = true - setBoolAnnotation(flags, flagname, "deprecated") + SetBoolAnnotation(flags, flagname, "deprecated", true) } if source := field.Tag.Get("source"); source != "" { setSourceAnnotation(flags, flagname, source) @@ -276,20 +280,23 @@ func bindConfig(flags FlagSet, prefix string, val reflect.Value, vars map[string panic(fmt.Sprintf("invalid field type: %s", field.Type.String())) } if onlyForSetup { - setBoolAnnotation(flags, flagname, "setup") + SetBoolAnnotation(flags, flagname, "setup", true) } if field.Tag.Get("user") == "true" { - setBoolAnnotation(flags, flagname, "user") + SetBoolAnnotation(flags, flagname, "user", true) + } + if field.Tag.Get(BasicHelpAnnotationName) == "true" { + SetBoolAnnotation(flags, flagname, BasicHelpAnnotationName, true) } markHidden := false if field.Tag.Get("hidden") == "true" { markHidden = true - setBoolAnnotation(flags, flagname, "hidden") + SetBoolAnnotation(flags, flagname, "hidden", true) } if field.Tag.Get("deprecated") == "true" { markHidden = true - setBoolAnnotation(flags, flagname, "deprecated") + SetBoolAnnotation(flags, flagname, "deprecated", true) } if source := field.Tag.Get("source"); source != "" { setSourceAnnotation(flags, flagname, source) @@ -343,13 +350,14 @@ func setStringAnnotation(flagset interface{}, name, key, value string) { } } -func setBoolAnnotation(flagset interface{}, name, key string) { +// SetBoolAnnotation sets an annotation (if it can) on flagset with a value of []string{"true|false"}. +func SetBoolAnnotation(flagset interface{}, name, key string, value bool) { flags, ok := flagset.(*pflag.FlagSet) if !ok { return } - err := flags.SetAnnotation(name, key, []string{"true"}) + err := flags.SetAnnotation(name, key, []string{strconv.FormatBool(value)}) if err != nil { panic(fmt.Sprintf("unable to set %s annotation for %s: %v", key, name, err)) }