Repository: projectdiscovery/goflags
Pull request #67 (2022-07-22 16:04:12)

v0.0.9


Patch:
diff --git a/go.mod b/go.mod
index dcca109..79ca025 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/projectdiscovery/fileutil v0.0.0-20210928100737-cab279c5d4b5
 	github.com/projectdiscovery/stringsutil v0.0.0-20210804142656-fd3c28dbaafe
-	github.com/stretchr/testify v1.7.2
+	github.com/stretchr/testify v1.8.0
 	gopkg.in/yaml.v3 v3.0.1
 )
 
diff --git a/go.sum b/go.sum
index 04f6f0d..b4a715c 100644
--- a/go.sum
+++ b/go.sum
@@ -14,9 +14,11 @@ github.com/projectdiscovery/fileutil v0.0.0-20210928100737-cab279c5d4b5/go.mod h
 github.com/projectdiscovery/stringsutil v0.0.0-20210804142656-fd3c28dbaafe h1:tQTgf5XLBgZbkJDPtnV3SfdP9tzz5ZWeDBwv8WhnH9Q=
 github.com/projectdiscovery/stringsutil v0.0.0-20210804142656-fd3c28dbaafe/go.mod h1:oTRc18WBv9t6BpaN9XBY+QmG28PUpsyDzRht56Qf49I=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
-github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/goflags.go b/goflags.go
index 4e7f549..b7255af 100644
--- a/goflags.go
+++ b/goflags.go
@@ -16,8 +16,8 @@ import (
 	"time"
 
 	"github.com/cnf/structhash"
-	"gopkg.in/yaml.v3"
 	"github.com/projectdiscovery/fileutil"
+	"gopkg.in/yaml.v3"
 )
 
 // FlagSet is a list of flags for an application
@@ -55,6 +55,7 @@ func (flagData *FlagData) Group(name string) {
 
 // NewFlagSet creates a new flagSet structure for the application
 func NewFlagSet() *FlagSet {
+	flag.CommandLine.ErrorHandling()
 	return &FlagSet{
 		flagKeys:              newInsertionOrderedMap(),
 		OtherOptionsGroupName: "other options",
@@ -344,7 +345,7 @@ func (flagSet *FlagSet) StringSliceVarP(field *StringSlice, long, short string,
 	optionMap[field] = options
 	for _, defaultItem := range defaultValue {
 		_ = field.Set(defaultItem)
-		values, _ := toStringSlice(defaultItem, options)
+		values, _ := ToStringSlice(defaultItem, options)
 		for _, value := range values {
 			_ = field.Set(value)
 		}
@@ -444,6 +445,19 @@ func (flagSet *FlagSet) DurationVar(field *time.Duration, long string, defaultVa
 }
 
 func (flagSet *FlagSet) usageFunc() {
+	var helpAsked bool
+
+	// Only show help usage if asked by user
+	for _, arg := range os.Args {
+		argStripped := strings.Trim(arg, "-")
+		if argStripped == "h" || argStripped == "help" {
+			helpAsked = true
+		}
+	}
+	if !helpAsked {
+		return
+	}
+
 	cliOutput := flagSet.CommandLine.Output()
 	fmt.Fprintf(cliOutput, "%s\n\n", flagSet.description)
 	fmt.Fprintf(cliOutput, "Usage:\n  %s [flags]\n\n", os.Args[0])
@@ -451,6 +465,16 @@ func (flagSet *FlagSet) usageFunc() {
 
 	writer := tabwriter.NewWriter(cliOutput, 0, 0, 1, ' ', 0)
 
+	// If user has specified group with help and we have groups, return
+	// with it's usage function
+	if len(flagSet.groups) > 0 && len(os.Args) == 3 {
+		group := flagSet.getGroupbyName(strings.ToLower(os.Args[2]))
+		if group.name != "" {
+			flagSet.displayGroupUsageFunc(newUniqueDeduper(), group, cliOutput, writer)
+			return
+		}
+	}
+
 	if len(flagSet.groups) > 0 {
 		flagSet.usageFuncForGroups(cliOutput, writer)
 	} else {
@@ -458,6 +482,15 @@ func (flagSet *FlagSet) usageFunc() {
 	}
 }
 
+func (flagSet *FlagSet) getGroupbyName(name string) groupData {
+	for _, group := range flagSet.groups {
+		if strings.EqualFold(group.name, name) || strings.EqualFold(group.description, name) {
+			return group
+		}
+	}
+	return groupData{}
+}
+
 // usageFuncInternal prints usage for command line flags
 func (flagSet *FlagSet) usageFuncInternal(writer *tabwriter.Writer) {
 	uniqueDeduper := newUniqueDeduper()
@@ -480,31 +513,7 @@ func (flagSet *FlagSet) usageFuncForGroups(cliOutput io.Writer, writer *tabwrite
 
 	var otherOptions []string
 	for _, group := range flagSet.groups {
-		fmt.Fprintf(cliOutput, "%s:\n", normalizeGroupDescription(group.description))
-
-		flagSet.flagKeys.forEach(func(key string, data *FlagData) {
-			if currentFlag := flagSet.CommandLine.Lookup(key); currentFlag != nil {
-
-				if data.group == "" {
-					if !uniqueDeduper.isUnique(data) {
-						return
-					}
-					otherOptions = append(otherOptions, createUsageString(data, currentFlag))
-					return
-				}
-				// Ignore the flag if it's not in our intended group
-				if !strings.EqualFold(data.group, group.name) {
-					return
-				}
-				if !uniqueDeduper.isUnique(data) {
-					return
-				}
-				result := createUsageString(data, currentFlag)
-				fmt.Fprint(writer, result, "\n")
-			}
-		})
-		writer.Flush()
-		fmt.Printf("\n")
+		otherOptions = append(otherOptions, flagSet.displayGroupUsageFunc(uniqueDeduper, group, cliOutput, writer)...)
 	}
 
 	// Print Any additional flag that may have been left
@@ -518,6 +527,36 @@ func (flagSet *FlagSet) usageFuncForGroups(cliOutput io.Writer, writer *tabwrite
 	}
 }
 
+// displayGroupUsageFunc displays usage for a group
+func (flagSet *FlagSet) displayGroupUsageFunc(uniqueDeduper *uniqueDeduper, group groupData, cliOutput io.Writer, writer *tabwriter.Writer) []string {
+	fmt.Fprintf(cliOutput, "%s:\n", normalizeGroupDescription(group.description))
+
+	var otherOptions []string
+	flagSet.flagKeys.forEach(func(key string, data *FlagData) {
+		if currentFlag := flagSet.CommandLine.Lookup(key); currentFlag != nil {
+			if data.group == "" {
+				if !uniqueDeduper.isUnique(data) {
+					return
+				}
+				otherOptions = append(otherOptions, createUsageString(data, currentFlag))
+				return
+			}
+			// Ignore the flag if it's not in our intended group
+			if !strings.EqualFold(data.group, group.name) {
+				return
+			}
+			if !uniqueDeduper.isUnique(data) {
+				return
+			}
+			result := createUsageString(data, currentFlag)
+			fmt.Fprint(writer, result, "\n")
+		}
+	})
+	writer.Flush()
+	fmt.Printf("\n")
+	return otherOptions
+}
+
 type uniqueDeduper struct {
 	hashes map[string]interface{}
 }
diff --git a/slice_common.go b/slice_common.go
index a2d398c..c466adf 100644
--- a/slice_common.go
+++ b/slice_common.go
@@ -51,7 +51,8 @@ type Options struct {
 	Normalize  func(string) string
 }
 
-func toStringSlice(value string, options Options) ([]string, error) {
+// ToStringSlice converts a value to string slice based on options
+func ToStringSlice(value string, options Options) ([]string, error) {
 	var result []string
 	if options.IsEmpty == nil && options.IsFromFile == nil && options.Normalize == nil {
 		return []string{value}, nil
diff --git a/string_slice.go b/string_slice.go
index b8e7e03..fd5449c 100644
--- a/string_slice.go
+++ b/string_slice.go
@@ -15,7 +15,7 @@ func (stringSlice *StringSlice) Set(value string) error {
 	if !ok {
 		option = StringSliceOptions
 	}
-	values, err := toStringSlice(value, option)
+	values, err := ToStringSlice(value, option)
 	if err != nil {
 		return err
 	}
diff --git a/string_slice_options.go b/string_slice_options.go
index 1b082ca..f6d4225 100644
--- a/string_slice_options.go
+++ b/string_slice_options.go
@@ -20,7 +20,11 @@ var CommaSeparatedStringSliceOptions = Options{
 // Tokenization: Comma
 // Normalization: None
 // Type: []string
-// Example: -flag test.txt # with test.txt containing on multiple lines value1 and nvalue2 => {"value1", "value2"}
+// test.txt content:
+// value1
+// value2
+//
+// Example: -flag test.txt => {"value1", "value2"}
 var FileCommaSeparatedStringSliceOptions = Options{
 	IsEmpty:    isEmpty,
 	IsFromFile: func(s string) bool { return true },
@@ -30,7 +34,7 @@ var FileCommaSeparatedStringSliceOptions = Options{
 // Tokenization: None
 // Normalization: Standard
 // Type: []string
-// Example: -flag /value/1 -flag value2 => {"/value/1", "value2"}
+// Example: -flag /value/1 -flag 'value2' => {"/value/1", "value2"}
 var NormalizedOriginalStringSliceOptions = Options{
 	IsEmpty:   isEmpty,
 	Normalize: normalize,


Test patch:
diff --git a/goflags_test.go b/goflags_test.go
index 0856765..74f78b5 100644
--- a/goflags_test.go
+++ b/goflags_test.go
@@ -115,6 +115,10 @@ func TestUsageOrder(t *testing.T) {
 	output := &bytes.Buffer{}
 	flagSet.CommandLine.SetOutput(output)
 
+	os.Args = []string{
+		os.Args[0],
+		"-h",
+	}
 	flagSet.usageFunc()
 
 	resultOutput := output.String()
diff --git a/string_slice_test.go b/string_slice_test.go
index 1d7f4ef..5fef201 100644
--- a/string_slice_test.go
+++ b/string_slice_test.go
@@ -2,6 +2,7 @@ package goflags
 
 import (
 	"fmt"
+	"os"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -44,7 +45,7 @@ func TestNormalizedStringSlicePositive(t *testing.T) {
 	}
 
 	for value, expected := range slices {
-		result, err := toStringSlice(value, NormalizedStringSliceOptions)
+		result, err := ToStringSlice(value, NormalizedStringSliceOptions)
 		fmt.Println(result)
 		assert.Nil(t, err)
 		assert.Equal(t, result, expected)
@@ -61,18 +62,38 @@ func TestNormalizedStringSliceNegative(t *testing.T) {
 	}
 
 	for _, value := range slices {
-		result, err := toStringSlice(value, NormalizedStringSliceOptions)
+		result, err := ToStringSlice(value, NormalizedStringSliceOptions)
 		assert.Nil(t, result)
 		assert.NotNil(t, err)
 	}
 }
 
 func TestNormalizedOriginalStringSlice(t *testing.T) {
-	result, err := toStringSlice("/Users/Home/Test/test.yaml", NormalizedOriginalStringSliceOptions)
+	result, err := ToStringSlice("/Users/Home/Test/test.yaml", NormalizedOriginalStringSliceOptions)
 	assert.Nil(t, err)
 	assert.Equal(t, []string{"/Users/Home/Test/test.yaml"}, result, "could not get correct path")
 
-	result, err = toStringSlice("'test user'", NormalizedOriginalStringSliceOptions)
+	result, err = ToStringSlice("'test user'", NormalizedOriginalStringSliceOptions)
 	assert.Nil(t, err)
 	assert.Equal(t, []string{"test user"}, result, "could not get correct path")
 }
+
+func TestFileNormalizedStringSliceOptions(t *testing.T) {
+	result, err := ToStringSlice("/Users/Home/Test/test.yaml", FileNormalizedStringSliceOptions)
+	assert.Nil(t, err)
+	assert.Equal(t, []string{"/users/home/test/test.yaml"}, result, "could not get correct path")
+
+	result, err = ToStringSlice("'Test User'", FileNormalizedStringSliceOptions)
+	assert.Nil(t, err)
+	assert.Equal(t, []string{"test user"}, result, "could not get correct path")
+}
+
+func TestFileStringSliceOptions(t *testing.T) {
+	filename := "test.txt"
+	_ = os.WriteFile(filename, []byte("value1,value2\nvalue3"), 0644)
+	defer os.RemoveAll(filename)
+
+	result, err := ToStringSlice(filename, FileStringSliceOptions)
+	assert.Nil(t, err)
+	assert.Equal(t, []string{"value1,value2", "value3"}, result, "could not get correct path")
+}