Skip to content

Commit c962edf

Browse files
committed
Fix misaligned command usage for 'help' commands of Teleport binaries (#51660)
* Fix an issue command help is not aligned for help command * align when unknown command/subcommand
1 parent f05f33f commit c962edf

File tree

2 files changed

+106
-12
lines changed

2 files changed

+106
-12
lines changed

lib/utils/cli.go

+25-12
Original file line numberDiff line numberDiff line change
@@ -391,25 +391,38 @@ func createUsageTemplate(opts ...func(*usageTemplateOptions)) string {
391391
// pre-parsing the arguments then applying any changes to the usage template if
392392
// necessary.
393393
func UpdateAppUsageTemplate(app *kingpin.Application, args []string) {
394-
// If ParseContext fails, kingpin will not show usage so there is no need
395-
// to update anything here. See app.Parse for more details.
396-
context, err := app.ParseContext(args)
397-
if err != nil {
398-
return
399-
}
400-
401394
app.UsageTemplate(createUsageTemplate(
402-
withCommandPrintfWidth(app, context),
395+
withCommandPrintfWidth(app, args),
403396
))
404397
}
405398

406-
// withCommandPrintfWidth returns an usage template option that
399+
// withCommandPrintfWidth returns a usage template option that
407400
// updates command printf width if longer than default.
408-
func withCommandPrintfWidth(app *kingpin.Application, context *kingpin.ParseContext) func(*usageTemplateOptions) {
401+
func withCommandPrintfWidth(app *kingpin.Application, args []string) func(*usageTemplateOptions) {
409402
return func(opt *usageTemplateOptions) {
410403
var commands []*kingpin.CmdModel
411-
if context.SelectedCommand != nil {
412-
commands = context.SelectedCommand.Model().FlattenedCommands()
404+
405+
// When selected command is "help", skip the "help" arg
406+
// so the intended command is selected for calculation.
407+
if len(args) > 0 && args[0] == "help" {
408+
args = args[1:]
409+
}
410+
411+
appContext, err := app.ParseContext(args)
412+
switch {
413+
case appContext == nil:
414+
slog.WarnContext(context.Background(), "No application context found")
415+
return
416+
417+
// Note that ParseContext may return the current selected command that's
418+
// causing the error. We should continue in those cases when appContext is
419+
// not nil.
420+
case err != nil:
421+
slog.InfoContext(context.Background(), "Error parsing application context", "error", err)
422+
}
423+
424+
if appContext.SelectedCommand != nil {
425+
commands = appContext.SelectedCommand.Model().FlattenedCommands()
413426
} else {
414427
commands = app.Model().FlattenedCommands()
415428
}

lib/utils/cli_test.go

+81
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
package utils
2020

2121
import (
22+
"bytes"
2223
"crypto/x509"
2324
"fmt"
2425
"io"
2526
"log/slog"
2627
"testing"
2728

29+
"github.com/alecthomas/kingpin/v2"
2830
"github.com/gravitational/trace"
2931
"github.com/stretchr/testify/require"
3032
)
@@ -161,3 +163,82 @@ func TestAllowWhitespace(t *testing.T) {
161163
require.Equal(t, tt.out, AllowWhitespace(tt.in), fmt.Sprintf("test case %v", i))
162164
}
163165
}
166+
167+
func TestUpdateAppUsageTemplate(t *testing.T) {
168+
makeApp := func(usageWriter io.Writer) *kingpin.Application {
169+
app := InitCLIParser("TestUpdateAppUsageTemplate", "some help message")
170+
app.UsageWriter(usageWriter)
171+
app.Terminate(func(int) {})
172+
173+
app.Command("hello", "Hello.")
174+
175+
create := app.Command("create", "Create.")
176+
create.Command("box", "Box.")
177+
create.Command("rocket", "Rocket.")
178+
return app
179+
}
180+
181+
tests := []struct {
182+
name string
183+
inputArgs []string
184+
outputContains string
185+
}{
186+
{
187+
name: "command width aligned for app help",
188+
inputArgs: []string{},
189+
outputContains: `
190+
Commands:
191+
help Show help.
192+
hello Hello.
193+
create box Box.
194+
create rocket Rocket.
195+
`,
196+
},
197+
{
198+
name: "command width aligned for command help",
199+
inputArgs: []string{"create"},
200+
outputContains: `
201+
Commands:
202+
create box Box.
203+
create rocket Rocket.
204+
`,
205+
},
206+
{
207+
name: "command width aligned for unknown command error",
208+
inputArgs: []string{"unknown"},
209+
outputContains: `
210+
Commands:
211+
help Show help.
212+
hello Hello.
213+
create box Box.
214+
create rocket Rocket.
215+
`,
216+
},
217+
}
218+
for _, tt := range tests {
219+
t.Run(tt.name, func(t *testing.T) {
220+
t.Run("help flag", func(t *testing.T) {
221+
var buffer bytes.Buffer
222+
app := makeApp(&buffer)
223+
args := append(tt.inputArgs, "--help")
224+
UpdateAppUsageTemplate(app, args)
225+
226+
app.Usage(args)
227+
require.Contains(t, buffer.String(), tt.outputContains)
228+
})
229+
230+
t.Run("help command", func(t *testing.T) {
231+
var buffer bytes.Buffer
232+
app := makeApp(&buffer)
233+
args := append([]string{"help"}, tt.inputArgs...)
234+
UpdateAppUsageTemplate(app, args)
235+
236+
// HelpCommand is triggered on PreAction during Parse.
237+
// See kingpin.Application.init for more details.
238+
_, err := app.Parse(args)
239+
require.NoError(t, err)
240+
require.Contains(t, buffer.String(), tt.outputContains)
241+
})
242+
})
243+
}
244+
}

0 commit comments

Comments
 (0)