mirror of
https://github.com/hibiken/asynq.git
synced 2025-10-07 18:42:02 +08:00
(cli): Improve help command output
This commit is contained in:
@@ -24,8 +24,11 @@ func init() {
|
||||
}
|
||||
|
||||
var cronCmd = &cobra.Command{
|
||||
Use: "cron",
|
||||
Use: "cron <command> [flags]",
|
||||
Short: "Manage cron",
|
||||
Annotations: map[string]string{
|
||||
"IsCore": "true",
|
||||
},
|
||||
}
|
||||
|
||||
var cronListCmd = &cobra.Command{
|
||||
|
@@ -19,8 +19,11 @@ func init() {
|
||||
}
|
||||
|
||||
var groupCmd = &cobra.Command{
|
||||
Use: "group",
|
||||
Use: "group <command> [flags]",
|
||||
Short: "Manage groups",
|
||||
Annotations: map[string]string{
|
||||
"IsCore": "true",
|
||||
},
|
||||
}
|
||||
|
||||
var groupListCmd = &cobra.Command{
|
||||
|
@@ -31,8 +31,11 @@ func init() {
|
||||
}
|
||||
|
||||
var queueCmd = &cobra.Command{
|
||||
Use: "queue",
|
||||
Use: "queue <command> [flags]",
|
||||
Short: "Manage queues",
|
||||
Annotations: map[string]string{
|
||||
"IsCore": "true",
|
||||
},
|
||||
}
|
||||
|
||||
var queueListCmd = &cobra.Command{
|
||||
|
@@ -14,11 +14,14 @@ import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/hibiken/asynq/internal/base"
|
||||
"github.com/hibiken/asynq/internal/rdb"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/viper"
|
||||
@@ -39,10 +42,22 @@ var (
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "asynq",
|
||||
Short: "A monitoring tool for asynq queues",
|
||||
Long: `Asynq is a montoring CLI to inspect tasks and queues managed by asynq.`,
|
||||
Use: "asynq <command> <subcommand> [flags]",
|
||||
Short: "Asynq CLI",
|
||||
Long: `Command line tool to inspect tasks and queues managed by Asynq`,
|
||||
Version: base.Version,
|
||||
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
|
||||
Example: heredoc.Doc(`
|
||||
$ asynq stats
|
||||
$ asynq queue pause myqueue
|
||||
$ asynq task ls --queue=myqueue --state=archived`),
|
||||
Annotations: map[string]string{
|
||||
"help:feedback": heredoc.Doc(`
|
||||
Open an issue at https://github.com/hibiken/asynq/issues/new/choose`),
|
||||
},
|
||||
}
|
||||
|
||||
var versionOutput = fmt.Sprintf("asynq version %s\n", base.Version)
|
||||
@@ -64,9 +79,212 @@ func Execute() {
|
||||
}
|
||||
}
|
||||
|
||||
func isRootCmd(cmd *cobra.Command) bool {
|
||||
return cmd != nil && !cmd.HasParent()
|
||||
}
|
||||
|
||||
// computes padding used when displaying both subcommands and flags
|
||||
func computePaddingForCommandsAndFlags(cmd *cobra.Command) int {
|
||||
max := 0
|
||||
for _, c := range cmd.Commands() {
|
||||
if n := utf8.RuneCountInString(c.Name() + ":"); n > max {
|
||||
max = n
|
||||
}
|
||||
}
|
||||
cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
|
||||
if n := utf8.RuneCountInString("--" + f.Name); n > max {
|
||||
max = n
|
||||
}
|
||||
})
|
||||
cmd.InheritedFlags().VisitAll(func(f *pflag.Flag) {
|
||||
if n := utf8.RuneCountInString("--" + f.Name); n > max {
|
||||
max = n
|
||||
}
|
||||
})
|
||||
return max
|
||||
}
|
||||
|
||||
// computes padding used when displaying local flags only
|
||||
func computePaddingForLocalFlagsOnly(cmd *cobra.Command) int {
|
||||
max := 0
|
||||
cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
|
||||
if n := utf8.RuneCountInString("--" + f.Name); n > max {
|
||||
max = n
|
||||
}
|
||||
})
|
||||
return max
|
||||
}
|
||||
|
||||
func rootHelpFunc(cmd *cobra.Command, args []string) {
|
||||
// Display helpful error message when user mistypes a subcommand (e.g. 'asynq queue lst').
|
||||
if isRootCmd(cmd.Parent()) && len(args) >= 2 && args[1] != "--help" && args[1] != "-h" {
|
||||
printSubcommandSuggestions(cmd, args[1])
|
||||
return
|
||||
}
|
||||
|
||||
padding := computePaddingForCommandsAndFlags(cmd)
|
||||
|
||||
var (
|
||||
coreCmds []string
|
||||
additionalCmds []string
|
||||
)
|
||||
for _, c := range cmd.Commands() {
|
||||
if c.Hidden || c.Short == "" {
|
||||
continue
|
||||
}
|
||||
s := rpad(c.Name()+":", padding) + c.Short
|
||||
if _, ok := c.Annotations["IsCore"]; ok {
|
||||
coreCmds = append(coreCmds, s)
|
||||
} else {
|
||||
additionalCmds = append(additionalCmds, s)
|
||||
}
|
||||
}
|
||||
|
||||
type helpEntry struct {
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
var helpEntries []*helpEntry
|
||||
desc := cmd.Long
|
||||
if desc == "" {
|
||||
desc = cmd.Short
|
||||
}
|
||||
if desc != "" {
|
||||
helpEntries = append(helpEntries, &helpEntry{"", desc})
|
||||
}
|
||||
helpEntries = append(helpEntries, &helpEntry{"USAGE", cmd.UseLine()})
|
||||
if len(coreCmds) > 0 {
|
||||
helpEntries = append(helpEntries, &helpEntry{"CORE COMMANDS", strings.Join(coreCmds, "\n")})
|
||||
}
|
||||
if len(additionalCmds) > 0 {
|
||||
helpEntries = append(helpEntries, &helpEntry{"ADDITIONAL COMMANDS", strings.Join(additionalCmds, "\n")})
|
||||
}
|
||||
if cmd.LocalFlags().HasFlags() {
|
||||
helpEntries = append(helpEntries, &helpEntry{"FLAGS", strings.Join(flagUsages(cmd.LocalFlags(), padding), "\n")})
|
||||
}
|
||||
if cmd.InheritedFlags().HasFlags() {
|
||||
helpEntries = append(helpEntries, &helpEntry{"INHERITED FLAGS", strings.Join(flagUsages(cmd.InheritedFlags(), padding), "\n")})
|
||||
}
|
||||
if cmd.Example != "" {
|
||||
helpEntries = append(helpEntries, &helpEntry{"EXAMPLES", cmd.Example})
|
||||
}
|
||||
helpEntries = append(helpEntries, &helpEntry{"LEARN MORE", heredoc.Doc(`
|
||||
Use 'asynq <command> <subcommand> --help' for more information about a command.
|
||||
Read a manual at https://github.com/hibiken/asynq/wiki/cli.
|
||||
Follow a tutorial at https://github.com/hibiken/asynq/wiki/cli-tutor`)})
|
||||
if s, ok := cmd.Annotations["help:feedback"]; ok {
|
||||
helpEntries = append(helpEntries, &helpEntry{"FEEDBACK", s})
|
||||
}
|
||||
|
||||
out := cmd.OutOrStdout()
|
||||
bold := color.New(color.Bold)
|
||||
for _, e := range helpEntries {
|
||||
if e.Title != "" {
|
||||
// If there is a title, add indentation to each line in the body
|
||||
bold.Fprintln(out, e.Title)
|
||||
fmt.Fprintln(out, indent(e.Body, 2 /* spaces */))
|
||||
} else {
|
||||
// If there is no title, print the body as is
|
||||
fmt.Fprintln(out, e.Body)
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
}
|
||||
|
||||
func rootUsageFunc(cmd *cobra.Command) error {
|
||||
out := cmd.OutOrStdout()
|
||||
fmt.Fprintf(out, "Usage: %s", cmd.UseLine())
|
||||
if subcmds := cmd.Commands(); len(subcmds) > 0 {
|
||||
fmt.Fprint(out, "\n\nAvailable commands:\n")
|
||||
for _, c := range subcmds {
|
||||
if c.Hidden {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(out, " %s\n", c.Name())
|
||||
}
|
||||
}
|
||||
|
||||
padding := computePaddingForLocalFlagsOnly(cmd)
|
||||
if cmd.LocalFlags().HasFlags() {
|
||||
fmt.Fprint(out, "\n\nFlags:\n")
|
||||
for _, usg := range flagUsages(cmd.LocalFlags(), padding) {
|
||||
fmt.Fprintf(out, " %s\n", usg)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printSubcommandSuggestions(cmd *cobra.Command, arg string) {
|
||||
out := cmd.OutOrStdout()
|
||||
fmt.Fprintf(out, "unknown command %q for %q\n", arg, cmd.CommandPath())
|
||||
if cmd.SuggestionsMinimumDistance <= 0 {
|
||||
cmd.SuggestionsMinimumDistance = 2
|
||||
}
|
||||
candidates := cmd.SuggestionsFor(arg)
|
||||
if len(candidates) > 0 {
|
||||
fmt.Fprint(out, "\nDid you mean this?\n")
|
||||
for _, c := range candidates {
|
||||
fmt.Fprintf(out, "\t%s\n", c)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
rootUsageFunc(cmd)
|
||||
}
|
||||
|
||||
// flagUsages returns a list of flag usage strings in the given FlagSet.
|
||||
func flagUsages(flagset *pflag.FlagSet, padding int) []string {
|
||||
var usgs []string
|
||||
// IDEA: Should we show the short-flag too?
|
||||
flagset.VisitAll(func(f *pflag.Flag) {
|
||||
s := rpad("--"+f.Name, padding) + f.Usage
|
||||
usgs = append(usgs, s)
|
||||
})
|
||||
return usgs
|
||||
}
|
||||
|
||||
// rpad adds padding to the right of a string.
|
||||
func rpad(s string, padding int) string {
|
||||
tmpl := fmt.Sprintf("%%-%ds ", padding)
|
||||
return fmt.Sprintf(tmpl, s)
|
||||
|
||||
}
|
||||
|
||||
// indent indents the given text by given spaces.
|
||||
func indent(text string, space int) string {
|
||||
if len(text) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
indentation := strings.Repeat(" ", space)
|
||||
lastRune := '\n'
|
||||
for _, r := range text {
|
||||
if lastRune == '\n' {
|
||||
b.WriteString(indentation)
|
||||
}
|
||||
b.WriteRune(r)
|
||||
lastRune = r
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// dedent removes any indentation from the given text.
|
||||
func dedent(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
var b strings.Builder
|
||||
for _, l := range lines {
|
||||
b.WriteString(strings.TrimLeftFunc(l, unicode.IsSpace))
|
||||
b.WriteRune('\n')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
// TODO: Update help command
|
||||
rootCmd.SetHelpFunc(rootHelpFunc)
|
||||
rootCmd.SetUsageFunc(rootUsageFunc)
|
||||
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.SetVersionTemplate(versionOutput)
|
||||
|
||||
|
@@ -21,8 +21,11 @@ func init() {
|
||||
}
|
||||
|
||||
var serverCmd = &cobra.Command{
|
||||
Use: "server",
|
||||
Use: "server <command> [flags]",
|
||||
Short: "Manage servers",
|
||||
Annotations: map[string]string{
|
||||
"IsCore": "true",
|
||||
},
|
||||
}
|
||||
|
||||
var serverListCmd = &cobra.Command{
|
||||
|
@@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/fatih/color"
|
||||
"github.com/hibiken/asynq/internal/rdb"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -24,21 +25,20 @@ import (
|
||||
// statsCmd represents the stats command
|
||||
var statsCmd = &cobra.Command{
|
||||
Use: "stats",
|
||||
Short: "Shows current state of the tasks and queues",
|
||||
Long: `Stats (aysnq stats) will show the overview of tasks and queues at that instant.
|
||||
Short: "View current state",
|
||||
Long: heredoc.Doc(`
|
||||
Stats shows the overview of tasks and queues at that instant.
|
||||
|
||||
Specifically, the command shows the following:
|
||||
* Number of tasks in each state
|
||||
* Number of tasks in each queue
|
||||
* Aggregate data for the current day
|
||||
* Basic information about the running redis instance
|
||||
|
||||
To monitor the tasks continuously, it's recommended that you run this
|
||||
command in conjunction with the watch command.
|
||||
|
||||
Example: watch -n 3 asynq stats -> Shows current state of tasks every three seconds`,
|
||||
The command shows the following:
|
||||
* Number of tasks in each state
|
||||
* Number of tasks in each queue
|
||||
* Aggregate data for the current day
|
||||
* Basic information about the running redis instance`),
|
||||
Args: cobra.NoArgs,
|
||||
Run: stats,
|
||||
Annotations: map[string]string{
|
||||
"IsCore": "true",
|
||||
},
|
||||
}
|
||||
|
||||
var jsonFlag bool
|
||||
|
@@ -72,8 +72,11 @@ func init() {
|
||||
}
|
||||
|
||||
var taskCmd = &cobra.Command{
|
||||
Use: "task",
|
||||
Use: "task <command> [flags]",
|
||||
Short: "Manage tasks",
|
||||
Annotations: map[string]string{
|
||||
"IsCore": "true",
|
||||
},
|
||||
}
|
||||
|
||||
var taskListCmd = &cobra.Command{
|
||||
|
@@ -3,6 +3,7 @@ module github.com/hibiken/asynq/tools
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1 // indirect
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/go-redis/redis/v8 v8.11.4
|
||||
github.com/hibiken/asynq v0.23.0
|
||||
@@ -11,5 +12,6 @@ require (
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/spf13/afero v1.1.2 // indirect
|
||||
github.com/spf13/cobra v1.1.1
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.7.0
|
||||
)
|
||||
|
@@ -14,6 +14,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A=
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
|
Reference in New Issue
Block a user