├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── autorelease.yml │ ├── build-test.yml │ ├── codeql-analysis.yml │ ├── dep-auto-merge.yml │ └── lint-test.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── callback_var.go ├── callback_var_test.go ├── duration_var.go ├── duration_var_test.go ├── dynamic_var.go ├── dynamic_var_test.go ├── enum_slice_var.go ├── enum_slice_var_test.go ├── enum_var.go ├── enum_var_test.go ├── examples └── basic │ └── main.go ├── go.mod ├── go.sum ├── goflags.go ├── goflags_test.go ├── insertionorderedmap.go ├── path.go ├── path_test.go ├── port.go ├── port_test.go ├── ports_data.json ├── ratelimit_var.go ├── ratelimit_var_test.go ├── runtime_map.go ├── runtime_map_test.go ├── size_var.go ├── size_var_test.go ├── slice_common.go ├── string_slice.go ├── string_slice_options.go └── string_slice_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Maintain dependencies for go modules 10 | - package-ecosystem: "gomod" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | target-branch: "dev" 15 | commit-message: 16 | prefix: "chore" 17 | include: "scope" 18 | labels: 19 | - "Type: Maintenance" 20 | groups: 21 | modules: 22 | patterns: ["github.com/projectdiscovery/*"] 23 | 24 | # # Maintain dependencies for docker 25 | # - package-ecosystem: "docker" 26 | # directory: "/" 27 | # schedule: 28 | # interval: "weekly" 29 | # target-branch: "dev" 30 | # commit-message: 31 | # prefix: "chore" 32 | # include: "scope" 33 | # 34 | # # Maintain dependencies for GitHub Actions 35 | # - package-ecosystem: "github-actions" 36 | # directory: "/" 37 | # schedule: 38 | # interval: "weekly" 39 | # target-branch: "dev" 40 | # commit-message: 41 | # prefix: "chore" 42 | # include: "scope" 43 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | categories: 6 | - title: 🎉 New Features 7 | labels: 8 | - "Type: Enhancement" 9 | - title: 🐞 Bugs Fixes 10 | labels: 11 | - "Type: Bug" 12 | - title: 🔨 Maintenance 13 | labels: 14 | - "Type: Maintenance" 15 | - title: Other Changes 16 | labels: 17 | - "*" -------------------------------------------------------------------------------- /.github/workflows/autorelease.yml: -------------------------------------------------------------------------------- 1 | name: 🔖 Auto release gh action 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Get Commit Count 18 | id: get_commit 19 | run: git rev-list `git rev-list --tags --no-walk --max-count=1`..HEAD --count | xargs -I {} echo COMMIT_COUNT={} >> $GITHUB_OUTPUT 20 | 21 | - name: Create release and tag 22 | if: ${{ steps.get_commit.outputs.COMMIT_COUNT > 0 }} 23 | id: tag_version 24 | uses: mathieudutour/github-tag-action@v6.1 25 | with: 26 | github_token: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Create a GitHub release 29 | if: ${{ steps.get_commit.outputs.COMMIT_COUNT > 0 }} 30 | uses: actions/create-release@v1 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | tag_name: ${{ steps.tag_version.outputs.new_tag }} 35 | release_name: Release ${{ steps.tag_version.outputs.new_tag }} 36 | body: ${{ steps.tag_version.outputs.changelog }} 37 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: 🔨 Build Test 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**.go' 7 | - '**.mod' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | name: Test Builds 13 | strategy: 14 | matrix: 15 | go-version: [1.21.x] 16 | os: [ubuntu-latest, windows-latest, macOS-latest] 17 | 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Set up Go 21 | uses: actions/setup-go@v3 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | 25 | - name: Check out code 26 | uses: actions/checkout@v3 27 | 28 | - name: Test 29 | run: go test -race ./... 30 | 31 | - name: Run Example 32 | run: go run . 33 | working-directory: examples/basic -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 🚨 CodeQL Analysis 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - dev 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | language: [ 'go' ] 22 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v3 27 | 28 | # Initializes the CodeQL tools for scanning. 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@v2 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v2 -------------------------------------------------------------------------------- /.github/workflows/dep-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: 🤖 dep auto merge 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | permissions: 10 | pull-requests: write 11 | issues: write 12 | repository-projects: write 13 | 14 | jobs: 15 | automerge: 16 | runs-on: ubuntu-latest 17 | if: github.actor == 'dependabot[bot]' 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | token: ${{ secrets.DEPENDABOT_PAT }} 22 | 23 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 24 | with: 25 | github-token: ${{ secrets.DEPENDABOT_PAT }} 26 | target: all -------------------------------------------------------------------------------- /.github/workflows/lint-test.yml: -------------------------------------------------------------------------------- 1 | name: 🙏🏻 Lint Test 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**.go' 7 | - '**.mod' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | lint: 12 | name: "Lint" 13 | if: "${{ !endsWith(github.actor, '[bot]') }}" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: projectdiscovery/actions/setup/go@v1 18 | - uses: projectdiscovery/actions/golangci-lint@v1 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE Settings 2 | /.idea 3 | /.vscode 4 | /.vs 5 | 6 | examples/basic/basic 7 | examples/basic/basic.exe 8 | 9 | .devcontainer -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ProjectDiscovery, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goflags 2 | 3 | [![License](https://img.shields.io/github/license/projectdiscovery/goflags)](LICENSE.md) 4 | ![Go version](https://img.shields.io/github/go-mod/go-version/projectdiscovery/goflags?filename=go.mod) 5 | [![Release](https://img.shields.io/github/release/projectdiscovery/goflags)](https://github.com/projectdiscovery/goflags/releases/) 6 | [![Checks](https://github.com/projectdiscovery/goflags/actions/workflows/build-test.yml/badge.svg)](https://github.com/projectdiscovery/goflags/actions/workflows/build-test.yml) 7 | 8 | An extension of the go `flag` library that adds convenience functions and functionalities like config file, better usage, short and long flag support, custom types for string slices and maps etc. 9 | 10 | ## Features 11 | 12 | - In-built YAML Configuration file support. 13 | - Better usage instructions 14 | - Short and long flags support 15 | - Custom String Slice types with different options (comma-separated,normalized,etc) 16 | - Custom Map type 17 | - Flags grouping support (CreateGroup,SetGroup) 18 | 19 | ## Usage 20 | 21 | The following types are supported by the goflags library. The `P` suffix means that the flag supports both a long and a short flag for the option. 22 | 23 | ### Flag Types 24 | 25 | | Function | Description | 26 | |--------------------------|---------------------------------------------------------------------| 27 | | BoolVar | Boolean value with long name | 28 | | BoolVarP | Boolean value with long short name | 29 | | DurationVar | Time Duration value with long name | 30 | | DurationVarP | Time Duration value with long short name | 31 | | IntVar | Integer value with long name | 32 | | IntVarP | Integer value with long short name | 33 | | PortVar | Port value with long name | 34 | | PortVarP | Port value with long short name | 35 | | RuntimeMapVar | Map value with long name | 36 | | RuntimeMapVarP | Map value with long short name | 37 | | StringSliceVar | String Slice value with long name and options | 38 | | StringSliceVarConfigOnly | String Slice value with long name read from config file only | 39 | | StringSliceVarP | String slice value with long short name and options | 40 | | StringVar | String value with long name | 41 | | StringVarEnv | String value with long short name read from environment | 42 | | StringVarP | String value with long short name | 43 | | Var | Custom value with long name implementing flag.Value interface | 44 | | VarP | Custom value with long short name implementing flag.Value interface | 45 | | EnumVar | Enum value with long name | 46 | | EnumVarP | Enum value with long short name | 47 | | CallbackVar | Callback function as value with long name | 48 | | CallbackVarP | Callback function as value with long short name | 49 | | SizeVar | String value with long name | 50 | | SizeVarP | String value with long short name | 51 | 52 | 53 | ### String Slice Options 54 | 55 | | String Slice Option | Tokenization | Normalization | Description | 56 | |--------------------------------------|--------------|---------------|-----------------------------------------------| 57 | | StringSliceOptions | None | None | Default String Slice | 58 | | CommaSeparatedStringSliceOptions | Comma | None | Comma-separated string slice | 59 | | FileCommaSeparatedStringSliceOptions | Comma | None | Comma-separated items from file/cli | 60 | | NormalizedOriginalStringSliceOptions | None | Standard | List of normalized string slice | 61 | | FileNormalizedStringSliceOptions | Comma | Standard | List of normalized string slice from file/cli | 62 | | FileStringSliceOptions | Standard | Standard | List of string slice from file | 63 | | NormalizedStringSliceOptions | Comma | Standard | List of normalized string slice | 64 | 65 | ## Example 66 | 67 | An example showing various options of the library is specified below. 68 | 69 | ```go 70 | package main 71 | 72 | import ( 73 | "fmt" 74 | "log" 75 | 76 | "github.com/projectdiscovery/goflags" 77 | ) 78 | 79 | type options struct { 80 | silent bool 81 | inputs goflags.StringSlice 82 | config string 83 | values goflags.RuntimeMap 84 | } 85 | 86 | const ( 87 | Nil goflags.EnumVariable = iota 88 | Type1 89 | Type2 90 | ) 91 | 92 | func main() { 93 | enumAllowedTypes := goflags.AllowdTypes{"type1": Type1, "type2": Type2} 94 | opt := &options{} 95 | 96 | flagSet := goflags.NewFlagSet() 97 | flagSet.SetDescription("Test program to demonstrate goflags options") 98 | 99 | flagSet.EnumVarP(&options.Type, "enum-type", "et", Nil, "Variable Type (type1/type2)", enumAllowedTypes) 100 | flagSet.BoolVar(&opt.silent, "silent", true, "show silent output") 101 | flagSet.StringSliceVarP(&opt.inputs, "inputs", "i", nil, "list of inputs (file,comma-separated)", goflags.FileCommaSeparatedStringSliceOptions) 102 | 103 | update := func(tool string ) func() { 104 | return func() { 105 | fmt.Printf("%v updated successfully!", tool) 106 | } 107 | } 108 | flagSet.CallbackVarP(update("tool_1"), "update", "up", "update tool_1") 109 | 110 | 111 | // Group example 112 | flagSet.CreateGroup("config", "Configuration", 113 | flagSet.StringVar(&opt.config, "config", "", "file to read config from"), 114 | flagSet.RuntimeMapVar(&opt.values, "values", nil, "key-value runtime values"), 115 | ) 116 | if err := flagSet.Parse(); err != nil { 117 | log.Fatalf("Could not parse flags: %s\n", err) 118 | } 119 | if opt.config != "" { 120 | if err := flagSet.MergeConfigFile(opt.config); err != nil { 121 | log.Fatalf("Could not merge config file: %s\n", err) 122 | } 123 | } 124 | fmt.Printf("silent: %v inputs: %v config: %v values: %v\n", opt.silent, opt.inputs, opt.config, opt.values) 125 | } 126 | ``` 127 | 128 | ### Thanks 129 | 130 | 1. spf13/cobra - For the very nice usage template for the command line. 131 | 2. nmap/nmap - For the service-port mapping and top-ports list. 132 | -------------------------------------------------------------------------------- /callback_var.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // CallBackFunc 9 | type CallBackFunc func() 10 | 11 | // callBackVar 12 | type callBackVar struct { 13 | Value CallBackFunc 14 | } 15 | 16 | // Set 17 | func (c *callBackVar) Set(s string) error { 18 | v, err := strconv.ParseBool(s) 19 | if err != nil { 20 | return fmt.Errorf("failed to parse callback flag") 21 | } 22 | if v { 23 | // if flag found execute callback 24 | c.Value() 25 | } 26 | return nil 27 | } 28 | 29 | // IsBoolFlag 30 | func (c *callBackVar) IsBoolFlag() bool { 31 | return true 32 | } 33 | 34 | // String 35 | func (c *callBackVar) String() string { 36 | return "false" 37 | } 38 | 39 | // CallbackVar adds a Callback flag with a longname 40 | func (flagSet *FlagSet) CallbackVar(callback CallBackFunc, long string, usage string) *FlagData { 41 | return flagSet.CallbackVarP(callback, long, "", usage) 42 | } 43 | 44 | // CallbackVarP adds a Callback flag with a shortname and longname 45 | func (flagSet *FlagSet) CallbackVarP(callback CallBackFunc, long, short string, usage string) *FlagData { 46 | if callback == nil { 47 | panic(fmt.Errorf("callback cannot be nil for flag -%v", long)) 48 | } 49 | flagData := &FlagData{ 50 | usage: usage, 51 | long: long, 52 | defaultValue: strconv.FormatBool(false), 53 | field: &callBackVar{Value: callback}, 54 | skipMarshal: true, 55 | } 56 | if short != "" { 57 | flagData.short = short 58 | flagSet.CommandLine.Var(flagData.field, short, usage) 59 | flagSet.flagKeys.Set(short, flagData) 60 | } 61 | flagSet.CommandLine.Var(flagData.field, long, usage) 62 | flagSet.flagKeys.Set(long, flagData) 63 | return flagData 64 | } 65 | -------------------------------------------------------------------------------- /callback_var_test.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestSuccessfulCallback(t *testing.T) { 15 | toolName := "tool_1" 16 | want := `updated successfully!` 17 | got := &bytes.Buffer{} 18 | 19 | flagSet := NewFlagSet() 20 | flagSet.CreateGroup("Update", "Update", 21 | flagSet.CallbackVar(updateCallbackFunc(toolName, got), "update", fmt.Sprintf("update %v to the latest released version", toolName)), 22 | flagSet.CallbackVarP(func() {}, "disable-update-check", "duc", "disable automatic update check"), 23 | ) 24 | os.Args = []string{ 25 | os.Args[0], 26 | "-update", 27 | } 28 | err := flagSet.Parse() 29 | assert.Nil(t, err) 30 | assert.Equal(t, want, got.String()) 31 | tearDown(t.Name()) 32 | } 33 | 34 | func TestFailCallback(t *testing.T) { 35 | toolName := "tool_1" 36 | got := &bytes.Buffer{} 37 | 38 | if os.Getenv("IS_SUB_PROCESS") == "1" { 39 | flagSet := NewFlagSet() 40 | flagSet.CommandLine.SetOutput(got) 41 | flagSet.CreateGroup("Update", "Update", 42 | flagSet.CallbackVar(nil, "update", fmt.Sprintf("update %v to the latest released version", toolName)), 43 | ) 44 | os.Args = []string{ 45 | os.Args[0], 46 | "-update", 47 | } 48 | _ = flagSet.Parse() 49 | } 50 | cmd := exec.Command(os.Args[0], "-test.run=TestFailCallback") 51 | cmd.Env = append(os.Environ(), "IS_SUB_PROCESS=1") 52 | err := cmd.Run() 53 | if e, ok := err.(*exec.ExitError); ok && !e.Success() { 54 | return 55 | } 56 | t.Fatalf("process ran with err %v, want exit error", err) 57 | tearDown(t.Name()) 58 | } 59 | 60 | func updateCallbackFunc(_ string, cliOutput io.Writer) func() { 61 | return func() { 62 | fmt.Fprintf(cliOutput, "updated successfully!") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /duration_var.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | timeutil "github.com/projectdiscovery/utils/time" 7 | ) 8 | 9 | type durationValue time.Duration 10 | 11 | func newDurationValue(val time.Duration, p *time.Duration) *durationValue { 12 | *p = val 13 | return (*durationValue)(p) 14 | } 15 | 16 | func (d *durationValue) Set(s string) error { 17 | v, err := timeutil.ParseDuration(s) 18 | if err != nil { 19 | err = errors.New("parse error") 20 | } 21 | *d = durationValue(v) 22 | return err 23 | } 24 | 25 | func (d *durationValue) Get() any { return time.Duration(*d) } 26 | 27 | func (d *durationValue) String() string { return (*time.Duration)(d).String() } 28 | 29 | // DurationVar adds a duration flag with a longname 30 | func (flagSet *FlagSet) DurationVar(field *time.Duration, long string, defaultValue time.Duration, usage string) *FlagData { 31 | return flagSet.DurationVarP(field, long, "", defaultValue, usage) 32 | } 33 | 34 | // DurationVarP adds a duration flag with a short name and long name. 35 | // It is equivalent to DurationVar but also allows specifying durations in days (e.g., "2d" for 2 days, which is equivalent to 2*24h). 36 | // The default unit for durations is seconds (ex: "10" => 10s). 37 | func (flagSet *FlagSet) DurationVarP(field *time.Duration, long, short string, defaultValue time.Duration, usage string) *FlagData { 38 | flagData := &FlagData{ 39 | usage: usage, 40 | long: long, 41 | defaultValue: defaultValue, 42 | } 43 | if short != "" { 44 | flagData.short = short 45 | flagSet.CommandLine.Var(newDurationValue(defaultValue, field), short, usage) 46 | flagSet.flagKeys.Set(short, flagData) 47 | } 48 | flagSet.CommandLine.Var(newDurationValue(defaultValue, field), long, usage) 49 | flagSet.flagKeys.Set(long, flagData) 50 | return flagData 51 | } 52 | -------------------------------------------------------------------------------- /duration_var_test.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestDurationVar(t *testing.T) { 12 | t.Run("day-unit", func(t *testing.T) { 13 | var tt time.Duration 14 | flagSet := NewFlagSet() 15 | flagSet.CreateGroup("Config", "Config", 16 | flagSet.DurationVarP(&tt, "time-out", "tm", 0, "timeout for the process"), 17 | ) 18 | os.Args = []string{ 19 | os.Args[0], 20 | "-time-out", "2d", 21 | } 22 | err := flagSet.Parse() 23 | assert.Nil(t, err) 24 | assert.Equal(t, 2*24*time.Hour, tt) 25 | tearDown(t.Name()) 26 | }) 27 | 28 | t.Run("without-unit", func(t *testing.T) { 29 | var tt time.Duration 30 | flagSet := NewFlagSet() 31 | flagSet.CreateGroup("Config", "Config", 32 | flagSet.DurationVarP(&tt, "time-out", "tm", 0, "timeout for the process"), 33 | ) 34 | os.Args = []string{ 35 | os.Args[0], 36 | "-time-out", "2", 37 | } 38 | err := flagSet.Parse() 39 | assert.Nil(t, err) 40 | assert.Equal(t, 2*time.Second, tt) 41 | tearDown(t.Name()) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /dynamic_var.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type dynamicFlag struct { 12 | field interface{} 13 | defaultValue interface{} 14 | name string 15 | } 16 | 17 | func (df *dynamicFlag) Set(value string) error { 18 | fieldKind := reflect.TypeOf(df.field).Elem().Kind() 19 | var ( 20 | optionWithoutValue bool 21 | pv bool 22 | ) 23 | if value == "t" || value == "T" || value == "true" || value == "TRUE" { 24 | pv = true 25 | optionWithoutValue = true 26 | } else if value == "f" || value == "F" || value == "false" || value == "FALSE" { 27 | pv = false 28 | } 29 | 30 | switch fieldKind { 31 | case reflect.Bool: 32 | boolField := df.field.(*bool) 33 | *boolField = pv 34 | case reflect.Int: 35 | intField := df.field.(*int) 36 | if optionWithoutValue { 37 | *intField = df.defaultValue.(int) 38 | return nil 39 | } 40 | newValue, err := strconv.Atoi(value) 41 | if err != nil { 42 | return err 43 | } 44 | *intField = newValue 45 | case reflect.Float64: 46 | floatField := df.field.(*float64) 47 | if optionWithoutValue { 48 | *floatField = df.defaultValue.(float64) 49 | return nil 50 | } 51 | newValue, err := strconv.ParseFloat(value, 64) 52 | if err != nil { 53 | return err 54 | } 55 | *floatField = newValue 56 | case reflect.String: 57 | stringField := df.field.(*string) 58 | if optionWithoutValue { 59 | *stringField = df.defaultValue.(string) 60 | return nil 61 | } 62 | *stringField = value 63 | case reflect.Slice: 64 | sliceField := df.field.(*[]string) 65 | if optionWithoutValue { 66 | *sliceField = df.defaultValue.([]string) 67 | return nil 68 | } 69 | *sliceField = append(*sliceField, strings.Split(value, ",")...) 70 | default: 71 | return errors.New("unsupported type") 72 | } 73 | return nil 74 | } 75 | 76 | func (df *dynamicFlag) IsBoolFlag() bool { 77 | return true 78 | } 79 | 80 | func (df *dynamicFlag) String() string { 81 | return df.name 82 | } 83 | 84 | // DynamicVar acts as flag with a default value or a option with value 85 | // example: 86 | // 87 | // var titleSize int 88 | // flagSet.DynamicVar(&titleSize, "title", 50, "first N characters of the title") 89 | // 90 | // > go run ./examples/basic -title or go run ./examples/basic -title=100 91 | // In case of `go run ./examples/basic -title` it will use default value 50 92 | func (flagSet *FlagSet) DynamicVar(field interface{}, long string, defaultValue interface{}, usage string) *FlagData { 93 | return flagSet.DynamicVarP(field, long, "", defaultValue, usage) 94 | } 95 | 96 | // DynamicVarP same as DynamicVar but with short name 97 | func (flagSet *FlagSet) DynamicVarP(field interface{}, long, short string, defaultValue interface{}, usage string) *FlagData { 98 | // validate field and defaultValue 99 | if reflect.TypeOf(field).Kind() != reflect.Ptr { 100 | panic(fmt.Errorf("-%v flag field must be a pointer", long)) 101 | } 102 | if reflect.TypeOf(field).Elem().Kind() != reflect.TypeOf(defaultValue).Kind() { 103 | panic(fmt.Errorf("-%v flag field and defaultValue mismatch: fied type is %v and defaultValue Type is %T", long, reflect.TypeOf(field).Elem().Kind(), defaultValue)) 104 | } 105 | if field == nil { 106 | panic(fmt.Errorf("field cannot be nil for flag -%v", long)) 107 | } 108 | 109 | var dynamicFlag dynamicFlag 110 | dynamicFlag.field = field 111 | dynamicFlag.name = long 112 | if defaultValue != nil { 113 | dynamicFlag.defaultValue = defaultValue 114 | } 115 | 116 | flagData := &FlagData{ 117 | usage: usage, 118 | long: long, 119 | defaultValue: defaultValue, 120 | } 121 | if short != "" { 122 | flagData.short = short 123 | flagSet.CommandLine.Var(&dynamicFlag, short, usage) 124 | flagSet.flagKeys.Set(short, flagData) 125 | } 126 | flagSet.CommandLine.Var(&dynamicFlag, long, usage) 127 | flagSet.flagKeys.Set(long, flagData) 128 | return flagData 129 | } 130 | -------------------------------------------------------------------------------- /dynamic_var_test.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDynamicVar(t *testing.T) { 11 | t.Run("with bool as type", func(t *testing.T) { 12 | var b bool 13 | flagSet := NewFlagSet() 14 | flagSet.CreateGroup("Option", "option", 15 | flagSet.DynamicVar(&b, "kev", false, "kev with or without value"), 16 | ) 17 | os.Args = []string{ 18 | os.Args[0], 19 | "-kev=false", 20 | } 21 | err := flagSet.Parse() 22 | assert.Nil(t, err) 23 | assert.Equal(t, false, b) 24 | tearDown(t.Name()) 25 | }) 26 | 27 | t.Run("without value for int as type", func(t *testing.T) { 28 | var i int 29 | flagSet := NewFlagSet() 30 | flagSet.CreateGroup("Option", "option", 31 | flagSet.DynamicVarP(&i, "concurrency", "c", 25, "concurrency for the process"), 32 | ) 33 | os.Args = []string{ 34 | os.Args[0], 35 | "-c", 36 | } 37 | err := flagSet.Parse() 38 | assert.Nil(t, err) 39 | assert.Equal(t, 25, i) 40 | tearDown(t.Name()) 41 | }) 42 | 43 | t.Run("with value for int as type", func(t *testing.T) { 44 | var i int 45 | flagSet := NewFlagSet() 46 | flagSet.CreateGroup("Option", "option", 47 | flagSet.DynamicVarP(&i, "concurrency", "c", 25, "concurrency for the process"), 48 | ) 49 | os.Args = []string{ 50 | os.Args[0], 51 | "-c=100", 52 | } 53 | err := flagSet.Parse() 54 | assert.Nil(t, err) 55 | assert.Equal(t, 100, i) 56 | tearDown(t.Name()) 57 | }) 58 | 59 | t.Run("with value for float64 as type", func(t *testing.T) { 60 | var f float64 61 | flagSet := NewFlagSet() 62 | flagSet.CreateGroup("Option", "option", 63 | flagSet.DynamicVarP(&f, "percentage", "p", 0.0, "percentage for the process"), 64 | ) 65 | os.Args = []string{ 66 | os.Args[0], 67 | "-p=100.0", 68 | } 69 | err := flagSet.Parse() 70 | assert.Nil(t, err) 71 | assert.Equal(t, 100.0, f) 72 | tearDown(t.Name()) 73 | }) 74 | 75 | t.Run("with value for string as type", func(t *testing.T) { 76 | var s string 77 | flagSet := NewFlagSet() 78 | flagSet.CreateGroup("Option", "option", 79 | flagSet.DynamicVarP(&s, "name", "n", "", "name of the user"), 80 | ) 81 | os.Args = []string{ 82 | os.Args[0], 83 | "-n=test", 84 | } 85 | err := flagSet.Parse() 86 | assert.Nil(t, err) 87 | assert.Equal(t, "test", s) 88 | tearDown(t.Name()) 89 | }) 90 | 91 | t.Run("with value for string slice as type", func(t *testing.T) { 92 | var s []string 93 | flagSet := NewFlagSet() 94 | flagSet.CreateGroup("Option", "option", 95 | flagSet.DynamicVarP(&s, "name", "n", []string{}, "name of the user"), 96 | ) 97 | os.Args = []string{ 98 | os.Args[0], 99 | "-n=test1,test2", 100 | } 101 | err := flagSet.Parse() 102 | assert.Nil(t, err) 103 | assert.Equal(t, []string{"test1", "test2"}, s) 104 | tearDown(t.Name()) 105 | }) 106 | 107 | } 108 | -------------------------------------------------------------------------------- /enum_slice_var.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type EnumSliceVar struct { 9 | allowedTypes AllowdTypes 10 | value *[]string 11 | } 12 | 13 | func (e *EnumSliceVar) String() string { 14 | if e.value != nil { 15 | return strings.Join(*e.value, ",") 16 | } 17 | return "" 18 | } 19 | 20 | func (e *EnumSliceVar) Set(value string) error { 21 | values := strings.Split(value, ",") 22 | for _, v := range values { 23 | _, ok := e.allowedTypes[v] 24 | if !ok { 25 | return fmt.Errorf("allowed values are %v", e.allowedTypes.String()) 26 | } 27 | } 28 | *e.value = values 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /enum_slice_var_test.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var enumSliceData []string 12 | 13 | func TestEnumSliceVar(t *testing.T) { 14 | t.Run("Test with single value", func(t *testing.T) { 15 | flagSet := NewFlagSet() 16 | flagSet.EnumSliceVar(&enumSliceData, "enum", []EnumVariable{Type1}, "enum", AllowdTypes{"type1": Type1, "type2": Type2}) 17 | os.Args = []string{ 18 | os.Args[0], 19 | "--enum", "type1", 20 | } 21 | err := flagSet.Parse() 22 | assert.Nil(t, err) 23 | assert.Equal(t, []string{"type1"}, enumSliceData) 24 | tearDown(t.Name()) 25 | }) 26 | 27 | t.Run("Test with multiple value", func(t *testing.T) { 28 | flagSet := NewFlagSet() 29 | flagSet.EnumSliceVar(&enumSliceData, "enum", []EnumVariable{Type1}, "enum", AllowdTypes{"type1": Type1, "type2": Type2}) 30 | os.Args = []string{ 31 | os.Args[0], 32 | "--enum", "type1,type2", 33 | } 34 | err := flagSet.Parse() 35 | assert.Nil(t, err) 36 | assert.Equal(t, []string{"type1", "type2"}, enumSliceData) 37 | tearDown(t.Name()) 38 | }) 39 | 40 | t.Run("Test with invalid value", func(t *testing.T) { 41 | if os.Getenv("IS_SUB_PROCESS") == "1" { 42 | flagSet := NewFlagSet() 43 | 44 | flagSet.EnumSliceVar(&enumSliceData, "enum", []EnumVariable{Nil}, "enum", AllowdTypes{"type1": Type1, "type2": Type2}) 45 | os.Args = []string{ 46 | os.Args[0], 47 | "--enum", "type3", 48 | } 49 | _ = flagSet.Parse() 50 | return 51 | } 52 | cmd := exec.Command(os.Args[0], "-test.run=TestFailEnumVar") 53 | cmd.Env = append(os.Environ(), "IS_SUB_PROCESS=1") 54 | err := cmd.Run() 55 | if e, ok := err.(*exec.ExitError); ok && !e.Success() { 56 | return 57 | } 58 | t.Fatalf("process ran with err %v, want exit error", err) 59 | tearDown(t.Name()) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /enum_var.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type EnumVariable int8 9 | 10 | func (e *EnumVariable) String() string { 11 | return fmt.Sprintf("%v", *e) 12 | } 13 | 14 | type AllowdTypes map[string]EnumVariable 15 | 16 | func (a AllowdTypes) String() string { 17 | var str string 18 | for k := range a { 19 | str += fmt.Sprintf("%s, ", k) 20 | } 21 | return strings.TrimSuffix(str, ", ") 22 | } 23 | 24 | type EnumVar struct { 25 | allowedTypes AllowdTypes 26 | value *string 27 | } 28 | 29 | func (e *EnumVar) String() string { 30 | if e.value != nil { 31 | return *e.value 32 | } 33 | return "" 34 | } 35 | 36 | func (e *EnumVar) Set(value string) error { 37 | _, ok := e.allowedTypes[value] 38 | if !ok { 39 | return fmt.Errorf("allowed values are %v", e.allowedTypes.String()) 40 | } 41 | *e.value = value 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /enum_var_test.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var enumString string 12 | 13 | const ( 14 | Nil EnumVariable = iota 15 | Type1 16 | Type2 17 | ) 18 | 19 | func TestSuccessfulEnumVar(t *testing.T) { 20 | flagSet := NewFlagSet() 21 | flagSet.EnumVar(&enumString, "enum", Type1, "enum", AllowdTypes{"type1": Type1, "type2": Type2}) 22 | os.Args = []string{ 23 | os.Args[0], 24 | "--enum", "type1", 25 | } 26 | err := flagSet.Parse() 27 | assert.Nil(t, err) 28 | assert.Equal(t, "type1", enumString) 29 | tearDown(t.Name()) 30 | } 31 | 32 | func TestFailEnumVar(t *testing.T) { 33 | if os.Getenv("IS_SUB_PROCESS") == "1" { 34 | flagSet := NewFlagSet() 35 | 36 | flagSet.EnumVar(&enumString, "enum", Nil, "enum", AllowdTypes{"type1": Type1, "type2": Type2}) 37 | os.Args = []string{ 38 | os.Args[0], 39 | "--enum", "type3", 40 | } 41 | _ = flagSet.Parse() 42 | return 43 | } 44 | cmd := exec.Command(os.Args[0], "-test.run=TestFailEnumVar") 45 | cmd.Env = append(os.Environ(), "IS_SUB_PROCESS=1") 46 | err := cmd.Run() 47 | if e, ok := err.(*exec.ExitError); ok && !e.Success() { 48 | return 49 | } 50 | t.Fatalf("process ran with err %v, want exit error", err) 51 | tearDown(t.Name()) 52 | } 53 | -------------------------------------------------------------------------------- /examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/projectdiscovery/goflags" 9 | ) 10 | 11 | type Options struct { 12 | name string 13 | Email goflags.StringSlice 14 | Phone string 15 | Address goflags.StringSlice 16 | fileSize goflags.Size 17 | duration time.Duration 18 | rls goflags.RateLimitMap 19 | severity []string 20 | // Dynamic 21 | titleSize int 22 | target string 23 | hashes []string 24 | } 25 | 26 | func main() { 27 | testOptions := &Options{} 28 | CheckUpdate := func() { 29 | fmt.Println("checking if new version is available") 30 | fmt.Println("updating tool....") 31 | } 32 | 33 | flagSet := goflags.NewFlagSet() 34 | flagSet.CreateGroup("info", "Info", 35 | flagSet.StringVarP(&testOptions.name, "name", "n", "", "name of the user"), 36 | flagSet.StringSliceVarP(&testOptions.Email, "email", "e", nil, "email of the user", goflags.CommaSeparatedStringSliceOptions), 37 | flagSet.RateLimitMapVarP(&testOptions.rls, "rate-limits", "rls", nil, "rate limits in format k=v/d i.e hackertarget=10/s", goflags.CommaSeparatedStringSliceOptions), 38 | ) 39 | flagSet.CreateGroup("additional", "Additional", 40 | flagSet.StringVarP(&testOptions.Phone, "phone", "ph", "", "phone of the user"), 41 | flagSet.StringSliceVarP(&testOptions.Address, "address", "add", nil, "address of the user", goflags.StringSliceOptions), 42 | flagSet.CallbackVarP(CheckUpdate, "update", "ut", "update this tool to latest version"), 43 | flagSet.SizeVarP(&testOptions.fileSize, "max-size", "ms", "", "max file size"), 44 | flagSet.DurationVar(&testOptions.duration, "timeout", time.Hour, "timeout"), 45 | flagSet.EnumSliceVarP(&testOptions.severity, "severity", "s", []goflags.EnumVariable{2}, "severity of the scan", goflags.AllowdTypes{"low": goflags.EnumVariable(0), "medium": goflags.EnumVariable(1), "high": goflags.EnumVariable(2)}), 46 | ) 47 | flagSet.CreateGroup("Dynmaic", "Dynamic", 48 | flagSet.DynamicVarP(&testOptions.titleSize, "title", "t", 50, "first N characters of the title"), 49 | flagSet.DynamicVarP(&testOptions.target, "target", "u", "https://example.com", "target url"), 50 | flagSet.DynamicVarP(&testOptions.hashes, "hashes", "hs", []string{"md5", "sha1"}, "supported hashes"), 51 | ) 52 | flagSet.SetCustomHelpText("EXAMPLE USAGE:\ngo run ./examples/basic [OPTIONS]") 53 | 54 | if err := flagSet.Parse(); err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | // ratelimits value is 59 | if len(testOptions.rls.AsMap()) > 0 { 60 | fmt.Printf("Got RateLimits: %+v\n", testOptions.rls) 61 | } 62 | 63 | if len(testOptions.severity) > 0 { 64 | fmt.Printf("Got Severity: %+v\n", testOptions.severity) 65 | } 66 | 67 | fmt.Println("Dynamic Values Output") 68 | fmt.Println("title size:", testOptions.titleSize) 69 | fmt.Println("target:", testOptions.target) 70 | fmt.Println("hashes:", testOptions.hashes) 71 | } 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/projectdiscovery/goflags 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 7 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 8 | github.com/pkg/errors v0.9.1 9 | github.com/stretchr/testify v1.9.0 10 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db 11 | gopkg.in/yaml.v3 v3.0.1 12 | ) 13 | 14 | require ( 15 | github.com/mattn/go-isatty v0.0.20 // indirect 16 | github.com/miekg/dns v1.1.56 // indirect 17 | github.com/projectdiscovery/blackrock v0.0.1 // indirect 18 | github.com/tidwall/gjson v1.14.3 // indirect 19 | github.com/tidwall/match v1.1.1 // indirect 20 | github.com/tidwall/pretty v1.2.0 // indirect 21 | golang.org/x/mod v0.17.0 // indirect 22 | golang.org/x/sync v0.10.0 // indirect 23 | golang.org/x/sys v0.28.0 // indirect 24 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 25 | ) 26 | 27 | require ( 28 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 29 | github.com/aymerick/douceur v0.2.0 // indirect 30 | github.com/davecgh/go-spew v1.1.1 // indirect 31 | github.com/gorilla/css v1.0.1 // indirect 32 | github.com/microcosm-cc/bluemonday v1.0.27 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | github.com/projectdiscovery/utils v0.4.12 35 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect 36 | golang.org/x/net v0.33.0 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= 2 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 3 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 4 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 5 | github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 h1:ox2F0PSMlrAAiAdknSRMDrAr8mfxPCfSZolH+/qQnyQ= 6 | github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 10 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 11 | github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 12 | github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 13 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 14 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 15 | github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 16 | github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 17 | github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= 18 | github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= 19 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 20 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ= 24 | github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss= 25 | github.com/projectdiscovery/utils v0.4.12 h1:3HE+4Go4iTwipeN2B+tC7xl7KS4BgXgp0BZaQXE2bjM= 26 | github.com/projectdiscovery/utils v0.4.12/go.mod h1:EDUNBDGTO+Tfl6YQj3ADg97iYp2h8IbCmpP24LMW3+E= 27 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= 28 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= 29 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 30 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 31 | github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= 32 | github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 33 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 34 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 35 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 36 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 37 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= 38 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 39 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 40 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 41 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 42 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 43 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 44 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 45 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 47 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 48 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 49 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 53 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 54 | -------------------------------------------------------------------------------- /goflags.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path" 11 | "reflect" 12 | "strconv" 13 | "strings" 14 | "text/tabwriter" 15 | "time" 16 | 17 | "github.com/cnf/structhash" 18 | "github.com/google/shlex" 19 | fileutil "github.com/projectdiscovery/utils/file" 20 | folderutil "github.com/projectdiscovery/utils/folder" 21 | permissionutil "github.com/projectdiscovery/utils/permission" 22 | "golang.org/x/exp/maps" 23 | "gopkg.in/yaml.v3" 24 | ) 25 | 26 | var ( 27 | DisableAutoConfigMigration = false 28 | ) 29 | 30 | // FlagSet is a list of flags for an application 31 | type FlagSet struct { 32 | CaseSensitive bool 33 | Marshal bool 34 | description string 35 | customHelpText string 36 | flagKeys InsertionOrderedMap 37 | groups []groupData 38 | CommandLine *flag.FlagSet 39 | configFilePath string 40 | 41 | // OtherOptionsGroupName is the name for all flags not in a group 42 | OtherOptionsGroupName string 43 | configOnlyKeys InsertionOrderedMap 44 | } 45 | 46 | type groupData struct { 47 | name string 48 | description string 49 | } 50 | 51 | type FlagData struct { 52 | usage string 53 | short string 54 | long string 55 | group string // unused unless set later 56 | defaultValue interface{} 57 | skipMarshal bool 58 | field flag.Value 59 | } 60 | 61 | // Group sets the group for a flag data 62 | func (flagData *FlagData) Group(name string) { 63 | flagData.group = name 64 | } 65 | 66 | // NewFlagSet creates a new flagSet structure for the application 67 | func NewFlagSet() *FlagSet { 68 | flag.CommandLine.ErrorHandling() 69 | return &FlagSet{ 70 | flagKeys: newInsertionOrderedMap(), 71 | OtherOptionsGroupName: "other options", 72 | CommandLine: flag.NewFlagSet(os.Args[0], flag.ExitOnError), 73 | configOnlyKeys: newInsertionOrderedMap(), 74 | } 75 | } 76 | 77 | func newInsertionOrderedMap() InsertionOrderedMap { 78 | return InsertionOrderedMap{values: make(map[string]*FlagData)} 79 | } 80 | 81 | // Hash returns the unique hash for a flagData structure 82 | // NOTE: Hash panics when the structure cannot be hashed. 83 | func (flagData *FlagData) Hash() string { 84 | hash, _ := structhash.Hash(flagData, 1) 85 | return hash 86 | } 87 | 88 | // SetDescription sets the description field for a flagSet to a value. 89 | func (flagSet *FlagSet) SetDescription(description string) { 90 | flagSet.description = description 91 | } 92 | 93 | // SetCustomHelpText sets the help text for a flagSet to a value. This variable appends text to the default help text. 94 | func (flagSet *FlagSet) SetCustomHelpText(helpText string) { 95 | flagSet.customHelpText = helpText 96 | } 97 | 98 | // SetGroup sets a group with name and description for the command line options 99 | // 100 | // The order in which groups are passed is also kept as is, similar to flags. 101 | func (flagSet *FlagSet) SetGroup(name, description string) { 102 | flagSet.groups = append(flagSet.groups, groupData{name: name, description: description}) 103 | } 104 | 105 | // MergeConfigFile reads a config file to merge values from. 106 | func (flagSet *FlagSet) MergeConfigFile(file string) error { 107 | return flagSet.readConfigFile(file) 108 | } 109 | 110 | // Parse parses the flags provided to the library. 111 | func (flagSet *FlagSet) Parse(args ...string) error { 112 | flagSet.CommandLine.SetOutput(os.Stdout) 113 | flagSet.CommandLine.Usage = flagSet.usageFunc 114 | toParse := os.Args[1:] 115 | if len(args) > 0 { 116 | toParse = args 117 | } 118 | _ = flagSet.CommandLine.Parse(toParse) 119 | configFilePath, _ := flagSet.GetConfigFilePath() 120 | 121 | // migrate data from old config dir to new one 122 | // Ref: https://github.com/projectdiscovery/nuclei/issues/3576 123 | if !DisableAutoConfigMigration { 124 | AttemptConfigMigration() 125 | } 126 | 127 | // if config file doesn't exist, create one 128 | if !fileutil.FileExists(configFilePath) { 129 | configData := flagSet.generateDefaultConfig() 130 | configFileDir := flagSet.GetToolConfigDir() 131 | if !fileutil.FolderExists(configFileDir) { 132 | _ = fileutil.CreateFolder(configFileDir) 133 | } 134 | return os.WriteFile(configFilePath, configData, permissionutil.ConfigFilePermission) 135 | } 136 | 137 | _ = flagSet.MergeConfigFile(configFilePath) // try to read default config after parsing flags 138 | return nil 139 | } 140 | 141 | // AttemptConfigMigration attempts to migrate config from old config dir to new one 142 | // migration condition 143 | // 1. old config dir exists 144 | // 2. new config dir doesn't exist 145 | // 3. old config dir is not same as new config dir 146 | func AttemptConfigMigration() { 147 | // migration condition 148 | // 1. old config dir exists 149 | // 2. new config dir doesn't exist 150 | // 3. old config dir is not same as new config dir 151 | flagSet := FlagSet{} // dummy flagset 152 | toolConfigDir := flagSet.GetToolConfigDir() 153 | if toolConfigDir != oldAppConfigDir && fileutil.FolderExists(oldAppConfigDir) && !fileutil.FolderExists(toolConfigDir) { 154 | _ = fileutil.CreateFolder(toolConfigDir) 155 | // move old config dir to new one 156 | _ = folderutil.SyncDirectory(oldAppConfigDir, toolConfigDir) 157 | } 158 | } 159 | 160 | // generateDefaultConfig generates a default YAML config file for a flagset. 161 | func (flagSet *FlagSet) generateDefaultConfig() []byte { 162 | hashes := make(map[string]struct{}) 163 | configBuffer := &bytes.Buffer{} 164 | configBuffer.WriteString("# ") 165 | configBuffer.WriteString(path.Base(os.Args[0])) 166 | configBuffer.WriteString(" config file\n# generated by https://github.com/projectdiscovery/goflags\n\n") 167 | 168 | // Attempts to marshal natively if proper flag is set, in case of errors fallback to normal mechanism 169 | if flagSet.Marshal { 170 | flagsToMarshall := make(map[string]interface{}) 171 | 172 | flagSet.flagKeys.forEach(func(key string, data *FlagData) { 173 | if !data.skipMarshal { 174 | flagsToMarshall[key] = data.defaultValue 175 | } 176 | }) 177 | 178 | flagSetBytes, err := yaml.Marshal(flagsToMarshall) 179 | if err == nil { 180 | configBuffer.Write(flagSetBytes) 181 | return configBuffer.Bytes() 182 | } 183 | } 184 | 185 | flagSet.flagKeys.forEach(func(key string, data *FlagData) { 186 | dataHash := data.Hash() 187 | if _, ok := hashes[dataHash]; ok { 188 | return 189 | } 190 | hashes[dataHash] = struct{}{} 191 | 192 | configBuffer.WriteString("# ") 193 | configBuffer.WriteString(strings.ToLower(data.usage)) 194 | configBuffer.WriteString("\n") 195 | configBuffer.WriteString("#") 196 | configBuffer.WriteString(data.long) 197 | configBuffer.WriteString(": ") 198 | switch dv := data.defaultValue.(type) { 199 | case string: 200 | configBuffer.WriteString(dv) 201 | case flag.Value: 202 | configBuffer.WriteString(dv.String()) 203 | case StringSlice: 204 | configBuffer.WriteString(dv.String()) 205 | } 206 | 207 | configBuffer.WriteString("\n\n") 208 | }) 209 | 210 | return bytes.TrimSuffix(configBuffer.Bytes(), []byte("\n\n")) 211 | } 212 | 213 | // CreateGroup within the flagset 214 | func (flagSet *FlagSet) CreateGroup(groupName, description string, flags ...*FlagData) { 215 | flagSet.SetGroup(groupName, description) 216 | for _, currentFlag := range flags { 217 | currentFlag.Group(groupName) 218 | } 219 | } 220 | 221 | // readConfigFile reads the config file and returns any flags 222 | // that might have been set by the config file. 223 | // 224 | // Command line flags however always take precedence over config file ones. 225 | func (flagSet *FlagSet) readConfigFile(filePath string) error { 226 | if empty, err := fileutil.IsEmpty(filePath); err == nil && empty { 227 | return nil 228 | } 229 | 230 | if isCommentOnly(filePath) { 231 | return nil 232 | } 233 | 234 | file, err := os.Open(filePath) 235 | if err != nil { 236 | return err 237 | } 238 | defer file.Close() 239 | 240 | data := make(map[string]interface{}) 241 | err = yaml.NewDecoder(file).Decode(&data) 242 | if err != nil { 243 | return err 244 | } 245 | flagSet.CommandLine.VisitAll(func(fl *flag.Flag) { 246 | item, ok := data[fl.Name] 247 | value := fl.Value.String() 248 | 249 | if strings.EqualFold(fl.DefValue, value) && ok { 250 | switch itemValue := item.(type) { 251 | case string: 252 | _ = fl.Value.Set(itemValue) 253 | case bool: 254 | _ = fl.Value.Set(strconv.FormatBool(itemValue)) 255 | case int: 256 | _ = fl.Value.Set(strconv.Itoa(itemValue)) 257 | case time.Duration: 258 | _ = fl.Value.Set(itemValue.String()) 259 | case []interface{}: 260 | for _, v := range itemValue { 261 | vStr, ok := v.(string) 262 | if ok { 263 | _ = fl.Value.Set(vStr) 264 | } 265 | } 266 | } 267 | } 268 | }) 269 | 270 | flagSet.configOnlyKeys.forEach(func(key string, flagData *FlagData) { 271 | item, ok := data[key] 272 | if ok { 273 | fl := flag.Lookup(key) 274 | if fl == nil { 275 | flag.Var(flagData.field, key, flagData.usage) 276 | fl = flag.Lookup(key) 277 | } 278 | 279 | switch data := item.(type) { 280 | case string: 281 | _ = fl.Value.Set(data) 282 | case bool: 283 | _ = fl.Value.Set(strconv.FormatBool(data)) 284 | case int: 285 | _ = fl.Value.Set(strconv.Itoa(data)) 286 | case []interface{}: 287 | for _, v := range data { 288 | vStr, ok := v.(string) 289 | if ok { 290 | _ = fl.Value.Set(vStr) 291 | } 292 | } 293 | } 294 | } 295 | }) 296 | return nil 297 | } 298 | 299 | // TODO: move to fileutil 300 | func isCommentOnly(filePath string) bool { 301 | file, err := os.Open(filePath) 302 | if err != nil { 303 | return false 304 | } 305 | defer file.Close() 306 | 307 | scanner := bufio.NewScanner(file) 308 | for scanner.Scan() { 309 | line := scanner.Text() 310 | if line != "" && !strings.HasPrefix(line, "#") { 311 | return false 312 | } 313 | } 314 | return true 315 | } 316 | 317 | // VarP adds a Var flag with a shortname and longname 318 | func (flagSet *FlagSet) VarP(field flag.Value, long, short, usage string) *FlagData { 319 | flagData := &FlagData{ 320 | usage: usage, 321 | long: long, 322 | defaultValue: field, 323 | } 324 | if short != "" { 325 | flagData.short = short 326 | flagSet.CommandLine.Var(field, short, usage) 327 | flagSet.flagKeys.Set(short, flagData) 328 | } 329 | flagSet.CommandLine.Var(field, long, usage) 330 | flagSet.flagKeys.Set(long, flagData) 331 | return flagData 332 | } 333 | 334 | // Var adds a Var flag with a longname 335 | func (flagSet *FlagSet) Var(field flag.Value, long, usage string) *FlagData { 336 | return flagSet.VarP(field, long, "", usage) 337 | } 338 | 339 | // StringVarEnv adds a string flag with a shortname and longname with a default value read from env variable 340 | // with a default value fallback 341 | func (flagSet *FlagSet) StringVarEnv(field *string, long, short, defaultValue, envName, usage string) *FlagData { 342 | if envValue, exists := os.LookupEnv(envName); exists { 343 | defaultValue = envValue 344 | } 345 | return flagSet.StringVarP(field, long, short, defaultValue, usage) 346 | } 347 | 348 | // StringVarP adds a string flag with a shortname and longname 349 | func (flagSet *FlagSet) StringVarP(field *string, long, short, defaultValue, usage string) *FlagData { 350 | flagData := &FlagData{ 351 | usage: usage, 352 | long: long, 353 | defaultValue: defaultValue, 354 | } 355 | if short != "" { 356 | flagData.short = short 357 | flagSet.CommandLine.StringVar(field, short, defaultValue, usage) 358 | flagSet.flagKeys.Set(short, flagData) 359 | } 360 | flagSet.CommandLine.StringVar(field, long, defaultValue, usage) 361 | flagSet.flagKeys.Set(long, flagData) 362 | return flagData 363 | } 364 | 365 | // StringVar adds a string flag with a longname 366 | func (flagSet *FlagSet) StringVar(field *string, long, defaultValue, usage string) *FlagData { 367 | return flagSet.StringVarP(field, long, "", defaultValue, usage) 368 | } 369 | 370 | // BoolVarP adds a bool flag with a shortname and longname 371 | func (flagSet *FlagSet) BoolVarP(field *bool, long, short string, defaultValue bool, usage string) *FlagData { 372 | flagData := &FlagData{ 373 | usage: usage, 374 | long: long, 375 | defaultValue: strconv.FormatBool(defaultValue), 376 | } 377 | if short != "" { 378 | flagData.short = short 379 | flagSet.CommandLine.BoolVar(field, short, defaultValue, usage) 380 | flagSet.flagKeys.Set(short, flagData) 381 | } 382 | flagSet.CommandLine.BoolVar(field, long, defaultValue, usage) 383 | flagSet.flagKeys.Set(long, flagData) 384 | return flagData 385 | } 386 | 387 | // BoolVar adds a bool flag with a longname 388 | func (flagSet *FlagSet) BoolVar(field *bool, long string, defaultValue bool, usage string) *FlagData { 389 | return flagSet.BoolVarP(field, long, "", defaultValue, usage) 390 | } 391 | 392 | // IntVarP adds a int flag with a shortname and longname 393 | func (flagSet *FlagSet) IntVarP(field *int, long, short string, defaultValue int, usage string) *FlagData { 394 | flagData := &FlagData{ 395 | usage: usage, 396 | short: short, 397 | long: long, 398 | defaultValue: strconv.Itoa(defaultValue), 399 | } 400 | if short != "" { 401 | flagData.short = short 402 | flagSet.CommandLine.IntVar(field, short, defaultValue, usage) 403 | flagSet.flagKeys.Set(short, flagData) 404 | } 405 | flagSet.CommandLine.IntVar(field, long, defaultValue, usage) 406 | flagSet.flagKeys.Set(long, flagData) 407 | return flagData 408 | } 409 | 410 | // IntVar adds a int flag with a longname 411 | func (flagSet *FlagSet) IntVar(field *int, long string, defaultValue int, usage string) *FlagData { 412 | return flagSet.IntVarP(field, long, "", defaultValue, usage) 413 | } 414 | 415 | // StringSliceVarP adds a string slice flag with a shortname and longname 416 | // Use options to customize the behavior 417 | func (flagSet *FlagSet) StringSliceVarP(field *StringSlice, long, short string, defaultValue StringSlice, usage string, options Options) *FlagData { 418 | optionMap[field] = options 419 | for _, defaultItem := range defaultValue { 420 | values, _ := ToStringSlice(defaultItem, options) 421 | for _, value := range values { 422 | _ = field.Set(value) 423 | } 424 | } 425 | optionDefaultValues[field] = *field 426 | flagData := &FlagData{ 427 | usage: usage, 428 | long: long, 429 | defaultValue: defaultValue, 430 | } 431 | if short != "" { 432 | flagData.short = short 433 | flagSet.CommandLine.Var(field, short, usage) 434 | flagSet.flagKeys.Set(short, flagData) 435 | } 436 | flagSet.CommandLine.Var(field, long, usage) 437 | flagSet.flagKeys.Set(long, flagData) 438 | return flagData 439 | } 440 | 441 | // StringSliceVar adds a string slice flag with a longname 442 | // Supports ONE value at a time. Adding multiple values require repeating the argument (-flag value1 -flag value2) 443 | // No value normalization is happening. 444 | func (flagSet *FlagSet) StringSliceVar(field *StringSlice, long string, defaultValue []string, usage string, options Options) *FlagData { 445 | return flagSet.StringSliceVarP(field, long, "", defaultValue, usage, options) 446 | } 447 | 448 | // StringSliceVarConfigOnly adds a string slice config value (without flag) with a longname 449 | func (flagSet *FlagSet) StringSliceVarConfigOnly(field *StringSlice, long string, defaultValue []string, usage string) *FlagData { 450 | for _, item := range defaultValue { 451 | _ = field.Set(item) 452 | } 453 | flagData := &FlagData{ 454 | usage: usage, 455 | long: long, 456 | defaultValue: defaultValue, 457 | field: field, 458 | } 459 | flagSet.configOnlyKeys.Set(long, flagData) 460 | flagSet.flagKeys.Set(long, flagData) 461 | return flagData 462 | } 463 | 464 | // RuntimeMapVarP adds a runtime only map flag with a longname 465 | func (flagSet *FlagSet) RuntimeMapVar(field *RuntimeMap, long string, defaultValue []string, usage string) *FlagData { 466 | return flagSet.RuntimeMapVarP(field, long, "", defaultValue, usage) 467 | } 468 | 469 | // RuntimeMapVarP adds a runtime only map flag with a shortname and longname 470 | func (flagSet *FlagSet) RuntimeMapVarP(field *RuntimeMap, long, short string, defaultValue []string, usage string) *FlagData { 471 | for _, item := range defaultValue { 472 | _ = field.Set(item) 473 | } 474 | 475 | flagData := &FlagData{ 476 | usage: usage, 477 | long: long, 478 | defaultValue: defaultValue, 479 | skipMarshal: true, 480 | } 481 | 482 | if short != "" { 483 | flagData.short = short 484 | flagSet.CommandLine.Var(field, short, usage) 485 | flagSet.flagKeys.Set(short, flagData) 486 | } 487 | flagSet.CommandLine.Var(field, long, usage) 488 | flagSet.flagKeys.Set(long, flagData) 489 | return flagData 490 | } 491 | 492 | // PortVar adds a port flag with a longname 493 | func (flagSet *FlagSet) PortVar(field *Port, long string, defaultValue []string, usage string) *FlagData { 494 | return flagSet.PortVarP(field, long, "", defaultValue, usage) 495 | } 496 | 497 | // PortVarP adds a port flag with a shortname and longname 498 | func (flagSet *FlagSet) PortVarP(field *Port, long, short string, defaultValue []string, usage string) *FlagData { 499 | for _, item := range defaultValue { 500 | _ = field.Set(item) 501 | } 502 | portOptionDefaultValues[field] = maps.Clone(field.kv) 503 | 504 | flagData := &FlagData{ 505 | usage: usage, 506 | long: long, 507 | defaultValue: defaultValue, 508 | skipMarshal: true, 509 | } 510 | 511 | if short != "" { 512 | flagData.short = short 513 | flagSet.CommandLine.Var(field, short, usage) 514 | flagSet.flagKeys.Set(short, flagData) 515 | } 516 | flagSet.CommandLine.Var(field, long, usage) 517 | flagSet.flagKeys.Set(long, flagData) 518 | return flagData 519 | } 520 | 521 | // EnumVar adds a enum flag with a longname 522 | func (flagSet *FlagSet) EnumVar(field *string, long string, defaultValue EnumVariable, usage string, allowedTypes AllowdTypes) *FlagData { 523 | return flagSet.EnumVarP(field, long, "", defaultValue, usage, allowedTypes) 524 | } 525 | 526 | // EnumVarP adds a enum flag with a shortname and longname 527 | func (flagSet *FlagSet) EnumVarP(field *string, long, short string, defaultValue EnumVariable, usage string, allowedTypes AllowdTypes) *FlagData { 528 | var hasDefaultValue bool 529 | for k, v := range allowedTypes { 530 | if v == defaultValue { 531 | hasDefaultValue = true 532 | *field = k 533 | } 534 | } 535 | if !hasDefaultValue { 536 | panic("undefined default value") 537 | } 538 | flagData := &FlagData{ 539 | usage: usage, 540 | long: long, 541 | defaultValue: *field, 542 | } 543 | if short != "" { 544 | flagData.short = short 545 | flagSet.CommandLine.Var(&EnumVar{allowedTypes, field}, short, usage) 546 | flagSet.flagKeys.Set(short, flagData) 547 | } 548 | flagSet.CommandLine.Var(&EnumVar{allowedTypes, field}, long, usage) 549 | flagSet.flagKeys.Set(long, flagData) 550 | return flagData 551 | } 552 | 553 | // EnumVar adds a enum flag with a longname 554 | func (flagSet *FlagSet) EnumSliceVar(field *[]string, long string, defaultValues []EnumVariable, usage string, allowedTypes AllowdTypes) *FlagData { 555 | return flagSet.EnumSliceVarP(field, long, "", defaultValues, usage, allowedTypes) 556 | } 557 | 558 | // EnumVarP adds a enum flag with a shortname and longname 559 | func (flagSet *FlagSet) EnumSliceVarP(field *[]string, long, short string, defaultValues []EnumVariable, usage string, allowedTypes AllowdTypes) *FlagData { 560 | var defaults []string 561 | for k, v := range allowedTypes { 562 | for _, defaultValue := range defaultValues { 563 | if v == defaultValue { 564 | defaults = append(defaults, k) 565 | } 566 | } 567 | } 568 | if len(defaults) == 0 { 569 | panic("undefined default value") 570 | } 571 | 572 | *field = defaults 573 | flagData := &FlagData{ 574 | usage: usage, 575 | long: long, 576 | defaultValue: strings.Join(*field, ","), 577 | } 578 | if short != "" { 579 | flagData.short = short 580 | flagSet.CommandLine.Var(&EnumSliceVar{allowedTypes, field}, short, usage) 581 | flagSet.flagKeys.Set(short, flagData) 582 | } 583 | flagSet.CommandLine.Var(&EnumSliceVar{allowedTypes, field}, long, usage) 584 | flagSet.flagKeys.Set(long, flagData) 585 | return flagData 586 | } 587 | 588 | func (flagSet *FlagSet) usageFunc() { 589 | var helpAsked bool 590 | 591 | // Only show help usage if asked by user 592 | for _, arg := range os.Args { 593 | argStripped := strings.Trim(arg, "-") 594 | if argStripped == "h" || argStripped == "help" { 595 | helpAsked = true 596 | } 597 | } 598 | if !helpAsked { 599 | return 600 | } 601 | 602 | cliOutput := flagSet.CommandLine.Output() 603 | fmt.Fprintf(cliOutput, "%s\n\n", flagSet.description) 604 | fmt.Fprintf(cliOutput, "Usage:\n %s [flags]\n\n", os.Args[0]) 605 | fmt.Fprintf(cliOutput, "Flags:\n") 606 | 607 | writer := tabwriter.NewWriter(cliOutput, 0, 0, 1, ' ', 0) 608 | 609 | // If a user has specified a group with help, and we have groups, return with the tool's usage function 610 | if len(flagSet.groups) > 0 && len(os.Args) == 3 { 611 | group := flagSet.getGroupbyName(strings.ToLower(os.Args[2])) 612 | if group.name != "" { 613 | flagSet.displayGroupUsageFunc(newUniqueDeduper(), group, cliOutput, writer) 614 | return 615 | } 616 | flag := flagSet.getFlagByName(os.Args[2]) 617 | if flag != nil { 618 | flagSet.displaySingleFlagUsageFunc(os.Args[2], flag, cliOutput, writer) 619 | return 620 | } 621 | } 622 | 623 | if len(flagSet.groups) > 0 { 624 | flagSet.usageFuncForGroups(cliOutput, writer) 625 | } else { 626 | flagSet.usageFuncInternal(writer) 627 | } 628 | 629 | // If there is a custom help text specified, print it 630 | if !isEmpty(flagSet.customHelpText) { 631 | fmt.Fprintf(cliOutput, "\n%s\n", flagSet.customHelpText) 632 | } 633 | } 634 | 635 | func (flagSet *FlagSet) getGroupbyName(name string) groupData { 636 | for _, group := range flagSet.groups { 637 | if strings.EqualFold(group.name, name) || strings.EqualFold(group.description, name) { 638 | return group 639 | } 640 | } 641 | return groupData{} 642 | } 643 | 644 | func (flagSet *FlagSet) getFlagByName(name string) *FlagData { 645 | var flagData *FlagData 646 | flagSet.flagKeys.forEach(func(key string, data *FlagData) { 647 | // check if the items are equal 648 | // - Case sensitive 649 | equal := flagSet.CaseSensitive && (data.long == name || data.short == name) 650 | // - Case insensitive 651 | equalFold := !flagSet.CaseSensitive && (strings.EqualFold(data.long, name) || strings.EqualFold(data.short, name)) 652 | if equal || equalFold { 653 | flagData = data 654 | return 655 | } 656 | }) 657 | return flagData 658 | } 659 | 660 | // usageFuncInternal prints usage for command line flags 661 | func (flagSet *FlagSet) usageFuncInternal(writer *tabwriter.Writer) { 662 | uniqueDeduper := newUniqueDeduper() 663 | 664 | flagSet.flagKeys.forEach(func(key string, data *FlagData) { 665 | if currentFlag := flagSet.CommandLine.Lookup(key); currentFlag != nil { 666 | if !uniqueDeduper.isUnique(data) { 667 | return 668 | } 669 | result := createUsageString(data, currentFlag) 670 | fmt.Fprint(writer, result, "\n") 671 | } 672 | }) 673 | writer.Flush() 674 | } 675 | 676 | // usageFuncForGroups prints usage for command line flags with grouping enabled 677 | func (flagSet *FlagSet) usageFuncForGroups(cliOutput io.Writer, writer *tabwriter.Writer) { 678 | uniqueDeduper := newUniqueDeduper() 679 | 680 | var otherOptions []string 681 | for _, group := range flagSet.groups { 682 | otherOptions = append(otherOptions, flagSet.displayGroupUsageFunc(uniqueDeduper, group, cliOutput, writer)...) 683 | } 684 | 685 | // Print Any additional flag that may have been left 686 | if len(otherOptions) > 0 { 687 | fmt.Fprintf(cliOutput, "%s:\n", normalizeGroupDescription(flagSet.OtherOptionsGroupName)) 688 | 689 | for _, option := range otherOptions { 690 | fmt.Fprint(writer, option, "\n") 691 | } 692 | writer.Flush() 693 | } 694 | } 695 | 696 | // displayGroupUsageFunc displays usage for a group 697 | func (flagSet *FlagSet) displayGroupUsageFunc(uniqueDeduper *uniqueDeduper, group groupData, cliOutput io.Writer, writer *tabwriter.Writer) []string { 698 | fmt.Fprintf(cliOutput, "%s:\n", normalizeGroupDescription(group.description)) 699 | 700 | var otherOptions []string 701 | flagSet.flagKeys.forEach(func(key string, data *FlagData) { 702 | if currentFlag := flagSet.CommandLine.Lookup(key); currentFlag != nil { 703 | if data.group == "" { 704 | if !uniqueDeduper.isUnique(data) { 705 | return 706 | } 707 | otherOptions = append(otherOptions, createUsageString(data, currentFlag)) 708 | return 709 | } 710 | // Ignore the flag if it's not in our intended group 711 | if !strings.EqualFold(data.group, group.name) { 712 | return 713 | } 714 | if !uniqueDeduper.isUnique(data) { 715 | return 716 | } 717 | result := createUsageString(data, currentFlag) 718 | fmt.Fprint(writer, result, "\n") 719 | } 720 | }) 721 | writer.Flush() 722 | fmt.Printf("\n") 723 | return otherOptions 724 | } 725 | 726 | // displaySingleFlagUsageFunc displays usage for a single flag 727 | func (flagSet *FlagSet) displaySingleFlagUsageFunc(name string, data *FlagData, _ io.Writer, writer *tabwriter.Writer) { 728 | if currentFlag := flagSet.CommandLine.Lookup(name); currentFlag != nil { 729 | result := createUsageString(data, currentFlag) 730 | fmt.Fprint(writer, result, "\n") 731 | writer.Flush() 732 | } 733 | } 734 | 735 | type uniqueDeduper struct { 736 | hashes map[string]interface{} 737 | } 738 | 739 | func newUniqueDeduper() *uniqueDeduper { 740 | return &uniqueDeduper{hashes: make(map[string]interface{})} 741 | } 742 | 743 | // isUnique returns true if the flag is unique during iteration 744 | func (u *uniqueDeduper) isUnique(data *FlagData) bool { 745 | dataHash := data.Hash() 746 | if _, ok := u.hashes[dataHash]; ok { 747 | return false // Don't print the value if printed previously 748 | } 749 | u.hashes[dataHash] = struct{}{} 750 | return true 751 | } 752 | 753 | func createUsageString(data *FlagData, currentFlag *flag.Flag) string { 754 | valueType := reflect.TypeOf(currentFlag.Value) 755 | 756 | result := createUsageFlagNames(data) 757 | result += createUsageTypeAndDescription(currentFlag, valueType) 758 | result += createUsageDefaultValue(data, currentFlag, valueType) 759 | 760 | return result 761 | } 762 | 763 | func createUsageDefaultValue(data *FlagData, currentFlag *flag.Flag, valueType reflect.Type) string { 764 | if !isZeroValue(currentFlag, currentFlag.DefValue) { 765 | defaultValueTemplate := " (default " 766 | switch valueType.String() { // ugly hack because "flag.stringValue" is not exported from the parent library 767 | case "*flag.stringValue": 768 | defaultValueTemplate += "%q" 769 | default: 770 | defaultValueTemplate += "%v" 771 | } 772 | defaultValueTemplate += ")" 773 | return fmt.Sprintf(defaultValueTemplate, data.defaultValue) 774 | } 775 | return "" 776 | } 777 | 778 | func createUsageTypeAndDescription(currentFlag *flag.Flag, valueType reflect.Type) string { 779 | var result string 780 | 781 | flagDisplayType, usage := flag.UnquoteUsage(currentFlag) 782 | if len(flagDisplayType) > 0 { 783 | if flagDisplayType == "value" { // hardcoded in the goflags library 784 | switch valueType.Kind() { 785 | case reflect.Ptr: 786 | pointerTypeElement := valueType.Elem() 787 | switch pointerTypeElement.Kind() { 788 | case reflect.Slice, reflect.Array: 789 | switch pointerTypeElement.Elem().Kind() { 790 | case reflect.String: 791 | flagDisplayType = "string[]" 792 | default: 793 | flagDisplayType = "value[]" 794 | } 795 | } 796 | } 797 | } 798 | result += " " + flagDisplayType 799 | } 800 | 801 | result += "\t\t" 802 | result += strings.ReplaceAll(usage, "\n", "\n"+strings.Repeat(" ", 4)+"\t") 803 | return result 804 | } 805 | 806 | func createUsageFlagNames(data *FlagData) string { 807 | flagNames := strings.Repeat(" ", 2) + "\t" 808 | 809 | var validFlags []string 810 | addValidParam := func(value string) { 811 | if !isEmpty(value) { 812 | validFlags = append(validFlags, fmt.Sprintf("-%s", value)) 813 | } 814 | } 815 | 816 | addValidParam(data.short) 817 | addValidParam(data.long) 818 | 819 | if len(validFlags) == 0 { 820 | panic("CLI arguments cannot be empty.") 821 | } 822 | 823 | flagNames += strings.Join(validFlags, ", ") 824 | return flagNames 825 | } 826 | 827 | // isZeroValue determines whether the string represents the zero 828 | // value for a flag. 829 | func isZeroValue(f *flag.Flag, value string) bool { 830 | // Build a zero value of the flag's Value type, and see if the 831 | // result of calling its String method equals the value passed in. 832 | // This works unless the Value type is itself an interface type. 833 | valueType := reflect.TypeOf(f.Value) 834 | var zeroValue reflect.Value 835 | if valueType.Kind() == reflect.Ptr { 836 | zeroValue = reflect.New(valueType.Elem()) 837 | } else { 838 | zeroValue = reflect.Zero(valueType) 839 | } 840 | return value == zeroValue.Interface().(flag.Value).String() 841 | } 842 | 843 | // normalizeGroupDescription returns normalized description field for group 844 | func normalizeGroupDescription(description string) string { 845 | return strings.ToUpper(description) 846 | } 847 | 848 | // GetArgsFromString allows splitting a string into arguments 849 | // following the same rules as the shell. 850 | func GetArgsFromString(str string) []string { 851 | args, _ := shlex.Split(str) 852 | return args 853 | } 854 | -------------------------------------------------------------------------------- /goflags_test.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "strconv" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | fileutil "github.com/projectdiscovery/utils/file" 16 | osutil "github.com/projectdiscovery/utils/os" 17 | permissionutil "github.com/projectdiscovery/utils/permission" 18 | "github.com/stretchr/testify/assert" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestGenerateDefaultConfig(t *testing.T) { 24 | flagSet := NewFlagSet() 25 | 26 | example := `# generated by https://github.com/projectdiscovery/goflags 27 | 28 | # default value for a test flag example 29 | #test: test-default-value 30 | 31 | # string slice flag example value 32 | #slice: ["item1", "item2"]` 33 | 34 | var data string 35 | var data2 StringSlice 36 | flagSet.StringVar(&data, "test", "test-default-value", "Default value for a test flag example") 37 | flagSet.StringSliceVar(&data2, "slice", []string{"item1", "item2"}, "String slice flag example value", StringSliceOptions) 38 | defaultConfig := string(flagSet.generateDefaultConfig()) 39 | parts := strings.SplitN(defaultConfig, "\n", 2) 40 | 41 | require.Equal(t, example, parts[1], "could not get correct default config") 42 | tearDown(t.Name()) 43 | } 44 | 45 | func TestConfigFileDataTypes(t *testing.T) { 46 | flagSet := NewFlagSet() 47 | var data string 48 | var data2 StringSlice 49 | var data3 int 50 | var data4 bool 51 | var data5 time.Duration 52 | 53 | flagSet.StringVar(&data, "string-value", "", "Default value for a test flag example") 54 | flagSet.StringSliceVar(&data2, "slice-value", []string{}, "String slice flag example value", StringSliceOptions) 55 | flagSet.IntVar(&data3, "int-value", 0, "Int value example") 56 | flagSet.BoolVar(&data4, "bool-value", false, "Bool value example") 57 | flagSet.DurationVar(&data5, "duration-value", time.Hour, "Bool value example") 58 | 59 | configFileData := ` 60 | string-value: test 61 | slice-value: 62 | - test 63 | - test2 64 | severity: 65 | - info 66 | - high 67 | int-value: 543 68 | bool-value: true 69 | duration-value: 1h` 70 | err := os.WriteFile("test.yaml", []byte(configFileData), permissionutil.ConfigFilePermission) 71 | require.Nil(t, err, "could not write temporary config") 72 | defer os.Remove("test.yaml") 73 | 74 | err = flagSet.MergeConfigFile("test.yaml") 75 | require.Nil(t, err, "could not merge temporary config") 76 | 77 | require.Equal(t, "test", data, "could not get correct string") 78 | require.Equal(t, StringSlice{"test", "test2"}, data2, "could not get correct string slice") 79 | require.Equal(t, 543, data3, "could not get correct int") 80 | require.Equal(t, true, data4, "could not get correct bool") 81 | require.Equal(t, time.Hour, data5, "could not get correct duration") 82 | 83 | tearDown(t.Name()) 84 | } 85 | 86 | func TestUsageOrder(t *testing.T) { 87 | flagSet := NewFlagSet() 88 | 89 | var stringData string 90 | var stringSliceData StringSlice 91 | var stringSliceData2 StringSlice 92 | var intData int 93 | var boolData bool 94 | var enumData string 95 | var enumSliceData []string 96 | 97 | flagSet.SetGroup("String", "String") 98 | flagSet.StringVar(&stringData, "string-value", "", "String example value example").Group("String") 99 | flagSet.StringVarP(&stringData, "", "ts2", "test-string", "String with default value example #2").Group("String") 100 | flagSet.StringVar(&stringData, "string-with-default-value", "test-string", "String with default value example").Group("String") 101 | flagSet.StringVarP(&stringData, "string-with-default-value2", "ts", "test-string", "String with default value example #2").Group("String") 102 | 103 | flagSet.SetGroup("StringSlice", "StringSlice") 104 | flagSet.StringSliceVar(&stringSliceData, "slice-value", []string{}, "String slice flag example value", StringSliceOptions).Group("StringSlice") 105 | flagSet.StringSliceVarP(&stringSliceData, "slice-value2", "sv", []string{}, "String slice flag example value #2", StringSliceOptions).Group("StringSlice") 106 | flagSet.StringSliceVar(&stringSliceData, "slice-with-default-value", []string{"a", "b", "c"}, "String slice flag with default example values", StringSliceOptions).Group("StringSlice") 107 | flagSet.StringSliceVarP(&stringSliceData2, "slice-with-default-value2", "swdf", []string{"a", "b", "c"}, "String slice flag with default example values #2", StringSliceOptions).Group("StringSlice") 108 | 109 | flagSet.SetGroup("Integer", "Integer") 110 | flagSet.IntVar(&intData, "int-value", 0, "Int value example").Group("Integer") 111 | flagSet.IntVarP(&intData, "int-value2", "iv", 0, "Int value example #2").Group("Integer") 112 | flagSet.IntVar(&intData, "int-with-default-value", 12, "Int with default value example").Group("Integer") 113 | flagSet.IntVarP(&intData, "int-with-default-value2", "iwdv", 12, "Int with default value example #2").Group("Integer") 114 | 115 | flagSet.SetGroup("Bool", "Boolean") 116 | flagSet.BoolVar(&boolData, "bool-value", false, "Bool value example").Group("Bool") 117 | flagSet.BoolVarP(&boolData, "bool-value2", "bv", false, "Bool value example #2").Group("Bool") 118 | flagSet.BoolVar(&boolData, "bool-with-default-value", true, "Bool with default value example").Group("Bool") 119 | flagSet.BoolVarP(&boolData, "bool-with-default-value2", "bwdv", true, "Bool with default value example #2").Group("Bool") 120 | 121 | flagSet.SetGroup("Enum", "Enum") 122 | flagSet.EnumVarP(&enumData, "enum-with-default-value", "en", EnumVariable(0), "Enum with default value(zero/one/two)", AllowdTypes{ 123 | "zero": EnumVariable(0), 124 | "one": EnumVariable(1), 125 | "two": EnumVariable(2), 126 | }).Group("Enum") 127 | 128 | flagSet.EnumSliceVarP(&enumSliceData, "enum-slice-with-default-value", "esn", []EnumVariable{EnumVariable(0)}, "Enum with default value(zero/one/two)", AllowdTypes{ 129 | "zero": EnumVariable(0), 130 | "one": EnumVariable(1), 131 | "two": EnumVariable(2), 132 | }).Group("Enum") 133 | 134 | flagSet.SetGroup("Update", "Update") 135 | flagSet.CallbackVar(func() {}, "update", "update tool_1 to the latest released version").Group("Update") 136 | flagSet.CallbackVarP(func() {}, "disable-update-check", "duc", "disable automatic update check").Group("Update") 137 | 138 | output := &bytes.Buffer{} 139 | flagSet.CommandLine.SetOutput(output) 140 | 141 | os.Args = []string{ 142 | os.Args[0], 143 | "-h", 144 | } 145 | flagSet.usageFunc() 146 | 147 | resultOutput := output.String() 148 | actual := resultOutput[strings.Index(resultOutput, "Flags:\n"):] 149 | fmt.Println(actual) 150 | 151 | expected := 152 | `Flags: 153 | STRING: 154 | -string-value string String example value example 155 | -ts2 string String with default value example #2 (default "test-string") 156 | -string-with-default-value string String with default value example (default "test-string") 157 | -ts, -string-with-default-value2 string String with default value example #2 (default "test-string") 158 | STRINGSLICE: 159 | -slice-value string[] String slice flag example value 160 | -sv, -slice-value2 string[] String slice flag example value #2 161 | -slice-with-default-value string[] String slice flag with default example values (default ["a", "b", "c"]) 162 | -swdf, -slice-with-default-value2 string[] String slice flag with default example values #2 (default ["a", "b", "c"]) 163 | INTEGER: 164 | -int-value int Int value example 165 | -iv, -int-value2 int Int value example #2 166 | -int-with-default-value int Int with default value example (default 12) 167 | -iwdv, -int-with-default-value2 int Int with default value example #2 (default 12) 168 | BOOLEAN: 169 | -bool-value Bool value example 170 | -bv, -bool-value2 Bool value example #2 171 | -bool-with-default-value Bool with default value example (default true) 172 | -bwdv, -bool-with-default-value2 Bool with default value example #2 (default true) 173 | ENUM: 174 | -en, -enum-with-default-value value Enum with default value(zero/one/two) (default zero) 175 | -esn, -enum-slice-with-default-value value Enum with default value(zero/one/two) (default zero) 176 | UPDATE: 177 | -update update tool_1 to the latest released version 178 | -duc, -disable-update-check disable automatic update check 179 | ` 180 | assert.Equal(t, expected, actual) 181 | 182 | tearDown(t.Name()) 183 | } 184 | 185 | func TestIncorrectStringFlagsCausePanic(t *testing.T) { 186 | flagSet := NewFlagSet() 187 | var stringData string 188 | 189 | flagSet.StringVar(&stringData, "", "test-string", "String with default value example") 190 | assert.Panics(t, flagSet.usageFunc) 191 | 192 | // env GOOS=linux GOARCH=amd64 go build main.go -o nuclei 193 | 194 | tearDown(t.Name()) 195 | } 196 | 197 | func TestIncorrectFlagsCausePanic(t *testing.T) { 198 | type flagPair struct { 199 | Short, Long string 200 | } 201 | 202 | createTestParameters := func() []flagPair { 203 | var result []flagPair 204 | result = append(result, flagPair{"", ""}) 205 | 206 | badFlagNames := [...]string{" ", "\t", "\n"} 207 | for _, badFlagName := range badFlagNames { 208 | result = append(result, flagPair{"", badFlagName}) 209 | result = append(result, flagPair{badFlagName, ""}) 210 | result = append(result, flagPair{badFlagName, badFlagName}) 211 | } 212 | return result 213 | } 214 | 215 | for index, tuple := range createTestParameters() { 216 | uniqueName := strconv.Itoa(index) 217 | t.Run(uniqueName, func(t *testing.T) { 218 | assert.Panics(t, func() { 219 | tearDown(uniqueName) 220 | 221 | flagSet := NewFlagSet() 222 | var stringData string 223 | 224 | flagSet.StringVarP(&stringData, tuple.Short, tuple.Long, "test-string", "String with default value example") 225 | flagSet.usageFunc() 226 | }) 227 | }) 228 | } 229 | } 230 | 231 | type testSliceValue []interface{} 232 | 233 | func (value testSliceValue) String() string { return "" } 234 | func (value testSliceValue) Set(string) error { return nil } 235 | 236 | func TestCustomSliceUsageType(t *testing.T) { 237 | testCases := map[string]flag.Flag{ 238 | "string[]": {Value: &StringSlice{}}, 239 | "value[]": {Value: &testSliceValue{}}, 240 | } 241 | 242 | for expected, currentFlag := range testCases { 243 | result := createUsageTypeAndDescription(¤tFlag, reflect.TypeOf(currentFlag.Value)) 244 | assert.Equal(t, expected, strings.TrimSpace(result)) 245 | } 246 | } 247 | 248 | func TestParseStringSlice(t *testing.T) { 249 | flagSet := NewFlagSet() 250 | 251 | var stringSlice StringSlice 252 | flagSet.StringSliceVarP(&stringSlice, "header", "H", []string{}, "Header values. Expected usage: -H \"header1\":\"value1\" -H \"header2\":\"value2\"", StringSliceOptions) 253 | 254 | header1 := "\"header1:value1\"" 255 | header2 := "\" HEADER 2: VALUE2 \"" 256 | header3 := "\"header3\":\"value3, value4\"" 257 | 258 | os.Args = []string{ 259 | os.Args[0], 260 | "-H", header1, 261 | "-header", header2, 262 | "-H", header3, 263 | } 264 | 265 | err := flagSet.Parse() 266 | assert.Nil(t, err) 267 | 268 | assert.Equal(t, StringSlice{header1, header2, header3}, stringSlice) 269 | tearDown(t.Name()) 270 | 271 | } 272 | 273 | func TestParseCommaSeparatedStringSlice(t *testing.T) { 274 | flagSet := NewFlagSet() 275 | 276 | var csStringSlice StringSlice 277 | flagSet.StringSliceVarP(&csStringSlice, "cs-value", "CSV", []string{}, "Comma Separated Values. Expected usage: -CSV value1,value2,value3", CommaSeparatedStringSliceOptions) 278 | 279 | valueCommon := `value1,Value2 ",value3` 280 | value1 := `value1` 281 | value2 := `Value2 "` 282 | value3 := `value3` 283 | 284 | os.Args = []string{ 285 | os.Args[0], 286 | "-CSV", valueCommon, 287 | } 288 | 289 | err := flagSet.Parse() 290 | assert.Nil(t, err) 291 | 292 | assert.Equal(t, csStringSlice, StringSlice{value1, value2, value3}) 293 | tearDown(t.Name()) 294 | } 295 | 296 | func TestParseFileCommaSeparatedStringSlice(t *testing.T) { 297 | flagSet := NewFlagSet() 298 | 299 | var csStringSlice StringSlice 300 | flagSet.StringSliceVarP(&csStringSlice, "cs-value", "CSV", []string{}, "Comma Separated Values. Expected usage: -CSV path/to/file", FileCommaSeparatedStringSliceOptions) 301 | 302 | testFile := "test.txt" 303 | value1 := `value1` 304 | value2 := `Value2 "` 305 | value3 := `value3` 306 | 307 | testFileData := `value1 308 | Value2 " 309 | value3` 310 | err := os.WriteFile(testFile, []byte(testFileData), permissionutil.ConfigFilePermission) 311 | require.Nil(t, err, "could not write temporary values file") 312 | defer os.Remove(testFile) 313 | 314 | os.Args = []string{ 315 | os.Args[0], 316 | "-CSV", testFile, 317 | } 318 | 319 | err = flagSet.Parse() 320 | assert.Nil(t, err) 321 | 322 | assert.Equal(t, csStringSlice, StringSlice{value1, value2, value3}) 323 | tearDown(t.Name()) 324 | } 325 | 326 | func TestConfigOnlyDataTypes(t *testing.T) { 327 | flagSet := NewFlagSet() 328 | var data StringSlice 329 | 330 | flagSet.StringSliceVarConfigOnly(&data, "config-only", []string{}, "String slice config only flag example") 331 | 332 | require.Nil(t, flagSet.CommandLine.Lookup("config-only"), "config-only flag should not be registered") 333 | 334 | configFileData := ` 335 | config-only: 336 | - test 337 | - test2 338 | ` 339 | err := os.WriteFile("test.yaml", []byte(configFileData), permissionutil.ConfigFilePermission) 340 | require.Nil(t, err, "could not write temporary config") 341 | defer os.Remove("test.yaml") 342 | 343 | err = flagSet.MergeConfigFile("test.yaml") 344 | require.Nil(t, err, "could not merge temporary config") 345 | 346 | require.Equal(t, StringSlice{"test", "test2"}, data, "could not get correct string slice") 347 | tearDown(t.Name()) 348 | } 349 | 350 | func TestSetDefaultStringSliceValue(t *testing.T) { 351 | var data StringSlice 352 | flagSet := NewFlagSet() 353 | flagSet.StringSliceVar(&data, "test", []string{"A,A,A"}, "Default value for a test flag example", CommaSeparatedStringSliceOptions) 354 | err := flagSet.CommandLine.Parse([]string{"-test", "item1"}) 355 | require.Nil(t, err) 356 | require.Equal(t, StringSlice{"item1"}, data, "could not get correct string slice") 357 | 358 | var data2 StringSlice 359 | flagSet2 := NewFlagSet() 360 | flagSet2.StringSliceVar(&data2, "test", []string{"A"}, "Default value for a test flag example", CommaSeparatedStringSliceOptions) 361 | err = flagSet2.CommandLine.Parse([]string{"-test", "item1,item2"}) 362 | require.Nil(t, err) 363 | require.Equal(t, StringSlice{"item1", "item2"}, data2, "could not get correct string slice") 364 | 365 | var data3 StringSlice 366 | flagSet3 := NewFlagSet() 367 | flagSet3.StringSliceVar(&data3, "test", []string{}, "Default value for a test flag example", CommaSeparatedStringSliceOptions) 368 | err = flagSet3.CommandLine.Parse([]string{"-test", "item1,item2"}) 369 | require.Nil(t, err) 370 | require.Equal(t, StringSlice{"item1", "item2"}, data3, "could not get correct string slice") 371 | 372 | var data4 StringSlice 373 | flagSet4 := NewFlagSet() 374 | flagSet4.StringSliceVar(&data4, "test", nil, "Default value for a test flag example", CommaSeparatedStringSliceOptions) 375 | err = flagSet4.CommandLine.Parse([]string{"-test", "item1,\"item2\""}) 376 | require.Nil(t, err) 377 | require.Equal(t, StringSlice{"item1", "item2"}, data4, "could not get correct string slice") 378 | 379 | tearDown(t.Name()) 380 | } 381 | 382 | func TestCaseSensitiveFlagSet(t *testing.T) { 383 | flagSet := NewFlagSet() 384 | flagSet.CaseSensitive = true 385 | var verbose, keyVal bool 386 | flagSet.CreateGroup("Test", "Test", 387 | flagSet.BoolVarP(&verbose, "verbose", "v", false, "show verbose output"), 388 | flagSet.BoolVarP(&keyVal, "var", "V", false, "custom vars in key=value format"), 389 | ) 390 | output := &bytes.Buffer{} 391 | flagSet.CommandLine.SetOutput(output) 392 | 393 | os.Args = []string{ 394 | os.Args[0], 395 | "-h", 396 | "V", 397 | } 398 | flagSet.usageFunc() 399 | 400 | resultOutput := output.String() 401 | actual := resultOutput[strings.Index(resultOutput, "Flags:\n"):] 402 | expected := "Flags:\n -V, -var custom vars in key=value format\n" 403 | assert.Equal(t, expected, actual) 404 | 405 | output = &bytes.Buffer{} 406 | flagSet.CommandLine.SetOutput(output) 407 | os.Args = []string{ 408 | os.Args[0], 409 | "-h", 410 | "v", 411 | } 412 | flagSet.usageFunc() 413 | 414 | resultOutput = output.String() 415 | actual = resultOutput[strings.Index(resultOutput, "Flags:\n"):] 416 | expected = "Flags:\n -v, -verbose show verbose output\n" 417 | assert.Equal(t, expected, actual) 418 | } 419 | 420 | func tearDown(uniqueValue string) { 421 | flag.CommandLine = flag.NewFlagSet(uniqueValue, flag.ContinueOnError) 422 | flag.CommandLine.Usage = flag.Usage 423 | } 424 | 425 | func TestConfigDirMigration(t *testing.T) { 426 | // remove any args added by previous test 427 | os.Args = []string{ 428 | os.Args[0], 429 | } 430 | // setup test old config dir 431 | createEmptyFilesinDir(t, oldAppConfigDir) 432 | 433 | flagset := NewFlagSet() 434 | flagset.CommandLine = flag.NewFlagSet("goflags", flag.ContinueOnError) 435 | newToolCfgDir := flagset.GetToolConfigDir() 436 | 437 | // cleanup and debug 438 | defer func() { 439 | // cleanup 440 | if t.Failed() { 441 | t.Logf("old config dir: %s", oldAppConfigDir) 442 | t.Logf("new config dir: %s", newToolCfgDir) 443 | cfgFiles, _ := os.ReadDir(oldAppConfigDir) 444 | for _, cfgFile := range cfgFiles { 445 | t.Logf("found config file in old dir : %s", cfgFile.Name()) 446 | } 447 | cfgFiles, _ = os.ReadDir(newToolCfgDir) 448 | for _, cfgFile := range cfgFiles { 449 | t.Logf("found config file in new dir : %s", cfgFile.Name()) 450 | } 451 | } 452 | _ = os.RemoveAll(oldAppConfigDir) 453 | _ = os.RemoveAll(newToolCfgDir) 454 | }() 455 | 456 | // remove new config dir if it already exists from previous test 457 | _ = os.RemoveAll(newToolCfgDir) 458 | 459 | // create test flag and parse it 460 | var testflag string 461 | flagset.CreateGroup("Example", "Example", 462 | flagset.StringVarP(&testflag, "test", "t", "", "test flag"), 463 | ) 464 | 465 | if err := flagset.Parse(); err != nil { 466 | require.Nil(t, err, "could not parse flags") 467 | } 468 | 469 | // oldAppConfigDir and newConfigDir is same in case of linux (unless sandbox or something else is used) 470 | // migration will only happen on windows & macOS (darwin) Ref: https://pkg.go.dev/os#UserConfigDir 471 | if oldAppConfigDir != newToolCfgDir || !osutil.IsLinux() { 472 | // check if config files are moved to new config dir 473 | require.FileExistsf(t, filepath.Join(newToolCfgDir, "config.yaml"), "config file not created in new config dir") 474 | require.FileExistsf(t, filepath.Join(newToolCfgDir, "provider-config.yaml"), "config file not created in new config dir") 475 | } 476 | 477 | tearDown(t.Name()) 478 | } 479 | 480 | func createEmptyFilesinDir(t *testing.T, dirname string) { 481 | if !fileutil.FolderExists(dirname) { 482 | _ = fileutil.CreateFolder(dirname) 483 | } 484 | // create empty yaml config files 485 | err := os.WriteFile(filepath.Join(oldAppConfigDir, "config.yaml"), []byte{}, os.ModePerm) 486 | require.Nil(t, err, "could not create old config file") 487 | err = os.WriteFile(filepath.Join(oldAppConfigDir, "provider-config.yaml"), []byte{}, os.ModePerm) 488 | require.Nil(t, err, "could not create old config file") 489 | } 490 | -------------------------------------------------------------------------------- /insertionorderedmap.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | type InsertionOrderedMap struct { 4 | values map[string]*FlagData 5 | keys []string `yaml:"-"` 6 | } 7 | 8 | func (insertionOrderedMap *InsertionOrderedMap) forEach(fn func(key string, data *FlagData)) { 9 | for _, key := range insertionOrderedMap.keys { 10 | fn(key, insertionOrderedMap.values[key]) 11 | } 12 | } 13 | 14 | func (insertionOrderedMap *InsertionOrderedMap) Set(key string, value *FlagData) { 15 | _, present := insertionOrderedMap.values[key] 16 | insertionOrderedMap.values[key] = value 17 | if !present { 18 | insertionOrderedMap.keys = append(insertionOrderedMap.keys, key) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | folderutil "github.com/projectdiscovery/utils/folder" 9 | ) 10 | 11 | var oldAppConfigDir = filepath.Join(folderutil.HomeDirOrDefault("."), ".config", getToolName()) 12 | 13 | // GetConfigFilePath returns the config file path 14 | func (flagSet *FlagSet) GetConfigFilePath() (string, error) { 15 | // return configFilePath if already set 16 | if flagSet.configFilePath != "" { 17 | return flagSet.configFilePath, nil 18 | } 19 | return filepath.Join(folderutil.AppConfigDirOrDefault(".", getToolName()), "config.yaml"), nil 20 | } 21 | 22 | // GetToolConfigDir returns the config dir path of the tool 23 | func (flagset *FlagSet) GetToolConfigDir() string { 24 | cfgFilePath, _ := flagset.GetConfigFilePath() 25 | return filepath.Dir(cfgFilePath) 26 | } 27 | 28 | // SetConfigFilePath sets custom config file path 29 | func (flagSet *FlagSet) SetConfigFilePath(filePath string) { 30 | flagSet.configFilePath = filePath 31 | } 32 | 33 | // getToolName returns the name of the tool 34 | func getToolName() string { 35 | appName := filepath.Base(os.Args[0]) 36 | return strings.TrimSuffix(appName, filepath.Ext(appName)) 37 | } 38 | -------------------------------------------------------------------------------- /path_test.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFlagSet_SetConfigFilePath(t *testing.T) { 11 | configFilePath := "/tmp/config.yaml" 12 | flagSet := NewFlagSet() 13 | 14 | var stringSlice StringSlice 15 | flagSet.StringSliceVarP(&stringSlice, "header", "H", []string{}, "Header values. Expected usage: -H \"header1\":\"value1\" -H \"header2\":\"value2\"", StringSliceOptions) 16 | os.Args = []string{ 17 | os.Args[0], 18 | } 19 | flagSet.SetConfigFilePath(configFilePath) 20 | 21 | err := flagSet.Parse() 22 | assert.Nil(t, err) 23 | gotFilePath, err := flagSet.GetConfigFilePath() 24 | assert.Nil(t, err) 25 | assert.Equal(t, configFilePath, gotFilePath) 26 | tearDown(t.Name()) 27 | } 28 | -------------------------------------------------------------------------------- /port.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | _ "embed" 10 | 11 | mapsutil "github.com/projectdiscovery/utils/maps" 12 | stringsutil "github.com/projectdiscovery/utils/strings" 13 | "golang.org/x/exp/maps" 14 | ) 15 | 16 | var ( 17 | //go:embed ports_data.json 18 | portsData string 19 | 20 | portOptionDefaultValues map[*Port]map[int]struct{} 21 | servicesMap map[string][]int 22 | ) 23 | 24 | func init() { 25 | err := json.Unmarshal([]byte(portsData), &servicesMap) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | portOptionDefaultValues = make(map[*Port]map[int]struct{}) 31 | } 32 | 33 | // Port is a list of unique ports in a normalized format 34 | type Port struct { 35 | kv map[int]struct{} 36 | } 37 | 38 | func (port Port) String() string { 39 | defaultBuilder := &strings.Builder{} 40 | defaultBuilder.WriteString("(") 41 | 42 | var items string 43 | for k := range port.kv { 44 | items += fmt.Sprintf("%d,", k) 45 | } 46 | defaultBuilder.WriteString(stringsutil.TrimSuffixAny(items, ",", "=")) 47 | defaultBuilder.WriteString(")") 48 | return defaultBuilder.String() 49 | } 50 | 51 | // Set inserts a value to the port map. A number of formats are accepted. 52 | func (port *Port) Set(value string) error { 53 | newKv := make(map[int]struct{}) 54 | port.normalizePortValue(newKv, value) 55 | 56 | // if new values are provided, we remove default ones 57 | if defaultValue, ok := portOptionDefaultValues[port]; ok { 58 | if maps.Equal(port.kv, defaultValue) { 59 | port.kv = make(map[int]struct{}) 60 | } 61 | } 62 | 63 | port.kv = mapsutil.Merge(port.kv, newKv) 64 | 65 | return nil 66 | } 67 | 68 | // AsPorts returns the ports list after normalization 69 | func (port *Port) AsPorts() []int { 70 | if port.kv == nil { 71 | return nil 72 | } 73 | ports := make([]int, 0, len(port.kv)) 74 | for k := range port.kv { 75 | ports = append(ports, k) 76 | } 77 | return ports 78 | } 79 | 80 | // normalizePortValues normalizes and returns a list of ports for a value. 81 | // 82 | // Supported values - 83 | // 84 | // 1,2 => ports: 1, 2 85 | // 1-10 => ports: 1 to 10 86 | // 1- => ports: 1 to 65535 87 | // -/*/full => ports: 1 to 65535 88 | // topxxx => ports: top most xxx common ports 89 | // ftp,http => ports: 21, 80 90 | // ftp* => ports: 20, 21, 574, 989, 990, 8021 91 | // U:53,T:25 => ports: 53 udp, 25 tcp 92 | func (port *Port) normalizePortValue(portsMap map[int]struct{}, value string) { 93 | // Handle top-xxx/*/- cases 94 | switch value { 95 | case "full", "-", "*": 96 | value = portsFull 97 | case "top-100": 98 | value = portsNmapTop100 99 | case "top-1000": 100 | value = portsNmapTop1000 101 | } 102 | 103 | values := strings.Split(value, ",") 104 | for _, item := range values { 105 | if ports, ok := servicesMap[item]; ok { 106 | // Handle ftp,http,etc service names 107 | port.appendPortsToKV(portsMap, ports) 108 | } else if strings.Contains(item, ":") { 109 | // Handle colon : based name like TCP:443 110 | port.parsePortColonSeparated(portsMap, item) 111 | } else if strings.HasSuffix(item, "*") { 112 | // Handle wildcard service names 113 | port.parseWildcardService(portsMap, item) 114 | } else if strings.Contains(item, "-") { 115 | // Handle dash based separated items 116 | port.parsePortDashSeparated(portsMap, item) 117 | } else { 118 | // Handle normal ports 119 | port.parsePortNumberItem(portsMap, item) 120 | } 121 | } 122 | } 123 | 124 | func (port *Port) appendPortsToKV(portsMap map[int]struct{}, ports []int) { 125 | for _, p := range ports { 126 | portsMap[p] = struct{}{} 127 | } 128 | } 129 | 130 | // parseWildcardService parses wildcard based service names 131 | func (port *Port) parseWildcardService(portsMap map[int]struct{}, item string) { 132 | stripped := strings.TrimSuffix(item, "*") 133 | for service, ports := range servicesMap { 134 | if strings.HasPrefix(service, stripped) { 135 | port.appendPortsToKV(portsMap, ports) 136 | } 137 | } 138 | } 139 | 140 | // parsePortDashSeparated parses dash separated ports 141 | func (port *Port) parsePortDashSeparated(portsMap map[int]struct{}, item string) { 142 | parts := strings.Split(item, "-") 143 | // Handle x- scenarios 144 | if len(parts) == 2 && parts[1] == "" { 145 | port.parsePortPairItems(portsMap, parts[0], "65535") 146 | } 147 | // Handle x-x port pairs 148 | if len(parts) == 2 { 149 | port.parsePortPairItems(portsMap, parts[0], parts[1]) 150 | } 151 | } 152 | 153 | // parsePortColonSeparated parses colon separated ports 154 | func (port *Port) parsePortColonSeparated(portsMap map[int]struct{}, item string) { 155 | items := strings.Split(item, ":") 156 | if len(items) == 2 { 157 | parsed, err := strconv.Atoi(items[1]) 158 | if err == nil && parsed > 0 { 159 | portsMap[parsed] = struct{}{} 160 | } 161 | } 162 | } 163 | 164 | // parsePortNumberItem parses a single port number 165 | func (port *Port) parsePortNumberItem(portsMap map[int]struct{}, item string) { 166 | parsed, err := strconv.Atoi(item) 167 | if err == nil && parsed > 0 { 168 | portsMap[parsed] = struct{}{} 169 | } 170 | } 171 | 172 | // parsePortPairItems parses port x-x pair items 173 | func (port *Port) parsePortPairItems(portsMap map[int]struct{}, first, second string) { 174 | firstParsed, err := strconv.Atoi(first) 175 | if err != nil { 176 | return 177 | } 178 | secondParsed, err := strconv.Atoi(second) 179 | if err != nil { 180 | return 181 | } 182 | for i := firstParsed; i <= secondParsed; i++ { 183 | portsMap[i] = struct{}{} 184 | } 185 | } 186 | 187 | const ( 188 | portsFull = "1-65535" 189 | portsNmapTop100 = "7,9,13,21-23,25-26,37,53,79-81,88,106,110-111,113,119,135,139,143-144,179,199,389,427,443-445,465,513-515,543-544,548,554,587,631,646,873,990,993,995,1025-1029,1110,1433,1720,1723,1755,1900,2000-2001,2049,2121,2717,3000,3128,3306,3389,3986,4899,5000,5009,5051,5060,5101,5190,5357,5432,5631,5666,5800,5900,6000-6001,6646,7070,8000,8008-8009,8080-8081,8443,8888,9100,9999-10000,32768,49152-49157" 190 | portsNmapTop1000 = "1,3-4,6-7,9,13,17,19-26,30,32-33,37,42-43,49,53,70,79-85,88-90,99-100,106,109-111,113,119,125,135,139,143-144,146,161,163,179,199,211-212,222,254-256,259,264,280,301,306,311,340,366,389,406-407,416-417,425,427,443-445,458,464-465,481,497,500,512-515,524,541,543-545,548,554-555,563,587,593,616-617,625,631,636,646,648,666-668,683,687,691,700,705,711,714,720,722,726,749,765,777,783,787,800-801,808,843,873,880,888,898,900-903,911-912,981,987,990,992-993,995,999-1002,1007,1009-1011,1021-1100,1102,1104-1108,1110-1114,1117,1119,1121-1124,1126,1130-1132,1137-1138,1141,1145,1147-1149,1151-1152,1154,1163-1166,1169,1174-1175,1183,1185-1187,1192,1198-1199,1201,1213,1216-1218,1233-1234,1236,1244,1247-1248,1259,1271-1272,1277,1287,1296,1300-1301,1309-1311,1322,1328,1334,1352,1417,1433-1434,1443,1455,1461,1494,1500-1501,1503,1521,1524,1533,1556,1580,1583,1594,1600,1641,1658,1666,1687-1688,1700,1717-1721,1723,1755,1761,1782-1783,1801,1805,1812,1839-1840,1862-1864,1875,1900,1914,1935,1947,1971-1972,1974,1984,1998-2010,2013,2020-2022,2030,2033-2035,2038,2040-2043,2045-2049,2065,2068,2099-2100,2103,2105-2107,2111,2119,2121,2126,2135,2144,2160-2161,2170,2179,2190-2191,2196,2200,2222,2251,2260,2288,2301,2323,2366,2381-2383,2393-2394,2399,2401,2492,2500,2522,2525,2557,2601-2602,2604-2605,2607-2608,2638,2701-2702,2710,2717-2718,2725,2800,2809,2811,2869,2875,2909-2910,2920,2967-2968,2998,3000-3001,3003,3005-3007,3011,3013,3017,3030-3031,3052,3071,3077,3128,3168,3211,3221,3260-3261,3268-3269,3283,3300-3301,3306,3322-3325,3333,3351,3367,3369-3372,3389-3390,3404,3476,3493,3517,3527,3546,3551,3580,3659,3689-3690,3703,3737,3766,3784,3800-3801,3809,3814,3826-3828,3851,3869,3871,3878,3880,3889,3905,3914,3918,3920,3945,3971,3986,3995,3998,4000-4006,4045,4111,4125-4126,4129,4224,4242,4279,4321,4343,4443-4446,4449,4550,4567,4662,4848,4899-4900,4998,5000-5004,5009,5030,5033,5050-5051,5054,5060-5061,5080,5087,5100-5102,5120,5190,5200,5214,5221-5222,5225-5226,5269,5280,5298,5357,5405,5414,5431-5432,5440,5500,5510,5544,5550,5555,5560,5566,5631,5633,5666,5678-5679,5718,5730,5800-5802,5810-5811,5815,5822,5825,5850,5859,5862,5877,5900-5904,5906-5907,5910-5911,5915,5922,5925,5950,5952,5959-5963,5987-5989,5998-6007,6009,6025,6059,6100-6101,6106,6112,6123,6129,6156,6346,6389,6502,6510,6543,6547,6565-6567,6580,6646,6666-6669,6689,6692,6699,6779,6788-6789,6792,6839,6881,6901,6969,7000-7002,7004,7007,7019,7025,7070,7100,7103,7106,7200-7201,7402,7435,7443,7496,7512,7625,7627,7676,7741,7777-7778,7800,7911,7920-7921,7937-7938,7999-8002,8007-8011,8021-8022,8031,8042,8045,8080-8090,8093,8099-8100,8180-8181,8192-8194,8200,8222,8254,8290-8292,8300,8333,8383,8400,8402,8443,8500,8600,8649,8651-8652,8654,8701,8800,8873,8888,8899,8994,9000-9003,9009-9011,9040,9050,9071,9080-9081,9090-9091,9099-9103,9110-9111,9200,9207,9220,9290,9415,9418,9485,9500,9502-9503,9535,9575,9593-9595,9618,9666,9876-9878,9898,9900,9917,9929,9943-9944,9968,9998-10004,10009-10010,10012,10024-10025,10082,10180,10215,10243,10566,10616-10617,10621,10626,10628-10629,10778,11110-11111,11967,12000,12174,12265,12345,13456,13722,13782-13783,14000,14238,14441-14442,15000,15002-15004,15660,15742,16000-16001,16012,16016,16018,16080,16113,16992-16993,17877,17988,18040,18101,18988,19101,19283,19315,19350,19780,19801,19842,20000,20005,20031,20221-20222,20828,21571,22939,23502,24444,24800,25734-25735,26214,27000,27352-27353,27355-27356,27715,28201,30000,30718,30951,31038,31337,32768-32785,33354,33899,34571-34573,35500,38292,40193,40911,41511,42510,44176,44442-44443,44501,45100,48080,49152-49161,49163,49165,49167,49175-49176,49400,49999-50003,50006,50300,50389,50500,50636,50800,51103,51493,52673,52822,52848,52869,54045,54328,55055-55056,55555,55600,56737-56738,57294,57797,58080,60020,60443,61532,61900,62078,63331,64623,64680,65000,65129,65389" 191 | ) 192 | -------------------------------------------------------------------------------- /port_test.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestPortType(t *testing.T) { 10 | port := &Port{} 11 | _ = port.Set("21-25,80,TCP:443") 12 | require.ElementsMatch(t, port.AsPorts(), []int{21, 22, 23, 24, 25, 80, 443}, "could not get correct ports") 13 | 14 | t.Run("comma-separated", func(t *testing.T) { 15 | port := &Port{} 16 | _ = port.Set("80,443") 17 | require.ElementsMatch(t, port.AsPorts(), []int{80, 443}, "could not get correct ports") 18 | }) 19 | t.Run("dash", func(t *testing.T) { 20 | port := &Port{} 21 | _ = port.Set("21-25") 22 | require.ElementsMatch(t, port.AsPorts(), []int{21, 22, 23, 24, 25}, "could not get correct ports") 23 | }) 24 | t.Run("dash-suffix", func(t *testing.T) { 25 | port := &Port{} 26 | _ = port.Set("1-") 27 | require.Len(t, port.AsPorts(), 65535, "could not get correct ports") 28 | }) 29 | t.Run("full", func(t *testing.T) { 30 | port := &Port{} 31 | _ = port.Set("full") 32 | require.Len(t, port.AsPorts(), 65535, "could not get correct ports") 33 | 34 | port = &Port{} 35 | _ = port.Set("*") 36 | require.Len(t, port.AsPorts(), 65535, "could not get correct ports") 37 | }) 38 | t.Run("top-xxx", func(t *testing.T) { 39 | port := &Port{} 40 | _ = port.Set("top-100") 41 | require.Len(t, port.AsPorts(), 100, "could not get correct ports") 42 | 43 | port = &Port{} 44 | _ = port.Set("top-1000") 45 | require.Len(t, port.AsPorts(), 1000, "could not get correct ports") 46 | }) 47 | t.Run("services", func(t *testing.T) { 48 | port := &Port{} 49 | _ = port.Set("http,ftp") 50 | require.ElementsMatch(t, port.AsPorts(), []int{80, 8008, 21}, "could not get correct ports") 51 | }) 52 | t.Run("services-wildcard", func(t *testing.T) { 53 | port := &Port{} 54 | _ = port.Set("ftp*") 55 | require.ElementsMatch(t, port.AsPorts(), []int{21, 20, 989, 990, 574, 8021}, "could not get correct ports") 56 | }) 57 | t.Run("colon", func(t *testing.T) { 58 | port := &Port{} 59 | _ = port.Set("TCP:443,UDP:53") 60 | require.ElementsMatch(t, port.AsPorts(), []int{443, 53}, "could not get correct ports") 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /ratelimit_var.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | "unicode" 10 | 11 | stringsutil "github.com/projectdiscovery/utils/strings" 12 | timeutil "github.com/projectdiscovery/utils/time" 13 | ) 14 | 15 | var ( 16 | MaxRateLimitTime = time.Minute // anything above time.Minute is not practical (for our use case) 17 | rateLimitOptionMap map[*RateLimitMap]Options 18 | ) 19 | 20 | func init() { 21 | rateLimitOptionMap = make(map[*RateLimitMap]Options) 22 | } 23 | 24 | type RateLimit struct { 25 | MaxCount uint 26 | Duration time.Duration 27 | } 28 | 29 | type RateLimitMap struct { 30 | kv map[string]RateLimit 31 | } 32 | 33 | // Set inserts a value to the map. Format: key=value 34 | func (rateLimitMap *RateLimitMap) Set(value string) error { 35 | if rateLimitMap.kv == nil { 36 | rateLimitMap.kv = make(map[string]RateLimit) 37 | } 38 | 39 | option, ok := rateLimitOptionMap[rateLimitMap] 40 | if !ok { 41 | option = StringSliceOptions 42 | } 43 | rateLimits, err := ToStringSlice(value, option) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | for _, rateLimit := range rateLimits { 49 | var k, v string 50 | if idxSep := strings.Index(rateLimit, kvSep); idxSep > 0 { 51 | k = rateLimit[:idxSep] 52 | v = rateLimit[idxSep+1:] 53 | } 54 | 55 | // note: 56 | // - inserting multiple times the same key will override the previous v 57 | // - empty string is legitimate rateLimit 58 | if k != "" { 59 | rateLimit, err := parseRateLimit(v) 60 | if err != nil { 61 | return err 62 | } 63 | rateLimitMap.kv[k] = rateLimit 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | // Del removes the specified key 70 | func (rateLimitMap *RateLimitMap) Del(key string) error { 71 | if rateLimitMap.kv == nil { 72 | return errors.New("empty runtime map") 73 | } 74 | delete(rateLimitMap.kv, key) 75 | return nil 76 | } 77 | 78 | // IsEmpty specifies if the underlying map is empty 79 | func (rateLimitMap *RateLimitMap) IsEmpty() bool { 80 | return len(rateLimitMap.kv) == 0 81 | } 82 | 83 | // AsMap returns the internal map as reference - changes are allowed 84 | func (rateLimitMap *RateLimitMap) AsMap() map[string]RateLimit { 85 | return rateLimitMap.kv 86 | } 87 | 88 | func (rateLimitMap RateLimitMap) String() string { 89 | defaultBuilder := &strings.Builder{} 90 | defaultBuilder.WriteString("{") 91 | 92 | var items string 93 | for k, v := range rateLimitMap.kv { 94 | items += fmt.Sprintf("\"%s\":\"%d/%s\",", k, v.MaxCount, v.Duration.String()) 95 | } 96 | defaultBuilder.WriteString(stringsutil.TrimSuffixAny(items, ",", ":")) 97 | defaultBuilder.WriteString("}") 98 | return defaultBuilder.String() 99 | } 100 | 101 | // RateLimitMapVar adds a ratelimit flag with a longname 102 | func (flagSet *FlagSet) RateLimitMapVar(field *RateLimitMap, long string, defaultValue []string, usage string, options Options) *FlagData { 103 | return flagSet.RateLimitMapVarP(field, long, "", defaultValue, usage, options) 104 | } 105 | 106 | // RateLimitMapVarP adds a ratelimit flag with a short name and long name. 107 | // It is equivalent to RateLimitMapVar, and also allows specifying ratelimits in days (e.g., "hackertarget=2/d" 2 requests per day, which is equivalent to 24h). 108 | func (flagSet *FlagSet) RateLimitMapVarP(field *RateLimitMap, long, short string, defaultValue StringSlice, usage string, options Options) *FlagData { 109 | if field == nil { 110 | panic(fmt.Errorf("field cannot be nil for flag -%v", long)) 111 | } 112 | 113 | rateLimitOptionMap[field] = options 114 | for _, defaultItem := range defaultValue { 115 | values, _ := ToStringSlice(defaultItem, options) 116 | for _, value := range values { 117 | if err := field.Set(value); err != nil { 118 | panic(fmt.Errorf("failed to set default value for flag -%v: %v", long, err)) 119 | } 120 | } 121 | } 122 | 123 | flagData := &FlagData{ 124 | usage: usage, 125 | long: long, 126 | defaultValue: defaultValue, 127 | skipMarshal: true, 128 | } 129 | if short != "" { 130 | flagData.short = short 131 | flagSet.CommandLine.Var(field, short, usage) 132 | flagSet.flagKeys.Set(short, flagData) 133 | } 134 | flagSet.CommandLine.Var(field, long, usage) 135 | flagSet.flagKeys.Set(long, flagData) 136 | return flagData 137 | } 138 | 139 | func parseRateLimit(s string) (RateLimit, error) { 140 | sArr := strings.Split(s, "/") 141 | 142 | if len(sArr) < 2 { 143 | return RateLimit{}, errors.New("parse error: expected format k=v/d (e.g., scanme.sh=10/s got " + s) 144 | } 145 | 146 | maxCount, err := strconv.ParseUint(sArr[0], 10, 64) 147 | if err != nil { 148 | return RateLimit{}, errors.New("parse error: " + err.Error()) 149 | } 150 | timeValue := sArr[1] 151 | if len(timeValue) > 0 { 152 | // check if time is given ex: 1s 153 | // if given value is just s (add prefix 1) 154 | firstChar := timeValue[0] 155 | if !unicode.IsDigit(rune(firstChar)) { 156 | timeValue = "1" + timeValue 157 | } 158 | } 159 | 160 | duration, err := timeutil.ParseDuration(timeValue) 161 | if err != nil { 162 | return RateLimit{}, errors.New("parse error: " + err.Error()) 163 | } 164 | 165 | if MaxRateLimitTime < duration { 166 | return RateLimit{}, fmt.Errorf("duration cannot be more than %v but got %v", MaxRateLimitTime, duration) 167 | } 168 | 169 | return RateLimit{MaxCount: uint(maxCount), Duration: duration}, nil 170 | } 171 | -------------------------------------------------------------------------------- /ratelimit_var_test.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRateLimitMapVar(t *testing.T) { 12 | 13 | t.Run("default-value", func(t *testing.T) { 14 | var rateLimitMap RateLimitMap 15 | flagSet := NewFlagSet() 16 | flagSet.CreateGroup("Config", "Config", 17 | flagSet.RateLimitMapVarP(&rateLimitMap, "rate-limits", "rls", []string{"hackertarget=1/ms"}, "rate limits", CommaSeparatedStringSliceOptions), 18 | ) 19 | os.Args = []string{ 20 | os.Args[0], 21 | } 22 | err := flagSet.Parse() 23 | assert.Nil(t, err) 24 | assert.Equal(t, RateLimit{MaxCount: 1, Duration: time.Millisecond}, rateLimitMap.AsMap()["hackertarget"]) 25 | tearDown(t.Name()) 26 | }) 27 | 28 | t.Run("multiple-default-value", func(t *testing.T) { 29 | var rateLimitMap RateLimitMap 30 | flagSet := NewFlagSet() 31 | flagSet.CreateGroup("Config", "Config", 32 | flagSet.RateLimitMapVarP(&rateLimitMap, "rate-limits", "rls", []string{"hackertarget=1/s,github=1/ms"}, "rate limits", CommaSeparatedStringSliceOptions), 33 | ) 34 | os.Args = []string{ 35 | os.Args[0], 36 | } 37 | err := flagSet.Parse() 38 | assert.Nil(t, err) 39 | assert.Equal(t, RateLimit{MaxCount: 1, Duration: time.Second}, rateLimitMap.AsMap()["hackertarget"]) 40 | assert.Equal(t, RateLimit{MaxCount: 1, Duration: time.Millisecond}, rateLimitMap.AsMap()["github"]) 41 | tearDown(t.Name()) 42 | }) 43 | 44 | t.Run("valid-rate-limit", func(t *testing.T) { 45 | var rateLimitMap RateLimitMap 46 | flagSet := NewFlagSet() 47 | flagSet.CreateGroup("Config", "Config", 48 | flagSet.RateLimitMapVarP(&rateLimitMap, "rate-limits", "rls", nil, "rate limits", CommaSeparatedStringSliceOptions), 49 | ) 50 | os.Args = []string{ 51 | os.Args[0], 52 | "-rls", "hackertarget=10/m", 53 | } 54 | err := flagSet.Parse() 55 | assert.Nil(t, err) 56 | assert.Equal(t, RateLimit{MaxCount: 10, Duration: time.Minute}, rateLimitMap.AsMap()["hackertarget"]) 57 | 58 | tearDown(t.Name()) 59 | }) 60 | 61 | t.Run("valid-rate-limits", func(t *testing.T) { 62 | var rateLimitMap RateLimitMap 63 | flagSet := NewFlagSet() 64 | flagSet.CreateGroup("Config", "Config", 65 | flagSet.RateLimitMapVarP(&rateLimitMap, "rate-limits", "rls", nil, "rate limits", CommaSeparatedStringSliceOptions), 66 | ) 67 | os.Args = []string{ 68 | os.Args[0], 69 | "-rls", "hackertarget=1/s,github=1/ms", 70 | } 71 | err := flagSet.Parse() 72 | assert.Nil(t, err) 73 | assert.Equal(t, RateLimit{MaxCount: 1, Duration: time.Second}, rateLimitMap.AsMap()["hackertarget"]) 74 | assert.Equal(t, RateLimit{MaxCount: 1, Duration: time.Millisecond}, rateLimitMap.AsMap()["github"]) 75 | tearDown(t.Name()) 76 | }) 77 | 78 | t.Run("without-unit", func(t *testing.T) { 79 | var rateLimitMap RateLimitMap 80 | err := rateLimitMap.Set("hackertarget=1") 81 | assert.NotNil(t, err) 82 | assert.ErrorContains(t, err, "parse error") 83 | tearDown(t.Name()) 84 | }) 85 | 86 | t.Run("invalid-unit", func(t *testing.T) { 87 | var rateLimitMap RateLimitMap 88 | err := rateLimitMap.Set("hackertarget=1/x") 89 | assert.NotNil(t, err) 90 | assert.ErrorContains(t, err, "parse error") 91 | tearDown(t.Name()) 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /runtime_map.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | fileutil "github.com/projectdiscovery/utils/file" 11 | stringsutil "github.com/projectdiscovery/utils/strings" 12 | ) 13 | 14 | const ( 15 | kvSep = "=" 16 | ) 17 | 18 | // RuntimeMap is a runtime only map of interfaces 19 | type RuntimeMap struct { 20 | kv map[string]interface{} 21 | } 22 | 23 | func (runtimeMap RuntimeMap) String() string { 24 | defaultBuilder := &strings.Builder{} 25 | defaultBuilder.WriteString("{") 26 | 27 | var items string 28 | for k, v := range runtimeMap.kv { 29 | items += fmt.Sprintf("\"%s\"=\"%s\"%s", k, v, kvSep) 30 | } 31 | defaultBuilder.WriteString(stringsutil.TrimSuffixAny(items, ",", "=")) 32 | defaultBuilder.WriteString("}") 33 | return defaultBuilder.String() 34 | } 35 | 36 | // Set inserts a value to the map. Format: key=value 37 | func (runtimeMap *RuntimeMap) Set(value string) error { 38 | if runtimeMap.kv == nil { 39 | runtimeMap.kv = make(map[string]interface{}) 40 | } 41 | var k, v string 42 | if idxSep := strings.Index(value, kvSep); idxSep > 0 { 43 | k = value[:idxSep] 44 | v = value[idxSep+1:] 45 | } else { 46 | // this could be a file if so check and load it 47 | if fileutil.FileExists(value) { 48 | f, err := os.Open(value) 49 | if err != nil { 50 | return err 51 | } 52 | defer f.Close() 53 | scanner := bufio.NewScanner(f) 54 | for scanner.Scan() { 55 | text := scanner.Text() 56 | if idxSep := strings.Index(text, kvSep); idxSep > 0 { 57 | runtimeMap.kv[text[:idxSep]] = text[idxSep+1:] 58 | } 59 | } 60 | if err := scanner.Err(); err != nil { 61 | return err 62 | } 63 | } 64 | } 65 | // note: 66 | // - inserting multiple times the same key will override the previous value 67 | // - empty string is legitimate value 68 | if k != "" { 69 | runtimeMap.kv[k] = v 70 | } 71 | return nil 72 | } 73 | 74 | // Del removes the specified key 75 | func (runtimeMap *RuntimeMap) Del(key string) error { 76 | if runtimeMap.kv == nil { 77 | return errors.New("empty runtime map") 78 | } 79 | delete(runtimeMap.kv, key) 80 | return nil 81 | } 82 | 83 | // IsEmpty specifies if the underlying map is empty 84 | func (runtimeMap *RuntimeMap) IsEmpty() bool { 85 | return len(runtimeMap.kv) == 0 86 | } 87 | 88 | // AsMap returns the internal map as reference - changes are allowed 89 | func (runtimeMap *RuntimeMap) AsMap() map[string]interface{} { 90 | return runtimeMap.kv 91 | } 92 | -------------------------------------------------------------------------------- /runtime_map_test.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestRuntimeMap(t *testing.T) { 12 | data := &RuntimeMap{} 13 | err := data.Set("variable=value") 14 | require.NoError(t, err, "could not set key-value") 15 | 16 | returned := data.AsMap()["variable"] 17 | require.Equal(t, "value", returned, "could not get correct return") 18 | 19 | t.Run("file", func(t *testing.T) { 20 | sb := &strings.Builder{} 21 | sb.WriteString("variable=value\n") 22 | sb.WriteString("variable2=value2\n") 23 | tempFile, err := os.CreateTemp(t.TempDir(), "test") 24 | require.NoError(t, err, "could not create temp file") 25 | defer tempFile.Close() 26 | _, err = tempFile.WriteString(sb.String()) 27 | require.NoError(t, err, "could not write to temp file") 28 | data2 := &RuntimeMap{} 29 | err = data2.Set(tempFile.Name()) 30 | require.NoError(t, err, "could not set key-value") 31 | require.Equal(t, 2, len(data2.AsMap()), "could not get correct number of key-values") 32 | require.Equal(t, "value", data2.AsMap()["variable"], "could not get correct value") 33 | require.Equal(t, "value2", data2.AsMap()["variable2"], "could not get correct value") 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /size_var.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | fileutil "github.com/projectdiscovery/utils/file" 8 | ) 9 | 10 | type Size int 11 | 12 | func (s *Size) Set(size string) error { 13 | sizeInBytes, err := fileutil.FileSizeToByteLen(size) 14 | if err != nil { 15 | return err 16 | } 17 | *s = Size(sizeInBytes) 18 | return nil 19 | } 20 | 21 | func (s *Size) String() string { 22 | return strconv.Itoa(int(*s)) 23 | } 24 | 25 | // SizeVar converts the given fileSize with a unit (kb, mb, gb, or tb) to bytes. 26 | // For example, '2kb' will be converted to 2048. 27 | // If no unit is provided, it will fallback to mb. e.g: '2' will be converted to 2097152. 28 | func (flagSet *FlagSet) SizeVar(field *Size, long string, defaultValue string, usage string) *FlagData { 29 | return flagSet.SizeVarP(field, long, "", defaultValue, usage) 30 | } 31 | 32 | // SizeVarP converts the given fileSize with a unit (kb, mb, gb, or tb) to bytes. 33 | // For example, '2kb' will be converted to 2048. 34 | // If no unit is provided, it will fallback to mb. e.g: '2' will be converted to 2097152. 35 | func (flagSet *FlagSet) SizeVarP(field *Size, long, short string, defaultValue string, usage string) *FlagData { 36 | if field == nil { 37 | panic(fmt.Errorf("field cannot be nil for flag -%v", long)) 38 | } 39 | if defaultValue != "" { 40 | if err := field.Set(defaultValue); err != nil { 41 | panic(fmt.Errorf("failed to set default value for flag -%v: %v", long, err)) 42 | } 43 | } 44 | flagData := &FlagData{ 45 | usage: usage, 46 | long: long, 47 | defaultValue: defaultValue, 48 | } 49 | if short != "" { 50 | flagData.short = short 51 | flagSet.CommandLine.Var(field, short, usage) 52 | flagSet.flagKeys.Set(short, flagData) 53 | } 54 | flagSet.CommandLine.Var(field, long, usage) 55 | flagSet.flagKeys.Set(long, flagData) 56 | return flagData 57 | } 58 | -------------------------------------------------------------------------------- /size_var_test.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSizeVar(t *testing.T) { 11 | t.Run("valid-size", func(t *testing.T) { 12 | var fileSize Size 13 | flagSet := NewFlagSet() 14 | flagSet.CreateGroup("Config", "Config", 15 | flagSet.SizeVarP(&fileSize, "max-size", "ms", "", "max size of the file"), 16 | ) 17 | os.Args = []string{ 18 | os.Args[0], 19 | "-max-size", "2kb", 20 | } 21 | err := flagSet.Parse() 22 | assert.Nil(t, err) 23 | assert.Equal(t, Size(2048), fileSize) 24 | tearDown(t.Name()) 25 | }) 26 | 27 | t.Run("default-value", func(t *testing.T) { 28 | var fileSize Size 29 | flagSet := NewFlagSet() 30 | flagSet.CreateGroup("Config", "Config", 31 | flagSet.SizeVarP(&fileSize, "max-size", "ms", "2kb", "max size of the file"), 32 | ) 33 | os.Args = []string{ 34 | os.Args[0], 35 | } 36 | err := flagSet.Parse() 37 | assert.Nil(t, err) 38 | assert.Equal(t, Size(2048), fileSize) 39 | tearDown(t.Name()) 40 | }) 41 | 42 | t.Run("without-unit", func(t *testing.T) { 43 | var fileSize Size 44 | err := fileSize.Set("2") 45 | assert.Nil(t, err) 46 | assert.Equal(t, Size(2097152), fileSize) 47 | tearDown(t.Name()) 48 | }) 49 | 50 | t.Run("invalid-size-unit", func(t *testing.T) { 51 | var fileSize Size 52 | err := fileSize.Set("2kilobytes") 53 | assert.NotNil(t, err) 54 | assert.ErrorContains(t, err, "parse error") 55 | tearDown(t.Name()) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /slice_common.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/pkg/errors" 7 | fileutil "github.com/projectdiscovery/utils/file" 8 | stringsutil "github.com/projectdiscovery/utils/strings" 9 | ) 10 | 11 | var quotes = []rune{'"', '\'', '`'} 12 | 13 | func isQuote(char rune) (bool, rune) { 14 | for _, quote := range quotes { 15 | if quote == char { 16 | return true, quote 17 | } 18 | } 19 | return false, 0 20 | } 21 | 22 | func searchPart(value string, stop rune) (bool, string) { 23 | var result string 24 | for _, char := range value { 25 | if char != stop { 26 | result += string(char) 27 | } else { 28 | return true, result 29 | } 30 | } 31 | return false, result 32 | } 33 | 34 | func ToString(slice []string) string { 35 | defaultBuilder := &strings.Builder{} 36 | defaultBuilder.WriteString("[") 37 | for i, k := range slice { 38 | defaultBuilder.WriteString("\"") 39 | defaultBuilder.WriteString(k) 40 | defaultBuilder.WriteString("\"") 41 | if i != len(slice)-1 { 42 | defaultBuilder.WriteString(", ") 43 | } 44 | } 45 | defaultBuilder.WriteString("]") 46 | return defaultBuilder.String() 47 | } 48 | 49 | type Options struct { 50 | // IsFromFile determines if the values are from file 51 | IsFromFile func(string) bool 52 | // IsEmpty determines if the values are empty 53 | IsEmpty func(string) bool 54 | // Normalize the value (eg. removing trailing spaces) 55 | Normalize func(string) string 56 | // IsRaw determines if the value should be considered as a raw string 57 | IsRaw func(string) bool 58 | } 59 | 60 | // ToStringSlice converts a value to string slice based on options 61 | func ToStringSlice(value string, options Options) ([]string, error) { 62 | var result []string 63 | if options.IsEmpty == nil && options.IsFromFile == nil && options.Normalize == nil { 64 | return []string{value}, nil 65 | } 66 | 67 | addPartToResult := func(part string) { 68 | if options.Normalize != nil { 69 | part = options.Normalize(part) 70 | } 71 | if !options.IsEmpty(part) { 72 | result = append(result, part) 73 | } 74 | } 75 | if fileutil.FileExists(value) && options.IsFromFile != nil && options.IsFromFile(value) { 76 | linesChan, err := fileutil.ReadFile(value) 77 | if err != nil { 78 | return nil, err 79 | } 80 | for line := range linesChan { 81 | addPartToResult(line) 82 | } 83 | } else if options.IsRaw != nil && options.IsRaw(value) { 84 | addPartToResult(value) 85 | } else { 86 | index := 0 87 | for index < len(value) { 88 | char := rune(value[index]) 89 | if isQuote, quote := isQuote(char); isQuote { 90 | quoteFound, part := searchPart(value[index+1:], quote) 91 | 92 | if !quoteFound { 93 | return nil, errors.New("Unclosed quote in path") 94 | } 95 | 96 | index += len(part) + 2 97 | 98 | addPartToResult(part) 99 | } else { 100 | commaFound, part := searchPart(value[index:], ',') 101 | 102 | if commaFound { 103 | index += len(part) + 1 104 | } else { 105 | index += len(part) 106 | } 107 | 108 | addPartToResult(part) 109 | } 110 | } 111 | } 112 | return result, nil 113 | } 114 | 115 | func isEmpty(s string) bool { 116 | return strings.TrimSpace(s) == "" 117 | } 118 | 119 | func isFromFile(_ string) bool { 120 | return true 121 | } 122 | 123 | func normalizeTrailingParts(s string) string { 124 | return stringsutil.NormalizeWithOptions(s, 125 | stringsutil.NormalizeOptions{ 126 | StripComments: true, 127 | TrimSpaces: true, 128 | }, 129 | ) 130 | } 131 | 132 | func normalize(s string) string { 133 | return stringsutil.NormalizeWithOptions(s, 134 | stringsutil.NormalizeOptions{ 135 | StripComments: true, 136 | TrimCutset: string(quotes), 137 | TrimSpaces: true, 138 | }, 139 | ) 140 | } 141 | 142 | func normalizeLowercase(s string) string { 143 | return stringsutil.NormalizeWithOptions(s, 144 | stringsutil.NormalizeOptions{ 145 | StripComments: true, 146 | TrimCutset: string(quotes), 147 | TrimSpaces: true, 148 | Lowercase: true, 149 | }, 150 | ) 151 | } 152 | -------------------------------------------------------------------------------- /string_slice.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | sliceutil "github.com/projectdiscovery/utils/slice" 5 | ) 6 | 7 | var ( 8 | optionMap map[*StringSlice]Options 9 | optionDefaultValues map[*StringSlice][]string 10 | ) 11 | 12 | func init() { 13 | optionMap = make(map[*StringSlice]Options) 14 | optionDefaultValues = make(map[*StringSlice][]string) 15 | } 16 | 17 | // StringSlice is a slice of strings 18 | type StringSlice []string 19 | 20 | // Set appends a value to the string slice. 21 | func (stringSlice *StringSlice) Set(value string) error { 22 | option, ok := optionMap[stringSlice] 23 | if !ok { 24 | option = StringSliceOptions 25 | } 26 | values, err := ToStringSlice(value, option) 27 | if err != nil { 28 | return err 29 | } 30 | // if new values are provided, we remove default ones 31 | if defaultValue, ok := optionDefaultValues[stringSlice]; ok { 32 | if sliceutil.Equal(*stringSlice, defaultValue) { 33 | *stringSlice = []string{} 34 | } 35 | } 36 | 37 | *stringSlice = append(*stringSlice, values...) 38 | return nil 39 | } 40 | 41 | func (stringSlice StringSlice) String() string { 42 | return ToString(stringSlice) 43 | } 44 | -------------------------------------------------------------------------------- /string_slice_options.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | // StringSliceOptions represents the default string slice (list of items) 4 | // Tokenization: None 5 | // Normalization: None 6 | // Type: []string 7 | // Example: -flag value1 -flag value2 => {"value1", "value2"} 8 | var StringSliceOptions = Options{} 9 | 10 | // CommaSeparatedStringSliceOptions represents a list of comma separated items 11 | // Tokenization: Comma 12 | // Normalization: None 13 | // Type: []string 14 | // Example: -flag value1,value2 => {"value1", "value2"} 15 | var CommaSeparatedStringSliceOptions = Options{ 16 | IsEmpty: isEmpty, 17 | } 18 | 19 | // FileCommaSeparatedStringSliceOptions represents a list of comma separated files containing items 20 | // Tokenization: Comma 21 | // Normalization: None 22 | // Type: []string 23 | // test.txt content: 24 | // value1 25 | // value2 26 | // 27 | // Example: -flag test.txt => {"value1", "value2"} 28 | var FileCommaSeparatedStringSliceOptions = Options{ 29 | IsEmpty: isEmpty, 30 | IsFromFile: isFromFile, 31 | } 32 | 33 | // NormalizedOriginalStringSliceOptions represents a list of items 34 | // Tokenization: None 35 | // Normalization: Standard 36 | // Type: []string 37 | // Example: -flag /value/1 -flag 'value2' => {"/value/1", "value2"} 38 | var NormalizedOriginalStringSliceOptions = Options{ 39 | IsEmpty: isEmpty, 40 | Normalize: normalize, 41 | } 42 | 43 | // FileNormalizedStringSliceOptions represents a list of path items 44 | // Tokenization: Comma 45 | // Normalization: Standard 46 | // Type: []string 47 | // Example: -flag /value/1 -flag value2 => {"/value/1", "value2"} 48 | var FileNormalizedStringSliceOptions = Options{ 49 | IsEmpty: isEmpty, 50 | Normalize: normalizeLowercase, 51 | IsFromFile: isFromFile, 52 | } 53 | 54 | // FileStringSliceOptions represents a list of items stored in a file 55 | // Tokenization: Standard 56 | // Normalization: Standard 57 | var FileStringSliceOptions = Options{ 58 | IsEmpty: isEmpty, 59 | Normalize: normalizeTrailingParts, 60 | IsFromFile: isFromFile, 61 | IsRaw: func(s string) bool { return true }, 62 | } 63 | 64 | // NormalizedStringSliceOptions represents a list of items 65 | // Tokenization: Comma 66 | // Normalization: Standard 67 | var NormalizedStringSliceOptions = Options{ 68 | IsEmpty: isEmpty, 69 | Normalize: normalizeLowercase, 70 | } 71 | 72 | // FileNormalizedOriginalStringSliceOptions represents a list of items stored in a file 73 | // Tokenization: Comma 74 | // Normalization: Standard 75 | var FileNormalizedOriginalStringSliceOptions = Options{ 76 | IsEmpty: isEmpty, 77 | Normalize: normalize, 78 | IsFromFile: isFromFile, 79 | } 80 | -------------------------------------------------------------------------------- /string_slice_test.go: -------------------------------------------------------------------------------- 1 | package goflags 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNormalizedStringSlice(t *testing.T) { 11 | expectedABC := []string{"aa", "bb", "cc"} 12 | expectedFilePath := []string{"/root/home/file0"} 13 | 14 | validTests := map[string][]string{ 15 | "aa,bb,cc": expectedABC, 16 | " aa, bb, cc ": expectedABC, 17 | " `aa`, 'bb', \"cc\" ": expectedABC, 18 | " `aa`, bb, \"cc\" ": expectedABC, 19 | " `aa, bb, cc\" ": expectedABC, 20 | " \"aa\", bb, cc\" ": expectedABC, 21 | "\n aa, \tbb, cc\r ": expectedABC, 22 | 23 | "\"value1\",value,'value3'": {"value1", "value", "value3"}, 24 | "\"value1\",VALUE,'value3'": {"value1", "value", "value3"}, 25 | 26 | "\"/root/home/file0\"": expectedFilePath, 27 | "'/root/home/file0'": expectedFilePath, 28 | "`/root/home/file0`": expectedFilePath, 29 | "\"/root/home/file0\",": expectedFilePath, 30 | ",\"/root/home/file0\",": expectedFilePath, 31 | ",\"/root/home/file0\"": expectedFilePath, 32 | ",,\"/root/home/file0\"": expectedFilePath, 33 | "\"\",,\"/root/home/file0\"": expectedFilePath, 34 | "\" \",\"/root/home/file0\"": expectedFilePath, 35 | "\"/root/home/file0\",\"\"": expectedFilePath, 36 | "/root/home/file0": expectedFilePath, 37 | 38 | "\"/root/home/file2\",\"/root/home/file3\"": {"/root/home/file2", "/root/home/file3"}, 39 | "/root/home/file4,/root/home/file5": {"/root/home/file4", "/root/home/file5"}, 40 | "\"/root/home/file4,/root/home/file5\"": {"/root/home/file4,/root/home/file5"}, 41 | "\"/root/home/file6\",/root/home/file7": {"/root/home/file6", "/root/home/file7"}, 42 | "\"c:\\my files\\bug,bounty\"": {"c:\\my files\\bug,bounty"}, 43 | "\"c:\\my files\\bug,bounty\",c:\\my_files\\bug bounty": {"c:\\my files\\bug,bounty", "c:\\my_files\\bug bounty"}, 44 | } 45 | 46 | for value, expected := range validTests { 47 | result, err := ToStringSlice(value, NormalizedStringSliceOptions) 48 | assert.Nil(t, err) 49 | assert.Equal(t, result, expected) 50 | } 51 | 52 | invalidTests := []string{ 53 | "\"/root/home/file0", 54 | "'/root/home/file0", 55 | "`/root/home/file0", 56 | "\"/root/home/file0'", 57 | "\"/root/home/file0`", 58 | } 59 | 60 | for _, value := range invalidTests { 61 | result, err := ToStringSlice(value, NormalizedStringSliceOptions) 62 | assert.Nil(t, result) 63 | assert.NotNil(t, err) 64 | } 65 | } 66 | 67 | func TestNormalizedOriginalStringSlice(t *testing.T) { 68 | result, err := ToStringSlice("/Users/Home/Test/test.yaml", NormalizedOriginalStringSliceOptions) 69 | assert.Nil(t, err) 70 | assert.Equal(t, []string{"/Users/Home/Test/test.yaml"}, result, "could not get correct path") 71 | 72 | result, err = ToStringSlice("'test user'", NormalizedOriginalStringSliceOptions) 73 | assert.Nil(t, err) 74 | assert.Equal(t, []string{"test user"}, result, "could not get correct path") 75 | } 76 | 77 | func TestFileNormalizedStringSliceOptions(t *testing.T) { 78 | result, err := ToStringSlice("/Users/Home/Test/test.yaml", FileNormalizedStringSliceOptions) 79 | assert.Nil(t, err) 80 | assert.Equal(t, []string{"/users/home/test/test.yaml"}, result, "could not get correct path") 81 | 82 | result, err = ToStringSlice("'Test User'", FileNormalizedStringSliceOptions) 83 | assert.Nil(t, err) 84 | assert.Equal(t, []string{"test user"}, result, "could not get correct path") 85 | } 86 | 87 | func TestFileStringSliceOptions(t *testing.T) { 88 | filename := "test.txt" 89 | _ = os.WriteFile(filename, []byte("# this is a comment\nvalue1,value2\nvalue3"), 0644) 90 | defer os.RemoveAll(filename) 91 | 92 | result, err := ToStringSlice(filename, FileStringSliceOptions) 93 | assert.Nil(t, err) 94 | assert.Equal(t, []string{"value1,value2", "value3"}, result, "could not get correct path") 95 | 96 | // command line input value 97 | result, err = ToStringSlice("string:\"contains, comma and quotes.\"", FileStringSliceOptions) 98 | assert.Nil(t, err) 99 | assert.Equal(t, []string{"string:\"contains, comma and quotes.\""}, result, "could not get correct path") 100 | } 101 | 102 | func TestFileNormalizedOriginalStringSliceOptions(t *testing.T) { 103 | result, err := ToStringSlice("/Users/Home/Test/test.yaml", FileNormalizedOriginalStringSliceOptions) 104 | assert.Nil(t, err) 105 | assert.Equal(t, []string{"/Users/Home/Test/test.yaml"}, result, "could not get correct path") 106 | 107 | result, err = ToStringSlice("'Test User'", FileNormalizedOriginalStringSliceOptions) 108 | assert.Nil(t, err) 109 | assert.Equal(t, []string{"Test User"}, result, "could not get correct path") 110 | } 111 | --------------------------------------------------------------------------------