diff --git a/examples/ilc.yml b/examples/ilc.yml index 40b5502..5d092ce 100644 --- a/examples/ilc.yml +++ b/examples/ilc.yml @@ -10,6 +10,8 @@ commands: Los Angeles International Airport: lax Munich International Airport: muc calendar: + aliases: + - cal description: Display a calendar for the month inputs: month: diff --git a/main.go b/main.go index 3ad7814..207cd8c 100644 --- a/main.go +++ b/main.go @@ -18,8 +18,9 @@ var ( func main() { mainFlagSet.Usage = func() { - u := NewUsage(os.Args[0:1], "ILC", "") - fmt.Printf("%s", u.String()) + u := NewUsage(mainFlagSet.Output()) + u.Entrypoint = os.Args[0:1] + u.Print() os.Exit(0) } mainFlagSet.BoolFunc("version", "Displays the version", func(_ string) error { diff --git a/runner.go b/runner.go index b4e5f87..362979d 100644 --- a/runner.go +++ b/runner.go @@ -26,10 +26,13 @@ type Runner struct { } func (r *Runner) printUsage(cs CommandSet) { - entrypoint := append([]string{}, r.Entrypoint...) - entrypoint = append(entrypoint, cs.String()) - u := NewUsage(entrypoint, "ILC", cs.Description()).ImportCommandSet(cs) - fmt.Fprint(r.Stderr, u.String()) + u := NewUsage(mainFlagSet.Output()).ImportCommandSet(cs) + u.Entrypoint = append([]string{}, r.Entrypoint...) + if s := cs.String(); s != "" { + u.Entrypoint = append(u.Entrypoint, s) + } + u.Description = cs.Description() + u.Print() os.Exit(0) } diff --git a/usage.go b/usage.go index 08b2be0..7ba673e 100644 --- a/usage.go +++ b/usage.go @@ -5,37 +5,44 @@ import ( "fmt" "io" "strings" + + "github.com/muesli/termenv" ) type Usage struct { - Title string - Description string - commandNames []string - commandDescs []string - inputNames []string - inputDescs []string - flagNames []string - flagDescs []string - Entrypoint []string + Title string + Description string + commands [][]string + inputs [][]string + flags [][]string + Entrypoint []string + output *termenv.Output } func (u Usage) printSection(b io.Writer, header, content string) { + o := u.output content = strings.ReplaceAll(content, "\n", "\n ") content = strings.TrimSuffix(content, " ") - fmt.Fprintf(b, "%s\n %s\n", header, content) + fmt.Fprintf(b, "%s\n %s\n", + o.String(header).Bold(), + content, + ) } -func (u Usage) printInstructions(b io.Writer, names, descs []string, header, prefix string) { - var s strings.Builder +func (u Usage) printInstructions(b io.Writer, entries [][]string, header, prefix string) { + // Each entry in entries begins with the description followed by additional names + s := strings.Builder{} col := 15 - for _, name := range names { + for i, entry := range entries { + name := strings.Join(entry[1:], ", ") col = max(col, len([]rune(name))) + entries[i] = []string{entry[0], name} } col = col + 5 format := fmt.Sprintf("%%-%ds %%s\n", col) - for i, name := range names { - desc := descs[i] - name = fmt.Sprintf("%s%s", prefix, name) + for _, entry := range entries { + desc := entry[0] + name := fmt.Sprintf("%s%s", prefix, entry[1]) fmt.Fprintf(&s, format, name, desc) } u.printSection(b, header, s.String()) @@ -46,7 +53,7 @@ func (u Usage) usage() string { if len(u.Entrypoint) > 0 { params = append(params, u.Entrypoint[0]) } - if len(u.flagNames) > 0 { + if len(u.flags) > 0 { params = append(params, "[flags]") } if len(u.Entrypoint) > 1 { @@ -54,52 +61,62 @@ func (u Usage) usage() string { } else { params = append(params, "") } - if len(u.commandNames) > 0 { + if len(u.commands) > 0 { params = append(params, "") } - if len(u.inputNames) > 0 { + if len(u.inputs) > 0 { params = append(params, "[inputs]") } return strings.Join(params, " ") } func (u Usage) String() string { - var b strings.Builder + b := strings.Builder{} + o := u.output + fmt.Fprintf(&b, "\n") if s := u.Title; s != "" { - fmt.Fprintf(&b, "%s\n\n", s) + fmt.Fprintf(&b, "%s\n\n", + o.String(s).Underline(), + ) } if s := u.Description; s != "" { fmt.Fprintf(&b, "%s\n\n", s) } + if b.Len() > 0 { + fmt.Fprintf(&b, "\n") + } if s := u.usage(); s != "" { u.printSection(&b, "USAGE", s) b.WriteString("\n") } - if len(u.commandNames) > 0 { - u.printInstructions(&b, u.commandNames, u.commandDescs, "COMMANDS", "") + if len(u.commands) > 0 { + u.printInstructions(&b, u.commands, "COMMANDS", "") } - if len(u.inputNames) > 0 { - u.printInstructions(&b, u.inputNames, u.inputDescs, "INPUTS", "-") + if len(u.inputs) > 0 { + u.printInstructions(&b, u.inputs, "INPUTS", "-") } - if len(u.flagNames) > 0 { - u.printInstructions(&b, u.flagNames, u.flagDescs, "FLAGS", "-") + if len(u.flags) > 0 { + u.printInstructions(&b, u.flags, "FLAGS", "-") } b.WriteString("\n") return b.String() } +func (u Usage) Print() error { + _, err := u.output.WriteString(u.String()) + return err +} + func (u Usage) ImportCommands(commands []ConfigCommand) Usage { for _, command := range commands { - u.commandNames = append(u.commandNames, command.Name) - u.commandDescs = append(u.commandDescs, command.Description) + u.commands = append(u.commands, append([]string{command.Description, command.Name}, command.Aliases...)) } return u } func (u Usage) ImportInputs(inputs []ConfigInput) Usage { for _, input := range inputs { - u.inputNames = append(u.inputNames, input.Name) - u.inputDescs = append(u.inputDescs, input.Description) + u.inputs = append(u.inputs, []string{input.Description, input.Name}) } return u } @@ -108,15 +125,13 @@ func (u Usage) ImportCommandSet(cs CommandSet) Usage { return u.ImportCommands(cs.Subcommands()).ImportInputs(cs.Inputs()) } -func NewUsage(entrypoint []string, title, desc string) Usage { +func NewUsage(tty io.Writer) Usage { u := Usage{ - Title: title, - Description: desc, - Entrypoint: entrypoint, + Title: "ILC", + output: termenv.NewOutput(tty), } mainFlagSet.VisitAll(func(f *flag.Flag) { - u.flagNames = append(u.flagNames, f.Name) - u.flagDescs = append(u.flagDescs, f.Usage) + u.flags = append(u.flags, []string{f.Usage, f.Name}) }) return u } diff --git a/usage_test.go b/usage_test.go index 455a3cc..8be36ae 100644 --- a/usage_test.go +++ b/usage_test.go @@ -1,6 +1,7 @@ package main import ( + "os" "testing" "github.com/stretchr/testify/assert" @@ -8,11 +9,13 @@ import ( func TestUsage_EmptyCommands(t *testing.T) { u := usageFixture() - u.commandNames = []string{} - expected := `test + u.commands = [][]string{} + expected := ` +test this is a fixture + USAGE ilc config.yaml subcommand [inputs] @@ -27,16 +30,18 @@ INPUTS func TestUsage_EmptyInputs(t *testing.T) { u := usageFixture() - u.inputNames = []string{} - expected := `test + u.inputs = [][]string{} + expected := ` +test this is a fixture + USAGE ilc config.yaml subcommand COMMANDS - a a subcommand + a, aa a subcommand b b subcommand @@ -47,15 +52,17 @@ COMMANDS func TestUsage_Entrypoint_Empty(t *testing.T) { u := usageFixture() u.Entrypoint = []string{} - expected := `test + expected := ` +test this is a fixture + USAGE [inputs] COMMANDS - a a subcommand + a, aa a subcommand b b subcommand INPUTS @@ -70,15 +77,17 @@ INPUTS func TestUsage_Entrypoint_One(t *testing.T) { u := usageFixture() u.Entrypoint = []string{"ilc"} - expected := `test + expected := ` +test this is a fixture + USAGE ilc [inputs] COMMANDS - a a subcommand + a, aa a subcommand b b subcommand INPUTS @@ -93,15 +102,17 @@ INPUTS func TestUsage_Entrypoint_Two(t *testing.T) { u := usageFixture() u.Entrypoint = []string{"ilc", "config.yaml"} - expected := `test + expected := ` +test this is a fixture + USAGE ilc config.yaml [inputs] COMMANDS - a a subcommand + a, aa a subcommand b b subcommand INPUTS @@ -116,15 +127,17 @@ INPUTS func TestUsage_Entrypoint_Many(t *testing.T) { u := usageFixture() u.Entrypoint = []string{"ilc", "config.yaml", "command", "subcommand"} - expected := `test + expected := ` +test this is a fixture + USAGE ilc config.yaml command subcommand [inputs] COMMANDS - a a subcommand + a, aa a subcommand b b subcommand INPUTS @@ -139,13 +152,15 @@ INPUTS func TestUsage_Title_Blank(t *testing.T) { u := usageFixture() u.Title = "" - expected := `this is a fixture + expected := ` +this is a fixture + USAGE ilc config.yaml subcommand [inputs] COMMANDS - a a subcommand + a, aa a subcommand b b subcommand INPUTS @@ -160,13 +175,15 @@ INPUTS func TestUsage_Description_Blank(t *testing.T) { u := usageFixture() u.Description = "" - expected := `test + expected := ` +test + USAGE ilc config.yaml subcommand [inputs] COMMANDS - a a subcommand + a, aa a subcommand b b subcommand INPUTS @@ -180,13 +197,16 @@ INPUTS func usageFixture() Usage { commands := []ConfigCommand{ - {Name: "a", Description: "a subcommand"}, + {Name: "a", Description: "a subcommand", Aliases: []string{"aa"}}, {Name: "b", Description: "b subcommand"}, } inputs := []ConfigInput{ {Name: "c", Description: "c input"}, {Name: "d", Description: "d input"}, } - entrypoint := []string{"ilc", "config.yaml", "subcommand"} - return NewUsage(entrypoint, "test", "this is a fixture").ImportCommands(commands).ImportInputs(inputs) + u := NewUsage(os.Stdout).ImportCommands(commands).ImportInputs(inputs) + u.Entrypoint = []string{"ilc", "config.yaml", "subcommand"} + u.Title = "test" + u.Description = "this is a fixture" + return u }