github.com/alecthomas/kong@v0.9.1-0.20240410131203-2ab5733f1179/help_test.go (about)

     1  package kong_test
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/alecthomas/assert/v2"
    10  	"github.com/alecthomas/kong"
    11  )
    12  
    13  func panicsTrue(t *testing.T, f func()) {
    14  	defer func() {
    15  		if value := recover(); value != nil {
    16  			if boolval, ok := value.(bool); !ok || !boolval {
    17  				t.Fatalf("expected panic with true but got %v", value)
    18  			}
    19  		}
    20  	}()
    21  	f()
    22  	t.Fatal("expected panic did not occur")
    23  }
    24  
    25  type threeArg struct {
    26  	RequiredThree bool   `required`
    27  	Three         string `arg`
    28  }
    29  
    30  func (threeArg) Help() string {
    31  	return `Detailed help provided through the HelpProvider interface.`
    32  }
    33  
    34  func TestHelpOptionalArgs(t *testing.T) {
    35  	var cli struct {
    36  		One string `arg:"" optional:"" help:"One optional arg."`
    37  		Two string `arg:"" optional:"" help:"Two optional arg."`
    38  	}
    39  	w := bytes.NewBuffer(nil)
    40  	exited := false
    41  	app := mustNew(t, &cli,
    42  		kong.Name("test-app"),
    43  		kong.Writers(w, w),
    44  		kong.Exit(func(int) {
    45  			exited = true
    46  			panic(true) // Panic to fake "exit".
    47  		}),
    48  	)
    49  	panicsTrue(t, func() {
    50  		_, err := app.Parse([]string{"--help"})
    51  		assert.NoError(t, err)
    52  	})
    53  	assert.True(t, exited)
    54  	expected := `Usage: test-app [<one> [<two>]] [flags]
    55  
    56  Arguments:
    57    [<one>]    One optional arg.
    58    [<two>]    Two optional arg.
    59  
    60  Flags:
    61    -h, --help    Show context-sensitive help.
    62  `
    63  	assert.Equal(t, expected, w.String())
    64  }
    65  
    66  func TestHelp(t *testing.T) {
    67  	var cli struct {
    68  		String   string         `help:"A string flag."`
    69  		Bool     bool           `help:"A bool flag with very long help that wraps a lot and is verbose and is really verbose."`
    70  		Slice    []string       `help:"A slice of strings." placeholder:"STR"`
    71  		Map      map[string]int `help:"A map of strings to ints."`
    72  		Required bool           `required help:"A required flag."`
    73  		Sort     bool           `negatable short:"s" help:"Is sortable or not."`
    74  
    75  		One struct {
    76  			Flag string `help:"Nested flag."`
    77  		} `cmd help:"A subcommand."`
    78  
    79  		Two struct {
    80  			Flag        string `help:"Nested flag under two."`
    81  			RequiredTwo bool   `required`
    82  
    83  			Three threeArg `arg help:"Sub-sub-arg."`
    84  
    85  			Four struct {
    86  			} `cmd help:"Sub-sub-command."`
    87  		} `cmd help:"Another subcommand."`
    88  	}
    89  
    90  	w := bytes.NewBuffer(nil)
    91  	exited := false
    92  	app := mustNew(t, &cli,
    93  		kong.Name("test-app"),
    94  		kong.Description("A test app."),
    95  		kong.Writers(w, w),
    96  		kong.Exit(func(int) {
    97  			exited = true
    98  			panic(true) // Panic to fake "exit".
    99  		}),
   100  	)
   101  
   102  	t.Run("Full", func(t *testing.T) {
   103  		panicsTrue(t, func() {
   104  			_, err := app.Parse([]string{"--help"})
   105  			assert.NoError(t, err)
   106  		})
   107  		assert.True(t, exited)
   108  		expected := `Usage: test-app --required <command> [flags]
   109  
   110  A test app.
   111  
   112  Flags:
   113    -h, --help                 Show context-sensitive help.
   114        --string=STRING        A string flag.
   115        --bool                 A bool flag with very long help that wraps a lot
   116                               and is verbose and is really verbose.
   117        --slice=STR,...        A slice of strings.
   118        --map=KEY=VALUE;...    A map of strings to ints.
   119        --required             A required flag.
   120    -s, --[no-]sort            Is sortable or not.
   121  
   122  Commands:
   123    one --required [flags]
   124      A subcommand.
   125  
   126    two <three> --required --required-two --required-three [flags]
   127      Sub-sub-arg.
   128  
   129    two four --required --required-two [flags]
   130      Sub-sub-command.
   131  
   132  Run "test-app <command> --help" for more information on a command.
   133  `
   134  		t.Log(w.String())
   135  		t.Log(expected)
   136  		assert.Equal(t, expected, w.String())
   137  	})
   138  
   139  	t.Run("Selected", func(t *testing.T) {
   140  		exited = false
   141  		w.Truncate(0)
   142  		panicsTrue(t, func() {
   143  			_, err := app.Parse([]string{"two", "hello", "--help"})
   144  			assert.NoError(t, err)
   145  		})
   146  		assert.True(t, exited)
   147  		expected := `Usage: test-app two <three> --required --required-two --required-three [flags]
   148  
   149  Sub-sub-arg.
   150  
   151  Detailed help provided through the HelpProvider interface.
   152  
   153  Flags:
   154    -h, --help                 Show context-sensitive help.
   155        --string=STRING        A string flag.
   156        --bool                 A bool flag with very long help that wraps a lot
   157                               and is verbose and is really verbose.
   158        --slice=STR,...        A slice of strings.
   159        --map=KEY=VALUE;...    A map of strings to ints.
   160        --required             A required flag.
   161    -s, --[no-]sort            Is sortable or not.
   162  
   163        --flag=STRING          Nested flag under two.
   164        --required-two
   165  
   166        --required-three
   167  `
   168  		t.Log(expected)
   169  		t.Log(w.String())
   170  		assert.Equal(t, expected, w.String())
   171  	})
   172  }
   173  
   174  func TestFlagsLast(t *testing.T) {
   175  	var cli struct {
   176  		String   string         `help:"A string flag."`
   177  		Bool     bool           `help:"A bool flag with very long help that wraps a lot and is verbose and is really verbose."`
   178  		Slice    []string       `help:"A slice of strings." placeholder:"STR"`
   179  		Map      map[string]int `help:"A map of strings to ints."`
   180  		Required bool           `required help:"A required flag."`
   181  
   182  		One struct {
   183  			Flag string `help:"Nested flag."`
   184  		} `cmd help:"A subcommand."`
   185  
   186  		Two struct {
   187  			Flag        string `help:"Nested flag under two."`
   188  			RequiredTwo bool   `required`
   189  
   190  			Three threeArg `arg help:"Sub-sub-arg."`
   191  
   192  			Four struct {
   193  			} `cmd help:"Sub-sub-command."`
   194  		} `cmd help:"Another subcommand."`
   195  	}
   196  
   197  	w := bytes.NewBuffer(nil)
   198  	exited := false
   199  	app := mustNew(t, &cli,
   200  		kong.Name("test-app"),
   201  		kong.Description("A test app."),
   202  		kong.HelpOptions{
   203  			FlagsLast: true,
   204  		},
   205  		kong.Writers(w, w),
   206  		kong.Exit(func(int) {
   207  			exited = true
   208  			panic(true) // Panic to fake "exit".
   209  		}),
   210  	)
   211  
   212  	t.Run("Full", func(t *testing.T) {
   213  		panicsTrue(t, func() {
   214  			_, err := app.Parse([]string{"--help"})
   215  			assert.NoError(t, err)
   216  		})
   217  		assert.True(t, exited)
   218  		expected := `Usage: test-app --required <command> [flags]
   219  
   220  A test app.
   221  
   222  Commands:
   223    one --required [flags]
   224      A subcommand.
   225  
   226    two <three> --required --required-two --required-three [flags]
   227      Sub-sub-arg.
   228  
   229    two four --required --required-two [flags]
   230      Sub-sub-command.
   231  
   232  Flags:
   233    -h, --help                 Show context-sensitive help.
   234        --string=STRING        A string flag.
   235        --bool                 A bool flag with very long help that wraps a lot
   236                               and is verbose and is really verbose.
   237        --slice=STR,...        A slice of strings.
   238        --map=KEY=VALUE;...    A map of strings to ints.
   239        --required             A required flag.
   240  
   241  Run "test-app <command> --help" for more information on a command.
   242  `
   243  		t.Log(w.String())
   244  		t.Log(expected)
   245  		assert.Equal(t, expected, w.String())
   246  	})
   247  
   248  	t.Run("Selected", func(t *testing.T) {
   249  		exited = false
   250  		w.Truncate(0)
   251  		panicsTrue(t, func() {
   252  			_, err := app.Parse([]string{"two", "hello", "--help"})
   253  			assert.NoError(t, err)
   254  		})
   255  		assert.True(t, exited)
   256  		expected := `Usage: test-app two <three> --required --required-two --required-three [flags]
   257  
   258  Sub-sub-arg.
   259  
   260  Detailed help provided through the HelpProvider interface.
   261  
   262  Flags:
   263    -h, --help                 Show context-sensitive help.
   264        --string=STRING        A string flag.
   265        --bool                 A bool flag with very long help that wraps a lot
   266                               and is verbose and is really verbose.
   267        --slice=STR,...        A slice of strings.
   268        --map=KEY=VALUE;...    A map of strings to ints.
   269        --required             A required flag.
   270  
   271        --flag=STRING          Nested flag under two.
   272        --required-two
   273  
   274        --required-three
   275  `
   276  		t.Log(expected)
   277  		t.Log(w.String())
   278  		assert.Equal(t, expected, w.String())
   279  	})
   280  }
   281  
   282  func TestHelpTree(t *testing.T) {
   283  	var cli struct {
   284  		One struct {
   285  			Thing struct {
   286  				Arg string `arg help:"argument"`
   287  			} `cmd help:"subcommand thing"`
   288  			Other struct {
   289  				Other string `arg help:"other arg"`
   290  			} `arg help:"subcommand other"`
   291  		} `cmd help:"subcommand one" group:"Group A" aliases:"un,uno"` // Groups are ignored in trees
   292  
   293  		Two struct {
   294  			Three threeArg `arg help:"Sub-sub-arg."`
   295  
   296  			Four struct {
   297  			} `cmd help:"Sub-sub-command." aliases:"for,fore"`
   298  		} `cmd help:"Another subcommand."`
   299  	}
   300  
   301  	w := bytes.NewBuffer(nil)
   302  	exited := false
   303  	app := mustNew(t, &cli,
   304  		kong.Name("test-app"),
   305  		kong.Description("A test app."),
   306  		kong.Writers(w, w),
   307  		kong.ConfigureHelp(kong.HelpOptions{
   308  			Tree:     true,
   309  			Indenter: kong.LineIndenter,
   310  		}),
   311  		kong.Exit(func(int) {
   312  			exited = true
   313  			panic(true) // Panic to fake "exit".
   314  		}),
   315  	)
   316  
   317  	t.Run("Full", func(t *testing.T) {
   318  		panicsTrue(t, func() {
   319  			_, err := app.Parse([]string{"--help"})
   320  			assert.NoError(t, err)
   321  		})
   322  		assert.True(t, exited)
   323  		expected := `Usage: test-app <command> [flags]
   324  
   325  A test app.
   326  
   327  Flags:
   328    -h, --help    Show context-sensitive help.
   329  
   330  Commands:
   331    one (un,uno)         subcommand one
   332    - thing              subcommand thing
   333      - <arg>            argument
   334    - <other>            subcommand other
   335  
   336    two                  Another subcommand.
   337    - <three>            Sub-sub-arg.
   338    - four (for,fore)    Sub-sub-command.
   339  
   340  Run "test-app <command> --help" for more information on a command.
   341  `
   342  		if expected != w.String() {
   343  			t.Errorf("help command returned:\n%v\n\nwant:\n%v", w.String(), expected)
   344  		}
   345  		assert.Equal(t, expected, w.String())
   346  	})
   347  
   348  	t.Run("Selected", func(t *testing.T) {
   349  		exited = false
   350  		w.Truncate(0)
   351  		panicsTrue(t, func() {
   352  			_, err := app.Parse([]string{"one", "--help"})
   353  			assert.NoError(t, err)
   354  		})
   355  		assert.True(t, exited)
   356  		expected := `Usage: test-app one (un,uno) <command> [flags]
   357  
   358  subcommand one
   359  
   360  Flags:
   361    -h, --help    Show context-sensitive help.
   362  
   363  Commands:
   364    thing      subcommand thing
   365    - <arg>    argument
   366  
   367    <other>    subcommand other
   368  `
   369  		if expected != w.String() {
   370  			t.Errorf("help command returned:\n%v\n\nwant:\n%v", w.String(), expected)
   371  		}
   372  		assert.Equal(t, expected, w.String())
   373  	})
   374  }
   375  
   376  func TestHelpCompactNoExpand(t *testing.T) {
   377  	var cli struct {
   378  		One struct {
   379  			Thing struct {
   380  				Arg string `arg help:"argument"`
   381  			} `cmd help:"subcommand thing"`
   382  			Other struct {
   383  				Other string `arg help:"other arg"`
   384  			} `arg help:"subcommand other"`
   385  		} `cmd help:"subcommand one" group:"Group A" aliases:"un,uno"` // Groups are ignored in trees
   386  
   387  		Two struct {
   388  			Three threeArg `arg help:"Sub-sub-arg."`
   389  
   390  			Four struct {
   391  			} `cmd help:"Sub-sub-command." aliases:"for,fore"`
   392  		} `cmd help:"Another subcommand."`
   393  	}
   394  
   395  	w := bytes.NewBuffer(nil)
   396  	exited := false
   397  	app := mustNew(t, &cli,
   398  		kong.Name("test-app"),
   399  		kong.Description("A test app."),
   400  		kong.Writers(w, w),
   401  		kong.ConfigureHelp(kong.HelpOptions{
   402  			Compact:             true,
   403  			NoExpandSubcommands: true,
   404  		}),
   405  		kong.Exit(func(int) {
   406  			exited = true
   407  			panic(true) // Panic to fake "exit".
   408  		}),
   409  	)
   410  
   411  	t.Run("Full", func(t *testing.T) {
   412  		panicsTrue(t, func() {
   413  			_, err := app.Parse([]string{"--help"})
   414  			assert.NoError(t, err)
   415  		})
   416  		assert.True(t, exited)
   417  		expected := `Usage: test-app <command> [flags]
   418  
   419  A test app.
   420  
   421  Flags:
   422    -h, --help    Show context-sensitive help.
   423  
   424  Commands:
   425    two    Another subcommand.
   426  
   427  Group A
   428    one (un,uno)    subcommand one
   429  
   430  Run "test-app <command> --help" for more information on a command.
   431  `
   432  		if expected != w.String() {
   433  			t.Errorf("help command returned:\n%v\n\nwant:\n%v", w.String(), expected)
   434  		}
   435  		assert.Equal(t, expected, w.String())
   436  	})
   437  
   438  	t.Run("Selected", func(t *testing.T) {
   439  		exited = false
   440  		w.Truncate(0)
   441  		panicsTrue(t, func() {
   442  			_, err := app.Parse([]string{"one", "--help"})
   443  			assert.NoError(t, err)
   444  		})
   445  		assert.True(t, exited)
   446  		expected := `Usage: test-app one (un,uno) <command> [flags]
   447  
   448  subcommand one
   449  
   450  Flags:
   451    -h, --help    Show context-sensitive help.
   452  
   453  Group A
   454    one (un,uno) thing      subcommand thing
   455    one (un,uno) <other>    subcommand other
   456  `
   457  		if expected != w.String() {
   458  			t.Errorf("help command returned:\n%v\n\nwant:\n%v", w.String(), expected)
   459  		}
   460  		assert.Equal(t, expected, w.String())
   461  	})
   462  }
   463  
   464  func TestEnvarAutoHelp(t *testing.T) {
   465  	var cli struct {
   466  		Flag string `env:"FLAG" help:"A flag."`
   467  	}
   468  	w := &strings.Builder{}
   469  	p := mustNew(t, &cli, kong.Writers(w, w), kong.Exit(func(int) {}))
   470  	_, err := p.Parse([]string{"--help"})
   471  	assert.NoError(t, err)
   472  	assert.Contains(t, w.String(), "A flag ($FLAG).")
   473  }
   474  
   475  func TestMultipleEnvarAutoHelp(t *testing.T) {
   476  	var cli struct {
   477  		Flag string `env:"FLAG1,FLAG2" help:"A flag."`
   478  	}
   479  	w := &strings.Builder{}
   480  	p := mustNew(t, &cli, kong.Writers(w, w), kong.Exit(func(int) {}))
   481  	_, err := p.Parse([]string{"--help"})
   482  	assert.NoError(t, err)
   483  	assert.Contains(t, w.String(), "A flag ($FLAG1, $FLAG2).")
   484  }
   485  
   486  //nolint:dupl // false positive
   487  func TestEnvarAutoHelpWithEnvPrefix(t *testing.T) {
   488  	type Anonymous struct {
   489  		Flag  string `env:"FLAG" help:"A flag."`
   490  		Other string `help:"A different flag."`
   491  	}
   492  	var cli struct {
   493  		Anonymous `envprefix:"ANON_"`
   494  	}
   495  	w := &strings.Builder{}
   496  	p := mustNew(t, &cli, kong.Writers(w, w), kong.Exit(func(int) {}))
   497  	_, err := p.Parse([]string{"--help"})
   498  	assert.NoError(t, err)
   499  	assert.Contains(t, w.String(), "A flag ($ANON_FLAG).")
   500  	assert.Contains(t, w.String(), "A different flag.")
   501  }
   502  
   503  //nolint:dupl // false positive
   504  func TestMultipleEnvarAutoHelpWithEnvPrefix(t *testing.T) {
   505  	type Anonymous struct {
   506  		Flag  string `env:"FLAG1,FLAG2" help:"A flag."`
   507  		Other string `help:"A different flag."`
   508  	}
   509  	var cli struct {
   510  		Anonymous `envprefix:"ANON_"`
   511  	}
   512  	w := &strings.Builder{}
   513  	p := mustNew(t, &cli, kong.Writers(w, w), kong.Exit(func(int) {}))
   514  	_, err := p.Parse([]string{"--help"})
   515  	assert.NoError(t, err)
   516  	assert.Contains(t, w.String(), "A flag ($ANON_FLAG1, $ANON_FLAG2).")
   517  	assert.Contains(t, w.String(), "A different flag.")
   518  }
   519  
   520  //nolint:dupl // false positive
   521  func TestCustomValueFormatter(t *testing.T) {
   522  	var cli struct {
   523  		Flag string `env:"FLAG" help:"A flag."`
   524  	}
   525  	w := &strings.Builder{}
   526  	p := mustNew(t, &cli,
   527  		kong.Writers(w, w),
   528  		kong.Exit(func(int) {}),
   529  		kong.ValueFormatter(func(value *kong.Value) string {
   530  			return value.Help
   531  		}),
   532  	)
   533  	_, err := p.Parse([]string{"--help"})
   534  	assert.NoError(t, err)
   535  	assert.Contains(t, w.String(), "A flag.")
   536  }
   537  
   538  //nolint:dupl // false positive
   539  func TestMultipleCustomValueFormatter(t *testing.T) {
   540  	var cli struct {
   541  		Flag string `env:"FLAG1,FLAG2" help:"A flag."`
   542  	}
   543  	w := &strings.Builder{}
   544  	p := mustNew(t, &cli,
   545  		kong.Writers(w, w),
   546  		kong.Exit(func(int) {}),
   547  		kong.ValueFormatter(func(value *kong.Value) string {
   548  			return value.Help
   549  		}),
   550  	)
   551  	_, err := p.Parse([]string{"--help"})
   552  	assert.NoError(t, err)
   553  	assert.Contains(t, w.String(), "A flag.")
   554  }
   555  
   556  func TestAutoGroup(t *testing.T) {
   557  	var cli struct {
   558  		GroupedAString string `help:"A string flag grouped in A."`
   559  		FreeString     string `help:"A non grouped string flag."`
   560  		GroupedBString string `help:"A string flag grouped in B."`
   561  		FreeBool       bool   `help:"A non grouped bool flag."`
   562  		GroupedABool   bool   `help:"A bool flag grouped in A."`
   563  
   564  		One struct {
   565  			Flag string `help:"Nested flag."`
   566  			// Group is inherited from the parent command
   567  			Thing struct {
   568  				Arg string `arg help:"argument"`
   569  			} `cmd help:"subcommand thing"`
   570  			Other struct {
   571  				Other string `arg help:"other arg"`
   572  			} `arg help:"subcommand other"`
   573  			// ... but a subcommand can override it
   574  			Stuff struct {
   575  				Stuff string `arg help:"argument"`
   576  			} `arg help:"subcommand stuff"`
   577  		} `cmd help:"A subcommand grouped in A."`
   578  
   579  		Two struct {
   580  			Grouped1String  string `help:"A string flag grouped in 1."`
   581  			AFreeString     string `help:"A non grouped string flag."`
   582  			Grouped2String  string `help:"A string flag grouped in 2."`
   583  			AGroupedAString bool   `help:"A string flag grouped in A."`
   584  			Grouped1Bool    bool   `help:"A bool flag grouped in 1."`
   585  		} `cmd help:"A non grouped subcommand."`
   586  
   587  		Four struct {
   588  			Flag string `help:"Nested flag."`
   589  		} `cmd help:"Another subcommand grouped in B."`
   590  
   591  		Three struct {
   592  			Flag string `help:"Nested flag."`
   593  		} `cmd help:"Another subcommand grouped in A."`
   594  	}
   595  	w := bytes.NewBuffer(nil)
   596  	app := mustNew(t, &cli,
   597  		kong.Writers(w, w),
   598  		kong.Exit(func(int) {}),
   599  		kong.AutoGroup(func(parent kong.Visitable, flag *kong.Flag) *kong.Group {
   600  			if node, ok := parent.(*kong.Node); ok {
   601  				return &kong.Group{
   602  					Key:   node.Name,
   603  					Title: strings.Title(node.Name) + " flags:", //nolint
   604  				}
   605  			}
   606  			return nil
   607  		}),
   608  	)
   609  	_, _ = app.Parse([]string{"--help", "two"})
   610  	assert.Equal(t, `Usage: test two [flags]
   611  
   612  A non grouped subcommand.
   613  
   614  Flags:
   615    -h, --help                       Show context-sensitive help.
   616        --grouped-a-string=STRING    A string flag grouped in A.
   617        --free-string=STRING         A non grouped string flag.
   618        --grouped-b-string=STRING    A string flag grouped in B.
   619        --free-bool                  A non grouped bool flag.
   620        --grouped-a-bool             A bool flag grouped in A.
   621  
   622  Two flags:
   623    --grouped-1-string=STRING    A string flag grouped in 1.
   624    --a-free-string=STRING       A non grouped string flag.
   625    --grouped-2-string=STRING    A string flag grouped in 2.
   626    --a-grouped-a-string         A string flag grouped in A.
   627    --grouped-1-bool             A bool flag grouped in 1.
   628  `, w.String())
   629  }
   630  
   631  func TestHelpGrouping(t *testing.T) {
   632  	var cli struct {
   633  		GroupedAString string `help:"A string flag grouped in A." group:"Group A"`
   634  		FreeString     string `help:"A non grouped string flag."`
   635  		GroupedBString string `help:"A string flag grouped in B." group:"Group B"`
   636  		FreeBool       bool   `help:"A non grouped bool flag."`
   637  		GroupedABool   bool   `help:"A bool flag grouped in A." group:"Group A"`
   638  
   639  		One struct {
   640  			Flag string `help:"Nested flag."`
   641  			// Group is inherited from the parent command
   642  			Thing struct {
   643  				Arg string `arg help:"argument"`
   644  			} `cmd help:"subcommand thing"`
   645  			Other struct {
   646  				Other string `arg help:"other arg"`
   647  			} `arg help:"subcommand other"`
   648  			// ... but a subcommand can override it
   649  			Stuff struct {
   650  				Stuff string `arg help:"argument"`
   651  			} `arg help:"subcommand stuff" group:"Group B"`
   652  		} `cmd help:"A subcommand grouped in A." group:"Group A"`
   653  
   654  		Two struct {
   655  			Grouped1String  string `help:"A string flag grouped in 1." group:"Group 1"`
   656  			AFreeString     string `help:"A non grouped string flag."`
   657  			Grouped2String  string `help:"A string flag grouped in 2." group:"Group 2"`
   658  			AGroupedAString bool   `help:"A string flag grouped in A." group:"Group A"`
   659  			Grouped1Bool    bool   `help:"A bool flag grouped in 1." group:"Group 1"`
   660  		} `cmd help:"A non grouped subcommand."`
   661  
   662  		Four struct {
   663  			Flag string `help:"Nested flag."`
   664  		} `cmd help:"Another subcommand grouped in B." group:"Group B"`
   665  
   666  		Three struct {
   667  			Flag string `help:"Nested flag."`
   668  		} `cmd help:"Another subcommand grouped in A." group:"Group A"`
   669  	}
   670  
   671  	w := bytes.NewBuffer(nil)
   672  	exited := false
   673  	app := mustNew(t, &cli,
   674  		kong.Name("test-app"),
   675  		kong.Description("A test app."),
   676  		kong.Groups{
   677  			"Group A":     "Group title taken from the kong.ExplicitGroups option\nA group header",
   678  			"Group 1":     "Another group title, this time without header",
   679  			"Unknown key": "",
   680  		},
   681  		kong.Writers(w, w),
   682  		kong.Exit(func(int) {
   683  			exited = true
   684  			panic(true) // Panic to fake "exit".
   685  		}),
   686  	)
   687  
   688  	t.Run("Full", func(t *testing.T) {
   689  		panicsTrue(t, func() {
   690  			_, err := app.Parse([]string{"--help"})
   691  			assert.True(t, exited)
   692  			assert.NoError(t, err)
   693  		})
   694  		expected := `Usage: test-app <command> [flags]
   695  
   696  A test app.
   697  
   698  Flags:
   699    -h, --help                  Show context-sensitive help.
   700        --free-string=STRING    A non grouped string flag.
   701        --free-bool             A non grouped bool flag.
   702  
   703  Group title taken from the kong.ExplicitGroups option
   704    A group header
   705  
   706    --grouped-a-string=STRING    A string flag grouped in A.
   707    --grouped-a-bool             A bool flag grouped in A.
   708  
   709  Group B
   710    --grouped-b-string=STRING    A string flag grouped in B.
   711  
   712  Commands:
   713    two [flags]
   714      A non grouped subcommand.
   715  
   716  Group title taken from the kong.ExplicitGroups option
   717    A group header
   718  
   719    one thing <arg> [flags]
   720      subcommand thing
   721  
   722    one <other> [flags]
   723      subcommand other
   724  
   725    three [flags]
   726      Another subcommand grouped in A.
   727  
   728  Group B
   729    one <stuff> [flags]
   730      subcommand stuff
   731  
   732    four [flags]
   733      Another subcommand grouped in B.
   734  
   735  Run "test-app <command> --help" for more information on a command.
   736  `
   737  		t.Log(w.String())
   738  		t.Log(expected)
   739  		assert.Equal(t, expected, w.String())
   740  	})
   741  
   742  	t.Run("Selected", func(t *testing.T) {
   743  		exited = false
   744  		w.Truncate(0)
   745  		panicsTrue(t, func() {
   746  			_, err := app.Parse([]string{"two", "--help"})
   747  			assert.NoError(t, err)
   748  			assert.True(t, exited)
   749  		})
   750  		expected := `Usage: test-app two [flags]
   751  
   752  A non grouped subcommand.
   753  
   754  Flags:
   755    -h, --help                    Show context-sensitive help.
   756        --free-string=STRING      A non grouped string flag.
   757        --free-bool               A non grouped bool flag.
   758  
   759        --a-free-string=STRING    A non grouped string flag.
   760  
   761  Group title taken from the kong.ExplicitGroups option
   762    A group header
   763  
   764    --grouped-a-string=STRING    A string flag grouped in A.
   765    --grouped-a-bool             A bool flag grouped in A.
   766  
   767    --a-grouped-a-string         A string flag grouped in A.
   768  
   769  Group B
   770    --grouped-b-string=STRING    A string flag grouped in B.
   771  
   772  Another group title, this time without header
   773    --grouped-1-string=STRING    A string flag grouped in 1.
   774    --grouped-1-bool             A bool flag grouped in 1.
   775  
   776  Group 2
   777    --grouped-2-string=STRING    A string flag grouped in 2.
   778  `
   779  		t.Log(expected)
   780  		t.Log(w.String())
   781  		assert.Equal(t, expected, w.String())
   782  	})
   783  }
   784  
   785  func TestUsageOnError(t *testing.T) {
   786  	var cli struct {
   787  		Flag string `help:"A required flag." required`
   788  	}
   789  	w := &strings.Builder{}
   790  	p := mustNew(t, &cli,
   791  		kong.Writers(w, w),
   792  		kong.Description("Some description."),
   793  		kong.Exit(func(int) {}),
   794  		kong.UsageOnError(),
   795  	)
   796  	_, err := p.Parse([]string{})
   797  	p.FatalIfErrorf(err)
   798  
   799  	expected := `Usage: test --flag=STRING [flags]
   800  
   801  Some description.
   802  
   803  Flags:
   804    -h, --help           Show context-sensitive help.
   805        --flag=STRING    A required flag.
   806  
   807  test: error: missing flags: --flag=STRING
   808  `
   809  	assert.Equal(t, expected, w.String())
   810  }
   811  
   812  func TestShortUsageOnError(t *testing.T) {
   813  	var cli struct {
   814  		Flag string `help:"A required flag." required`
   815  	}
   816  	w := &strings.Builder{}
   817  	p := mustNew(t, &cli,
   818  		kong.Writers(w, w),
   819  		kong.Description("Some description."),
   820  		kong.Exit(func(int) {}),
   821  		kong.ShortUsageOnError(),
   822  	)
   823  	_, err := p.Parse([]string{})
   824  	assert.Error(t, err)
   825  	p.FatalIfErrorf(err)
   826  
   827  	expected := `Usage: test --flag=STRING [flags]
   828  Run "test --help" for more information.
   829  
   830  test: error: missing flags: --flag=STRING
   831  `
   832  	assert.Equal(t, expected, w.String())
   833  }
   834  
   835  func TestCustomShortUsageOnError(t *testing.T) {
   836  	var cli struct {
   837  		Flag string `help:"A required flag." required`
   838  	}
   839  	w := &strings.Builder{}
   840  	shortHelp := func(_ kong.HelpOptions, ctx *kong.Context) error {
   841  		fmt.Fprintln(ctx.Stdout, "🤷 wish I could help")
   842  		return nil
   843  	}
   844  	p := mustNew(t, &cli,
   845  		kong.Writers(w, w),
   846  		kong.Description("Some description."),
   847  		kong.Exit(func(int) {}),
   848  		kong.ShortHelp(shortHelp),
   849  		kong.ShortUsageOnError(),
   850  	)
   851  	_, err := p.Parse([]string{})
   852  	assert.Error(t, err)
   853  	p.FatalIfErrorf(err)
   854  
   855  	expected := `🤷 wish I could help
   856  
   857  test: error: missing flags: --flag=STRING
   858  `
   859  	assert.Equal(t, expected, w.String())
   860  }