package goflags import ( "bytes" "flag" "fmt" "os" "path/filepath" "reflect" "strconv" "strings" "testing" "time" fileutil "github.com/projectdiscovery/utils/file" osutil "github.com/projectdiscovery/utils/os" permissionutil "github.com/projectdiscovery/utils/permission" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGenerateDefaultConfig(t *testing.T) { flagSet := NewFlagSet() example := `# generated by https://github.com/projectdiscovery/goflags # default value for a test flag example #test: test-default-value # string slice flag example value #slice: ["item1", "item2"]` var data string var data2 StringSlice flagSet.StringVar(&data, "test", "test-default-value", "Default value for a test flag example") flagSet.StringSliceVar(&data2, "slice", []string{"item1", "item2"}, "String slice flag example value", StringSliceOptions) defaultConfig := string(flagSet.generateDefaultConfig()) parts := strings.SplitN(defaultConfig, "\n", 2) require.Equal(t, example, parts[1], "could not get correct default config") tearDown(t.Name()) } func TestConfigFileDataTypes(t *testing.T) { flagSet := NewFlagSet() var data string var data2 StringSlice var data3 int var data4 bool var data5 time.Duration flagSet.StringVar(&data, "string-value", "", "Default value for a test flag example") flagSet.StringSliceVar(&data2, "slice-value", []string{}, "String slice flag example value", StringSliceOptions) flagSet.IntVar(&data3, "int-value", 0, "Int value example") flagSet.BoolVar(&data4, "bool-value", false, "Bool value example") flagSet.DurationVar(&data5, "duration-value", time.Hour, "Bool value example") configFileData := ` string-value: test slice-value: - test - test2 severity: - info - high int-value: 543 bool-value: true duration-value: 1h` err := os.WriteFile("test.yaml", []byte(configFileData), permissionutil.ConfigFilePermission) require.Nil(t, err, "could not write temporary config") defer os.Remove("test.yaml") err = flagSet.MergeConfigFile("test.yaml") require.Nil(t, err, "could not merge temporary config") require.Equal(t, "test", data, "could not get correct string") require.Equal(t, StringSlice{"test", "test2"}, data2, "could not get correct string slice") require.Equal(t, 543, data3, "could not get correct int") require.Equal(t, true, data4, "could not get correct bool") require.Equal(t, time.Hour, data5, "could not get correct duration") tearDown(t.Name()) } func TestUsageOrder(t *testing.T) { flagSet := NewFlagSet() var stringData string var stringSliceData StringSlice var stringSliceData2 StringSlice var intData int var boolData bool var enumData string var enumSliceData []string flagSet.SetGroup("String", "String") flagSet.StringVar(&stringData, "string-value", "", "String example value example").Group("String") flagSet.StringVarP(&stringData, "", "ts2", "test-string", "String with default value example #2").Group("String") flagSet.StringVar(&stringData, "string-with-default-value", "test-string", "String with default value example").Group("String") flagSet.StringVarP(&stringData, "string-with-default-value2", "ts", "test-string", "String with default value example #2").Group("String") flagSet.SetGroup("StringSlice", "StringSlice") flagSet.StringSliceVar(&stringSliceData, "slice-value", []string{}, "String slice flag example value", StringSliceOptions).Group("StringSlice") flagSet.StringSliceVarP(&stringSliceData, "slice-value2", "sv", []string{}, "String slice flag example value #2", StringSliceOptions).Group("StringSlice") flagSet.StringSliceVar(&stringSliceData, "slice-with-default-value", []string{"a", "b", "c"}, "String slice flag with default example values", StringSliceOptions).Group("StringSlice") flagSet.StringSliceVarP(&stringSliceData2, "slice-with-default-value2", "swdf", []string{"a", "b", "c"}, "String slice flag with default example values #2", StringSliceOptions).Group("StringSlice") flagSet.SetGroup("Integer", "Integer") flagSet.IntVar(&intData, "int-value", 0, "Int value example").Group("Integer") flagSet.IntVarP(&intData, "int-value2", "iv", 0, "Int value example #2").Group("Integer") flagSet.IntVar(&intData, "int-with-default-value", 12, "Int with default value example").Group("Integer") flagSet.IntVarP(&intData, "int-with-default-value2", "iwdv", 12, "Int with default value example #2").Group("Integer") flagSet.SetGroup("Bool", "Boolean") flagSet.BoolVar(&boolData, "bool-value", false, "Bool value example").Group("Bool") flagSet.BoolVarP(&boolData, "bool-value2", "bv", false, "Bool value example #2").Group("Bool") flagSet.BoolVar(&boolData, "bool-with-default-value", true, "Bool with default value example").Group("Bool") flagSet.BoolVarP(&boolData, "bool-with-default-value2", "bwdv", true, "Bool with default value example #2").Group("Bool") flagSet.SetGroup("Enum", "Enum") flagSet.EnumVarP(&enumData, "enum-with-default-value", "en", EnumVariable(0), "Enum with default value(zero/one/two)", AllowdTypes{ "zero": EnumVariable(0), "one": EnumVariable(1), "two": EnumVariable(2), }).Group("Enum") flagSet.EnumSliceVarP(&enumSliceData, "enum-slice-with-default-value", "esn", []EnumVariable{EnumVariable(0)}, "Enum with default value(zero/one/two)", AllowdTypes{ "zero": EnumVariable(0), "one": EnumVariable(1), "two": EnumVariable(2), }).Group("Enum") flagSet.SetGroup("Update", "Update") flagSet.CallbackVar(func() {}, "update", "update tool_1 to the latest released version").Group("Update") flagSet.CallbackVarP(func() {}, "disable-update-check", "duc", "disable automatic update check").Group("Update") output := &bytes.Buffer{} flagSet.CommandLine.SetOutput(output) os.Args = []string{ os.Args[0], "-h", } flagSet.usageFunc() resultOutput := output.String() actual := resultOutput[strings.Index(resultOutput, "Flags:\n"):] fmt.Println(actual) expected := `Flags: STRING: -string-value string String example value example -ts2 string String with default value example #2 (default "test-string") -string-with-default-value string String with default value example (default "test-string") -ts, -string-with-default-value2 string String with default value example #2 (default "test-string") STRINGSLICE: -slice-value string[] String slice flag example value -sv, -slice-value2 string[] String slice flag example value #2 -slice-with-default-value string[] String slice flag with default example values (default ["a", "b", "c"]) -swdf, -slice-with-default-value2 string[] String slice flag with default example values #2 (default ["a", "b", "c"]) INTEGER: -int-value int Int value example -iv, -int-value2 int Int value example #2 -int-with-default-value int Int with default value example (default 12) -iwdv, -int-with-default-value2 int Int with default value example #2 (default 12) BOOLEAN: -bool-value Bool value example -bv, -bool-value2 Bool value example #2 -bool-with-default-value Bool with default value example (default true) -bwdv, -bool-with-default-value2 Bool with default value example #2 (default true) ENUM: -en, -enum-with-default-value value Enum with default value(zero/one/two) (default zero) -esn, -enum-slice-with-default-value value Enum with default value(zero/one/two) (default zero) UPDATE: -update update tool_1 to the latest released version -duc, -disable-update-check disable automatic update check ` assert.Equal(t, expected, actual) tearDown(t.Name()) } func TestIncorrectStringFlagsCausePanic(t *testing.T) { flagSet := NewFlagSet() var stringData string flagSet.StringVar(&stringData, "", "test-string", "String with default value example") assert.Panics(t, flagSet.usageFunc) // env GOOS=linux GOARCH=amd64 go build main.go -o nuclei tearDown(t.Name()) } func TestIncorrectFlagsCausePanic(t *testing.T) { type flagPair struct { Short, Long string } createTestParameters := func() []flagPair { var result []flagPair result = append(result, flagPair{"", ""}) badFlagNames := [...]string{" ", "\t", "\n"} for _, badFlagName := range badFlagNames { result = append(result, flagPair{"", badFlagName}) result = append(result, flagPair{badFlagName, ""}) result = append(result, flagPair{badFlagName, badFlagName}) } return result } for index, tuple := range createTestParameters() { uniqueName := strconv.Itoa(index) t.Run(uniqueName, func(t *testing.T) { assert.Panics(t, func() { tearDown(uniqueName) flagSet := NewFlagSet() var stringData string flagSet.StringVarP(&stringData, tuple.Short, tuple.Long, "test-string", "String with default value example") flagSet.usageFunc() }) }) } } type testSliceValue []interface{} func (value testSliceValue) String() string { return "" } func (value testSliceValue) Set(string) error { return nil } func TestCustomSliceUsageType(t *testing.T) { testCases := map[string]flag.Flag{ "string[]": {Value: &StringSlice{}}, "value[]": {Value: &testSliceValue{}}, } for expected, currentFlag := range testCases { result := createUsageTypeAndDescription(¤tFlag, reflect.TypeOf(currentFlag.Value)) assert.Equal(t, expected, strings.TrimSpace(result)) } } func TestParseStringSlice(t *testing.T) { flagSet := NewFlagSet() var stringSlice StringSlice flagSet.StringSliceVarP(&stringSlice, "header", "H", []string{}, "Header values. Expected usage: -H \"header1\":\"value1\" -H \"header2\":\"value2\"", StringSliceOptions) header1 := "\"header1:value1\"" header2 := "\" HEADER 2: VALUE2 \"" header3 := "\"header3\":\"value3, value4\"" os.Args = []string{ os.Args[0], "-H", header1, "-header", header2, "-H", header3, } err := flagSet.Parse() assert.Nil(t, err) assert.Equal(t, StringSlice{header1, header2, header3}, stringSlice) tearDown(t.Name()) } func TestParseCommaSeparatedStringSlice(t *testing.T) { flagSet := NewFlagSet() var csStringSlice StringSlice flagSet.StringSliceVarP(&csStringSlice, "cs-value", "CSV", []string{}, "Comma Separated Values. Expected usage: -CSV value1,value2,value3", CommaSeparatedStringSliceOptions) valueCommon := `value1,Value2 ",value3` value1 := `value1` value2 := `Value2 "` value3 := `value3` os.Args = []string{ os.Args[0], "-CSV", valueCommon, } err := flagSet.Parse() assert.Nil(t, err) assert.Equal(t, csStringSlice, StringSlice{value1, value2, value3}) tearDown(t.Name()) } func TestParseFileCommaSeparatedStringSlice(t *testing.T) { flagSet := NewFlagSet() var csStringSlice StringSlice flagSet.StringSliceVarP(&csStringSlice, "cs-value", "CSV", []string{}, "Comma Separated Values. Expected usage: -CSV path/to/file", FileCommaSeparatedStringSliceOptions) testFile := "test.txt" value1 := `value1` value2 := `Value2 "` value3 := `value3` testFileData := `value1 Value2 " value3` err := os.WriteFile(testFile, []byte(testFileData), permissionutil.ConfigFilePermission) require.Nil(t, err, "could not write temporary values file") defer os.Remove(testFile) os.Args = []string{ os.Args[0], "-CSV", testFile, } err = flagSet.Parse() assert.Nil(t, err) assert.Equal(t, csStringSlice, StringSlice{value1, value2, value3}) tearDown(t.Name()) } func TestConfigOnlyDataTypes(t *testing.T) { flagSet := NewFlagSet() var data StringSlice flagSet.StringSliceVarConfigOnly(&data, "config-only", []string{}, "String slice config only flag example") require.Nil(t, flagSet.CommandLine.Lookup("config-only"), "config-only flag should not be registered") configFileData := ` config-only: - test - test2 ` err := os.WriteFile("test.yaml", []byte(configFileData), permissionutil.ConfigFilePermission) require.Nil(t, err, "could not write temporary config") defer os.Remove("test.yaml") err = flagSet.MergeConfigFile("test.yaml") require.Nil(t, err, "could not merge temporary config") require.Equal(t, StringSlice{"test", "test2"}, data, "could not get correct string slice") tearDown(t.Name()) } func TestSetDefaultStringSliceValue(t *testing.T) { var data StringSlice flagSet := NewFlagSet() flagSet.StringSliceVar(&data, "test", []string{"A,A,A"}, "Default value for a test flag example", CommaSeparatedStringSliceOptions) err := flagSet.CommandLine.Parse([]string{"-test", "item1"}) require.Nil(t, err) require.Equal(t, StringSlice{"item1"}, data, "could not get correct string slice") var data2 StringSlice flagSet2 := NewFlagSet() flagSet2.StringSliceVar(&data2, "test", []string{"A"}, "Default value for a test flag example", CommaSeparatedStringSliceOptions) err = flagSet2.CommandLine.Parse([]string{"-test", "item1,item2"}) require.Nil(t, err) require.Equal(t, StringSlice{"item1", "item2"}, data2, "could not get correct string slice") var data3 StringSlice flagSet3 := NewFlagSet() flagSet3.StringSliceVar(&data3, "test", []string{}, "Default value for a test flag example", CommaSeparatedStringSliceOptions) err = flagSet3.CommandLine.Parse([]string{"-test", "item1,item2"}) require.Nil(t, err) require.Equal(t, StringSlice{"item1", "item2"}, data3, "could not get correct string slice") var data4 StringSlice flagSet4 := NewFlagSet() flagSet4.StringSliceVar(&data4, "test", nil, "Default value for a test flag example", CommaSeparatedStringSliceOptions) err = flagSet4.CommandLine.Parse([]string{"-test", "item1,\"item2\""}) require.Nil(t, err) require.Equal(t, StringSlice{"item1", "item2"}, data4, "could not get correct string slice") tearDown(t.Name()) } func TestCaseSensitiveFlagSet(t *testing.T) { flagSet := NewFlagSet() flagSet.CaseSensitive = true var verbose, keyVal bool flagSet.CreateGroup("Test", "Test", flagSet.BoolVarP(&verbose, "verbose", "v", false, "show verbose output"), flagSet.BoolVarP(&keyVal, "var", "V", false, "custom vars in key=value format"), ) output := &bytes.Buffer{} flagSet.CommandLine.SetOutput(output) os.Args = []string{ os.Args[0], "-h", "V", } flagSet.usageFunc() resultOutput := output.String() actual := resultOutput[strings.Index(resultOutput, "Flags:\n"):] expected := "Flags:\n -V, -var custom vars in key=value format\n" assert.Equal(t, expected, actual) output = &bytes.Buffer{} flagSet.CommandLine.SetOutput(output) os.Args = []string{ os.Args[0], "-h", "v", } flagSet.usageFunc() resultOutput = output.String() actual = resultOutput[strings.Index(resultOutput, "Flags:\n"):] expected = "Flags:\n -v, -verbose show verbose output\n" assert.Equal(t, expected, actual) } func tearDown(uniqueValue string) { flag.CommandLine = flag.NewFlagSet(uniqueValue, flag.ContinueOnError) flag.CommandLine.Usage = flag.Usage } func TestConfigDirMigration(t *testing.T) { // remove any args added by previous test os.Args = []string{ os.Args[0], } // setup test old config dir createEmptyFilesinDir(t, oldAppConfigDir) flagset := NewFlagSet() flagset.CommandLine = flag.NewFlagSet("goflags", flag.ContinueOnError) newToolCfgDir := flagset.GetToolConfigDir() // cleanup and debug defer func() { // cleanup if t.Failed() { t.Logf("old config dir: %s", oldAppConfigDir) t.Logf("new config dir: %s", newToolCfgDir) cfgFiles, _ := os.ReadDir(oldAppConfigDir) for _, cfgFile := range cfgFiles { t.Logf("found config file in old dir : %s", cfgFile.Name()) } cfgFiles, _ = os.ReadDir(newToolCfgDir) for _, cfgFile := range cfgFiles { t.Logf("found config file in new dir : %s", cfgFile.Name()) } } _ = os.RemoveAll(oldAppConfigDir) _ = os.RemoveAll(newToolCfgDir) }() // remove new config dir if it already exists from previous test _ = os.RemoveAll(newToolCfgDir) // create test flag and parse it var testflag string flagset.CreateGroup("Example", "Example", flagset.StringVarP(&testflag, "test", "t", "", "test flag"), ) if err := flagset.Parse(); err != nil { require.Nil(t, err, "could not parse flags") } // oldAppConfigDir and newConfigDir is same in case of linux (unless sandbox or something else is used) // migration will only happen on windows & macOS (darwin) Ref: https://pkg.go.dev/os#UserConfigDir if oldAppConfigDir != newToolCfgDir || !osutil.IsLinux() { // check if config files are moved to new config dir require.FileExistsf(t, filepath.Join(newToolCfgDir, "config.yaml"), "config file not created in new config dir") require.FileExistsf(t, filepath.Join(newToolCfgDir, "provider-config.yaml"), "config file not created in new config dir") } tearDown(t.Name()) } func createEmptyFilesinDir(t *testing.T, dirname string) { if !fileutil.FolderExists(dirname) { _ = fileutil.CreateFolder(dirname) } // create empty yaml config files err := os.WriteFile(filepath.Join(oldAppConfigDir, "config.yaml"), []byte{}, os.ModePerm) require.Nil(t, err, "could not create old config file") err = os.WriteFile(filepath.Join(oldAppConfigDir, "provider-config.yaml"), []byte{}, os.ModePerm) require.Nil(t, err, "could not create old config file") }