├── go.sum ├── internal ├── completion │ ├── test │ │ ├── test_tree │ │ │ ├── ..aFile2 │ │ │ ├── .aFile2 │ │ │ ├── aFile1 │ │ │ ├── aFile2 │ │ │ ├── cFile1 │ │ │ ├── cFile2 │ │ │ ├── ...aFile2 │ │ │ ├── bDir1 │ │ │ │ ├── .file │ │ │ │ └── file │ │ │ └── bDir2 │ │ │ │ └── file │ │ └── go.mod │ ├── documentation.go │ ├── file_test.go │ ├── file.go │ ├── completion_test.go │ └── completion.go ├── sliceiterator │ ├── sliceiterator.go │ └── sliceiterator_test.go ├── help │ ├── help.go │ └── help_test.go └── option │ ├── option_test.go │ └── option.go ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── go.mod ├── .gitignore ├── docs ├── tool │ ├── sourceme.bash │ └── main.go ├── script │ ├── sourceme.bash │ └── main.go ├── new-project-templates.adoc └── parsing.adoc ├── examples ├── dag │ ├── sourceme.bash │ └── main.go ├── complex │ ├── sourceme.bash │ ├── build-i18n-es.sh │ ├── lswrapper │ │ └── lswrapper.go │ ├── slow │ │ └── slow.go │ ├── log │ │ └── log.go │ ├── show │ │ └── show.go │ ├── complete │ │ └── complete.go │ ├── greet │ │ └── greet.go │ └── main.go └── myscript │ ├── sourceme.bash │ └── main.go ├── errors.go ├── dag ├── color.go ├── color_test.go └── README.adoc ├── Makefile ├── example_dispatch_test.go ├── example_dispatch_b_test.go ├── example_dispatch_c_test.go ├── interrupt.go ├── example_test.go ├── helpers.go ├── testhelpers_test.go ├── text └── variables.go ├── documentation.go ├── isoption.go ├── example_minimal_test.go ├── user_help.go ├── isoption_test.go ├── user_test.go ├── api_test.go └── LICENSE /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/completion/test/test_tree/..aFile2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/completion/test/test_tree/.aFile2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/completion/test/test_tree/aFile1: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/completion/test/test_tree/aFile2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/completion/test/test_tree/cFile1: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/completion/test/test_tree/cFile2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [DavidGamba] 2 | -------------------------------------------------------------------------------- /internal/completion/test/test_tree/...aFile2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/completion/test/test_tree/bDir1/.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/completion/test/test_tree/bDir1/file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/completion/test/test_tree/bDir2/file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DavidGamba/go-getoptions 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /internal/completion/test/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DavidGamba/go-getoptions/test/completion/test 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | examples/complex/complex 2 | examples/myscript/myscript 3 | examples/dag/dag 4 | docs/tool/tool 5 | docs/script/script 6 | coverage.txt 7 | coverage.html 8 | -------------------------------------------------------------------------------- /docs/tool/sourceme.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Remove existing entries to ensure the right one is loaded 4 | # This is not required when the completion one liner is loaded in your bashrc. 5 | complete -r ./tool 2>/dev/null 6 | 7 | complete -o default -C "$PWD/tool" tool 8 | -------------------------------------------------------------------------------- /examples/dag/sourceme.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Remove existing entries to ensure the right one is loaded 4 | # This is not required when the completion one liner is loaded in your bashrc. 5 | complete -r ./dag 2>/dev/null 6 | 7 | complete -o default -C "$PWD/dag" dag 8 | -------------------------------------------------------------------------------- /docs/script/sourceme.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Remove existing entries to ensure the right one is loaded 4 | # This is not required when the completion one liner is loaded in your bashrc. 5 | complete -r ./script 2>/dev/null 6 | 7 | complete -o default -C "$PWD/script" script 8 | -------------------------------------------------------------------------------- /examples/complex/sourceme.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Remove existing entries to ensure the right one is loaded 4 | # This is not required when the completion one liner is loaded in your bashrc. 5 | complete -r ./complex 2>/dev/null 6 | 7 | complete -o default -C "$PWD/complex" complex 8 | -------------------------------------------------------------------------------- /examples/myscript/sourceme.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Remove existing entries to ensure the right one is loaded 4 | # This is not required when the completion one liner is loaded in your bashrc. 5 | complete -r ./myscript 2>/dev/null 6 | 7 | complete -o default -C "$PWD/myscript" myscript 8 | -------------------------------------------------------------------------------- /examples/complex/build-i18n-es.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | go build \ 4 | -ldflags "-X \"github.com/DavidGamba/go-getoptions/text.ErrorMissingRequiredOption=falta opción requerida '%s'\" 5 | -X \"github.com/DavidGamba/go-getoptions/text.HelpNameHeader=NOMBRE\" 6 | -X \"github.com/DavidGamba/go-getoptions/text.HelpSynopsisHeader=SINOPSIS\" 7 | -X \"github.com/DavidGamba/go-getoptions/text.HelpCommandsHeader=COMANDOS\" 8 | -X \"github.com/DavidGamba/go-getoptions/text.HelpRequiredOptionsHeader=PARAMETROS REQUERIDOS\" 9 | -X \"github.com/DavidGamba/go-getoptions/text.HelpOptionsHeader=OPCIONES\"" 10 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package getoptions 10 | 11 | import ( 12 | "errors" 13 | "fmt" 14 | ) 15 | 16 | // ErrorHelpCalled - Indicates the help has been handled. 17 | var ErrorHelpCalled = fmt.Errorf("help called") 18 | 19 | // ErrorParsing - Indicates that there was an error with cli args parsing 20 | var ErrorParsing = errors.New("") 21 | 22 | // ErrorNotFound - Generic not found error 23 | var ErrorNotFound = fmt.Errorf("not found") 24 | -------------------------------------------------------------------------------- /examples/complex/lswrapper/lswrapper.go: -------------------------------------------------------------------------------- 1 | package lswrapper 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/DavidGamba/go-getoptions" 11 | ) 12 | 13 | var Logger = log.New(io.Discard, "log ", log.LstdFlags) 14 | 15 | // NewCommand - Populate Options definition 16 | func NewCommand(parent *getoptions.GetOpt) *getoptions.GetOpt { 17 | opt := parent.NewCommand("lswrapper", "wrapper to ls").SetCommandFn(Run) 18 | opt.SetUnknownMode(getoptions.Pass) 19 | opt.UnsetOptions() 20 | return opt 21 | } 22 | 23 | // Run - Command entry point 24 | func Run(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 25 | c := exec.CommandContext(ctx, "ls", args...) 26 | c.Stdout = os.Stdout 27 | c.Stderr = os.Stderr 28 | err := c.Run() 29 | if err != nil { 30 | return err 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /dag/color.go: -------------------------------------------------------------------------------- 1 | package dag 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func (g *Graph) colorInfo(format string) string { 9 | return g.color(g.InfoColor, format) 10 | } 11 | 12 | func (g *Graph) colorInfoBold(format string) string { 13 | return g.color(g.InfoBoldColor, format) 14 | } 15 | 16 | func (g *Graph) colorError(format string) string { 17 | return g.color(g.ErrorColor, format) 18 | } 19 | 20 | func (g *Graph) colorErrorBold(format string) string { 21 | return g.color(g.ErrorBoldColor, format) 22 | } 23 | 24 | func (g *Graph) color(color string, format string) string { 25 | if !g.UseColor { 26 | return format 27 | } 28 | if !strings.HasSuffix(format, "\n") { 29 | return fmt.Sprintf("\033[%sm%s\033[0m", color, format) 30 | } 31 | 32 | format = format[:len(format)-1] 33 | return fmt.Sprintf("\033[%sm%s\033[0m\n", color, format) 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | 4 | on: 5 | - push 6 | - pull_request 7 | 8 | jobs: 9 | test: 10 | name: Lib 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | go: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x, 1.21.x, 1.22.x, 1.23.x, 1.24.x] 15 | # os: [ubuntu-latest, macos-latest, windows-latest] 16 | os: [ubuntu-latest, macos-latest] 17 | steps: 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Go 1.x 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ matrix.go }} 25 | id: go 26 | 27 | - run: go version 28 | 29 | - name: Make 30 | run: make test 31 | 32 | - name: Update coverage profile 33 | run: bash <(curl -s https://codecov.io/bash) 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | go test -race -coverprofile=coverage.txt -covermode=atomic ./ ./internal/completion/ ./internal/option ./internal/help ./dag ./internal/sliceiterator ./text 5 | cd examples/complex && go build . && cd ../.. 6 | cd examples/dag && go build . && cd ../.. 7 | cd examples/myscript && go build . && cd ../.. 8 | cd docs/tool && go build . && cd ../.. 9 | cd docs/script && go build . && cd ../.. 10 | 11 | race: 12 | go test -race ./dag -count=1 13 | 14 | view: test 15 | go tool cover -html=coverage.txt -o coverage.html 16 | 17 | # Assumes github.com/dgryski/semgrep-go is checked out in ../ 18 | rule-check: 19 | semgrep -f ../semgrep-go . 20 | for dir in ./ ./internal/completion ./internal/option ./internal/help ./dag ; do \ 21 | echo $$dir ; \ 22 | ruleguard -c=0 -rules ../semgrep-go/ruleguard.rules.go $$dir ; \ 23 | done 24 | 25 | 26 | lint: 27 | golangci-lint run --enable-all \ 28 | -D funlen \ 29 | -D dupl \ 30 | -D lll \ 31 | -D gocognit \ 32 | -D exhaustivestruct \ 33 | -D cyclop 34 | -------------------------------------------------------------------------------- /docs/script/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/DavidGamba/go-getoptions" 11 | ) 12 | 13 | var Logger = log.New(io.Discard, "", log.LstdFlags) 14 | 15 | func main() { 16 | os.Exit(program(os.Args)) 17 | } 18 | 19 | func program(args []string) int { 20 | opt := getoptions.New() 21 | opt.Bool("help", false, opt.Alias("?")) 22 | opt.Bool("debug", false, opt.GetEnv("DEBUG")) 23 | remaining, err := opt.Parse(args[1:]) 24 | if opt.Called("help") { 25 | fmt.Println(opt.Help()) 26 | return 1 27 | } 28 | if err != nil { 29 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 30 | return 1 31 | } 32 | if opt.Called("debug") { 33 | Logger.SetOutput(os.Stderr) 34 | } 35 | Logger.Println(remaining) 36 | 37 | ctx, cancel, done := getoptions.InterruptContext() 38 | defer func() { cancel(); <-done }() 39 | 40 | err = run(ctx) 41 | if err != nil { 42 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 43 | return 1 44 | } 45 | return 0 46 | } 47 | 48 | func run(ctx context.Context) error { 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /examples/complex/slow/slow.go: -------------------------------------------------------------------------------- 1 | package slow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "time" 9 | 10 | "github.com/DavidGamba/go-getoptions" 11 | ) 12 | 13 | var Logger = log.New(io.Discard, "show ", log.LstdFlags) 14 | 15 | var iterations int 16 | 17 | // NewCommand - Populate Options definition 18 | func NewCommand(parent *getoptions.GetOpt) *getoptions.GetOpt { 19 | opt := parent.NewCommand("slow", "Run something in a very slow way (please cancel me with Ctrl-C)") 20 | opt.IntVar(&iterations, "iterations", 5) 21 | opt.SetCommandFn(Run) 22 | return opt 23 | } 24 | 25 | // Run - Command entry point 26 | func Run(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 27 | Logger.Printf("args to slow: %v\n", args) 28 | if opt.Called("iterations") { 29 | fmt.Printf("iterations overriden with: %d\n", opt.Value("iterations")) 30 | } 31 | for i := 0; i < iterations; i++ { 32 | select { 33 | case <-ctx.Done(): 34 | fmt.Println("Cleaning up...") 35 | return nil 36 | default: 37 | } 38 | fmt.Printf("Sleeping: %d\n", i) 39 | time.Sleep(1 * time.Second) 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /example_dispatch_test.go: -------------------------------------------------------------------------------- 1 | package getoptions_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/DavidGamba/go-getoptions" 10 | ) 11 | 12 | func ExampleGetOpt_Dispatch() { 13 | runFn := func(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 14 | fmt.Println("a, b, c, d") 15 | return nil 16 | } 17 | 18 | opt := getoptions.New() 19 | opt.NewCommand("list", "list stuff").SetCommandFn(runFn) 20 | opt.HelpCommand("help", opt.Alias("?")) 21 | remaining, err := opt.Parse([]string{"list"}) // <- argv set to call command 22 | if err != nil { 23 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 24 | os.Exit(1) 25 | } 26 | 27 | getoptions.Writer = os.Stdout // Print help to stdout instead of stderr for test purpose 28 | 29 | err = opt.Dispatch(context.Background(), remaining) 30 | if err != nil { 31 | if errors.Is(err, getoptions.ErrorHelpCalled) { 32 | os.Exit(1) 33 | } 34 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 35 | if errors.Is(err, getoptions.ErrorParsing) { 36 | fmt.Fprintf(os.Stderr, "\n"+opt.Help()) 37 | } 38 | os.Exit(1) 39 | } 40 | 41 | // Output: 42 | // a, b, c, d 43 | } 44 | -------------------------------------------------------------------------------- /docs/tool/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | 11 | "github.com/DavidGamba/go-getoptions" 12 | ) 13 | 14 | var Logger = log.New(os.Stderr, "", log.LstdFlags) 15 | 16 | func main() { 17 | os.Exit(program(os.Args)) 18 | } 19 | 20 | func program(args []string) int { 21 | opt := getoptions.New() 22 | opt.Bool("quiet", false, opt.GetEnv("QUIET")) 23 | opt.SetUnknownMode(getoptions.Pass) 24 | opt.NewCommand("cmd", "description").SetCommandFn(Run) 25 | opt.HelpCommand("help", opt.Alias("?")) 26 | remaining, err := opt.Parse(args[1:]) 27 | if err != nil { 28 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 29 | return 1 30 | } 31 | if opt.Called("quiet") { 32 | Logger.SetOutput(io.Discard) 33 | } 34 | Logger.Println(remaining) 35 | 36 | ctx, cancel, done := getoptions.InterruptContext() 37 | defer func() { cancel(); <-done }() 38 | 39 | err = opt.Dispatch(ctx, remaining) 40 | if err != nil { 41 | if errors.Is(err, getoptions.ErrorHelpCalled) { 42 | return 1 43 | } 44 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 45 | if errors.Is(err, getoptions.ErrorParsing) { 46 | fmt.Fprintf(os.Stderr, "\n%s", opt.Help()) 47 | } 48 | return 1 49 | } 50 | return 0 51 | } 52 | 53 | func Run(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 54 | Logger.Printf("Running") 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /example_dispatch_b_test.go: -------------------------------------------------------------------------------- 1 | package getoptions_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/DavidGamba/go-getoptions" 10 | ) 11 | 12 | func ExampleGetOpt_Dispatch_bHelp() { 13 | runFn := func(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 14 | return nil 15 | } 16 | 17 | opt := getoptions.New() 18 | opt.Bool("debug", false) 19 | opt.NewCommand("list", "list stuff").SetCommandFn(runFn) 20 | opt.HelpCommand("help", opt.Alias("?"), opt.Description("Show this help")) 21 | remaining, err := opt.Parse([]string{"help"}) // <- argv set to call help 22 | if err != nil { 23 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 24 | os.Exit(1) 25 | } 26 | 27 | getoptions.Writer = os.Stdout // Print help to stdout instead of stderr for test purpose 28 | 29 | err = opt.Dispatch(context.Background(), remaining) 30 | if err != nil { 31 | if !errors.Is(err, getoptions.ErrorHelpCalled) { 32 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 33 | os.Exit(1) 34 | } 35 | } 36 | 37 | // Output: 38 | // SYNOPSIS: 39 | // go-getoptions.test [--debug] [--help|-?] [] 40 | // 41 | // COMMANDS: 42 | // list list stuff 43 | // 44 | // OPTIONS: 45 | // --debug (default: false) 46 | // 47 | // --help|-? Show this help (default: false) 48 | // 49 | // Use 'go-getoptions.test help ' for extra details. 50 | } 51 | -------------------------------------------------------------------------------- /example_dispatch_c_test.go: -------------------------------------------------------------------------------- 1 | package getoptions_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/DavidGamba/go-getoptions" 10 | ) 11 | 12 | func ExampleGetOpt_Dispatch_cCommandHelp() { 13 | runFn := func(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 14 | return nil 15 | } 16 | 17 | opt := getoptions.New() 18 | opt.Bool("debug", false) 19 | list := opt.NewCommand("list", "list stuff").SetCommandFn(runFn) 20 | list.Bool("list-opt", false) 21 | opt.HelpCommand("help", opt.Alias("?")) 22 | remaining, err := opt.Parse([]string{"help", "list"}) // <- argv set to call command help 23 | if err != nil { 24 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 25 | os.Exit(1) 26 | } 27 | 28 | getoptions.Writer = os.Stdout // Print help to stdout instead of stderr for test purpose 29 | 30 | err = opt.Dispatch(context.Background(), remaining) 31 | if err != nil { 32 | if !errors.Is(err, getoptions.ErrorHelpCalled) { 33 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 34 | os.Exit(1) 35 | } 36 | } 37 | 38 | // Output: 39 | // NAME: 40 | // go-getoptions.test list - list stuff 41 | // 42 | // SYNOPSIS: 43 | // go-getoptions.test list [--debug] [--help|-?] [--list-opt] [] 44 | // 45 | // OPTIONS: 46 | // --debug (default: false) 47 | // 48 | // --help|-? (default: false) 49 | // 50 | // --list-opt (default: false) 51 | // 52 | } 53 | -------------------------------------------------------------------------------- /internal/completion/documentation.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | /* 10 | Package completion - provides a Tree structure that can be used to define a program's completions. 11 | 12 | Example Tree: 13 | 14 | mygit 15 | ├ log 16 | │ ├ sublog 17 | │ │ ├ --help 18 | │ │ ├ 19 | │ │ └ 20 | │ ├ --help 21 | │ └ 22 | ├ show 23 | │ ├ --help 24 | │ ├ --dir= 25 | │ └ 26 | ├ --help 27 | └ --version 28 | 29 | A tree node can have children and leaves. 30 | The children are commands, the leaves can be options, file completions, custom completions and options that trigger custom file completions (--dir=). 31 | 32 | Completions have a hierarchy, commands are shown before file completions, and options are only shown if `-` is passed as part of the COMPLINE. 33 | 34 | For custom completions a full list of completions must be passed as leaves to the node. 35 | However, there file and dir completions are provided as a convenience. 36 | 37 | Custom completions for options are triggered with the `=` sing after the full option test has been provided. 38 | */ 39 | package completion 40 | -------------------------------------------------------------------------------- /docs/new-project-templates.adoc: -------------------------------------------------------------------------------- 1 | = New Project Templates 2 | 3 | == Single Purpose Script 4 | 5 | When creating a new single purpose script (in other words, with no sub-commands) use the template below: 6 | 7 | include::script/main.go[] 8 | 9 | The template has a single `os.Exit`. 10 | `os.Exit` shouldn't be used anywhere else on your script (otherwise defer statements will not run). 11 | 12 | The template ensures your script exits with 0 for success, 1 for error. 13 | If there is a need for a custom exit status code, the error returned from `realMain` should be checked and the logic implemented there. 14 | 15 | The template also creates a default context that captures `os.Interrupt`, `syscall.SIGHUP` and `syscall.SIGTERM`. 16 | 17 | == Complex Script 18 | 19 | When creating a complex tool (in other words, one that implements sub-commands) use the template below: 20 | 21 | include::tool/main.go[] 22 | 23 | The template has a single `os.Exit`. 24 | `os.Exit` shouldn't be used anywhere else on your script (otherwise defer statements will not run). 25 | 26 | The template ensures your script exits with 0 for success, 1 for error. 27 | If there is a need for a custom exit status code, the error returned from `realMain` should be checked and the logic implemented there. 28 | 29 | The template also creates a default context that captures `os.Interrupt`, `syscall.SIGHUP` and `syscall.SIGTERM`. 30 | 31 | The template doesn't run any code at the top level command other than displaying the help. 32 | 33 | Finally, the template has an example command (`cmd`) with an example implementation (`Run`). 34 | -------------------------------------------------------------------------------- /interrupt.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package getoptions 10 | 11 | import ( 12 | "context" 13 | "fmt" 14 | "os" 15 | "os/signal" 16 | "syscall" 17 | 18 | "github.com/DavidGamba/go-getoptions/text" 19 | ) 20 | 21 | // InterruptContext - Creates a top level context that listens to os.Interrupt, syscall.SIGHUP and syscall.SIGTERM and calls the CancelFunc if the signals are triggered. 22 | // When the listener finishes its work, it sends a message to the done channel. 23 | // 24 | // Use: 25 | // 26 | // func main() { ... 27 | // ctx, cancel, done := getoptions.InterruptContext() 28 | // defer func() { cancel(); <-done }() 29 | // 30 | // NOTE: InterruptContext is a method to reuse gopt.Writer 31 | func InterruptContext() (ctx context.Context, cancel context.CancelFunc, done chan struct{}) { 32 | done = make(chan struct{}, 1) 33 | ctx, cancel = context.WithCancel(context.Background()) 34 | signals := make(chan os.Signal, 1) 35 | signal.Notify(signals, os.Interrupt, syscall.SIGHUP, syscall.SIGTERM) 36 | go func() { 37 | defer func() { 38 | signal.Stop(signals) 39 | cancel() 40 | done <- struct{}{} 41 | }() 42 | select { 43 | case <-signals: 44 | fmt.Fprintf(Writer, "\n%s\n", text.MessageOnInterrupt) 45 | case <-ctx.Done(): 46 | } 47 | }() 48 | return ctx, cancel, done 49 | } 50 | -------------------------------------------------------------------------------- /examples/complex/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "strings" 9 | 10 | "github.com/DavidGamba/go-getoptions" 11 | ) 12 | 13 | var Logger = log.New(io.Discard, "log ", log.LstdFlags) 14 | 15 | // NewCommand - Populate Options definition 16 | func NewCommand(parent *getoptions.GetOpt) *getoptions.GetOpt { 17 | opt := parent.NewCommand("log", "Show application logs") 18 | opt.String("level", "INFO", opt.Description("filter debug level"), opt.ValidValues("ERROR", "DEBUG", "INFO")) 19 | opt.SetCommandFn(Run) 20 | return opt 21 | } 22 | 23 | // Run - Command entry point 24 | func Run(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 25 | Logger.Printf("log args: %v\n", args) 26 | filterLevel := opt.Value("level").(string) 27 | 28 | logLines := []string{ 29 | `1900/01/01 01:01:01 INFO beginning of logs`, 30 | `1900/01/01 01:01:02 DEBUG user 'david.gamba' failed login attempt`, 31 | `1900/01/01 01:01:03 INFO user 'david.gamba' logged in`, 32 | `1900/01/01 01:01:04 ERROR request by user 'david.gamba' crashed the system`, 33 | } 34 | 35 | if !opt.Called("level") { 36 | fmt.Println(strings.Join(logLines, "\n")) 37 | } else { 38 | for _, e := range logLines { 39 | switch filterLevel { 40 | case "DEBUG": 41 | fmt.Println(e) 42 | case "INFO": 43 | if strings.Contains(e, "INFO") || strings.Contains(e, "ERROR") { 44 | fmt.Println(e) 45 | } 46 | case "ERROR": 47 | if strings.Contains(e, "ERROR") { 48 | fmt.Println(e) 49 | } 50 | } 51 | } 52 | } 53 | 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /examples/complex/show/show.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | 9 | "github.com/DavidGamba/go-getoptions" 10 | ) 11 | 12 | var Logger = log.New(io.Discard, "show ", log.LstdFlags) 13 | 14 | // NewCommand - Populate Options definition 15 | func NewCommand(parent *getoptions.GetOpt) *getoptions.GetOpt { 16 | opt := parent.NewCommand("show", "Show various types of objects") 17 | opt.Bool("show-option", false) 18 | opt.String("password", "", opt.GetEnv("PASSWORD"), opt.Alias("p")) 19 | opt.SetCommandFn(Run) 20 | return opt 21 | } 22 | 23 | // Run - Command entry point 24 | func Run(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 25 | showOption := opt.Value("show-option").(bool) 26 | password := opt.Value("password").(string) 27 | 28 | nopt := getoptions.New() 29 | nopt.Bool("show-option", showOption, opt.SetCalled(opt.Called("show-option"))) 30 | nopt.String("password", password, opt.SetCalled(opt.Called("password"))) 31 | nopt.Int("number", 123, opt.SetCalled(true)) 32 | nopt.Float64("float", 3.14) 33 | 34 | err := CommandFn(ctx, nopt, []string{}) 35 | if err != nil { 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | func CommandFn(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 42 | Logger.Printf("args to show: %v\n", args) 43 | fmt.Printf("show output... %v\n", args) 44 | if opt.Called("show-option") { 45 | fmt.Printf("show option was called...\n") 46 | } 47 | if opt.Called("password") { 48 | fmt.Printf("The secret was... %s\n", opt.Value("password")) 49 | } 50 | if opt.Called("number") { 51 | fmt.Printf("show number: %d\n", opt.Value("number")) 52 | } 53 | fmt.Printf("show float: %f\n", opt.Value("float")) 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /examples/complex/complete/complete.go: -------------------------------------------------------------------------------- 1 | package complete 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "strings" 10 | 11 | "github.com/DavidGamba/go-getoptions" 12 | ) 13 | 14 | var Logger = log.New(io.Discard, "log ", log.LstdFlags) 15 | 16 | // NewCommand - Populate Options definition 17 | func NewCommand(parent *getoptions.GetOpt) *getoptions.GetOpt { 18 | opt := parent.NewCommand("complete", "Example completions") 19 | opt.SetCommandFn(Run) 20 | opt.String("completeme", "", opt.SuggestedValuesFn(func(target, partial string) []string { 21 | fmt.Fprintf(os.Stderr, "\npartial: %v\n", partial) 22 | return []string{"complete", "completeme", "completeme2"} 23 | })) 24 | opt.ArgCompletions("dev-east", "dev-west", "staging-east", "prod-east", "prod-west", "prod-south") 25 | opt.ArgCompletionsFns(func(target string, prev []string, s string) []string { 26 | if len(prev) == 0 { 27 | if strings.HasPrefix("dev-", s) { 28 | return []string{"dev-hola/", "dev-hello"} 29 | } 30 | if strings.HasPrefix("dev-h", s) { 31 | return []string{"dev-hola/", "dev-hello"} 32 | } 33 | if strings.HasPrefix("dev-hello", s) { 34 | return []string{"dev-hello"} 35 | } 36 | if strings.HasPrefix("dev-hola/", s) { 37 | return []string{"dev-hola/a", "dev-hola/b", "dev-hola/" + target} 38 | } 39 | } 40 | if len(prev) == 1 { 41 | Logger.Printf("prev: %v\n", prev) 42 | if strings.HasPrefix(prev[0], "dev-hola/") { 43 | return []string{"second-hola/a", "second-hola/b", "second-hola/" + target} 44 | } 45 | return []string{"second-arg-a", "second-arg-b"} 46 | } 47 | return []string{} 48 | }) 49 | return opt 50 | } 51 | 52 | // Run - Command entry point 53 | func Run(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 54 | Logger.Printf("args: %v\n", args) 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /examples/complex/greet/greet.go: -------------------------------------------------------------------------------- 1 | package greet 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | 9 | "github.com/DavidGamba/go-getoptions" 10 | ) 11 | 12 | var Logger = log.New(io.Discard, "log ", log.LstdFlags) 13 | 14 | // NewCommand - Populate Options definition 15 | func NewCommand(parent *getoptions.GetOpt) *getoptions.GetOpt { 16 | opt := parent.NewCommand("message", "Subcommands example") 17 | GreetNewCommand(opt) 18 | ByeNewCommand(opt) 19 | return opt 20 | } 21 | 22 | func GreetNewCommand(parent *getoptions.GetOpt) *getoptions.GetOpt { 23 | opt := parent.NewCommand("greet", "Hi in multiple languages") 24 | en := opt.NewCommand("en", "greet in English").SetCommandFn(RunEnglish) 25 | en.String("name", "", opt.Required("")) 26 | es := opt.NewCommand("es", "greet in Spanish").SetCommandFn(RunSpanish) 27 | es.String("name", "", opt.Required("")) 28 | return opt 29 | } 30 | 31 | func ByeNewCommand(parent *getoptions.GetOpt) *getoptions.GetOpt { 32 | opt := parent.NewCommand("bye", "Bye in multiple languages") 33 | en := opt.NewCommand("en", "bye in English").SetCommandFn(RunByeEnglish) 34 | en.String("name", "", opt.Required("")) 35 | es := opt.NewCommand("es", "bye in Spanish").SetCommandFn(RunByeSpanish) 36 | es.String("name", "", opt.Required("")) 37 | return opt 38 | } 39 | 40 | func RunEnglish(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 41 | fmt.Printf("Hello %s!\n", opt.Value("name")) 42 | return nil 43 | } 44 | 45 | func RunSpanish(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 46 | fmt.Printf("Hola %s!\n", opt.Value("name")) 47 | return nil 48 | } 49 | 50 | func RunByeEnglish(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 51 | fmt.Printf("Bye %s!\n", opt.Value("name")) 52 | return nil 53 | } 54 | 55 | func RunByeSpanish(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 56 | fmt.Printf("Adios %s!\n", opt.Value("name")) 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /examples/myscript/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | 11 | "github.com/DavidGamba/go-getoptions" 12 | ) 13 | 14 | var Logger = log.New(os.Stderr, "", log.LstdFlags) 15 | 16 | func main() { 17 | os.Exit(program(os.Args)) 18 | } 19 | 20 | func program(args []string) int { 21 | ctx, cancel, done := getoptions.InterruptContext() 22 | defer func() { cancel(); <-done }() 23 | 24 | opt := getoptions.New() 25 | opt.Self("myscript", "Simple demo script") 26 | opt.Bool("debug", false, opt.GetEnv("DEBUG")) 27 | opt.Int("greet", 0, opt.Required(), opt.Description("Number of times to greet.")) 28 | opt.StringMap("list", 1, 99, opt.Description("Greeting list by language.")) 29 | opt.Bool("quiet", false, opt.GetEnv("QUIET")) 30 | opt.HelpSynopsisArg("", "Name to greet.") 31 | opt.SetCommandFn(Run) 32 | opt.HelpCommand("help", opt.Alias("?")) 33 | remaining, err := opt.Parse(args[1:]) 34 | if err != nil { 35 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 36 | return 1 37 | } 38 | if opt.Called("quiet") { 39 | Logger.SetOutput(io.Discard) 40 | } 41 | 42 | err = opt.Dispatch(ctx, remaining) 43 | if err != nil { 44 | if errors.Is(err, getoptions.ErrorHelpCalled) { 45 | return 1 46 | } 47 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 48 | if errors.Is(err, getoptions.ErrorParsing) { 49 | fmt.Fprintf(os.Stderr, "\n"+opt.Help()) 50 | } 51 | return 1 52 | } 53 | return 0 54 | } 55 | 56 | func Run(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 57 | // Get arguments and options 58 | name, _, err := opt.GetRequiredArg(args) 59 | if err != nil { 60 | return err 61 | } 62 | greetCount := opt.Value("greet").(int) 63 | list := opt.Value("list").(map[string]string) 64 | 65 | Logger.Printf("Running: %v", args) 66 | 67 | // Use the int variable 68 | for i := 0; i < greetCount; i++ { 69 | fmt.Printf("Hello %s, from go-getoptions!\n", name) 70 | } 71 | 72 | // Use the map[string]string variable 73 | if len(list) > 0 { 74 | fmt.Printf("Greeting List:\n") 75 | for k, v := range list { 76 | fmt.Printf("\t%s=%s\n", k, v) 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package getoptions_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | 9 | "github.com/DavidGamba/go-getoptions" 10 | ) 11 | 12 | var logger = log.New(io.Discard, "DEBUG: ", log.LstdFlags) 13 | 14 | func Example() { 15 | // Declare the variables you want your options to update 16 | var debug bool 17 | var greetCount int 18 | var list map[string]string 19 | 20 | // Declare the GetOptions object 21 | opt := getoptions.New() 22 | 23 | // Options definition 24 | opt.Bool("help", false, opt.Alias("h", "?")) // Aliases can be defined 25 | opt.BoolVar(&debug, "debug", false) 26 | opt.IntVar(&greetCount, "greet", 0, 27 | opt.Required(), 28 | opt.Description("Number of times to greet."), // Set the automated help description 29 | opt.ArgName("number"), // Change the help synopsis arg from the default to 30 | ) 31 | opt.StringMapVar(&list, "list", 1, 99, 32 | opt.Description("Greeting list by language."), 33 | opt.ArgName("lang=msg"), // Change the help synopsis arg from to 34 | ) 35 | 36 | // // Parse cmdline arguments os.Args[1:] 37 | remaining, err := opt.Parse([]string{"-g", "2", "-l", "en='Hello World'", "es='Hola Mundo'"}) 38 | 39 | // Handle help before handling user errors 40 | if opt.Called("help") { 41 | fmt.Fprint(os.Stderr, opt.Help()) 42 | os.Exit(1) 43 | } 44 | 45 | // Handle user errors 46 | if err != nil { 47 | fmt.Fprintf(os.Stderr, "ERROR: %s\n\n", err) 48 | fmt.Fprint(os.Stderr, opt.Help(getoptions.HelpSynopsis)) 49 | os.Exit(1) 50 | } 51 | 52 | // Use the passed command line options... Enjoy! 53 | if debug { 54 | logger.SetOutput(os.Stderr) 55 | } 56 | logger.Printf("Unhandled CLI args: %v\n", remaining) 57 | 58 | // Use the int variable 59 | for i := 0; i < greetCount; i++ { 60 | fmt.Println("Hello World, from go-getoptions!") 61 | } 62 | 63 | // Use the map[string]string variable 64 | if len(list) > 0 { 65 | fmt.Printf("Greeting List:\n") 66 | for k, v := range list { 67 | fmt.Printf("\t%s=%s\n", k, v) 68 | } 69 | } 70 | 71 | // Unordered output: 72 | // Hello World, from go-getoptions! 73 | // Hello World, from go-getoptions! 74 | // Greeting List: 75 | // en='Hello World' 76 | // es='Hola Mundo' 77 | } 78 | -------------------------------------------------------------------------------- /internal/sliceiterator/sliceiterator.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | // Package sliceiterator - builds an iterator from a slice to allow peaking for the next value. 10 | package sliceiterator 11 | 12 | // Iterator - iterator data 13 | type Iterator struct { 14 | data *[]string 15 | idx int 16 | } 17 | 18 | // New - builds a string Iterator 19 | func New(s *[]string) *Iterator { 20 | return &Iterator{data: s, idx: -1} 21 | } 22 | 23 | // Size - returns Iterator size 24 | func (a *Iterator) Size() int { 25 | return len(*a.data) 26 | } 27 | 28 | // Index - return current index. 29 | func (a *Iterator) Index() int { 30 | return a.idx 31 | } 32 | 33 | // Next - moves the index forward and returns a bool to indicate if there is another value. 34 | func (a *Iterator) Next() bool { 35 | if a.idx < len(*a.data) { 36 | a.idx++ 37 | } 38 | return a.idx < len(*a.data) 39 | } 40 | 41 | // ExistsNext - tells if there is more data to be read. 42 | func (a *Iterator) ExistsNext() bool { 43 | return a.idx+1 < len(*a.data) 44 | } 45 | 46 | // Value - returns value at current index or an empty string if you are trying to read the value after having fully read the list. 47 | func (a *Iterator) Value() string { 48 | if a.idx >= len(*a.data) { 49 | return "" 50 | } 51 | return (*a.data)[a.idx] 52 | } 53 | 54 | // PeekNextValue - Returns the next value and indicates whether or not it is valid. 55 | func (a *Iterator) PeekNextValue() (string, bool) { 56 | if a.idx+1 >= len(*a.data) { 57 | return "", false 58 | } 59 | return (*a.data)[a.idx+1], true 60 | } 61 | 62 | // IsLast - Tells if the current element is the last. 63 | func (a *Iterator) IsLast() bool { 64 | return a.idx == len(*a.data)-1 65 | } 66 | 67 | // Remaining - Get all remaining values index inclusive. 68 | func (a *Iterator) Remaining() []string { 69 | if a.idx >= len(*a.data) { 70 | return []string{} 71 | } 72 | return (*a.data)[a.idx:] 73 | } 74 | 75 | // Reset - resets the index of the Iterator. 76 | func (a *Iterator) Reset() { 77 | a.idx = -1 78 | } 79 | -------------------------------------------------------------------------------- /internal/sliceiterator/sliceiterator_test.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package sliceiterator 10 | 11 | import ( 12 | "reflect" 13 | "testing" 14 | ) 15 | 16 | func TestIterator(t *testing.T) { 17 | data := []string{"a", "b", "c", "d"} 18 | i := New(&data) 19 | if i.Size() != len(data) { 20 | t.Errorf("wrong size: %d\n", i.Size()) 21 | } 22 | if i.Index() != -1 { 23 | t.Errorf("wrong initial index: %d\n", i.Index()) 24 | } 25 | for i.Next() { 26 | if i.Index() < len(data)-1 { 27 | if !i.ExistsNext() { 28 | t.Errorf("wrong ExistsNext: idx %d, size %d", i.Index(), i.Size()) 29 | } 30 | } 31 | if i.Index() == 0 { 32 | if i.Value() != "a" { 33 | t.Errorf("wrong value: %s\n", i.Value()) 34 | } 35 | } 36 | if i.Index() == 2 { 37 | if i.Value() != "c" { 38 | t.Errorf("wrong value: %s\n", i.Value()) 39 | } 40 | val, ok := i.PeekNextValue() 41 | if !ok { 42 | t.Errorf("wrong next value: %v\n", val) 43 | } 44 | if val != "d" { 45 | t.Errorf("wrong next value: %v\n", val) 46 | } 47 | if !reflect.DeepEqual(i.Remaining(), []string{"c", "d"}) { 48 | t.Errorf("wrong remaining value: %v\n", i.Remaining()) 49 | } 50 | if i.IsLast() { 51 | t.Errorf("not last\n") 52 | } 53 | } 54 | if i.Index() == 3 && !i.IsLast() { 55 | t.Errorf("last not marked properly\n") 56 | } 57 | } 58 | if i.ExistsNext() { 59 | t.Errorf("wrong ExistsNext: idx %d, size %d", i.Index(), i.Size()) 60 | } 61 | if i.Next() != false { 62 | t.Errorf("wrong next return\n") 63 | } 64 | if i.Value() != "" { 65 | t.Errorf("wrong value: %s\n", i.Value()) 66 | } 67 | if i.Index() != len(data) { 68 | t.Errorf("wrong final index: %d\n", i.Index()) 69 | } 70 | val, ok := i.PeekNextValue() 71 | if ok { 72 | t.Errorf("wrong next value: %v\n", val) 73 | } 74 | if val != "" { 75 | t.Errorf("wrong next value: %v\n", val) 76 | } 77 | if !reflect.DeepEqual(i.Remaining(), []string{}) { 78 | t.Errorf("wrong remaining value: %v\n", i.Remaining()) 79 | } 80 | i.Reset() 81 | if i.Index() != -1 { 82 | t.Errorf("wrong index after reset: %d\n", i.Index()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/complex/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | complexcomplete "github.com/DavidGamba/go-getoptions/examples/complex/complete" 11 | complexgreet "github.com/DavidGamba/go-getoptions/examples/complex/greet" 12 | complexlog "github.com/DavidGamba/go-getoptions/examples/complex/log" 13 | complexlswrapper "github.com/DavidGamba/go-getoptions/examples/complex/lswrapper" 14 | complexshow "github.com/DavidGamba/go-getoptions/examples/complex/show" 15 | complexslow "github.com/DavidGamba/go-getoptions/examples/complex/slow" 16 | 17 | "github.com/DavidGamba/go-getoptions" 18 | ) 19 | 20 | var Logger = log.New(io.Discard, "", log.LstdFlags) 21 | 22 | func main() { 23 | os.Exit(program(os.Args)) 24 | } 25 | 26 | func program(args []string) int { 27 | // getoptions.Logger.SetOutput(os.Stderr) 28 | opt := getoptions.New() 29 | // opt.SetUnknownMode(getoptions.Pass) 30 | opt.Bool("debug", false, opt.GetEnv("DEBUG")) 31 | opt.Bool("flag", false) 32 | opt.Bool("fleg", false) 33 | opt.String("profile", "default", opt.ValidValues("default", "dev", "staging", "prod")) 34 | complexgreet.NewCommand(opt) 35 | complexlog.NewCommand(opt) 36 | complexlswrapper.NewCommand(opt) 37 | complexshow.NewCommand(opt) 38 | complexslow.NewCommand(opt) 39 | complexcomplete.NewCommand(opt) 40 | opt.HelpCommand("help", opt.Alias("?")) 41 | remaining, err := opt.Parse(args[1:]) 42 | if err != nil { 43 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 44 | return 1 45 | } 46 | if opt.Called("debug") { 47 | Logger.SetOutput(os.Stderr) 48 | complexgreet.Logger.SetOutput(os.Stderr) 49 | complexlog.Logger.SetOutput(os.Stderr) 50 | complexlswrapper.Logger.SetOutput(os.Stderr) 51 | complexshow.Logger.SetOutput(os.Stderr) 52 | complexslow.Logger.SetOutput(os.Stderr) 53 | complexcomplete.Logger.SetOutput(os.Stderr) 54 | } 55 | if opt.Called("profile") { 56 | Logger.Printf("profile: %s\n", opt.Value("profile")) 57 | } 58 | Logger.Printf("Remaning cli args: %v", remaining) 59 | 60 | ctx, cancel, done := getoptions.InterruptContext() 61 | defer func() { cancel(); <-done }() 62 | 63 | err = opt.Dispatch(ctx, remaining) 64 | if err != nil { 65 | if errors.Is(err, getoptions.ErrorHelpCalled) { 66 | return 1 67 | } 68 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 69 | if errors.Is(err, getoptions.ErrorParsing) { 70 | fmt.Fprintf(os.Stderr, "\n"+opt.Help()) 71 | } 72 | return 1 73 | } 74 | return 0 75 | } 76 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package getoptions 10 | 11 | import ( 12 | "fmt" 13 | "strconv" 14 | 15 | "github.com/DavidGamba/go-getoptions/text" 16 | ) 17 | 18 | // GetRequiredArg - Get the next argument from the args list and error if it doesn't exist. 19 | // By default the error will include the HelpSynopsis section but it can be overriden with the list of sections or getoptions.HelpNone. 20 | // 21 | // If the arguments have been named with `opt.HelpSynopsisArg` then the error will include the argument name. 22 | func (gopt *GetOpt) GetRequiredArg(args []string, sections ...HelpSection) (string, []string, error) { 23 | if len(args) < 1 { 24 | if len(gopt.programTree.SynopsisArgs) > gopt.programTree.SynopsisArgsIdx { 25 | argName := gopt.programTree.SynopsisArgs[gopt.programTree.SynopsisArgsIdx].Arg 26 | fmt.Fprintf(Writer, text.ErrorMissingRequiredNamedArgument+"\n", argName) 27 | } else { 28 | fmt.Fprintf(Writer, "%s\n", text.ErrorMissingRequiredArgument) 29 | } 30 | if sections != nil { 31 | fmt.Fprintf(Writer, "%s", gopt.Help(sections...)) 32 | } else { 33 | fmt.Fprintf(Writer, "%s", gopt.Help(HelpSynopsis)) 34 | } 35 | gopt.programTree.SynopsisArgsIdx++ 36 | return "", args, ErrorHelpCalled 37 | } 38 | gopt.programTree.SynopsisArgsIdx++ 39 | return args[0], args[1:], nil 40 | } 41 | 42 | // Same as GetRequiredArg but converts the argument to an int. 43 | func (gopt *GetOpt) GetRequiredArgInt(args []string, sections ...HelpSection) (int, []string, error) { 44 | arg, args, err := gopt.GetRequiredArg(args, sections...) 45 | if err != nil { 46 | return 0, args, err 47 | } 48 | i, err := strconv.Atoi(arg) 49 | if err != nil { 50 | return 0, args, fmt.Errorf(text.ErrorConvertArgumentToInt, arg) 51 | } 52 | return i, args, nil 53 | } 54 | 55 | // Same as GetRequiredArg but converts the argument to a float64. 56 | func (gopt *GetOpt) GetRequiredArgFloat64(args []string, sections ...HelpSection) (float64, []string, error) { 57 | arg, args, err := gopt.GetRequiredArg(args, sections...) 58 | if err != nil { 59 | return 0, args, err 60 | } 61 | f, err := strconv.ParseFloat(arg, 64) 62 | if err != nil { 63 | return 0, args, fmt.Errorf(text.ErrorConvertArgumentToFloat64, arg) 64 | } 65 | return f, args, nil 66 | } 67 | -------------------------------------------------------------------------------- /dag/color_test.go: -------------------------------------------------------------------------------- 1 | package dag 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "github.com/DavidGamba/go-getoptions" 14 | ) 15 | 16 | func setupLoggingWithoutTime() *bytes.Buffer { 17 | s := "" 18 | buf := bytes.NewBufferString(s) 19 | Logger.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime)) 20 | Logger.SetOutput(buf) 21 | return buf 22 | } 23 | 24 | func TestColor(t *testing.T) { 25 | buf := setupLoggingWithoutTime() 26 | t.Cleanup(func() { t.Log(buf.String()) }) 27 | 28 | var err error 29 | 30 | sm := sync.Mutex{} 31 | results := []int{} 32 | generateFn := func(n int) getoptions.CommandFn { 33 | return func(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 34 | time.Sleep(30 * time.Millisecond) 35 | if n == 2 { 36 | return fmt.Errorf("failure reason") 37 | } 38 | sm.Lock() 39 | results = append(results, n) 40 | sm.Unlock() 41 | return nil 42 | } 43 | } 44 | 45 | tm := NewTaskMap() 46 | tm.Add("t1", generateFn(1)) 47 | tm.Add("t2", generateFn(2)) 48 | tm.Add("t3", generateFn(3)) 49 | 50 | g := NewGraph("test graph").SetSerial() 51 | g.UseColor = true 52 | g.TaskDependsOn(tm.Get("t1"), tm.Get("t2"), tm.Get("t3")) 53 | g.TaskDependsOn(tm.Get("t2"), tm.Get("t3")) 54 | 55 | // Validate before running 56 | err = g.Validate(tm) 57 | if err != nil { 58 | t.Errorf("Unexpected error: %s\n", err) 59 | } 60 | 61 | err = g.Run(context.Background(), nil, nil) 62 | var errs *Errors 63 | if err == nil || !errors.As(err, &errs) { 64 | t.Fatalf("Unexpected error: %s\n", err) 65 | } 66 | if len(errs.Errors) != 2 { 67 | t.Fatalf("Unexpected error size, %d: %s\n", len(errs.Errors), err) 68 | } 69 | if errs.Errors[0].Error() != "Task test graph:t2 error: failure reason" { 70 | t.Fatalf("Unexpected error: %s\n", errs.Errors[0]) 71 | } 72 | if !errors.Is(errs.Errors[1], ErrorTaskSkipped) { 73 | t.Fatalf("Unexpected error: %s\n", errs.Errors[1]) 74 | } 75 | if len(results) != 1 || results[0] != 3 { 76 | t.Errorf("Wrong list: %v, len: %d, 0: %d\n", results, len(results), results[0]) 77 | } 78 | 79 | expected := "\033[34mRunning Task \033[0m\033[36;1mtest graph:t3\033[0m\n" + 80 | "\033[34mCompleted Task \033[0m\033[36;1mtest graph:t3\033[0m\033[34m in 00m:00s\033[0m\n" + 81 | "\033[34mRunning Task \033[0m\033[36;1mtest graph:t2\033[0m\n" + 82 | "\033[34mCompleted Task \033[0m\033[36;1mtest graph:t2\033[0m\033[34m in 00m:00s\033[0m\n" + 83 | "\033[31mTask \033[0m\033[35;1mtest graph:t2\033[0m\033[31m error: failure reason\033[0m\n" + 84 | "\033[31mTask \033[0m\033[35;1mtest graph:t1\033[0m\033[31m error: skipped\033[0m\n" + 85 | "\033[34mCompleted \033[0m\033[36;1mtest graph\033[0m\033[34m Run in 00m:00s\033[0m\n" 86 | if buf.String() != expected { 87 | t.Errorf("Wrong output:\n'%s'\nexpected:\n'%s'\n", buf.String(), expected) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /internal/completion/file_test.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package completion 10 | 11 | import ( 12 | "reflect" 13 | "regexp" 14 | "testing" 15 | ) 16 | 17 | func TestListDir(t *testing.T) { 18 | tests := []struct { 19 | name string 20 | dirname string 21 | prefix string 22 | list []string 23 | err string 24 | }{ 25 | {"dir", "test/test_tree", "", []string{"aFile1", "aFile2", ".aFile2", "..aFile2", "...aFile2", "bDir1/", "bDir2/", "cFile1", "cFile2"}, ""}, 26 | {"dir", "test/test_tree", ".", []string{"./", "../", ".aFile2", "..aFile2", "...aFile2"}, ""}, 27 | {"dir", "test/test_tree", "b", []string{"bDir1/", "bDir2/"}, ""}, 28 | {"dir", "test/test_tree", "bDir1", []string{"bDir1/", "bDir1/ "}, ""}, 29 | {"dir", "test/test_tree", "bDir1/", []string{"bDir1/file", "bDir1/.file"}, ""}, 30 | {"dir", "test/test_tree", "bDir1/f", []string{"bDir1/file"}, ""}, 31 | {"dir", "test/test_tree/bDir1", "../", []string{"../aFile1", "../aFile2", "../.aFile2", "../..aFile2", "../...aFile2", "../bDir1/", "../bDir2/", "../cFile1", "../cFile2"}, ""}, 32 | {"dir", "test/test_tree/bDir1", "../.", []string{".././", "../../", "../.aFile2", "../..aFile2", "../...aFile2"}, ""}, 33 | {"error", "x", "", []string{}, "open x: no such file or directory"}, 34 | {"error", "test/test_tree/aFile1", "", []string{}, "not a directory"}, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | got, gotErr := listDir(tt.dirname, tt.prefix) 39 | if gotErr == nil && tt.err != "" { 40 | t.Errorf("getFileList() got = '%v', want '%v'", gotErr, tt.err) 41 | } 42 | r, err := regexp.Compile(tt.err) 43 | if err != nil { 44 | t.Fatalf("bad regex in test: %s", err) 45 | } 46 | if gotErr != nil && !r.MatchString(gotErr.Error()) { 47 | t.Errorf("getFileList() got = '%s', want '%s'", gotErr.Error(), tt.err) 48 | } 49 | if !reflect.DeepEqual(got, tt.list) { 50 | t.Errorf("getFileList() got = %v, want %v", got, tt.list) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func TestSortForCompletion(t *testing.T) { 57 | tests := []struct { 58 | name string 59 | list []string 60 | sorted []string 61 | }{ 62 | {"basic", []string{"b", "a"}, []string{"a", "b"}}, 63 | {"level up", []string{"..", ".", "a"}, []string{".", "..", "a"}}, 64 | {"level up", []string{".", "..", "a"}, []string{".", "..", "a"}}, 65 | {"level up", []string{"a", ".", ".."}, []string{".", "..", "a"}}, 66 | {"level up", []string{"../", "./"}, []string{"./", "../"}}, 67 | {"level up", []string{"../../", ".././"}, []string{".././", "../../"}}, 68 | } 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | sortForCompletion(tt.list) 72 | if !reflect.DeepEqual(tt.list, tt.sorted) { 73 | t.Errorf("sortForCompletion() got = %v, want %v", tt.list, tt.sorted) 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /testhelpers_test.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package getoptions 10 | 11 | import ( 12 | "bytes" 13 | "encoding/json" 14 | "errors" 15 | "fmt" 16 | "os" 17 | "testing" 18 | ) 19 | 20 | func checkError(t *testing.T, got, expected error) { 21 | t.Helper() 22 | if (got == nil && expected != nil) || (got != nil && expected == nil) || (got != nil && expected != nil && !errors.Is(got, expected)) { 23 | t.Errorf("wrong error received: got = '%#v', want '%#v'", got, expected) 24 | } 25 | } 26 | 27 | func setupLogging() *bytes.Buffer { 28 | s := "" 29 | buf := bytes.NewBufferString(s) 30 | Logger.SetOutput(buf) 31 | return buf 32 | } 33 | 34 | // setupTestLogging - Defines an output for the default Logger and returns a 35 | // function that prints the output if the output is not empty. 36 | // 37 | // Usage: 38 | // 39 | // logTestOutput := setupTestLogging(t) 40 | // defer logTestOutput() 41 | func setupTestLogging(t *testing.T) func() { 42 | s := "" 43 | buf := bytes.NewBufferString(s) 44 | Logger.SetOutput(buf) 45 | return func() { 46 | if len(buf.String()) > 0 { 47 | t.Log("\n" + buf.String()) 48 | } 49 | } 50 | } 51 | 52 | // Test helper to compare two string outputs and find the first difference 53 | func firstDiff(got, expected string) string { 54 | same := "" 55 | for i, gc := range got { 56 | if len([]rune(expected)) <= i { 57 | return fmt.Sprintf("got:\n%s\nIndex: %d | diff: got '%s' - exp '%s'\n", got, len(expected), got, expected) 58 | } 59 | if gc != []rune(expected)[i] { 60 | return fmt.Sprintf("got:\n%s\nIndex: %d | diff: got '%c' - exp '%c'\n%s\n", got, i, gc, []rune(expected)[i], same) 61 | } 62 | same += string(gc) 63 | } 64 | if len(expected) > len(got) { 65 | return fmt.Sprintf("got:\n%s\nIndex: %d | diff: got '%s' - exp '%s'\n", got, len(got), got, expected) 66 | } 67 | return "" 68 | } 69 | 70 | func getNode(tree *programTree, element ...string) (*programTree, error) { 71 | if len(element) == 0 { 72 | return tree, nil 73 | } 74 | if child, ok := tree.ChildCommands[element[0]]; ok { 75 | return getNode(child, element[1:]...) 76 | } 77 | return tree, fmt.Errorf("not found") 78 | } 79 | 80 | func stringPT(n *programTree) string { 81 | data, err := json.MarshalIndent(n.str(), "", " ") 82 | if err != nil { 83 | return "" 84 | } 85 | return string(data) 86 | } 87 | 88 | func programTreeError(expected, got *programTree) string { 89 | return fmt.Sprintf("expected:\n%s\ngot:\n%s\n", stringPT(expected), stringPT(got)) 90 | } 91 | 92 | func spewToFileDiff(t *testing.T, expected, got interface{}) string { 93 | return fmt.Sprintf("expected, got: %s %s\n", spewToFile(t, expected, "expected"), spewToFile(t, got, "got")) 94 | } 95 | 96 | func spewToFile(t *testing.T, e interface{}, label string) string { 97 | f, err := os.CreateTemp("/tmp/", "spew-") 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | defer f.Close() 102 | _, _ = f.WriteString(label + "\n") 103 | fmt.Fprintf(f, "%v\n", e) 104 | return f.Name() 105 | } 106 | -------------------------------------------------------------------------------- /dag/README.adoc: -------------------------------------------------------------------------------- 1 | = Directed Acyclic Graph Build System 2 | 3 | image:https://pkg.go.dev/badge/github.com/DavidGamba/go-getoptions/dag.svg["Go Reference", link="https://pkg.go.dev/github.com/DavidGamba/go-getoptions/dag"] 4 | 5 | Lightweight Directed Acyclic Graph (DAG) Build System. 6 | 7 | It allows building a list of tasks and then running the tasks in different DAG trees. 8 | 9 | The tree dependencies are calculated and tasks that have met their dependencies are run in parallel. 10 | There is an option to run it serially for cases where user interaction is required. 11 | 12 | == Usage 13 | 14 | A detailed example can be found in link:../examples/dag/main.go[] 15 | 16 | To see what the build system would do run: 17 | 18 | ---- 19 | $ go run main.go build --quiet --dot 20 | 21 | digraph G { 22 | label = "build graph"; 23 | rankdir = TB; 24 | "bt3"; 25 | "bt1"; 26 | "bt3" -> "bt1"; 27 | "bt2"; 28 | "bt3" -> "bt2"; 29 | } 30 | ---- 31 | 32 | ---- 33 | $ go run main.go clean --quiet --dot 34 | 35 | digraph G { 36 | label = "clean graph"; 37 | rankdir = TB; 38 | "ct1"; 39 | "ct3"; 40 | "ct1" -> "ct3"; 41 | "ct2"; 42 | "ct2" -> "ct3"; 43 | } 44 | ---- 45 | 46 | ---- 47 | $ go run main.go 48 | SYNOPSIS: 49 | main [--dot] [--help|-?] [--quiet] [] 50 | 51 | COMMANDS: 52 | build build project artifacts 53 | clean clean project artifacts 54 | help Use 'main help ' for extra details. 55 | 56 | OPTIONS: 57 | --dot Generate graphviz dot diagram (default: false) 58 | 59 | --help|-? (default: false) 60 | 61 | --quiet (default: false) 62 | 63 | Use 'main help ' for extra details. 64 | exit status 1 65 | ---- 66 | 67 | == Motivation 68 | 69 | I want something better than Bash scripts and Makefiles but not as limiting as Bazel, Buck and Please (never tried Pants but they are all inspired by Blaze). 70 | Scons and Gradle are hard to work with. 71 | 72 | Mage is awesome but the first thing I saw a developer do with it was run it in a debugger which is not possible with the current design. 73 | Mage builds a separate go file that it then compiles and runs. It also relies on panic/recover for its control flow. 74 | 75 | This build system leverages go-getoptions and a DAG tree to provide an alternative. 76 | 77 | == Build System 78 | 79 | In order to have a build system you need a couple of pieces: 80 | 81 | * Define task dependencies (This package takes care of that). 82 | It builds a tree for you that it then runs in parallel when possible. 83 | 84 | * Define target and sources dependencies. 85 | In other words, if my sources have changed I need to rebuild my targets. 86 | Use the https://github.com/DavidGamba/dgtools/tree/master/fsmodtime["github.com/DavidGamba/dgtools/fsmodtime"] package. 87 | 88 | * Task idempotency. 89 | This so you can run your build system tasks over and over without risk. 90 | This one is on you! 91 | 92 | Finally, having an easy to use `os/exec` wrapper also helps a lot: https://github.com/DavidGamba/dgtools/tree/master/run["github.com/DavidGamba/dgtools/run"] 93 | 94 | == ROADMAP 95 | 96 | * Allow changing the ticker duration 97 | * Add message every 30 seconds on what task is running. 98 | 99 | == License 100 | 101 | This file is part of go-getoptions. 102 | 103 | Copyright (C) 2015-2025 David Gamba Rios 104 | 105 | This Source Code Form is subject to the terms of the Mozilla Public 106 | License, v. 2.0. If a copy of the MPL was not distributed with this 107 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 108 | -------------------------------------------------------------------------------- /examples/dag/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "time" 11 | 12 | "github.com/DavidGamba/go-getoptions" 13 | "github.com/DavidGamba/go-getoptions/dag" 14 | ) 15 | 16 | var TM *dag.TaskMap 17 | 18 | var Logger = log.New(os.Stderr, "", log.LstdFlags) 19 | 20 | func main() { 21 | os.Exit(program(os.Args)) 22 | } 23 | 24 | func program(args []string) int { 25 | opt := getoptions.New() 26 | opt.Bool("quiet", false) 27 | opt.Bool("dot", false, opt.Description("Generate graphviz dot diagram")) 28 | opt.SetUnknownMode(getoptions.Pass) 29 | opt.NewCommand("build", "build project artifacts").SetCommandFn(Build) 30 | opt.NewCommand("clean", "clean project artifacts").SetCommandFn(Clean) 31 | opt.HelpCommand("help", opt.Alias("?")) 32 | remaining, err := opt.Parse(args[1:]) 33 | if err != nil { 34 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 35 | return 1 36 | } 37 | if opt.Called("quiet") { 38 | Logger.SetOutput(io.Discard) 39 | } 40 | 41 | ctx, cancel, done := getoptions.InterruptContext() 42 | defer func() { cancel(); <-done }() 43 | 44 | TM = dag.NewTaskMap() 45 | TM.Add("bt1", buildTask1) 46 | TM.Add("bt2", buildTask2) 47 | TM.Add("bt3", buildTask3) 48 | TM.Add("ct1", cleanTask1) 49 | TM.Add("ct2", cleanTask2) 50 | TM.Add("ct3", cleanTask3) 51 | 52 | err = opt.Dispatch(ctx, remaining) 53 | if err != nil { 54 | if errors.Is(err, getoptions.ErrorHelpCalled) { 55 | return 1 56 | } 57 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 58 | if errors.Is(err, getoptions.ErrorParsing) { 59 | fmt.Fprintf(os.Stderr, "\n"+opt.Help()) 60 | } 61 | return 1 62 | } 63 | return 0 64 | } 65 | 66 | func Build(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 67 | Logger.Printf("Running build command") 68 | g := dag.NewGraph("build graph") 69 | g.TaskDependsOn(TM.Get("bt3"), TM.Get("bt1"), TM.Get("bt2")) 70 | err := g.Validate(TM) 71 | if err != nil { 72 | return err 73 | } 74 | if opt.Called("dot") { 75 | fmt.Printf("%s\n", g) 76 | return nil 77 | } 78 | return g.Run(ctx, opt, args) 79 | } 80 | 81 | func Clean(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 82 | Logger.Printf("Running clean command") 83 | g := dag.NewGraph("clean graph") 84 | g.TaskDependsOn(TM.Get("ct1"), TM.Get("ct3")) 85 | g.TaskDependsOn(TM.Get("ct2"), TM.Get("ct3")) 86 | err := g.Validate(TM) 87 | if err != nil { 88 | return err 89 | } 90 | if opt.Called("dot") { 91 | fmt.Printf("%s\n", g) 92 | return nil 93 | } 94 | return g.Run(ctx, opt, args) 95 | } 96 | 97 | func buildTask1(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 98 | fmt.Printf("Build first artifact\n") 99 | time.Sleep(1 * time.Second) 100 | return nil 101 | } 102 | 103 | func buildTask2(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 104 | fmt.Printf("Build second artifact\n") 105 | time.Sleep(1 * time.Second) 106 | return nil 107 | } 108 | 109 | func buildTask3(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 110 | fmt.Printf("Build third artifact, depends on 1 and 2\n") 111 | time.Sleep(1 * time.Second) 112 | return nil 113 | } 114 | 115 | func cleanTask1(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 116 | fmt.Printf("Clean first artifact, 3 must not exist\n") 117 | time.Sleep(1 * time.Second) 118 | return nil 119 | } 120 | 121 | func cleanTask2(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 122 | fmt.Printf("Clean second artifact, 3 must not exist\n") 123 | time.Sleep(1 * time.Second) 124 | return nil 125 | } 126 | 127 | func cleanTask3(ctx context.Context, opt *getoptions.GetOpt, args []string) error { 128 | fmt.Printf("Clean third artifact\n") 129 | time.Sleep(1 * time.Second) 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /text/variables.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | // Package text - User facing strings. 10 | package text 11 | 12 | // ErrorMissingArgument holds the text for missing argument error. 13 | // It has a string placeholder '%s' for the name of the option missing the argument. 14 | var ErrorMissingArgument = "Missing argument for option '%s'!" 15 | 16 | // ErrorAmbiguousArgument holds the text for ambiguous argument error. 17 | // It has a string placeholder '%s' for the passed option and a []string list of matches. 18 | var ErrorAmbiguousArgument = "Ambiguous option '%s', matches %v!" 19 | 20 | // ErrorMissingRequiredOption holds the text for missing required option error. 21 | // It has a string placeholder '%s' for the name of the missing option. 22 | var ErrorMissingRequiredOption = "Missing required parameter '%s'" 23 | 24 | var ErrorMissingRequiredArgument = "ERROR: Missing required argument" 25 | 26 | var ErrorMissingRequiredNamedArgument = "ERROR: Missing %s" 27 | 28 | // ErrorArgumentIsNotKeyValue holds the text for Map type options where the argument is not of key=value type. 29 | // It has a string placeholder '%s' for the name of the option missing the argument. 30 | var ErrorArgumentIsNotKeyValue = "Argument error for option '%s': Should be of type 'key=value'!" 31 | 32 | // ErrorArgumentWithDash holds the text for missing argument error in cases where the next argument looks like an option (starts with '-'). 33 | // It has a string placeholder '%s' for the name of the option missing the argument. 34 | var ErrorArgumentWithDash = "Missing argument for option '%s'!\n" + 35 | "If passing arguments that start with '-' use --option=-argument" 36 | 37 | // ErrorConvertToInt holds the text for Int Coversion argument error. 38 | // It has two string placeholders ('%s'). The first one for the name of the option with the wrong argument and the second one for the argument that could not be converted. 39 | var ErrorConvertToInt = "Argument error for option '%s': Can't convert string to int: '%s'" 40 | 41 | var ErrorConvertArgumentToInt = "Argument error: Can't convert string to int: '%s'" 42 | 43 | // ErrorConvertToFloat64 holds the text for Float64 Coversion argument error. 44 | // It has two string placeholders ('%s'). The first one for the name of the option with the wrong argument and the second one for the argument that could not be converted. 45 | var ErrorConvertToFloat64 = "Argument error for option '%s': Can't convert string to float64: '%s'" 46 | 47 | var ErrorConvertArgumentToFloat64 = "Argument error: Can't convert string to float64: '%s'" 48 | 49 | // WarningOnUnknown holds the text for the unknown option message. 50 | // It has a string placeholder '%s' for the name of the option missing the argument. 51 | // This one includes the WARNING prefix because it is printed directly to the Writer output. 52 | var WarningOnUnknown = "WARNING: Unknown option '%s'" 53 | 54 | var MessageOnUnknown = "Unknown option '%s'" 55 | 56 | // MessageOnInterrupt holds the text for the message to be printed when an interrupt is received. 57 | var MessageOnInterrupt = "Interrupt signal received" 58 | 59 | // HelpNameHeader holds the header text for the command name 60 | var HelpNameHeader = "NAME" 61 | 62 | // HelpSynopsisHeader holds the header text for the synopsis 63 | var HelpSynopsisHeader = "SYNOPSIS" 64 | 65 | // HelpCommandsHeader holds the header text for the command list 66 | var HelpCommandsHeader = "COMMANDS" 67 | 68 | // HelpRequiredOptionsHeader holds the header text for the required parameters 69 | var HelpRequiredOptionsHeader = "REQUIRED PARAMETERS" 70 | 71 | // HelpArgumentsHeader holds the header text for the argument list 72 | var HelpArgumentsHeader = "ARGUMENTS" 73 | 74 | // HelpOptionsHeader holds the header text for the option list 75 | var HelpOptionsHeader = "OPTIONS" 76 | -------------------------------------------------------------------------------- /documentation.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | /* 10 | Package getoptions - Go option parser inspired on the flexibility of Perl’s 11 | GetOpt::Long. 12 | 13 | It will operate on any given slice of strings and return the remaining (non 14 | used) command line arguments. This allows to easily subcommand. 15 | 16 | See https://github.com/DavidGamba/go-getoptions for extra documentation details. 17 | 18 | # Features 19 | 20 | • Allow passing options and non-options in any order. 21 | 22 | • Support for `--long` options. 23 | 24 | • Support for short (`-s`) options with flexible behaviour (see https://github.com/DavidGamba/go-getoptions#operation_modes for details): 25 | 26 | - Normal (default) 27 | - Bundling 28 | - SingleDash 29 | 30 | • `Called()` method indicates if the option was passed on the command line. 31 | 32 | • Multiple aliases for the same option. e.g. `help`, `man`. 33 | 34 | • `CalledAs()` method indicates what alias was used to call the option on the command line. 35 | 36 | • Simple synopsis and option list automated help. 37 | 38 | • Boolean, String, Int and Float64 type options. 39 | 40 | • Options with Array arguments. 41 | The same option can be used multiple times with different arguments. 42 | The list of arguments will be saved into an Array like structure inside the program. 43 | 44 | • Options with array arguments and multiple entries. 45 | For example: `color --rgb 10 20 30 --next-option` 46 | 47 | • When using integer array options with multiple arguments, positive integer ranges are allowed. 48 | For example: `1..3` to indicate `1 2 3`. 49 | 50 | • Options with key value arguments and multiple entries. 51 | 52 | • Options with Key Value arguments. 53 | This allows the same option to be used multiple times with arguments of key value type. 54 | For example: `rpmbuild --define name=myrpm --define version=123`. 55 | 56 | • Supports passing `--` to stop parsing arguments (everything after will be left in the `remaining []string`). 57 | 58 | • Supports command line options with '='. 59 | For example: You can use `--string=mystring` and `--string mystring`. 60 | 61 | • Allows passing arguments to options that start with dash `-` when passed after equal. 62 | For example: `--string=--hello` and `--int=-123`. 63 | 64 | • Options with optional arguments. 65 | If the default argument is not passed the default is set. 66 | For example: You can call `--int 123` which yields `123` or `--int` which yields the given default. 67 | 68 | • Allows abbreviations when the provided option is not ambiguous. 69 | For example: An option called `build` can be called with `--b`, `--bu`, `--bui`, `--buil` and `--build` as long as there is no ambiguity. 70 | In the case of ambiguity, the shortest non ambiguous combination is required. 71 | 72 | • Support for the lonesome dash "-". 73 | To indicate, for example, when to read input from STDIO. 74 | 75 | • Incremental options. 76 | Allows the same option to be called multiple times to increment a counter. 77 | 78 | • Supports case sensitive options. 79 | For example, you can use `v` to define `verbose` and `V` to define `Version`. 80 | 81 | • Support indicating if an option is required and allows overriding default error message. 82 | 83 | • Errors exposed as public variables to allow overriding them for internationalization. 84 | 85 | • Supports subcommands (stop parsing arguments when non option is passed). 86 | 87 | • Multiple ways of managing unknown options: 88 | - Fail on unknown (default). 89 | - Warn on unknown. 90 | - Pass through, allows for subcommands and can be combined with Require Order. 91 | 92 | • Require order: Allows for subcommands. Stop parsing arguments when the first non-option is found. 93 | When mixed with Pass through, it also stops parsing arguments when the first unmatched option is found. 94 | 95 | # Panic 96 | 97 | The library will panic if it finds that the programmer (not end user): 98 | 99 | • Defined the same alias twice. 100 | 101 | • Defined wrong min and max values for SliceMulti methods. 102 | */ 103 | package getoptions 104 | -------------------------------------------------------------------------------- /isoption.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package getoptions 10 | 11 | import ( 12 | "regexp" 13 | "strings" 14 | ) 15 | 16 | // 1: leading dashes 17 | // 2: option 18 | // 3: =arg 19 | var isOptionRegex = regexp.MustCompile(`^(--?)([^=]+)(.*?)$`) 20 | 21 | // 1: leading dashes or / 22 | // 2: option 23 | // 3: =arg or :arg 24 | var isOptionRegexWindows = regexp.MustCompile(`^(--?|/)([^=:]+)(.*?)$`) 25 | 26 | type optionPair struct { 27 | Option string 28 | // We allow multiple args in case of splitting on comma. 29 | // TODO: Verify where we are handling integer ranges (1..10) and maybe move that logic here as well. 30 | Args []string 31 | } 32 | 33 | /* 34 | isOption - Check if the given string is an option (starts with - or --, or / when windows support mode is set). 35 | Return the option(s) without the starting dash and their argument if the string contained one. 36 | The behaviour changes depending on the mode: normal, bundling or singleDash. 37 | Also, handle the single dash '-' especial option. 38 | 39 | The options are returned as pairs of options and arguments 40 | At this level we don't aggregate results in case we have -- and then other options, basically we can parse one option at a time. 41 | This makes the caller have to aggregate multiple calls to the same option. 42 | 43 | When windows support mode is set all short options `-`, long options `--` and Windows `/` options are allowed. 44 | Windows support mode also adds : as a valid argument indicator. 45 | For example: /baudrate:115200 /baudrate=115200 --baudrate=115200 --baudrate:115200 are all valid. 46 | */ 47 | func isOption(s string, mode Mode, windows bool) ([]optionPair, bool) { 48 | // Handle especial cases 49 | switch s { 50 | case "--": 51 | // Option parsing termination (--) is not identified by isOption as an option. 52 | // It is the caller's responsibility. 53 | return []optionPair{{Option: "--"}}, false 54 | case "-": 55 | return []optionPair{{Option: "-"}}, true 56 | } 57 | 58 | var match []string 59 | if windows { 60 | match = isOptionRegexWindows.FindStringSubmatch(s) 61 | } else { 62 | match = isOptionRegex.FindStringSubmatch(s) 63 | } 64 | if len(match) > 0 { 65 | // check long option 66 | if match[1] == "--" || match[1] == "/" { 67 | opt := optionPair{} 68 | opt.Option = match[2] 69 | var args string 70 | if strings.HasPrefix(match[3], "=") { 71 | args = strings.TrimPrefix(match[3], "=") 72 | } else if strings.HasPrefix(match[3], ":") { 73 | args = strings.TrimPrefix(match[3], ":") 74 | } 75 | if args != "" { 76 | // TODO: Here is where we could split on comma 77 | opt.Args = []string{args} 78 | } 79 | return []optionPair{opt}, true 80 | } 81 | // check short option 82 | switch mode { 83 | case Bundling: 84 | opts := []optionPair{} 85 | for _, option := range strings.Split(match[2], "") { 86 | opt := optionPair{} 87 | opt.Option = option 88 | opts = append(opts, opt) 89 | } 90 | if len(opts) > 0 { 91 | var args string 92 | if strings.HasPrefix(match[3], "=") { 93 | args = strings.TrimPrefix(match[3], "=") 94 | } else if strings.HasPrefix(match[3], ":") { 95 | args = strings.TrimPrefix(match[3], ":") 96 | } 97 | if args != "" { 98 | opts[len(opts)-1].Args = []string{args} 99 | } 100 | } 101 | return opts, true 102 | case SingleDash: 103 | opts := []optionPair{{Option: string([]rune(match[2])[0])}} 104 | if len(match[2]) > 1 || len(match[3]) > 0 { 105 | args := string([]rune(match[2])[1:]) + match[3] 106 | opts[0].Args = []string{args} 107 | } 108 | return opts, true 109 | default: 110 | opt := optionPair{} 111 | opt.Option = match[2] 112 | var args string 113 | if strings.HasPrefix(match[3], "=") { 114 | args = strings.TrimPrefix(match[3], "=") 115 | } else if strings.HasPrefix(match[3], ":") { 116 | args = strings.TrimPrefix(match[3], ":") 117 | } 118 | if args != "" { 119 | opt.Args = []string{args} 120 | } 121 | return []optionPair{opt}, true 122 | } 123 | } 124 | return []optionPair{}, false 125 | } 126 | -------------------------------------------------------------------------------- /example_minimal_test.go: -------------------------------------------------------------------------------- 1 | // These examples demonstrate more intricate uses of the go-getoptions package. 2 | package getoptions_test 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | 8 | "github.com/DavidGamba/go-getoptions" // As getoptions 9 | ) 10 | 11 | func ExampleGetOpt_Alias() { 12 | opt := getoptions.New() 13 | opt.Bool("help", false, opt.Alias("?")) 14 | _, _ = opt.Parse([]string{"-?"}) 15 | 16 | if opt.Called("help") { 17 | fmt.Println("help called") 18 | } 19 | 20 | if opt.CalledAs("help") == "?" { 21 | fmt.Println("help called as ?") 22 | } 23 | 24 | // Output: 25 | // help called 26 | // help called as ? 27 | } 28 | 29 | func ExampleGetOpt_ArgName() { 30 | opt := getoptions.New() 31 | opt.Bool("help", false, opt.Alias("?")) 32 | opt.String("host-default", "") 33 | opt.String("host-custom", "", opt.ArgName("hostname")) 34 | _, _ = opt.Parse([]string{"-?"}) 35 | 36 | if opt.Called("help") { 37 | fmt.Println(opt.Help()) 38 | } 39 | // Output: 40 | // SYNOPSIS: 41 | // go-getoptions.test [--help|-?] [--host-custom ] 42 | // [--host-default ] [] 43 | // 44 | // OPTIONS: 45 | // --help|-? (default: false) 46 | // 47 | // --host-custom (default: "") 48 | // 49 | // --host-default (default: "") 50 | } 51 | 52 | func ExampleGetOpt_Bool() { 53 | opt := getoptions.New() 54 | opt.Bool("help", false, opt.Alias("?")) 55 | _, _ = opt.Parse([]string{"-?"}) 56 | 57 | if opt.Called("help") { 58 | fmt.Println(opt.Help()) 59 | } 60 | 61 | // Output: 62 | // SYNOPSIS: 63 | // go-getoptions.test [--help|-?] [] 64 | // 65 | // OPTIONS: 66 | // --help|-? (default: false) 67 | } 68 | 69 | func ExampleGetOpt_BoolVar() { 70 | var help bool 71 | opt := getoptions.New() 72 | opt.BoolVar(&help, "help", false, opt.Alias("?")) 73 | _, _ = opt.Parse([]string{"-?"}) 74 | 75 | if help { 76 | fmt.Println(opt.Help()) 77 | } 78 | 79 | // Output: 80 | // SYNOPSIS: 81 | // go-getoptions.test [--help|-?] [] 82 | // 83 | // OPTIONS: 84 | // --help|-? (default: false) 85 | } 86 | 87 | func ExampleGetOpt_Called() { 88 | opt := getoptions.New() 89 | opt.Bool("help", false, opt.Alias("?")) 90 | _, _ = opt.Parse([]string{"-?"}) 91 | 92 | if opt.Called("help") { 93 | fmt.Println("help called") 94 | } 95 | 96 | if opt.CalledAs("help") == "?" { 97 | fmt.Println("help called as ?") 98 | } 99 | 100 | // Output: 101 | // help called 102 | // help called as ? 103 | } 104 | 105 | func ExampleGetOpt_CalledAs() { 106 | opt := getoptions.New() 107 | opt.Bool("help", false, opt.Alias("?")) 108 | _, _ = opt.Parse([]string{"-?"}) 109 | 110 | if opt.Called("help") { 111 | fmt.Println("help called") 112 | } 113 | 114 | if opt.CalledAs("help") == "?" { 115 | fmt.Println("help called as ?") 116 | } 117 | 118 | // Output: 119 | // help called 120 | // help called as ? 121 | } 122 | 123 | func ExampleGetOpt_Description() { 124 | opt := getoptions.New() 125 | opt.Bool("help", false, opt.Alias("?"), opt.Description("Show help.")) 126 | opt.String("hostname", "golang.org", opt.ArgName("host|IP"), opt.Description("Hostname to use.")) 127 | opt.String("user", "", opt.ArgName("user_id"), opt.Required(), opt.Description("User to login as.")) 128 | opt.HelpSynopsisArg("[]", "File with hostnames.") 129 | _, _ = opt.Parse([]string{"-?"}) 130 | 131 | if opt.Called("help") { 132 | fmt.Println(opt.Help()) 133 | } 134 | // Output: 135 | // SYNOPSIS: 136 | // go-getoptions.test --user [--help|-?] [--hostname ] 137 | // [] 138 | // 139 | // ARGUMENTS: 140 | // [] File with hostnames. 141 | // 142 | // REQUIRED PARAMETERS: 143 | // --user User to login as. 144 | // 145 | // OPTIONS: 146 | // --help|-? Show help. (default: false) 147 | // 148 | // --hostname Hostname to use. (default: "golang.org") 149 | } 150 | 151 | func ExampleGetOpt_GetEnv() { 152 | os.Setenv("_AWS_PROFILE", "production") 153 | 154 | var profile string 155 | opt := getoptions.New() 156 | opt.StringVar(&profile, "profile", "default", opt.GetEnv("_AWS_PROFILE")) 157 | _, _ = opt.Parse([]string{}) 158 | 159 | fmt.Println(profile) 160 | os.Unsetenv("_AWS_PROFILE") 161 | 162 | // Output: 163 | // production 164 | } 165 | -------------------------------------------------------------------------------- /internal/completion/file.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package completion 10 | 11 | import ( 12 | "os" 13 | "path/filepath" 14 | "sort" 15 | "strings" 16 | ) 17 | 18 | // readDirNoSort - Same as ioutil/ReadDir but doesn't sort results. 19 | // 20 | // Taken from https://golang.org/src/io/ioutil/ioutil.go 21 | // Copyright 2009 The Go Authors. All rights reserved. 22 | // Use of this source code is governed by a BSD-style 23 | // license that can be found in the LICENSE file. 24 | func readDirNoSort(dirname string) ([]os.FileInfo, error) { 25 | f, err := os.Open(dirname) 26 | if err != nil { 27 | return nil, err 28 | } 29 | list, err := f.Readdir(-1) 30 | f.Close() 31 | if err != nil { 32 | return nil, err 33 | } 34 | return list, nil 35 | } 36 | 37 | // trimLeftDots - Given a string it trims the leading dots (".") and returns a count of how many were removed. 38 | func trimLeftDots(s string) (int, string) { 39 | charFound := false 40 | count := 0 41 | return count, strings.TrimLeftFunc(s, func(r rune) bool { 42 | if !charFound && r == '.' { 43 | count++ 44 | return true 45 | } 46 | return false 47 | }) 48 | } 49 | 50 | // trimLeftDashes - Given a string it trims the leading dashes ("-") and returns a count of how many were removed. 51 | func trimLeftDashes(s string) (int, string) { 52 | charFound := false 53 | count := 0 54 | return count, strings.TrimLeftFunc(s, func(r rune) bool { 55 | if !charFound && r == '-' { 56 | count++ 57 | return true 58 | } 59 | return false 60 | }) 61 | } 62 | 63 | // sortForCompletion - Places hidden files in the same sort position as their non hidden counterparts. 64 | // Also used for sorting options in the same fashion. 65 | // Example: 66 | // 67 | // file.txt 68 | // .file.txt.~ 69 | // .hidden.txt 70 | // ..hidden.txt.~ 71 | // 72 | // -d 73 | // --debug 74 | // -h 75 | // --help 76 | func sortForCompletion(list []string) { 77 | sort.Slice(list, 78 | func(i, j int) bool { 79 | var a, b string 80 | if filepath.Dir(list[i]) == filepath.Dir(list[j]) { 81 | a = filepath.Base(list[i]) 82 | b = filepath.Base(list[j]) 83 | } else { 84 | a = list[i] 85 | b = list[j] 86 | } 87 | 88 | // . always is less 89 | if filepath.Base(list[i]) == "." { 90 | return true 91 | } 92 | if filepath.Base(list[j]) == "." { 93 | return false 94 | } 95 | // .. is always less in any other case 96 | if filepath.Base(list[i]) == ".." { 97 | return true 98 | } 99 | if filepath.Base(list[j]) == ".." { 100 | return false 101 | } 102 | 103 | an, a := trimLeftDots(a) 104 | bn, b := trimLeftDots(b) 105 | if a == b { 106 | return an < bn 107 | } 108 | an, a = trimLeftDashes(a) 109 | bn, b = trimLeftDashes(b) 110 | if a == b { 111 | return an < bn 112 | } 113 | return a < b 114 | }) 115 | } 116 | 117 | // listDir - Given a dir and a prefix returns a list of files in the dir filtered by their prefix. 118 | // NOTE: dot (".") is a valid dirname. 119 | func listDir(dirname string, prefix string) ([]string, error) { 120 | filenames := []string{} 121 | usedDirname := dirname 122 | dir := "" 123 | if strings.Contains(prefix, "/") { 124 | dir = filepath.Dir(prefix) + string(os.PathSeparator) 125 | prefix = strings.TrimPrefix(prefix, dir) 126 | usedDirname = filepath.Join(dirname, dir) + string(os.PathSeparator) 127 | } 128 | switch prefix { 129 | case ".": 130 | filenames = append(filenames, dir+"./") 131 | filenames = append(filenames, dir+"../") 132 | case "..": 133 | filenames = append(filenames, dir+"../") 134 | } 135 | fileInfoList, err := readDirNoSort(usedDirname) 136 | if err != nil { 137 | Debug.Printf("listDir - dirname %s, prefix %s > files %v\n", dirname, prefix, filenames) 138 | return filenames, err 139 | } 140 | for _, fi := range fileInfoList { 141 | name := fi.Name() 142 | if !strings.HasPrefix(name, prefix) { 143 | continue 144 | } 145 | if dirname != usedDirname { 146 | name = filepath.Join(dir, name) 147 | } 148 | if fi.IsDir() { 149 | filenames = append(filenames, name+"/") 150 | } else { 151 | filenames = append(filenames, name) 152 | } 153 | } 154 | sortForCompletion(filenames) 155 | if len(filenames) == 1 && strings.HasSuffix(filenames[0], "/") { 156 | filenames = append(filenames, filenames[0]+" ") 157 | } 158 | Debug.Printf("listDir - dirname %s, prefix %s > files %v\n", dirname, prefix, filenames) 159 | return filenames, err 160 | } 161 | -------------------------------------------------------------------------------- /user_help.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package getoptions 10 | 11 | import ( 12 | "context" 13 | "fmt" 14 | 15 | "github.com/DavidGamba/go-getoptions/internal/help" 16 | "github.com/DavidGamba/go-getoptions/internal/option" 17 | ) 18 | 19 | // HelpSection - Indicates what portion of the help to return. 20 | type HelpSection int 21 | 22 | // Help Output Types 23 | const ( 24 | HelpNone HelpSection = iota 25 | helpDefaultName 26 | HelpName 27 | HelpSynopsis 28 | HelpCommandList 29 | HelpOptionList 30 | HelpCommandInfo 31 | ) 32 | 33 | func getCurrentNodeName(n *programTree) string { 34 | if n.Parent != nil { 35 | parentName := getCurrentNodeName(n.Parent) 36 | return fmt.Sprintf("%s %s", parentName, n.Name) 37 | } 38 | return n.Name 39 | } 40 | 41 | // Help - Default help string that is composed of all available sections. 42 | func (gopt *GetOpt) Help(sections ...HelpSection) string { 43 | if gopt.finalNode != nil { 44 | return helpOutput(gopt.finalNode, sections...) 45 | } 46 | return helpOutput(gopt.programTree, sections...) 47 | } 48 | 49 | func helpOutput(node *programTree, sections ...HelpSection) string { 50 | if len(sections) == 0 { 51 | // Print all in the following order 52 | sections = []HelpSection{helpDefaultName, HelpSynopsis, HelpCommandList, HelpOptionList, HelpCommandInfo} 53 | } 54 | helpTxt := "" 55 | 56 | scriptName := getCurrentNodeName(node) 57 | 58 | options := []*option.Option{} 59 | for k, option := range node.ChildOptions { 60 | // filter out aliases 61 | if k != option.Name { 62 | continue 63 | } 64 | options = append(options, option) 65 | } 66 | 67 | for _, section := range sections { 68 | switch section { 69 | // Default name only prints name if the name or description is set. 70 | // The explicit type always prints it. 71 | case helpDefaultName: 72 | if node.Parent != nil || node.Description != "" { 73 | helpTxt += help.Name("", scriptName, node.Description) 74 | helpTxt += "\n" 75 | } 76 | case HelpName: 77 | helpTxt += help.Name("", scriptName, node.Description) 78 | helpTxt += "\n" 79 | case HelpSynopsis: 80 | commands := []string{} 81 | for _, command := range node.ChildCommands { 82 | if command.Name == node.HelpCommandName { 83 | continue 84 | } 85 | commands = append(commands, command.Name) 86 | } 87 | helpTxt += help.Synopsis("", scriptName, node.SynopsisArgs, options, commands) 88 | helpTxt += "\n" 89 | case HelpCommandList: 90 | m := make(map[string]string) 91 | for _, command := range node.ChildCommands { 92 | if command.Name == node.HelpCommandName { 93 | continue 94 | } 95 | m[command.Name] = command.Description 96 | } 97 | commands := help.CommandList(m) 98 | if commands != "" { 99 | helpTxt += commands 100 | helpTxt += "\n" 101 | } 102 | case HelpOptionList: 103 | helpTxt += help.OptionList(node.SynopsisArgs, options) 104 | case HelpCommandInfo: 105 | // Index of 1 because when there is a child command, help is always injected 106 | if node.HelpCommandName != "" && len(node.ChildCommands) > 1 { 107 | helpTxt += fmt.Sprintf("Use '%s help ' for extra details.\n", scriptName) 108 | } 109 | } 110 | } 111 | 112 | return helpTxt 113 | } 114 | 115 | // HelpCommand - Declares a help command and a help option. 116 | // Additionally, it allows to define aliases to the help option. 117 | // 118 | // For example: 119 | // 120 | // opt.HelpCommand("help", opt.Description("show this help"), opt.Alias("?")) 121 | // 122 | // NOTE: Define after all other commands have been defined. 123 | func (gopt *GetOpt) HelpCommand(name string, fns ...ModifyFn) { 124 | // TODO: Think about panicking on double call to this method 125 | 126 | // Define help option 127 | gopt.Bool(name, false, fns...) 128 | 129 | cmdFn := func(parent *programTree) { 130 | suggestions := []string{} 131 | for k := range parent.ChildCommands { 132 | if k != name { 133 | suggestions = append(suggestions, k) 134 | } 135 | } 136 | cmd := &GetOpt{} 137 | command := &programTree{ 138 | Name: name, 139 | HelpCommandName: name, 140 | ChildCommands: map[string]*programTree{}, 141 | ChildOptions: map[string]*option.Option{}, 142 | Parent: parent, 143 | Level: parent.Level + 1, 144 | Suggestions: suggestions, 145 | } 146 | cmd.programTree = command 147 | parent.AddChildCommand(name, command) 148 | cmd.SetCommandFn(runHelp) 149 | cmd.HelpSynopsisArg("", "") 150 | } 151 | 152 | // set HelpCommandName 153 | runOnParentAndChildrenCommands(gopt.programTree, func(n *programTree) { 154 | n.HelpCommandName = name 155 | }) 156 | 157 | runOnParentAndChildrenCommands(gopt.programTree, func(n *programTree) { 158 | 159 | // Add help command to all commands that have children 160 | // if len(n.ChildCommands) > 0 && n.Name != name { 161 | 162 | // Add help command to all commands, including wrappers 163 | if n.Name != name { 164 | cmdFn(n) 165 | } 166 | }) 167 | 168 | copyOptionsFromParent(gopt.programTree) 169 | } 170 | 171 | func runHelp(ctx context.Context, opt *GetOpt, args []string) error { 172 | if len(args) > 0 { 173 | for _, command := range opt.programTree.Parent.ChildCommands { 174 | if command.Name == args[0] { 175 | fmt.Fprint(Writer, helpOutput(command)) 176 | return ErrorHelpCalled 177 | } 178 | } 179 | return fmt.Errorf("no help topic for '%s'", args[0]) 180 | } 181 | fmt.Fprint(Writer, helpOutput(opt.programTree.Parent)) 182 | return ErrorHelpCalled 183 | } 184 | 185 | func runOnParentAndChildrenCommands(parent *programTree, fn func(*programTree)) { 186 | fn(parent) 187 | for _, command := range parent.ChildCommands { 188 | runOnParentAndChildrenCommands(command, fn) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /isoption_test.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package getoptions 10 | 11 | import ( 12 | "reflect" 13 | "testing" 14 | ) 15 | 16 | func TestIsOption(t *testing.T) { 17 | cases := []struct { 18 | name string 19 | in string 20 | mode Mode 21 | optPair []optionPair 22 | is bool 23 | }{ 24 | {"lone dash", "-", Normal, []optionPair{{Option: "-"}}, true}, 25 | {"lone dash", "-", Bundling, []optionPair{{Option: "-"}}, true}, 26 | {"lone dash", "-", SingleDash, []optionPair{{Option: "-"}}, true}, 27 | 28 | {"double dash", "--", Normal, []optionPair{{Option: "--"}}, false}, 29 | {"double dash", "--", Bundling, []optionPair{{Option: "--"}}, false}, 30 | {"double dash", "--", SingleDash, []optionPair{{Option: "--"}}, false}, 31 | 32 | {"no option", "opt", Normal, []optionPair{}, false}, 33 | {"no option", "opt", Bundling, []optionPair{}, false}, 34 | {"no option", "opt", SingleDash, []optionPair{}, false}, 35 | 36 | {"Long option", "--opt", Normal, []optionPair{{Option: "opt"}}, true}, 37 | {"Long option", "--opt", Bundling, []optionPair{{Option: "opt"}}, true}, 38 | {"Long option", "--opt", SingleDash, []optionPair{{Option: "opt"}}, true}, 39 | 40 | {"Long option with arg", "--opt=arg", Normal, []optionPair{{Option: "opt", Args: []string{"arg"}}}, true}, 41 | {"Long option with arg", "--opt=arg", Bundling, []optionPair{{Option: "opt", Args: []string{"arg"}}}, true}, 42 | {"Long option with arg", "--opt=arg", SingleDash, []optionPair{{Option: "opt", Args: []string{"arg"}}}, true}, 43 | 44 | {"short option", "-opt", Normal, []optionPair{{Option: "opt"}}, true}, 45 | {"short option", "-opt", Bundling, []optionPair{{Option: "o"}, {Option: "p"}, {Option: "t"}}, true}, 46 | {"short option", "-opt", SingleDash, []optionPair{{Option: "o", Args: []string{"pt"}}}, true}, 47 | 48 | {"short option single letter", "-o", Normal, []optionPair{{Option: "o"}}, true}, 49 | {"short option single letter", "-o", Bundling, []optionPair{{Option: "o"}}, true}, 50 | {"short option single letter", "-o", SingleDash, []optionPair{{Option: "o"}}, true}, 51 | 52 | {"short option with arg", "-opt=arg", Normal, []optionPair{{Option: "opt", Args: []string{"arg"}}}, true}, 53 | {"short option with arg", "-opt=arg", Bundling, []optionPair{{Option: "o"}, {Option: "p"}, {Option: "t", Args: []string{"arg"}}}, true}, 54 | {"short option with arg", "-opt=arg", SingleDash, []optionPair{{Option: "o", Args: []string{"pt=arg"}}}, true}, 55 | } 56 | windowsCases := []struct { 57 | name string 58 | in string 59 | mode Mode 60 | optPair []optionPair 61 | is bool 62 | }{ 63 | {"Long option", "/opt", Normal, []optionPair{{Option: "opt"}}, true}, 64 | {"Long option", "/opt", Bundling, []optionPair{{Option: "opt"}}, true}, 65 | {"Long option", "/opt", SingleDash, []optionPair{{Option: "opt"}}, true}, 66 | 67 | {"Long option with arg", "/opt:arg", Normal, []optionPair{{Option: "opt", Args: []string{"arg"}}}, true}, 68 | {"Long option with arg", "/opt:arg", Bundling, []optionPair{{Option: "opt", Args: []string{"arg"}}}, true}, 69 | {"Long option with arg", "/opt:arg", SingleDash, []optionPair{{Option: "opt", Args: []string{"arg"}}}, true}, 70 | {"Long option with arg", "/opt=arg", Normal, []optionPair{{Option: "opt", Args: []string{"arg"}}}, true}, 71 | {"Long option with arg", "/opt=arg", Bundling, []optionPair{{Option: "opt", Args: []string{"arg"}}}, true}, 72 | {"Long option with arg", "/opt=arg", SingleDash, []optionPair{{Option: "opt", Args: []string{"arg"}}}, true}, 73 | 74 | {"short option with arg", "-opt:arg", Normal, []optionPair{{Option: "opt", Args: []string{"arg"}}}, true}, 75 | {"short option with arg", "-opt:arg", Bundling, []optionPair{{Option: "o"}, {Option: "p"}, {Option: "t", Args: []string{"arg"}}}, true}, 76 | {"short option with arg", "-opt:arg", SingleDash, []optionPair{{Option: "o", Args: []string{"pt:arg"}}}, true}, 77 | 78 | {"Edge case", "/opt:=arg", Normal, []optionPair{{Option: "opt", Args: []string{"=arg"}}}, true}, 79 | {"Edge case", "/opt:=arg", Bundling, []optionPair{{Option: "opt", Args: []string{"=arg"}}}, true}, 80 | {"Edge case", "/opt:=arg", SingleDash, []optionPair{{Option: "opt", Args: []string{"=arg"}}}, true}, 81 | {"Edge case", "/opt=:arg", Normal, []optionPair{{Option: "opt", Args: []string{":arg"}}}, true}, 82 | {"Edge case", "/opt=:arg", Bundling, []optionPair{{Option: "opt", Args: []string{":arg"}}}, true}, 83 | {"Edge case", "/opt=:arg", SingleDash, []optionPair{{Option: "opt", Args: []string{":arg"}}}, true}, 84 | {"Edge case", "/opt==arg", Normal, []optionPair{{Option: "opt", Args: []string{"=arg"}}}, true}, 85 | {"Edge case", "/opt==arg", Bundling, []optionPair{{Option: "opt", Args: []string{"=arg"}}}, true}, 86 | {"Edge case", "/opt==arg", SingleDash, []optionPair{{Option: "opt", Args: []string{"=arg"}}}, true}, 87 | {"Edge case", "/opt::arg", Normal, []optionPair{{Option: "opt", Args: []string{":arg"}}}, true}, 88 | {"Edge case", "/opt::arg", Bundling, []optionPair{{Option: "opt", Args: []string{":arg"}}}, true}, 89 | {"Edge case", "/opt::arg", SingleDash, []optionPair{{Option: "opt", Args: []string{":arg"}}}, true}, 90 | } 91 | for _, tt := range cases { 92 | t.Run(tt.name, func(t *testing.T) { 93 | buf := setupLogging() 94 | optPair, is := isOption(tt.in, tt.mode, false) 95 | if !reflect.DeepEqual(optPair, tt.optPair) || is != tt.is { 96 | t.Errorf("isOption(%q, %q) == (%q, %v), want (%q, %v)", 97 | tt.in, tt.mode, optPair, is, tt.optPair, tt.is) 98 | } 99 | t.Log(buf.String()) 100 | }) 101 | } 102 | for _, tt := range append(cases, windowsCases...) { 103 | t.Run("windows "+tt.name, func(t *testing.T) { 104 | buf := setupLogging() 105 | optPair, is := isOption(tt.in, tt.mode, true) 106 | if !reflect.DeepEqual(optPair, tt.optPair) || is != tt.is { 107 | t.Errorf("isOption(%q, %q) == (%q, %v), want (%q, %v)", 108 | tt.in, tt.mode, optPair, is, tt.optPair, tt.is) 109 | } 110 | t.Log(buf.String()) 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /internal/completion/completion_test.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package completion 10 | 11 | import ( 12 | "bytes" 13 | "os" 14 | "reflect" 15 | "testing" 16 | ) 17 | 18 | func setupLogging() *bytes.Buffer { 19 | s := "" 20 | buf := bytes.NewBufferString(s) 21 | Debug.SetOutput(buf) 22 | return buf 23 | } 24 | 25 | func TestGetChildNames(t *testing.T) { 26 | Debug.SetOutput(os.Stderr) 27 | 28 | // Tree setup 29 | rootNode := NewNode("executable", Root, nil) 30 | rootNode.AddChild(NewNode("options", OptionsNode, []string{"--version", "--help", "-v", "-h"})) 31 | rootNode.AddChild(NewNode("options", OptionsWithCompletion, []string{"--profile", "-p"})) 32 | 33 | logNode := NewNode("log", CommandNode, nil) 34 | rootNode.AddChild(logNode) 35 | 36 | loggerNode := NewNode("logger", CommandNode, nil) 37 | loggerNode.AddChild(NewNode("test/test_tree/bDir1", FileListNode, nil)) 38 | rootNode.AddChild(loggerNode) 39 | 40 | showNode := NewNode("show", CommandNode, nil) 41 | showNode.AddChild(NewNode("custom", CustomNode, []string{"--hola", "..hola", "abcd1234", "bbcd/1234"})) 42 | rootNode.AddChild(showNode) 43 | 44 | sublogNode := NewNode("sublog", CommandNode, nil) 45 | logNode.AddChild(sublogNode) 46 | 47 | logNode.AddChild(NewNode("options", OptionsNode, []string{"--help"})) 48 | logNode.AddChild(NewNode("test/test_tree", FileListNode, nil)) 49 | 50 | // Test Raw Completions 51 | tests := []struct { 52 | name string 53 | node *Node 54 | prefix string 55 | results []string 56 | }{ 57 | {"get commands", rootNode, "", []string{"log", "logger", "show"}}, 58 | {"get commands", rootNode, "log", []string{"log", "logger"}}, 59 | {"get commands", rootNode, "show", []string{"show"}}, 60 | {"get options", rootNode, "-", []string{"-h", "--help", "-p", "--profile", "-v", "--version"}}, 61 | {"get options", rootNode, "-h", []string{"-h"}}, 62 | {"get commands", rootNode.GetChildByName("x"), "", []string{}}, 63 | {"filter out hidden files", rootNode.GetChildByName("log"), "", []string{"sublog", "aFile1", "aFile2", "bDir1/", "bDir2/", "cFile1", "cFile2"}}, 64 | {"filter out hidden files", rootNode.GetChildByName("logger"), "", []string{"file"}}, 65 | {"show hidden files", rootNode.GetChildByName("log"), ".", []string{"./", "../", ".aFile2", "..aFile2", "...aFile2"}}, 66 | {"show dir contents", rootNode.GetChildByName("log"), "bDir1/", []string{"bDir1/file", "bDir1/.file"}}, 67 | {"Recurse back", rootNode.GetChildByName("log"), "..", []string{"../", "..aFile2", "...aFile2"}}, 68 | {"Recurse back", rootNode.GetChildByName("logger"), "..", []string{"../", "../ "}}, 69 | {"Recurse back", rootNode.GetChildByName("logger"), "../", []string{"../aFile1", "../aFile2", "../.aFile2", "../..aFile2", "../...aFile2", "../bDir1/", "../bDir2/", "../cFile1", "../cFile2"}}, 70 | {"Recurse back", rootNode.GetChildByName("logger"), "../.", []string{".././", "../../", "../.aFile2", "../..aFile2", "../...aFile2"}}, 71 | {"Recurse back", rootNode.GetChildByName("logger"), "../..", []string{"../../", "../..aFile2", "../...aFile2"}}, 72 | {"show dir contents", rootNode.GetChildByName("logger"), "../.a", []string{"../.aFile2"}}, 73 | {"Full match", rootNode.GetChildByName("logger"), "../.aFile2", []string{"../.aFile2"}}, 74 | {"show custom output", rootNode.GetChildByName("show"), "", []string{"abcd1234", "bbcd/1234", "..hola", "--hola"}}, 75 | } 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | buf := setupLogging() 79 | Debug.Printf("TestGetChildNames - name: %s, prefix: %s\n", tt.name, tt.prefix) 80 | got := tt.node.Completions(tt.prefix) 81 | if !reflect.DeepEqual(got, tt.results) { 82 | t.Errorf("(%s).Completions(%s) got = '%#v', want '%#v'", tt.node.Name, tt.prefix, got, tt.results) 83 | } 84 | t.Log(buf.String()) 85 | }) 86 | } 87 | 88 | // Test Completions with CompLine 89 | compLineTests := []struct { 90 | name string 91 | node *Node 92 | compLine string 93 | results []string 94 | }{ 95 | {"nil", rootNode, "", []string{}}, 96 | {"top level", rootNode, "./executable ", []string{"log", "logger", "show"}}, 97 | {"top level", rootNode, "./executable ", []string{"log", "logger", "show"}}, 98 | {"top level", rootNode, "./executable ", []string{"log", "logger", "show"}}, 99 | {"top level", rootNode, "./executable l", []string{"log", "logger"}}, 100 | {"top level", rootNode, "./executable l", []string{"log", "logger"}}, 101 | {"top level", rootNode, "./executable l", []string{"log", "logger"}}, 102 | {"top level", rootNode, "./executable lo", []string{"log", "logger"}}, 103 | {"top level", rootNode, "./executable log", []string{"log", "logger"}}, 104 | {"top level", rootNode, "./executable log", []string{"log", "logger"}}, 105 | {"top level", rootNode, "./executable sh", []string{"show"}}, 106 | {"options", rootNode, "./executable -", []string{"-h", "--help", "-p", "--profile", "-v", "--version"}}, 107 | {"options", rootNode, "./executable -h", []string{"-h"}}, 108 | {"options", rootNode, "./executable -h ", []string{"log", "logger", "show"}}, 109 | {"options", rootNode, "./executable -h l", []string{"log", "logger"}}, 110 | {"options", rootNode, "./executable --help l", []string{"log", "logger"}}, 111 | {"options", rootNode, "./executable --profile l", []string{"log", "logger"}}, 112 | {"options", rootNode, "./executable --profile=dev l", []string{"log", "logger"}}, 113 | {"options", rootNode, "./executable --pro", []string{"--profile"}}, 114 | {"options", rootNode, "./executable --profile", []string{"--profile"}}, 115 | {"options", rootNode, "./executable --profile=", []string{}}, 116 | {"options", rootNode, "./executable --profile=dev", []string{}}, 117 | {"options", rootNode, "./executable --profile dev", []string{"dev"}}, 118 | {"options", rootNode, "./executable --profile dev l", []string{"log", "logger"}}, 119 | {"command", rootNode, "./executable log ", []string{"sublog", "aFile1", "aFile2", "bDir1/", "bDir2/", "cFile1", "cFile2"}}, 120 | {"command", rootNode, "./executable log bDir1/f", []string{"bDir1/file"}}, 121 | {"command", rootNode, "./executable log bDir1/file ", []string{"sublog", "aFile1", "aFile2", "bDir1/", "bDir2/", "cFile1", "cFile2"}}, 122 | {"command", rootNode, "./executable log bDir1/file -", []string{"--help"}}, 123 | {"command", rootNode, "./executable log bDir1/file -", []string{"--help"}}, 124 | {"command", rootNode, "./executable logger ../.a", []string{"../.aFile2"}}, 125 | {"command", rootNode, "./executable logger ../.aFile2", []string{"../.aFile2"}}, 126 | {"command", rootNode, "./executable show", []string{"abcd1234", "bbcd/1234", "..hola", "--hola"}}, 127 | {"not a valid arg", rootNode, "./executable dev", []string{}}, 128 | } 129 | for _, tt := range compLineTests { 130 | t.Run(tt.name, func(t *testing.T) { 131 | buf := setupLogging() 132 | got := tt.node.CompLineComplete(false, tt.compLine) 133 | if !reflect.DeepEqual(got, tt.results) { 134 | t.Errorf("CompLineComplete() got = '%#v', want '%#v'", got, tt.results) 135 | } 136 | t.Log(buf.String()) 137 | }) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /internal/help/help.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | /* 10 | Package help - internal help handling code. 11 | */ 12 | package help 13 | 14 | import ( 15 | "fmt" 16 | "sort" 17 | "strconv" 18 | "strings" 19 | 20 | "github.com/DavidGamba/go-getoptions/internal/option" 21 | "github.com/DavidGamba/go-getoptions/text" 22 | ) 23 | 24 | type SynopsisArg struct { 25 | Arg string 26 | Description string 27 | } 28 | 29 | // Indentation - Number of spaces used for indentation. 30 | var Indentation = 4 31 | 32 | func indent(s string) string { 33 | return fmt.Sprintf("%s%s", strings.Repeat(" ", Indentation), s) 34 | } 35 | 36 | func wrapFn(wrap bool, open, close string) func(s string) string { 37 | if wrap { 38 | return func(s string) string { 39 | return fmt.Sprintf("%s%s%s", open, s, close) 40 | } 41 | } 42 | return func(s string) string { 43 | return s 44 | } 45 | } 46 | 47 | // Name - 48 | func Name(scriptName, name, description string) string { 49 | out := scriptName 50 | if scriptName != "" { 51 | out += fmt.Sprintf(" %s", name) 52 | } else { 53 | out += name 54 | } 55 | if description != "" { 56 | out += fmt.Sprintf(" - %s", strings.ReplaceAll(description, "\n", "\n"+strings.Repeat(" ", Indentation*2))) 57 | } 58 | return fmt.Sprintf("%s:\n%s\n", text.HelpNameHeader, indent(out)) 59 | } 60 | 61 | // Synopsis - Return a default synopsis. 62 | func Synopsis(scriptName, name string, args []SynopsisArg, options []*option.Option, commands []string) string { 63 | synopsisName := scriptName 64 | if scriptName != "" { 65 | synopsisName += fmt.Sprintf(" %s", name) 66 | } else { 67 | synopsisName += name 68 | } 69 | synopsisName = indent(synopsisName) 70 | normalOptions := []*option.Option{} 71 | requiredOptions := []*option.Option{} 72 | for _, option := range options { 73 | if option.IsRequired { 74 | requiredOptions = append(requiredOptions, option) 75 | } else { 76 | normalOptions = append(normalOptions, option) 77 | } 78 | } 79 | option.Sort(normalOptions) 80 | option.Sort(requiredOptions) 81 | optSynopsis := func(opt *option.Option) string { 82 | txt := "" 83 | wrap := wrapFn(!opt.IsRequired, "[", "]") 84 | switch opt.OptType { 85 | case option.BoolType, option.StringType, option.IntType, option.Float64Type: 86 | txt += wrap(opt.HelpSynopsis) 87 | case option.StringRepeatType, option.IntRepeatType, option.StringMapType: 88 | if opt.IsRequired { 89 | wrap = wrapFn(opt.IsRequired, "<", ">") 90 | } 91 | txt += wrap(opt.HelpSynopsis) + "..." 92 | } 93 | return txt 94 | } 95 | var out string 96 | line := synopsisName 97 | for _, option := range append(requiredOptions, normalOptions...) { 98 | syn := optSynopsis(option) 99 | // fmt.Printf("%d - %d - %d | %s | %s\n", len(line), len(syn), len(line)+len(syn), syn, line) 100 | if len(line)+len(syn) > 80 { 101 | out += line + "\n" 102 | line = fmt.Sprintf("%s %s", strings.Repeat(" ", len(synopsisName)), syn) 103 | } else { 104 | line += fmt.Sprintf(" %s", syn) 105 | } 106 | } 107 | syn := "" 108 | if len(commands) > 0 { 109 | syn += " " 110 | } 111 | if len(args) == 0 { 112 | syn += "[]" 113 | } else { 114 | aa := []string{} 115 | for _, a := range args { 116 | aa = append(aa, a.Arg) 117 | } 118 | syn += strings.Join(aa, " ") 119 | } 120 | if len(line)+len(syn) > 80 { 121 | out += line + "\n" 122 | line = fmt.Sprintf("%s %s", strings.Repeat(" ", len(synopsisName)), syn) 123 | } else { 124 | line += fmt.Sprintf(" %s", syn) 125 | } 126 | out += line 127 | return fmt.Sprintf("%s:\n%s\n", text.HelpSynopsisHeader, out) 128 | } 129 | 130 | // CommandList - 131 | // commandMap => name: description 132 | func CommandList(commandMap map[string]string) string { 133 | if len(commandMap) == 0 { 134 | return "" 135 | } 136 | names := []string{} 137 | for name := range commandMap { 138 | names = append(names, name) 139 | } 140 | sort.Strings(names) 141 | factor := longestStringLen(names) 142 | out := "" 143 | for _, command := range names { 144 | out += indent(fmt.Sprintf("%s %s\n", pad(true, command, factor), strings.ReplaceAll(commandMap[command], "\n", "\n "+indent(pad(true, "", factor))))) 145 | } 146 | return fmt.Sprintf("%s:\n%s", text.HelpCommandsHeader, out) 147 | } 148 | 149 | // longestStringLen - Given a slice of strings it returns the length of the longest string in the slice 150 | func longestStringLen(s []string) int { 151 | i := 0 152 | for _, e := range s { 153 | if len(e) > i { 154 | i = len(e) 155 | } 156 | } 157 | return i 158 | } 159 | 160 | // pad - Given a string and a padding factor it will return the string padded with spaces. 161 | // 162 | // Example: 163 | // 164 | // pad(true, "--flag", 8) -> '--flag ' 165 | func pad(do bool, s string, factor int) string { 166 | if do { 167 | return fmt.Sprintf("%-"+strconv.Itoa(factor)+"s", s) 168 | } 169 | return s 170 | } 171 | 172 | // OptionList - Return a formatted list of options and their descriptions. 173 | func OptionList(args []SynopsisArg, options []*option.Option) string { 174 | synopsisLength := 0 175 | normalOptions := []*option.Option{} 176 | requiredOptions := []*option.Option{} 177 | for _, opt := range options { 178 | l := len(opt.HelpSynopsis) 179 | if l > synopsisLength { 180 | synopsisLength = l 181 | } 182 | if opt.IsRequired { 183 | requiredOptions = append(requiredOptions, opt) 184 | } else { 185 | normalOptions = append(normalOptions, opt) 186 | } 187 | } 188 | option.Sort(normalOptions) 189 | option.Sort(requiredOptions) 190 | helpString := func(opt *option.Option) string { 191 | txt := "" 192 | factor := synopsisLength + 4 193 | padding := strings.Repeat(" ", factor) 194 | txt += indent(pad(!opt.IsRequired || opt.Description != "" || opt.EnvVar != "", opt.HelpSynopsis, factor)) 195 | if opt.Description != "" { 196 | description := strings.ReplaceAll(opt.Description, "\n", "\n "+padding) 197 | txt += description 198 | } 199 | if !opt.IsRequired { 200 | if opt.Description != "" { 201 | txt += " " 202 | } 203 | txt += fmt.Sprintf("(default: %s", opt.DefaultStr) 204 | if opt.EnvVar != "" { 205 | txt += fmt.Sprintf(", env: %s", opt.EnvVar) 206 | } 207 | txt += ")\n\n" 208 | } else { 209 | if opt.EnvVar != "" { 210 | if opt.Description != "" { 211 | txt += " " 212 | } 213 | txt += fmt.Sprintf("(env: %s)", opt.EnvVar) 214 | } 215 | txt += "\n\n" 216 | } 217 | return txt 218 | } 219 | argString := func(arg *SynopsisArg) string { 220 | txt := "" 221 | factor := synopsisLength + 4 222 | padding := strings.Repeat(" ", factor) 223 | txt += indent(pad(arg.Description != "", arg.Arg, factor)) 224 | if arg.Description != "" { 225 | description := strings.ReplaceAll(arg.Description, "\n", "\n "+padding) 226 | txt += description 227 | } 228 | txt += "\n\n" 229 | return txt 230 | } 231 | out := "" 232 | 233 | if len(args) != 0 && 234 | !((len(args) == 1 && args[0].Arg == "") || 235 | (len(args) == 1 && args[0].Description == "")) { 236 | 237 | for _, arg := range args { 238 | l := len(arg.Arg) 239 | if l > synopsisLength { 240 | synopsisLength = l 241 | } 242 | } 243 | 244 | out += fmt.Sprintf("%s:\n", text.HelpArgumentsHeader) 245 | for _, arg := range args { 246 | out += argString(&arg) 247 | } 248 | } 249 | 250 | if len(requiredOptions) > 0 { 251 | out += fmt.Sprintf("%s:\n", text.HelpRequiredOptionsHeader) 252 | for _, option := range requiredOptions { 253 | out += helpString(option) 254 | } 255 | } 256 | if len(normalOptions) > 0 { 257 | out += fmt.Sprintf("%s:\n", text.HelpOptionsHeader) 258 | for _, option := range normalOptions { 259 | out += helpString(option) 260 | } 261 | } 262 | return out 263 | } 264 | -------------------------------------------------------------------------------- /docs/parsing.adoc: -------------------------------------------------------------------------------- 1 | = Parsing CLI Args 2 | 3 | The core of the option parser is how CLI Arguments are parsed and how they are mapped to the expected program structure. 4 | 5 | The program structure is defined by: 6 | * The options the program expects. 7 | * The commands and sub commands the program can have. 8 | * The options the commands and subcommands expect. 9 | * The arguments or text input that the program or commands/subcommands expect. 10 | 11 | A proper data structure enables the option parser to easily map CLI Arguments into the items mentioned above. 12 | The wrong data structure will lead to convoluted APIs. 13 | 14 | One unexplored aspect of a proper data structure is built-in auto-completion of commands, subcommands, options and even in some cases arguments. 15 | In this regard most tools generate a very large bash completion side tool. 16 | go-getoptions in contrast only requires a single line of bash to generate all possible completions. 17 | 18 | This documents reflects the data structure used by go-getoptions that enables trees of commands/subcommands, options, arguments and built-in completion. 19 | 20 | == Parsing approach 21 | 22 | You cannot parse the CLI Args in isolation, to parse CLI Arguments you require the program data structure, or at the very least, the option definition in advanced. 23 | 24 | The following example will help explain: 25 | 26 | $ ./programname --option some text 27 | 28 | When looking at the ambiguous input above you can have the following possibilities (just to name a few): 29 | 30 | . Option `--option` with argument `some` plus text input `text`. 31 | . Option `--option` with argument `some` plus command call `text`. 32 | . Option `--option` with command call `some` and argument `text`. 33 | . Option `--option` with arguments `some` and `text` (some CLI parsers allow more than one argument per option). 34 | 35 | Now, some option parsers ignore this complexity by forcing ordering. 36 | In other words, no options are allowed before calling a command. 37 | However, even in that case, you can't tell if `--option` expects an argument or not. 38 | 39 | === Program Tree 40 | 41 | With the above in mind, the first step before being able to parse CLI Arguments is to build the program's data structure. 42 | For go-getoptions that data structure is defined as a double linked tree with a root node. 43 | 44 | [source, go] 45 | ---- 46 | type programTree struct { 47 | Type argType 48 | Name string 49 | ChildCommands map[string]*programTree 50 | ChildOptions map[string]*option.Option 51 | ChildText []*string 52 | Parent *programTree 53 | Level int 54 | command 55 | } 56 | 57 | type argType int 58 | 59 | const ( 60 | argTypeProgname argType = iota // The root node type 61 | argTypeCommand // The node type used for commands and subcommands 62 | argTypeOption // The node type used for options 63 | argTypeText // The node type used for regular cli arguments 64 | argTypeTerminator // -- 65 | ) 66 | 67 | type command struct { 68 | CommandFn CommandFn 69 | } 70 | ---- 71 | 72 | There are multiple types of arguments, of particular interest there are: 73 | 74 | * argTypeProgname: The root node type. 75 | Holds os.Args[0] as its name. 76 | 77 | * argTypeCommand: The node type used for commands and subcommands. 78 | 79 | * argTypeOption: The node type used for options. 80 | 81 | * argTypeText: The node type used for regular CLI arguments. 82 | 83 | 84 | Once the tree is built, the parser can walk through the tree as it walks through each of the passed in CLI arguments to determine if the argument is an expected option, a command or just a text argument. 85 | 86 | Additionally, since there is a data structure that is walked at the same time as CLI arguments are parsed, the parser can suggest next level completions (by listing the children) to the shell completion system. 87 | 88 | An example tree can be found in the tests. 89 | 90 | The building of the tree is located in `user.go` because the tree definition is directly created by the user defining commands and options. 91 | This is the user facing entry point. 92 | 93 | === CLI Arguments Tree 94 | 95 | The workflow of parsing the given CLI Arguments goes as follows: 96 | 97 | . Handle special cases. 98 | Currently only `--` as the terminator. 99 | If found, any remaining arguments are considered to be text arguments. 100 | 101 | . Check if the argument is an option, in other words, it starts with `-` or `--`. 102 | The lonesome dash `-` is a valid option used in many programs to signal STDIN input. 103 | + 104 | Knowing the program data structure is of particular importance at this stage to know if the option expects the following CLI argument(s) as arguments to the option. 105 | 106 | . Check if the argument is a command or a subcommand. 107 | This is done by comparing the program data structure to validate that the input is a valid command at any given depth. 108 | 109 | === Error handling during parsing 110 | 111 | TODO: this section needs to be filled after validating the tool UX if presenting errors during completions. 112 | 113 | 114 | === Auto-completion support 115 | 116 | 117 | First read the CLI line and the use isOptionV2 on each element to get a list of option pairs. 118 | 119 | isOptionV2 - Enhanced version of isOption, this one returns pairs of options and arguments 120 | At this level we don't agregate results in case we have -- and then other options, basically we can parse one option at a time. 121 | 122 | type optionPair struct { 123 | Option string 124 | // We allow multiple args in case of splitting on comma. 125 | Args []string 126 | } 127 | 128 | == Current parsing 129 | 130 | Currently options are parsed in the call to `Parse` and commands are handled in the call to `Dispatch`. 131 | All CLI arguments are parsed and options are matched regardless of order (unless `SetRequireOrder`). 132 | Unmatched options and commands are kept in the remaining array (Set `opt.SetUnknownMode(Pass)`) and they are parsed on the call to `Dispatch` or a subsequent `Parse` call. 133 | This is a very flexible approach but it breaks with completion. 134 | 135 | While option parsing can be done at stages with correct results and great flexibility, completions need to know the entire tree for them to suggest the correct subset of valid options. 136 | Not maintaining two tree walking solutions is the motivation for this refactor. 137 | 138 | The new parsing procedure works like this: 139 | 140 | On setup, when setting options and commands (calls to opt.String, opt.StringVar and opt.NewCommand for example), the ProgramTree is built. 141 | 142 | The ProgramTree build copies the options set at the current level into its children. 143 | In that way, each child has a complete set of options it supports at every level. 144 | The copy is done by passing a pointer so if an option is set at a higher level the results will trickle down to the children. 145 | 146 | 147 | == Edge cases 148 | 149 | === Passing options out of order 150 | 151 | For a definition like the following: 152 | 153 | [source, yaml] 154 | ---- 155 | program: 156 | - --opt1 157 | - cmd1: 158 | - --cmd1opt1 159 | - cmd2: 160 | - --cmd2opt1 161 | ---- 162 | 163 | Caling the program as: `program --cmd1opt1 cmd1 --opt1` 164 | Has the options out of order. 165 | This should be allowed by default and only be disallowed when `SetRequireOrder` is set. 166 | 167 | One way to accomplish this is to pass the options to the child when calling `NewCommand`, in this way the actual validation of options can happen at a single level. 168 | 169 | === Non inmediate argument to slice options that matches command 170 | 171 | * minimun has higher precedence, for example, if option has a minimun of 3 then this results in all args to opt: `program --opt arg1 command arg3` 172 | 173 | * command has the next highest precedence, for example, if option has a maximun of 2 or more, the following results in a call to command, not the word command as an argument to opt: 174 | `program 175 | 176 | use `program --opt arg1 --opt command --opt arg3` instead. 177 | 178 | * optionals have lower precedence than command. 179 | 180 | === Text input before command 181 | 182 | Not allowed, too ambiguous and it can mask errors that lead to just bad UI. 183 | 184 | 185 | == What I want 186 | 187 | AST that looks like this: 188 | 189 | NOTE: I could start at this level and have an array or CLIargs as a parse result or I could do one level up and have a root CLIarg type with the name of the program. 190 | Having the root level might be helpful with help generation. 191 | 192 | CLIarg{ 193 | Type: 0, # 0 - program name, 1 - text, 2 - option, 3 - command, 4 - terminator (--) 194 | Name: os.Args[0], 195 | Children: [ 196 | CLIarg{ 197 | Type: 1, # 0 - text, 1 - option, 2 - command, 3 - terminator (--) 198 | Name: "force", 199 | Args: [], 200 | Children: nil, 201 | }, 202 | CLIarg{ 203 | Type: 1, 204 | Name: "verbosity", 205 | Args: [2], 206 | Children: nil, 207 | }, 208 | CLIarg{ 209 | Type: 0, 210 | Name: "sometext_dirname", # Text allowed anywhere, should there be an option for it? 211 | Args: [], 212 | Children: nil, 213 | } 214 | CLIarg{ 215 | Type: 1, 216 | Name: "profile", 217 | Args: ["dev"], 218 | Children: nil, 219 | } 220 | CLIarg{ 221 | Type: 2, 222 | Name: "command1", 223 | Args: [], 224 | Children: [ 225 | CLIarg{ 226 | Type: 2, 227 | Name: "subcommand1", 228 | Args: [], 229 | Children: [ 230 | CLIarg{ 231 | Type: 1, 232 | Name: "opt1", 233 | Args: ["hello", "hola,adios"], # split on comma is not done at this level 234 | Children: nil, 235 | }, 236 | CLIarg{ 237 | Type: 1, 238 | Name: "opt2", 239 | Args: ["lang=en", "def=hello"], 240 | Children: nil, 241 | }, 242 | CLIarg{ 243 | Type: 3, 244 | Name: "--", 245 | Args: [], 246 | Children: [ 247 | CLIarg{ 248 | Type: 0, 249 | Name: "command1", # shouldn't match anything 250 | Args: [], 251 | Children, nil, 252 | }, 253 | ], 254 | }, 255 | ], 256 | }, 257 | ], 258 | }, 259 | ], 260 | } 261 | -------------------------------------------------------------------------------- /internal/completion/completion.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package completion 10 | 11 | import ( 12 | "io" 13 | "log" 14 | "regexp" 15 | "strings" 16 | ) 17 | 18 | // Debug - Debug logger set to io.Discard by default 19 | var Debug = log.New(io.Discard, "DEBUG: ", log.Ldate|log.Ltime|log.Lshortfile) 20 | 21 | /* 22 | Node - 23 | 24 | Example: 25 | 26 | mygit 27 | ├ log 28 | │ ├ sublog 29 | │ │ ├ --help 30 | │ │ ├ 31 | │ │ └ 32 | │ ├ --help 33 | │ └ 34 | ├ show 35 | │ ├ --help 36 | │ ├ --dir= 37 | │ └ 38 | ├ --help 39 | └ --version 40 | */ 41 | type Node struct { 42 | Name string // Name of the node. For StringNode Kinds, this is the completion. 43 | Kind kind // Kind of node. 44 | Children []*Node 45 | Entries []string // Use as completions for OptionsNode and CustomNode Kind. 46 | // TODO: Maybe add sibling completion that gets activated with = for options 47 | } 48 | 49 | // CompletionType - 50 | type kind int 51 | 52 | const ( 53 | // Root - 54 | Root kind = iota 55 | 56 | // CommandNode - Node used to complete the name of the command. 57 | CommandNode 58 | 59 | // FileListNode - Regular file completion you would expect. 60 | // Name used as the dir to start completing results from. 61 | // TODO: Allow ignore case. 62 | FileListNode 63 | 64 | // OptionsNode - Only enabled if prefix starts with - 65 | OptionsNode 66 | 67 | // OptionsWithCompletion - Only enabled if prefix starts with - 68 | OptionsWithCompletion 69 | 70 | // CustomNode - 71 | CustomNode 72 | ) 73 | 74 | // NewNode - 75 | func NewNode(name string, kind kind, entries []string) *Node { 76 | if entries == nil { 77 | entries = []string{} 78 | } 79 | return &Node{ 80 | Name: name, 81 | Kind: kind, 82 | Entries: entries, 83 | } 84 | } 85 | 86 | // AddChild - 87 | // TODO: Probably make sure that the name is not already in use since we find them by name. 88 | func (n *Node) AddChild(node *Node) { 89 | n.Children = append(n.Children, node) 90 | } 91 | 92 | // SelfCompletions - 93 | func (n *Node) SelfCompletions(prefix string) []string { 94 | switch n.Kind { 95 | case CommandNode: 96 | if strings.HasPrefix(n.Name, prefix) { 97 | Debug.Printf("SelfCompletions - node: %s > %v\n", n.Name, []string{n.Name}) 98 | return []string{n.Name} 99 | } 100 | case FileListNode: 101 | files, _ := listDir(n.Name, prefix) 102 | if strings.HasPrefix(prefix, ".") { 103 | Debug.Printf("SelfCompletions - node: %s > %v\n", n.Name, files) 104 | return files 105 | } 106 | // Don't return hidden files unless requested by the prefix 107 | ff := discardByPrefix(files, ".") 108 | Debug.Printf("SelfCompletions - node: %s > %v\n", n.Name, ff) 109 | return ff 110 | case OptionsNode: 111 | if strings.HasPrefix(prefix, "-") { 112 | sortForCompletion(n.Entries) 113 | ee := keepByPrefix(n.Entries, prefix) 114 | Debug.Printf("SelfCompletions - node: %s > %v\n", n.Name, ee) 115 | return ee 116 | } 117 | case OptionsWithCompletion: 118 | if strings.HasPrefix(prefix, "-") { 119 | sortForCompletion(n.Entries) 120 | ee := keepByPrefix(n.Entries, prefix) 121 | Debug.Printf("SelfCompletions - node: %s > %v\n", n.Name, ee) 122 | return ee 123 | } 124 | case CustomNode: 125 | sortForCompletion(n.Entries) 126 | ee := keepByPrefix(n.Entries, prefix) 127 | Debug.Printf("SelfCompletions - node: %s > %v\n", n.Name, ee) 128 | return ee 129 | } 130 | Debug.Printf("SelfCompletions - node: %s > %v\n", n.Name, []string{}) 131 | return []string{} 132 | } 133 | 134 | // Completions - 135 | func (n *Node) Completions(prefix string) []string { 136 | results := []string{} 137 | stringNodeResults := []string{} 138 | optionResults := []string{} 139 | for _, child := range n.Children { 140 | switch child.Kind { 141 | case CommandNode: 142 | stringNodeResults = append(stringNodeResults, child.SelfCompletions(prefix)...) 143 | case OptionsNode, OptionsWithCompletion: 144 | optionResults = append(optionResults, child.SelfCompletions(prefix)...) 145 | default: 146 | results = append(results, child.SelfCompletions(prefix)...) 147 | } 148 | } 149 | sortForCompletion(results) 150 | sortForCompletion(optionResults) 151 | // Put command completions first, then options, then anything else 152 | r := append(stringNodeResults, optionResults...) 153 | r = append(r, results...) 154 | Debug.Printf("Completions - node: %s, prefix %s > %v\n", n.Name, prefix, r) 155 | return r 156 | } 157 | 158 | // GetChildByName - Traverses to the children and returns the first one to match name. 159 | func (n *Node) GetChildByName(name string) *Node { 160 | for _, child := range n.Children { 161 | if child.Name == name { 162 | return child 163 | } 164 | } 165 | return NewNode("", Root, []string{}) 166 | } 167 | 168 | func (n *Node) GetChildrenByKind(kind kind) []*Node { 169 | children := []*Node{} 170 | for _, child := range n.Children { 171 | if child.Kind == kind { 172 | children = append(children, child) 173 | } 174 | } 175 | return children 176 | } 177 | 178 | // keepByPrefix - Given a list and a prefix filter, it returns a list subset of the elements that start with the prefix. 179 | func keepByPrefix(list []string, prefix string) []string { 180 | keepList := []string{} 181 | for _, e := range list { 182 | if strings.HasPrefix(e, prefix) { 183 | keepList = append(keepList, e) 184 | } 185 | } 186 | return keepList 187 | } 188 | 189 | // discardByPrefix - Given a list and a prefix filter, it returns a list subset of the elements that Do not start with the prefix. 190 | func discardByPrefix(list []string, prefix string) []string { 191 | keepList := []string{} 192 | for _, e := range list { 193 | if !strings.HasPrefix(e, prefix) { 194 | keepList = append(keepList, e) 195 | } 196 | } 197 | return keepList 198 | } 199 | 200 | // CompLineComplete - Given a compLine (get it with os.Getenv("COMP_LINE")) it returns a list of completions. 201 | func (n *Node) CompLineComplete(lastWasOption bool, compLine string) []string { 202 | // TODO: This split might not consider files that have spaces in them. 203 | re := regexp.MustCompile(`\s+`) 204 | compLineParts := re.Split(compLine, -1) 205 | 206 | // return compLineParts 207 | if len(compLineParts) == 0 || compLineParts[0] == "" { 208 | Debug.Printf("CompLineComplete - node: %s, compLine %s > %v - Empty compLineParts\n", n.Name, compLine, []string{}) 209 | return []string{} 210 | } 211 | 212 | // Drop the executable or command 213 | compLineParts = compLineParts[1:] 214 | 215 | // We have a possibly partial request 216 | if len(compLineParts) >= 1 { 217 | current := compLineParts[0] 218 | 219 | cc := n.Completions(current) 220 | if len(compLineParts) == 1 && len(cc) > 1 { 221 | Debug.Printf("CompLineComplete - node: %s, compLine %s > %v - Multiple completions for this compLine\n", n.Name, compLine, cc) 222 | return cc 223 | } 224 | // Check if the current fully matches a command (child node) 225 | child := n.GetChildByName(current) 226 | if child.Kind == CommandNode && child.Name == current { 227 | Debug.Printf("CompLineComplete - node: %s, compLine %s - Recursing into command %s\n", n.Name, compLine, current) 228 | // Recurse into the child node's completion 229 | return child.CompLineComplete(false, strings.Join(compLineParts, " ")) 230 | } 231 | // Check if the current fully matches an option 232 | list := n.GetChildrenByKind(OptionsNode) 233 | list = append(list, n.GetChildrenByKind(CustomNode)...) 234 | for _, child := range list { 235 | for _, e := range child.Entries { 236 | if current == e { 237 | if len(compLineParts) == 1 { 238 | Debug.Printf("CompLineComplete - node: %s, compLine %s > %v - Fully Matched Option/Custom\n", n.Name, compLine, current) 239 | return []string{current} 240 | } 241 | Debug.Printf("CompLineComplete - node: %s, compLine %s - Fully matched Option/Custom %s, recursing to self\n", n.Name, compLine, current) 242 | // Recurse into the node self completion 243 | return n.CompLineComplete(false, strings.Join(compLineParts, " ")) 244 | } 245 | } 246 | } 247 | // Check if the current fully matches an option 248 | list = n.GetChildrenByKind(OptionsWithCompletion) 249 | list = append(list, n.GetChildrenByKind(CustomNode)...) 250 | for _, child := range list { 251 | for _, e := range child.Entries { 252 | if current == e { 253 | if len(compLineParts) == 1 { 254 | Debug.Printf("CompLineComplete - node: %s, compLine %s > %v - Fully Matched Option/Custom\n", n.Name, compLine, current) 255 | return []string{current} 256 | } 257 | Debug.Printf("CompLineComplete - node: %s, compLine %s - Fully matched Option/Custom %s, recursing to self\n", n.Name, compLine, current) 258 | // Recurse into the node self completion 259 | return n.CompLineComplete(true, strings.Join(compLineParts, " ")) 260 | } 261 | if strings.HasPrefix(current, e+"=") { 262 | if len(compLineParts) == 1 { 263 | Debug.Printf("CompLineComplete - node: %s, compLine %s > %v - Fully Matched Option/Custom with =\n", n.Name, compLine, current) 264 | return n.Completions(current) 265 | } 266 | Debug.Printf("CompLineComplete - node: %s, compLine %s - Fully matched Option/Custom with = %s, recursing to self\n", n.Name, compLine, current) 267 | // Recurse into the node self completion 268 | return n.CompLineComplete(false, strings.Join(compLineParts, " ")) 269 | } 270 | } 271 | } 272 | // Get FileList completions after all other completions 273 | for _, child := range n.GetChildrenByKind(FileListNode) { 274 | cc := child.SelfCompletions(current) 275 | for _, e := range cc { 276 | if current == e { 277 | if len(compLineParts) == 1 { 278 | Debug.Printf("CompLineComplete - node: %s, compLine %s > %v - Fully matched File\n", n.Name, compLine, current) 279 | return []string{current} 280 | } 281 | Debug.Printf("CompLineComplete - node: %s, compLine %s - Fully matched File %s, recursing to self\n", n.Name, compLine, current) 282 | // Recurse into the node self completion 283 | return n.CompLineComplete(false, strings.Join(compLineParts, " ")) 284 | } 285 | } 286 | } 287 | 288 | // Doesn't match anything but previous arg was an option 289 | if lastWasOption { 290 | Debug.Printf("CompLineComplete - node: %s, compLine %s - Previous was option %s, recursing to self\n", n.Name, compLine, current) 291 | if len(compLineParts) == 1 { 292 | return []string{current} 293 | } 294 | return n.CompLineComplete(false, strings.Join(compLineParts, " ")) 295 | } 296 | 297 | // Return a partial match 298 | Debug.Printf("CompLineComplete - node: %s, compLine %s - Partial match %s\n", n.Name, compLine, current) 299 | return n.Completions(current) 300 | } 301 | 302 | Debug.Printf("CompLineComplete - node: %s, compLine %s > [] - Return all results\n", n.Name, compLine) 303 | // No partial request, return all results 304 | return n.Completions("") 305 | } 306 | -------------------------------------------------------------------------------- /internal/help/help_test.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package help 10 | 11 | import ( 12 | "fmt" 13 | "os" 14 | "path/filepath" 15 | "testing" 16 | 17 | "github.com/DavidGamba/go-getoptions/internal/option" 18 | ) 19 | 20 | func firstDiff(got, expected string) string { 21 | same := "" 22 | for i, gc := range got { 23 | if len([]rune(expected)) <= i { 24 | return fmt.Sprintf("Index: %d | diff: got '%s' - exp '%s'\n", len(expected), got, expected) 25 | } 26 | if gc != []rune(expected)[i] { 27 | return fmt.Sprintf("Index: %d | diff: got '%c' - exp '%c'\nsame '%s'\n", i, gc, []rune(expected)[i], same) 28 | } 29 | same += string(gc) 30 | } 31 | if len(expected) > len(got) { 32 | return fmt.Sprintf("Index: %d | diff: got '%s' - exp '%s'\n", len(got), got, expected) 33 | } 34 | return "" 35 | } 36 | 37 | func TestHelp(t *testing.T) { 38 | scriptName := filepath.Base(os.Args[0]) 39 | 40 | boolOpt := func() *option.Option { b := false; return option.New("bool", option.BoolType, &b).SetAlias("b") } 41 | intOpt := func() *option.Option { i := 0; return option.New("int", option.IntType, &i) } 42 | floatOpt := func() *option.Option { f := 0.0; return option.New("float", option.Float64Type, &f) } 43 | ssOpt := func() *option.Option { ss := []string{}; return option.New("ss", option.StringRepeatType, &ss) } 44 | iiOpt := func() *option.Option { ii := []int{}; return option.New("ii", option.IntRepeatType, &ii) } 45 | mOpt := func() *option.Option { m := map[string]string{}; return option.New("m", option.StringMapType, &m) } 46 | 47 | tests := []struct { 48 | name string 49 | got string 50 | expected string 51 | }{ 52 | {"Name", Name("", scriptName, ""), `NAME: 53 | help.test 54 | `}, 55 | {"Name", Name(scriptName, "log", ""), `NAME: 56 | help.test log 57 | `}, 58 | {"Name", Name(scriptName, "log", "logs output..."), `NAME: 59 | help.test log - logs output... 60 | `}, 61 | {"Name", Name(scriptName, "multiline", "multiline\ndescription\nthat is very long"), `NAME: 62 | help.test multiline - multiline 63 | description 64 | that is very long 65 | `}, 66 | {"Synopsis", Synopsis("", scriptName, []SynopsisArg{}, nil, []string{}), `SYNOPSIS: 67 | help.test [] 68 | `}, 69 | {"Synopsis", Synopsis(scriptName, "log", []SynopsisArg{}, nil, []string{}), `SYNOPSIS: 70 | help.test log [] 71 | `}, 72 | {"Synopsis", Synopsis(scriptName, "log", []SynopsisArg{{Arg: ""}}, nil, []string{}), `SYNOPSIS: 73 | help.test log 74 | `}, 75 | {"Synopsis", Synopsis(scriptName, "log", []SynopsisArg{{Arg: ""}, {Arg: ""}}, nil, []string{}), `SYNOPSIS: 76 | help.test log 77 | `}, 78 | { 79 | "Synopsis", Synopsis(scriptName, "log", []SynopsisArg{}, 80 | []*option.Option{func() *option.Option { b := false; return option.New("bool", option.BoolType, &b) }()}, []string{}), 81 | `SYNOPSIS: 82 | help.test log [--bool] [] 83 | `, 84 | }, 85 | { 86 | "Synopsis", Synopsis(scriptName, "log", []SynopsisArg{}, 87 | []*option.Option{boolOpt()}, []string{}), 88 | `SYNOPSIS: 89 | help.test log [--bool|-b] [] 90 | `, 91 | }, 92 | { 93 | "Synopsis", Synopsis(scriptName, "log", []SynopsisArg{}, 94 | []*option.Option{ 95 | boolOpt(), 96 | intOpt(), 97 | floatOpt(), 98 | ssOpt(), 99 | iiOpt(), 100 | mOpt(), 101 | }, []string{}), 102 | `SYNOPSIS: 103 | help.test log [--bool|-b] [--float ] [--ii ]... [--int ] 104 | [-m ]... [--ss ]... [] 105 | `, 106 | }, 107 | { 108 | "Synopsis", Synopsis(scriptName, "log", []SynopsisArg{}, 109 | []*option.Option{ 110 | boolOpt().SetRequired(""), 111 | intOpt().SetRequired(""), 112 | floatOpt().SetRequired(""), 113 | ssOpt().SetRequired(""), 114 | iiOpt().SetRequired(""), 115 | mOpt().SetRequired(""), 116 | }, []string{}), 117 | `SYNOPSIS: 118 | help.test log --bool|-b --float <--ii >... --int 119 | <-m >... <--ss >... [] 120 | `, 121 | }, 122 | { 123 | "Synopsis", Synopsis(scriptName, "log", []SynopsisArg{}, 124 | []*option.Option{ 125 | boolOpt().SetRequired(""), 126 | intOpt().SetRequired(""), 127 | floatOpt().SetRequired(""), 128 | ssOpt().SetRequired(""), 129 | iiOpt().SetRequired(""), 130 | mOpt().SetRequired(""), 131 | }, []string{"log", "show"}), 132 | `SYNOPSIS: 133 | help.test log --bool|-b --float <--ii >... --int 134 | <-m >... <--ss >... [] 135 | `, 136 | }, 137 | { 138 | "Synopsis", Synopsis(scriptName, "log", []SynopsisArg{}, 139 | []*option.Option{ 140 | boolOpt().SetRequired(""), 141 | intOpt().SetRequired(""), 142 | floatOpt().SetRequired(""), 143 | ssOpt().SetRequired(""), 144 | iiOpt().SetRequired(""), 145 | mOpt().SetRequired(""), 146 | func() *option.Option { m := map[string]string{}; return option.New("z", option.StringMapType, &m) }().SetRequired(""), 147 | }, []string{"log", "show"}), 148 | `SYNOPSIS: 149 | help.test log --bool|-b --float <--ii >... --int 150 | <-m >... <--ss >... <-z >... 151 | [] 152 | `, 153 | }, 154 | {"OptionList nil", OptionList(nil, nil), ""}, 155 | {"OptionList empty", OptionList(nil, []*option.Option{}), ""}, 156 | {"OptionList args without description", OptionList([]SynopsisArg{{Arg: ""}}, []*option.Option{}), ""}, 157 | {"OptionList args", OptionList([]SynopsisArg{{Arg: "", Description: "File with inputs"}}, []*option.Option{}), `ARGUMENTS: 158 | File with inputs 159 | 160 | `}, 161 | {"OptionList multiple args", OptionList([]SynopsisArg{ 162 | {Arg: "", Description: "File with inputs"}, 163 | {Arg: "", Description: "output dir"}}, []*option.Option{}), `ARGUMENTS: 164 | File with inputs 165 | 166 | output dir 167 | 168 | `}, 169 | {"OptionList default str", OptionList(nil, []*option.Option{ 170 | boolOpt().SetDefaultStr("false"), 171 | intOpt().SetDefaultStr("0"), 172 | floatOpt().SetDefaultStr("0.0"), 173 | ssOpt().SetDefaultStr("[]"), 174 | iiOpt().SetDefaultStr("[]"), 175 | mOpt().SetDefaultStr("{}"), 176 | }), `OPTIONS: 177 | --bool|-b (default: false) 178 | 179 | --float (default: 0.0) 180 | 181 | --ii (default: []) 182 | 183 | --int (default: 0) 184 | 185 | -m (default: {}) 186 | 187 | --ss (default: []) 188 | 189 | `}, 190 | { 191 | "OptionList required", OptionList(nil, []*option.Option{ 192 | boolOpt().SetRequired(""), 193 | intOpt().SetRequired(""), 194 | floatOpt().SetRequired(""), 195 | ssOpt().SetRequired(""), 196 | iiOpt().SetRequired(""), 197 | mOpt().SetRequired(""), 198 | }), 199 | `REQUIRED PARAMETERS: 200 | --bool|-b 201 | 202 | --float 203 | 204 | --ii 205 | 206 | --int 207 | 208 | -m 209 | 210 | --ss 211 | 212 | `, 213 | }, 214 | {"OptionList multi line", OptionList(nil, []*option.Option{ 215 | boolOpt().SetDefaultStr("false").SetDescription("bool"), 216 | intOpt().SetDefaultStr("0").SetDescription("int\nmultiline description"), 217 | floatOpt().SetDefaultStr("0.0").SetDescription("float"), 218 | ssOpt().SetDefaultStr("[]").SetDescription("string repeat"), 219 | iiOpt().SetDefaultStr("[]").SetDescription("int repeat"), 220 | mOpt().SetDefaultStr("{}").SetDescription("map"), 221 | }), `OPTIONS: 222 | --bool|-b bool (default: false) 223 | 224 | --float float (default: 0.0) 225 | 226 | --ii int repeat (default: []) 227 | 228 | --int int 229 | multiline description (default: 0) 230 | 231 | -m map (default: {}) 232 | 233 | --ss string repeat (default: []) 234 | 235 | `}, 236 | {"OptionList", OptionList(nil, []*option.Option{ 237 | boolOpt().SetDefaultStr("false").SetDescription("bool").SetRequired(""), 238 | intOpt().SetDefaultStr("0").SetDescription("int\nmultiline description"), 239 | floatOpt().SetDefaultStr("0.0").SetDescription("float").SetRequired(""), 240 | func() *option.Option { 241 | ss := []string{} 242 | return option.New("string-repeat", option.StringRepeatType, &ss) 243 | }().SetDefaultStr("[]").SetDescription("string repeat").SetHelpArgName("my_value"), 244 | iiOpt().SetDefaultStr("[]").SetDescription("int repeat").SetRequired(""), 245 | mOpt().SetDefaultStr("{}").SetDescription("map"), 246 | }), `REQUIRED PARAMETERS: 247 | --bool|-b bool 248 | 249 | --float float 250 | 251 | --ii int repeat 252 | 253 | OPTIONS: 254 | --int int 255 | multiline description (default: 0) 256 | 257 | -m map (default: {}) 258 | 259 | --string-repeat string repeat (default: []) 260 | 261 | `}, 262 | {"OptionList", OptionList([]SynopsisArg{ 263 | {Arg: "", Description: "File with inputs"}, 264 | {Arg: "", Description: "output dir"}}, 265 | []*option.Option{ 266 | boolOpt().SetDefaultStr("false").SetDescription("bool").SetRequired("").SetEnvVar("BOOL"), 267 | intOpt().SetDefaultStr("0").SetDescription("int\nmultiline description").SetEnvVar("INT"), 268 | floatOpt().SetDefaultStr("0.0").SetDescription("float").SetRequired("").SetEnvVar("FLOAT"), 269 | func() *option.Option { 270 | ss := []string{} 271 | return option.New("string-repeat", option.StringRepeatType, &ss) 272 | }().SetDefaultStr("[]").SetDescription("string repeat").SetHelpArgName("my_value").SetEnvVar("STRING_REPEAT"), 273 | iiOpt().SetDefaultStr("[]").SetDescription("int repeat").SetRequired("").SetEnvVar("II"), 274 | mOpt().SetDefaultStr("{}").SetDescription("map").SetEnvVar("M"), 275 | }), `ARGUMENTS: 276 | File with inputs 277 | 278 | output dir 279 | 280 | REQUIRED PARAMETERS: 281 | --bool|-b bool (env: BOOL) 282 | 283 | --float float (env: FLOAT) 284 | 285 | --ii int repeat (env: II) 286 | 287 | OPTIONS: 288 | --int int 289 | multiline description (default: 0, env: INT) 290 | 291 | -m map (default: {}, env: M) 292 | 293 | --string-repeat string repeat (default: [], env: STRING_REPEAT) 294 | 295 | `}, 296 | {"CommandList", CommandList(nil), ""}, 297 | {"CommandList", CommandList(map[string]string{}), ""}, 298 | {"CommandList", CommandList( 299 | map[string]string{"log": "log output", "show": "show output", "multi": "multiline\ndescription\nthat is long"}, 300 | ), `COMMANDS: 301 | log log output 302 | multi multiline 303 | description 304 | that is long 305 | show show output 306 | `}, 307 | } 308 | for _, tt := range tests { 309 | t.Run(tt.name, func(t *testing.T) { 310 | if tt.got != tt.expected { 311 | t.Errorf("Error\ngot: %s\n%s", tt.got, firstDiff(tt.got, tt.expected)) 312 | } 313 | }) 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /internal/option/option_test.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package option 10 | 11 | import ( 12 | "errors" 13 | "fmt" 14 | "reflect" 15 | "testing" 16 | 17 | "github.com/DavidGamba/go-getoptions/text" 18 | ) 19 | 20 | func TestOption(t *testing.T) { 21 | tests := []struct { 22 | name string 23 | option *Option 24 | input []string 25 | output interface{} 26 | err error 27 | }{ 28 | {"empty", func() *Option { 29 | b := false 30 | return New("help", BoolType, &b) 31 | }(), []string{}, true, nil}, 32 | {"empty", func() *Option { 33 | b := true 34 | return New("help", BoolType, &b) 35 | }(), []string{}, false, nil}, 36 | {"bool", func() *Option { 37 | b := false 38 | return New("help", BoolType, &b) 39 | }(), []string{""}, true, nil}, 40 | {"bool", func() *Option { 41 | b := true 42 | return New("help", BoolType, &b) 43 | }(), []string{""}, false, nil}, 44 | {"bool setbool", func() *Option { 45 | b := true 46 | return New("help", BoolType, &b).SetBool(b) 47 | }(), []string{""}, false, nil}, 48 | {"bool setbool", func() *Option { 49 | b := false 50 | return New("help", BoolType, &b).SetBool(b) 51 | }(), []string{""}, true, nil}, 52 | {"bool env", func() *Option { 53 | b := false 54 | return New("help", BoolType, &b) 55 | }(), []string{"true"}, true, nil}, 56 | {"bool env", func() *Option { 57 | b := false 58 | return New("help", BoolType, &b) 59 | }(), []string{"false"}, false, nil}, 60 | {"bool env", func() *Option { 61 | b := true 62 | return New("help", BoolType, &b) 63 | }(), []string{"true"}, true, nil}, 64 | {"bool env", func() *Option { 65 | b := true 66 | return New("help", BoolType, &b) 67 | }(), []string{"false"}, false, nil}, 68 | 69 | {"string", func() *Option { 70 | s := "" 71 | return New("help", StringType, &s) 72 | }(), []string{""}, "", nil}, 73 | {"string", func() *Option { 74 | s := "" 75 | return New("help", StringType, &s) 76 | }(), []string{"hola"}, "hola", nil}, 77 | {"string", func() *Option { 78 | s := "" 79 | return New("help", StringType, &s).SetString("xxx") 80 | }(), []string{""}, "", nil}, 81 | {"string", func() *Option { 82 | s := "" 83 | return New("help", StringType, &s).SetString("xxx") 84 | }(), []string{"hola"}, "hola", nil}, 85 | 86 | {"string optional", func() *Option { 87 | s := "" 88 | return New("help", StringOptionalType, &s).SetString("xxx") 89 | }(), []string{"hola"}, "hola", nil}, 90 | {"string optional", func() *Option { 91 | s := "" 92 | return New("help", StringOptionalType, &s).SetString("xxx") 93 | }(), []string{}, "xxx", nil}, 94 | 95 | {"string valid values", func() *Option { 96 | s := "" 97 | o := New("help", StringType, &s) 98 | o.ValidValues = []string{"a", "b", "c"} 99 | return o 100 | }(), []string{"a"}, "a", nil}, 101 | {"string valid values", func() *Option { 102 | s := "" 103 | o := New("help", StringType, &s) 104 | o.ValidValues = []string{"a", "b", "c"} 105 | return o 106 | }(), []string{"d"}, "", fmt.Errorf("wrong value for option '%s', valid values are %q", "help", []string{"a", "b", "c"})}, 107 | 108 | {"int", func() *Option { 109 | i := 0 110 | return New("help", IntType, &i) 111 | }(), []string{"123"}, 123, nil}, 112 | {"int", func() *Option { 113 | i := 0 114 | return New("help", IntType, &i).SetInt(456) 115 | }(), []string{"123"}, 123, nil}, 116 | { 117 | "int error", func() *Option { 118 | i := 0 119 | return New("help", IntType, &i) 120 | }(), 121 | []string{"123x"}, 122 | 0, 123 | fmt.Errorf(text.ErrorConvertToInt, "", "123x"), 124 | }, 125 | { 126 | "int error alias", func() *Option { 127 | i := 0 128 | return New("help", IntType, &i).SetCalled("int") 129 | }(), 130 | []string{"123x"}, 131 | 0, 132 | fmt.Errorf(text.ErrorConvertToInt, "int", "123x"), 133 | }, 134 | 135 | {"int optinal", func() *Option { 136 | i := 0 137 | return New("help", IntOptionalType, &i) 138 | }(), []string{"123"}, 123, nil}, 139 | {"int optinal", func() *Option { 140 | i := 0 141 | return New("help", IntOptionalType, &i) 142 | }(), []string{}, 0, nil}, 143 | {"int optinal", func() *Option { 144 | i := 0 145 | return New("help", IntOptionalType, &i).SetInt(456) 146 | }(), []string{"123"}, 123, nil}, 147 | {"int optinal", func() *Option { 148 | i := 0 149 | return New("help", IntOptionalType, &i).SetInt(456) 150 | }(), []string{}, 456, nil}, 151 | { 152 | "int optinal error", func() *Option { 153 | i := 0 154 | return New("help", IntOptionalType, &i) 155 | }(), 156 | []string{"123x"}, 157 | 0, 158 | fmt.Errorf(text.ErrorConvertToInt, "", "123x"), 159 | }, 160 | { 161 | "int optinal error alias", func() *Option { 162 | i := 0 163 | return New("help", IntOptionalType, &i).SetCalled("int") 164 | }(), 165 | []string{"123x"}, 166 | 0, 167 | fmt.Errorf(text.ErrorConvertToInt, "int", "123x"), 168 | }, 169 | 170 | {"increment", func() *Option { 171 | i := 0 172 | return New("help", IncrementType, &i) 173 | }(), []string{}, 1, nil}, 174 | {"increment", func() *Option { 175 | i := 0 176 | return New("help", IncrementType, &i) 177 | }(), []string{"x"}, 1, nil}, 178 | {"increment", func() *Option { 179 | i := 0 180 | return New("help", IncrementType, &i).SetInt(456) 181 | }(), []string{}, 457, nil}, 182 | 183 | {"float64", func() *Option { 184 | f := 0.0 185 | return New("help", Float64Type, &f) 186 | }(), []string{"123.123"}, 123.123, nil}, 187 | { 188 | "float64 error", func() *Option { 189 | f := 0.0 190 | return New("help", Float64Type, &f) 191 | }(), 192 | []string{"123x"}, 193 | 0.0, 194 | fmt.Errorf(text.ErrorConvertToFloat64, "", "123x"), 195 | }, 196 | { 197 | "float64 error alias", func() *Option { 198 | f := 0.0 199 | return New("help", Float64Type, &f).SetCalled("float") 200 | }(), 201 | []string{"123x"}, 202 | 0.0, 203 | fmt.Errorf(text.ErrorConvertToFloat64, "float", "123x"), 204 | }, 205 | 206 | {"float64 optional", func() *Option { 207 | f := 0.0 208 | return New("help", Float64OptionalType, &f) 209 | }(), []string{"123.123"}, 123.123, nil}, 210 | {"float64 optional", func() *Option { 211 | f := 0.0 212 | return New("help", Float64OptionalType, &f) 213 | }(), []string{}, 0.0, nil}, 214 | { 215 | "float64 optional error", func() *Option { 216 | f := 0.0 217 | return New("help", Float64OptionalType, &f) 218 | }(), 219 | []string{"123x"}, 220 | 0.0, 221 | fmt.Errorf(text.ErrorConvertToFloat64, "", "123x"), 222 | }, 223 | { 224 | "float64 optional error alias", func() *Option { 225 | f := 0.0 226 | return New("help", Float64OptionalType, &f).SetCalled("float") 227 | }(), 228 | []string{"123x"}, 229 | 0.0, 230 | fmt.Errorf(text.ErrorConvertToFloat64, "float", "123x"), 231 | }, 232 | 233 | {"string slice", func() *Option { 234 | ss := []string{} 235 | return New("help", StringRepeatType, &ss) 236 | }(), []string{"hola", "mundo"}, []string{"hola", "mundo"}, nil}, 237 | 238 | {"int slice", func() *Option { 239 | ii := []int{} 240 | return New("help", IntRepeatType, &ii) 241 | }(), []string{"123", "456"}, []int{123, 456}, nil}, 242 | { 243 | "int slice error", func() *Option { 244 | ii := []int{} 245 | return New("help", IntRepeatType, &ii) 246 | }(), 247 | []string{"x"}, 248 | []int{}, 249 | fmt.Errorf(text.ErrorConvertToInt, "", "x"), 250 | }, 251 | 252 | {"float64 slice", func() *Option { 253 | ii := []float64{} 254 | return New("help", Float64RepeatType, &ii) 255 | }(), []string{"123.456", "456.789"}, []float64{123.456, 456.789}, nil}, 256 | { 257 | "float64 slice error", func() *Option { 258 | ii := []float64{} 259 | return New("help", Float64RepeatType, &ii) 260 | }(), 261 | []string{"x"}, 262 | []float64{}, 263 | fmt.Errorf(text.ErrorConvertToFloat64, "", "x"), 264 | }, 265 | 266 | {"int slice range", func() *Option { 267 | ii := []int{} 268 | return New("help", IntRepeatType, &ii) 269 | }(), []string{"1..5"}, []int{1, 2, 3, 4, 5}, nil}, 270 | { 271 | "int slice range error", func() *Option { 272 | ii := []int{} 273 | return New("help", IntRepeatType, &ii) 274 | }(), 275 | []string{"x..5"}, 276 | []int{}, 277 | fmt.Errorf(text.ErrorConvertToInt, "", "x..5"), 278 | }, 279 | { 280 | "int slice range error", func() *Option { 281 | ii := []int{} 282 | return New("help", IntRepeatType, &ii) 283 | }(), 284 | []string{"1..x"}, 285 | []int{}, 286 | fmt.Errorf(text.ErrorConvertToInt, "", "1..x"), 287 | }, 288 | { 289 | "int slice range error", func() *Option { 290 | ii := []int{} 291 | return New("help", IntRepeatType, &ii) 292 | }(), 293 | []string{"5..1"}, 294 | []int{}, 295 | fmt.Errorf(text.ErrorConvertToInt, "", "5..1"), 296 | }, 297 | 298 | {"map", func() *Option { 299 | m := make(map[string]string) 300 | return New("help", StringMapType, &m) 301 | }(), []string{"hola=mundo"}, map[string]string{"hola": "mundo"}, nil}, 302 | {"map", func() *Option { 303 | m := make(map[string]string) 304 | opt := New("help", StringMapType, &m) 305 | opt.MapKeysToLower = true 306 | return opt 307 | }(), []string{"Hola=Mundo"}, map[string]string{"hola": "Mundo"}, nil}, 308 | // TODO: Currently map is only handling one argument at a time so the test below fails. 309 | // It seems like the caller is handling this properly so I don't really know if this is needed here. 310 | // {"map", func() *Option { 311 | // m := make(map[string]string) 312 | // return New("help", StringMapType, &m) 313 | // }(), []string{"hola=mundo", "hello=world"}, map[string]string{"hola": "mundo", "hello": "world"}, nil}, 314 | { 315 | "map error", func() *Option { 316 | m := make(map[string]string) 317 | return New("help", StringMapType, &m) 318 | }(), 319 | []string{"hola"}, 320 | map[string]string{}, 321 | fmt.Errorf(text.ErrorArgumentIsNotKeyValue, ""), 322 | }, 323 | } 324 | for _, tt := range tests { 325 | t.Run(tt.name, func(t *testing.T) { 326 | err := tt.option.Save(tt.input...) 327 | if err == nil && tt.err != nil { 328 | t.Errorf("got = '%#v', want '%#v'", err, tt.err) 329 | } 330 | if err != nil && tt.err == nil { 331 | t.Errorf("got = '%#v', want '%#v'", err, tt.err) 332 | } 333 | if err != nil && tt.err != nil && err.Error() != tt.err.Error() { 334 | t.Errorf("got = '%#v', want '%#v'", err, tt.err) 335 | } 336 | got := tt.option.Value() 337 | if !reflect.DeepEqual(got, tt.output) { 338 | t.Errorf("got = '%#v', want '%#v'", got, tt.output) 339 | } 340 | }) 341 | } 342 | } 343 | 344 | func TestRequired(t *testing.T) { 345 | tests := []struct { 346 | name string 347 | option *Option 348 | input []string 349 | output interface{} 350 | err error 351 | errRequired error 352 | }{ 353 | {"bool", func() *Option { 354 | b := false 355 | return New("help", BoolType, &b) 356 | }(), []string{""}, true, nil, nil}, 357 | {"bool", func() *Option { 358 | b := false 359 | return New("help", BoolType, &b).SetRequired("") 360 | }(), []string{""}, true, nil, ErrorMissingRequiredOption}, 361 | {"bool", func() *Option { 362 | b := false 363 | return New("help", BoolType, &b).SetRequired("err") 364 | }(), []string{""}, true, nil, ErrorMissingRequiredOption}, 365 | } 366 | for _, tt := range tests { 367 | t.Run(tt.name, func(t *testing.T) { 368 | err := tt.option.Save(tt.input...) 369 | if err == nil && tt.err != nil { 370 | t.Errorf("got = '%#v', want '%#v'", err, tt.err) 371 | } 372 | if err != nil && tt.err == nil { 373 | t.Errorf("got = '%#v', want '%#v'", err, tt.err) 374 | } 375 | if err != nil && tt.err != nil && err.Error() != tt.err.Error() { 376 | t.Errorf("got = '%#v', want '%#v'", err, tt.err) 377 | } 378 | got := tt.option.Value() 379 | if !reflect.DeepEqual(got, tt.output) { 380 | t.Errorf("got = '%#v', want '%#v'", got, tt.output) 381 | } 382 | err = tt.option.CheckRequired() 383 | if err == nil && tt.errRequired != nil { 384 | t.Errorf("got = '%#v', want '%#v'", err, tt.errRequired) 385 | } 386 | if err != nil && tt.errRequired == nil { 387 | t.Errorf("got = '%#v', want '%#v'", err, tt.errRequired) 388 | } 389 | if err != nil && tt.errRequired != nil && !errors.Is(err, tt.errRequired) { 390 | t.Errorf("got = '%#v', want '%#v'", err, tt.errRequired) 391 | } 392 | }) 393 | } 394 | } 395 | 396 | func TestOther(t *testing.T) { 397 | i := 0 398 | opt := New("help", IntType, &i).SetAlias("?", "h").SetDescription("int help").SetHelpArgName("myint").SetDefaultStr("5").SetEnvVar("ENV_VAR") 399 | got := opt.Aliases 400 | expected := []string{"help", "?", "h"} 401 | if !reflect.DeepEqual(got, expected) { 402 | t.Errorf("got = '%#v', want '%#v'", got, expected) 403 | } 404 | if opt.Int() != 0 { 405 | t.Errorf("got = '%#v', want '%#v'", opt.Int(), 0) 406 | } 407 | i = 3 408 | if opt.Int() != 3 { 409 | t.Errorf("got = '%#v', want '%#v'", opt.Int(), 0) 410 | } 411 | if opt.Description != "int help" { 412 | t.Errorf("got = '%#v', want '%#v'", opt.Description, "int help") 413 | } 414 | if opt.HelpArgName != "myint" { 415 | t.Errorf("got = '%#v', want '%#v'", opt.HelpArgName, "myint") 416 | } 417 | if opt.DefaultStr != "5" { 418 | t.Errorf("got = '%#v', want '%#v'", opt.DefaultStr, "5") 419 | } 420 | if opt.EnvVar != "ENV_VAR" { 421 | t.Errorf("got = '%#v', want '%#v'", opt.EnvVar, "ENV_VAR") 422 | } 423 | 424 | b := true 425 | list := []*Option{New("b", BoolType, &b), New("a", BoolType, &b), New("c", BoolType, &b)} 426 | expectedList := []*Option{New("a", BoolType, &b), New("b", BoolType, &b), New("c", BoolType, &b)} 427 | Sort(list) 428 | if !reflect.DeepEqual(list, expectedList) { 429 | t.Errorf("got = '%#v', want '%#v'", list, expectedList) 430 | } 431 | 432 | ii := []int{} 433 | opt = New("help", IntRepeatType, &ii) 434 | opt.MaxArgs = 2 435 | opt.Synopsis() 436 | if opt.HelpSynopsis != "--help ..." { 437 | t.Errorf("got = '%#v', want '%#v'", opt.HelpSynopsis, "--help ...") 438 | } 439 | } 440 | 441 | func TestValidateMinMaxArgs(t *testing.T) { 442 | tests := []struct { 443 | name string 444 | option *Option 445 | err error 446 | }{ 447 | {"valid", func() *Option { 448 | ii := []int{} 449 | o := New("help", IntRepeatType, &ii) 450 | o.MinArgs = 1 451 | o.MaxArgs = 1 452 | return o 453 | }(), nil}, 454 | {"invalid", func() *Option { 455 | ii := []int{} 456 | o := New("help", IntRepeatType, &ii) 457 | o.MinArgs = 2 458 | o.MaxArgs = 1 459 | return o 460 | }(), fmt.Errorf("max should be > 0 and > min")}, 461 | {"invalid", func() *Option { 462 | ii := []int{} 463 | o := New("help", IntRepeatType, &ii) 464 | o.MinArgs = -1 465 | o.MaxArgs = 1 466 | return o 467 | }(), fmt.Errorf("min should be > 0")}, 468 | } 469 | for _, tt := range tests { 470 | t.Run(tt.name, func(t *testing.T) { 471 | err := tt.option.ValidateMinMaxArgs() 472 | if err == nil && tt.err != nil { 473 | t.Errorf("got = '%#v', want '%#v'", err, tt.err) 474 | } 475 | if err != nil && tt.err == nil { 476 | t.Errorf("got = '%#v', want '%#v'", err, tt.err) 477 | } 478 | if err != nil && tt.err != nil && err.Error() != tt.err.Error() { 479 | t.Errorf("got = '%#v', want '%#v'", err, tt.err) 480 | } 481 | }) 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /user_test.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package getoptions 10 | 11 | import ( 12 | "bytes" 13 | "context" 14 | "os" 15 | "path/filepath" 16 | "reflect" 17 | "strings" 18 | "testing" 19 | 20 | "github.com/DavidGamba/go-getoptions/internal/option" 21 | ) 22 | 23 | // User facing tree construction tests. 24 | 25 | func setupOpt() *GetOpt { 26 | opt := New() 27 | opt.String("rootopt1", "") 28 | 29 | cmd1 := opt.NewCommand("cmd1", "") 30 | cmd1.String("cmd1opt1", "") 31 | cmd2 := opt.NewCommand("cmd2", "") 32 | cmd2.String("cmd2opt1", "") 33 | 34 | sub1cmd1 := cmd1.NewCommand("sub1cmd1", "") 35 | sub1cmd1.String("sub1cmd1opt1", "") 36 | 37 | sub2cmd1 := cmd1.NewCommand("sub2cmd1", "") 38 | sub2cmd1.String("-", "") 39 | return opt 40 | } 41 | 42 | func TestStr(t *testing.T) { 43 | n := setupOpt().programTree 44 | str := n.str() 45 | if str.Name != "go-getoptions.test" { 46 | t.Errorf("wrong value: %s\n", stringPT(n)) 47 | } 48 | } 49 | 50 | func TestTrees(t *testing.T) { 51 | buf := setupLogging() 52 | 53 | t.Run("programTree", func(t *testing.T) { 54 | root := &programTree{ 55 | Name: filepath.Base(os.Args[0]), 56 | ChildCommands: map[string]*programTree{}, 57 | ChildOptions: map[string]*option.Option{}, 58 | } 59 | rootopt1Data := "" 60 | rootopt1 := option.New("rootopt1", option.StringType, &rootopt1Data) 61 | cmd1 := &programTree{ 62 | Name: "cmd1", 63 | Parent: root, 64 | ChildCommands: map[string]*programTree{}, 65 | ChildOptions: map[string]*option.Option{}, 66 | Level: 1, 67 | } 68 | cmd1opt1Data := "" 69 | cmd1opt1 := option.New("cmd1opt1", option.StringType, &cmd1opt1Data) 70 | sub1cmd1 := &programTree{ 71 | Name: "sub1cmd1", 72 | Parent: cmd1, 73 | ChildCommands: map[string]*programTree{}, 74 | ChildOptions: map[string]*option.Option{}, 75 | Level: 2, 76 | } 77 | sub1cmd1opt1Data := "" 78 | sub1cmd1opt1 := option.New("sub1cmd1opt1", option.StringType, &sub1cmd1opt1Data) 79 | sub2cmd1 := &programTree{ 80 | Name: "sub2cmd1", 81 | Parent: cmd1, 82 | ChildCommands: map[string]*programTree{}, 83 | ChildOptions: map[string]*option.Option{}, 84 | Level: 2, 85 | } 86 | sub2cmd1opt1Data := "" 87 | sub2cmd1opt1 := option.New("-", option.StringType, &sub2cmd1opt1Data) 88 | cmd2 := &programTree{ 89 | Name: "cmd2", 90 | Parent: root, 91 | ChildCommands: map[string]*programTree{}, 92 | ChildOptions: map[string]*option.Option{}, 93 | Level: 1, 94 | } 95 | cmd2opt1Data := "" 96 | cmd2opt1 := option.New("cmd2opt1", option.StringType, &cmd2opt1Data) 97 | 98 | root.ChildOptions["rootopt1"] = rootopt1 99 | root.ChildCommands["cmd1"] = cmd1 100 | root.ChildCommands["cmd2"] = cmd2 101 | 102 | // rootopt1Copycmd1 := rootopt1.Copy().SetParent(cmd1) 103 | rootopt1Copycmd1 := rootopt1 104 | cmd1.ChildOptions["rootopt1"] = rootopt1Copycmd1 105 | cmd1.ChildOptions["cmd1opt1"] = cmd1opt1 106 | cmd1.ChildCommands["sub1cmd1"] = sub1cmd1 107 | cmd1.ChildCommands["sub2cmd1"] = sub2cmd1 108 | 109 | // rootopt1Copycmd2 := rootopt1.Copy().SetParent(cmd2) 110 | rootopt1Copycmd2 := rootopt1 111 | cmd2.ChildOptions["rootopt1"] = rootopt1Copycmd2 112 | cmd2.ChildOptions["cmd2opt1"] = cmd2opt1 113 | 114 | // rootopt1Copysub1cmd1 := rootopt1.Copy().SetParent(sub1cmd1) 115 | rootopt1Copysub1cmd1 := rootopt1 116 | // cmd1opt1Copysub1cmd1 := cmd1opt1.Copy().SetParent(sub1cmd1) 117 | cmd1opt1Copysub1cmd1 := cmd1opt1 118 | 119 | sub1cmd1.ChildOptions["rootopt1"] = rootopt1Copysub1cmd1 120 | sub1cmd1.ChildOptions["cmd1opt1"] = cmd1opt1Copysub1cmd1 121 | sub1cmd1.ChildOptions["sub1cmd1opt1"] = sub1cmd1opt1 122 | 123 | // rootopt1Copysub2cmd1 := rootopt1.Copy().SetParent(sub2cmd1) 124 | rootopt1Copysub2cmd1 := rootopt1 125 | // cmd1opt1Copysub2cmd1 := cmd1opt1.Copy().SetParent(sub2cmd1) 126 | cmd1opt1Copysub2cmd1 := cmd1opt1 127 | sub2cmd1.ChildOptions["rootopt1"] = rootopt1Copysub2cmd1 128 | sub2cmd1.ChildOptions["cmd1opt1"] = cmd1opt1Copysub2cmd1 129 | sub2cmd1.ChildOptions["-"] = sub2cmd1opt1 130 | 131 | tree := setupOpt().programTree 132 | if !reflect.DeepEqual(root, tree) { 133 | t.Errorf(spewToFileDiff(t, root, tree)) 134 | t.Fatalf(programTreeError(root, tree)) 135 | } 136 | 137 | n, err := getNode(tree) 138 | if err != nil { 139 | t.Fatalf("unexpected error: %s", err) 140 | } 141 | 142 | if !reflect.DeepEqual(root, n) { 143 | t.Errorf(spewToFileDiff(t, root, n)) 144 | t.Fatalf(programTreeError(root, tree)) 145 | } 146 | 147 | n, err = getNode(tree, []string{}...) 148 | if err != nil { 149 | t.Errorf("unexpected error: %s", err) 150 | } 151 | 152 | if !reflect.DeepEqual(root, n) { 153 | t.Errorf(spewToFileDiff(t, root, n)) 154 | t.Fatalf(programTreeError(root, n)) 155 | } 156 | 157 | n, err = getNode(tree, "cmd1") 158 | if err != nil { 159 | t.Errorf("unexpected error: %s", err) 160 | } 161 | 162 | if !reflect.DeepEqual(cmd1, n) { 163 | t.Errorf(spewToFileDiff(t, cmd1, n)) 164 | t.Fatalf(programTreeError(cmd1, n)) 165 | } 166 | 167 | n, err = getNode(tree, "cmd1", "sub1cmd1") 168 | if err != nil { 169 | t.Errorf("unexpected error: %s", err) 170 | } 171 | 172 | if !reflect.DeepEqual(sub1cmd1, n) { 173 | t.Errorf(spewToFileDiff(t, sub1cmd1, n)) 174 | t.Fatalf(programTreeError(sub1cmd1, n)) 175 | } 176 | 177 | n, err = getNode(tree, "cmd2") 178 | if err != nil { 179 | t.Errorf("unexpected error: %s", err) 180 | } 181 | 182 | if !reflect.DeepEqual(cmd2, n) { 183 | t.Errorf(spewToFileDiff(t, cmd2, n)) 184 | t.Fatalf(programTreeError(cmd2, n)) 185 | } 186 | }) 187 | 188 | t.Cleanup(func() { t.Log(buf.String()) }) 189 | } 190 | 191 | func TestCompletion(t *testing.T) { 192 | fn := func(ctx context.Context, opt *GetOpt, args []string) error { 193 | return nil 194 | } 195 | called := false 196 | exitFn = func(code int) { called = true } 197 | 198 | cleanup := func() { 199 | os.Setenv("COMP_LINE", "") 200 | os.Setenv("ZSHELL", "") 201 | completionWriter = os.Stdout 202 | Writer = os.Stderr 203 | called = false 204 | } 205 | 206 | tests := []struct { 207 | name string 208 | setup func() 209 | args []string 210 | expected string 211 | err string 212 | }{ 213 | {"option", func() { os.Setenv("COMP_LINE", "./program --f") }, []string{}, "--f\n--flag\n--fleg\n", ""}, 214 | {"option", func() { os.Setenv("COMP_LINE", "./program --fl") }, []string{}, "--flag\n--fleg\n", ""}, 215 | {"option", func() { os.Setenv("COMP_LINE", "./program --d") }, []string{}, "--debug\n", ""}, 216 | {"command", func() { os.Setenv("COMP_LINE", "./program h") }, []string{}, "help \n", ""}, 217 | {"command", func() { os.Setenv("COMP_LINE", "./program help ") }, []string{}, "log\nshow\n", ""}, 218 | // TODO: --profile= when there are suggestions is probably not wanted 219 | {"command", func() { os.Setenv("COMP_LINE", "./program --profile") }, []string{}, "--profile=\n--profile=dev\n--profile=production\n--profile=staging\n", ""}, 220 | {"command", func() { os.Setenv("COMP_LINE", "./program --profile=") }, []string{}, "dev\nproduction\nstaging\n", ""}, 221 | {"command", func() { os.Setenv("COMP_LINE", "./program --profile=p") }, []string{}, "production\n", ""}, 222 | {"command", func() { os.Setenv("COMP_LINE", "./program lo ") }, []string{"./program", "lo", "./program"}, "log \n", ""}, 223 | {"command", func() { os.Setenv("COMP_LINE", "./program show sub-show ") }, []string{}, "hello\nhelp\npassword\nprofile\n", ""}, 224 | {"command", func() { os.Setenv("COMP_LINE", "./program log sub-log ") }, []string{}, "debug\nerror\nhelp\ninfo\n", ""}, 225 | {"command", func() { os.Setenv("COMP_LINE", "./program log sub-log i") }, []string{}, "infinity\ninfo\ninformational\n", ""}, 226 | {"command", func() { os.Setenv("COMP_LINE", "./program show --level") }, []string{}, "--level=\n--level=\n", ""}, 227 | {"command", func() { os.Setenv("COMP_LINE", "./program show --level=") }, []string{}, "debug\nerror\ninfo\n", ""}, 228 | {"command", func() { os.Setenv("COMP_LINE", "./program show --level=i") }, []string{}, "infinity\ninfo\ninformational\n", ""}, 229 | 230 | // zshell 231 | {"zshell option", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program --f") }, []string{}, "--f\n--flag\n--fleg\n", ""}, 232 | {"zshell option", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program --fl") }, []string{}, "--flag\n--fleg\n", ""}, 233 | {"zshell option", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program --d") }, []string{}, "--debug\n", ""}, 234 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program h") }, []string{}, "help\n", ""}, 235 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program help ") }, []string{}, "log\nshow\n", ""}, 236 | // TODO: --profile= when there are suggestions is probably not wanted 237 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program --profile") }, []string{}, "--profile=\n--profile=dev\n--profile=production\n--profile=staging\n", ""}, 238 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program --profile=") }, []string{}, "--profile=dev\n--profile=production\n--profile=staging\n", ""}, 239 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program --profile=p") }, []string{}, "--profile=production\n", ""}, 240 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program lo ") }, []string{"./program", "lo", "./program"}, "log\n", ""}, 241 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program show sub-show ") }, []string{}, "hello\nhelp\npassword\nprofile\n", ""}, 242 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program log sub-log ") }, []string{}, "debug\nerror\nhelp\ninfo\n", ""}, 243 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program log sub-log i") }, []string{}, "infinity\ninfo\ninformational\n", ""}, 244 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program show --level") }, []string{}, "--level=\n--level=\n", ""}, 245 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program show --level=") }, []string{}, "--level=debug\n--level=error\n--level=info\n", ""}, 246 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program show --level=i") }, []string{}, "--level=infinity\n--level=info\n--level=informational\n", ""}, 247 | } 248 | validValuesTests := []struct { 249 | name string 250 | setup func() 251 | args []string 252 | expected string 253 | err string 254 | }{ 255 | {"command", func() { os.Setenv("COMP_LINE", "./program --profile=a ") }, []string{}, "", ` 256 | ERROR: wrong value for option 'profile', valid values are ["dev" "staging" "production"] 257 | `}, 258 | {"zshell command", func() { os.Setenv("ZSHELL", "true"); os.Setenv("COMP_LINE", "./program --profile=a ") }, []string{}, "", ` 259 | ERROR: wrong value for option 'profile', valid values are ["dev" "staging" "production"] 260 | `}, 261 | } 262 | for _, tt := range tests { 263 | t.Run(tt.name, func(t *testing.T) { 264 | tt.setup() 265 | completionBuf := new(bytes.Buffer) 266 | completionWriter = completionBuf 267 | buf := new(bytes.Buffer) 268 | logger := new(bytes.Buffer) 269 | Writer = buf 270 | 271 | Logger.SetOutput(logger) 272 | 273 | opt := New() 274 | opt.Bool("flag", false, opt.Alias("f")) 275 | opt.Bool("fleg", false) 276 | opt.Bool("debug", false) 277 | opt.String("profile", "", opt.SuggestedValues("dev", "staging", "production")) 278 | logCmd := opt.NewCommand("log", "").SetCommandFn(fn) 279 | logCmd.NewCommand("sub-log", "").SetCommandFn(fn).ArgCompletionsFns(func(target string, prev []string, s string) []string { 280 | if strings.HasPrefix(s, "i") { 281 | return []string{"info", "informational", "infinity"} 282 | } 283 | return []string{"info", "debug", "error"} 284 | }) 285 | showCmd := opt.NewCommand("show", "").SetCommandFn(fn) 286 | showCmd.String("level", "", opt.SuggestedValuesFn(func(target string, s string) []string { 287 | if strings.HasPrefix(s, "i") { 288 | return []string{"info", "informational", "infinity"} 289 | } 290 | return []string{"info", "debug", "error"} 291 | })) 292 | showCmd.NewCommand("sub-show", "").SetCommandFn(fn).ArgCompletions("profile", "password", "hello") 293 | opt.HelpCommand("help") 294 | _, err := opt.Parse(tt.args) 295 | if err != nil { 296 | t.Errorf("Unexpected error: %s", err) 297 | } 298 | if !called { 299 | t.Errorf("COMP_LINE set and exit wasn't called") 300 | } 301 | if completionBuf.String() != tt.expected { 302 | t.Errorf("Error\ngot: '%s', expected: '%s'\n", completionBuf.String(), tt.expected) 303 | t.Errorf("diff:\n%s", firstDiff(completionBuf.String(), tt.expected)) 304 | t.Errorf("log: %s\n", logger.String()) 305 | } 306 | if buf.String() != tt.err { 307 | t.Errorf("buf: %s\n", buf.String()) 308 | t.Errorf("diff:\n%s", firstDiff(buf.String(), tt.err)) 309 | t.Errorf("log: %s\n", logger.String()) 310 | } 311 | cleanup() 312 | }) 313 | } 314 | // TODO: Only difference between these two sets of tests is that the second one uses ValidValues rather than SuggestedValues. 315 | for _, tt := range append(tests, validValuesTests...) { 316 | t.Run(tt.name, func(t *testing.T) { 317 | tt.setup() 318 | completionBuf := new(bytes.Buffer) 319 | completionWriter = completionBuf 320 | buf := new(bytes.Buffer) 321 | logger := new(bytes.Buffer) 322 | Writer = buf 323 | 324 | Logger.SetOutput(logger) 325 | 326 | opt := New() 327 | opt.Bool("flag", false, opt.Alias("f")) 328 | opt.Bool("fleg", false) 329 | opt.Bool("debug", false) 330 | opt.String("profile", "", opt.ValidValues("dev", "staging", "production")) 331 | logCmd := opt.NewCommand("log", "").SetCommandFn(fn) 332 | logCmd.NewCommand("sub-log", "").SetCommandFn(fn).ArgCompletionsFns(func(target string, prev []string, s string) []string { 333 | if strings.HasPrefix(s, "i") { 334 | return []string{"info", "informational", "infinity"} 335 | } 336 | return []string{"info", "debug", "error"} 337 | }) 338 | showCmd := opt.NewCommand("show", "").SetCommandFn(fn) 339 | showCmd.String("level", "", opt.SuggestedValuesFn(func(target string, s string) []string { 340 | if strings.HasPrefix(s, "i") { 341 | return []string{"info", "informational", "infinity"} 342 | } 343 | return []string{"info", "debug", "error"} 344 | })) 345 | showCmd.NewCommand("sub-show", "").SetCommandFn(fn).ArgCompletions("profile", "password", "hello") 346 | opt.HelpCommand("help") 347 | _, err := opt.Parse(tt.args) 348 | if err != nil { 349 | t.Errorf("Unexpected error: %s", err) 350 | } 351 | if !called { 352 | t.Errorf("COMP_LINE set and exit wasn't called") 353 | } 354 | if completionBuf.String() != tt.expected { 355 | t.Errorf("Error\ngot: '%s', expected: '%s'\n", completionBuf.String(), tt.expected) 356 | t.Errorf("diff:\n%s", firstDiff(completionBuf.String(), tt.expected)) 357 | t.Errorf("log: %s\n", logger.String()) 358 | } 359 | if buf.String() != tt.err { 360 | t.Errorf("buf: %s\n", buf.String()) 361 | t.Errorf("diff:\n%s", firstDiff(buf.String(), tt.err)) 362 | t.Errorf("log: %s\n", logger.String()) 363 | } 364 | cleanup() 365 | }) 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | package getoptions 10 | 11 | import ( 12 | "reflect" 13 | "testing" 14 | ) 15 | 16 | func TestParseCLIArgs(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | args []string 20 | mode Mode 21 | expected *programTree 22 | completions completions 23 | err error 24 | }{ 25 | {"empty", nil, Normal, setupOpt().programTree, []string{}, nil}, 26 | 27 | {"empty", []string{}, Normal, setupOpt().programTree, []string{}, nil}, 28 | 29 | {"text", []string{"txt", "txt2"}, Normal, func() *programTree { 30 | n := setupOpt().programTree 31 | n.ChildText = append(n.ChildText, "txt") 32 | n.ChildText = append(n.ChildText, "txt2") 33 | return n 34 | }(), []string{}, nil}, 35 | 36 | {"command", []string{"cmd1"}, Normal, func() *programTree { 37 | n, err := getNode(setupOpt().programTree, "cmd1") 38 | if err != nil { 39 | panic(err) 40 | } 41 | return n 42 | }(), []string{}, nil}, 43 | 44 | {"text to command", []string{"cmd1", "txt", "txt2"}, Normal, func() *programTree { 45 | n, err := getNode(setupOpt().programTree, "cmd1") 46 | if err != nil { 47 | panic(err) 48 | } 49 | n.ChildText = append(n.ChildText, "txt") 50 | n.ChildText = append(n.ChildText, "txt2") 51 | return n 52 | }(), []string{}, nil}, 53 | 54 | {"text to sub command", []string{"cmd1", "sub1cmd1", "txt", "txt2"}, Normal, func() *programTree { 55 | n, err := getNode(setupOpt().programTree, "cmd1", "sub1cmd1") 56 | if err != nil { 57 | panic(err) 58 | } 59 | n.ChildText = append(n.ChildText, "txt") 60 | n.ChildText = append(n.ChildText, "txt2") 61 | return n 62 | }(), []string{}, nil}, 63 | 64 | {"option with arg", []string{"--rootopt1=hello", "txt", "txt2"}, Normal, func() *programTree { 65 | n := setupOpt().programTree 66 | opt, ok := n.ChildOptions["rootopt1"] 67 | if !ok { 68 | t.Fatalf("not found") 69 | } 70 | opt.Called = true 71 | opt.UsedAlias = "rootopt1" 72 | err := opt.Save("hello") 73 | if err != nil { 74 | t.Fatalf("unexpected error: %s", err) 75 | } 76 | n.ChildText = append(n.ChildText, "txt") 77 | n.ChildText = append(n.ChildText, "txt2") 78 | return n 79 | }(), []string{}, nil}, 80 | 81 | {"option", []string{"--rootopt1", "hello", "txt", "txt2"}, Normal, func() *programTree { 82 | n := setupOpt().programTree 83 | opt, ok := n.ChildOptions["rootopt1"] 84 | if !ok { 85 | t.Fatalf("not found") 86 | } 87 | opt.Called = true 88 | opt.UsedAlias = "rootopt1" 89 | err := opt.Save("hello") 90 | if err != nil { 91 | t.Fatalf("unexpected error: %s", err) 92 | } 93 | n.ChildText = append(n.ChildText, "txt") 94 | n.ChildText = append(n.ChildText, "txt2") 95 | return n 96 | }(), []string{}, nil}, 97 | 98 | {"option error missing argument", []string{"--rootopt1"}, Normal, func() *programTree { 99 | n := setupOpt().programTree 100 | opt, ok := n.ChildOptions["rootopt1"] 101 | if !ok { 102 | t.Fatalf("not found") 103 | } 104 | opt.Called = true 105 | opt.UsedAlias = "rootopt1" 106 | return n 107 | }(), []string{}, ErrorParsing}, 108 | 109 | {"terminator", []string{"--", "--opt1", "txt", "txt2"}, Normal, func() *programTree { 110 | n := setupOpt().programTree 111 | n.ChildText = append(n.ChildText, "--opt1") 112 | n.ChildText = append(n.ChildText, "txt") 113 | n.ChildText = append(n.ChildText, "txt2") 114 | return n 115 | }(), []string{}, nil}, 116 | 117 | {"lonesome dash", []string{"cmd1", "sub2cmd1", "-", "txt", "txt2"}, Normal, func() *programTree { 118 | tree := setupOpt().programTree 119 | n, err := getNode(tree, "cmd1", "sub2cmd1") 120 | if err != nil { 121 | t.Fatalf("unexpected error: %s, %s", err, stringPT(n)) 122 | } 123 | opt, ok := n.ChildOptions["-"] 124 | if !ok { 125 | t.Fatalf("not found: %s", stringPT(n)) 126 | } 127 | opt.Called = true 128 | opt.UsedAlias = "-" 129 | err = opt.Save("txt") 130 | if err != nil { 131 | t.Fatalf("unexpected error: %s", err) 132 | } 133 | n.ChildText = append(n.ChildText, "txt2") 134 | return n 135 | }(), []string{"-", "--cmd1opt1", "--rootopt1"}, nil}, 136 | 137 | {"root option to command", []string{"cmd1", "--rootopt1", "hello"}, Normal, func() *programTree { 138 | tree := setupOpt().programTree 139 | n, err := getNode(tree, "cmd1") 140 | if err != nil { 141 | t.Fatalf("unexpected error: %s, %s", err, stringPT(n)) 142 | } 143 | opt, ok := n.ChildOptions["rootopt1"] 144 | if !ok { 145 | t.Fatalf("not found: %s", stringPT(n)) 146 | } 147 | opt.Called = true 148 | opt.UsedAlias = "rootopt1" 149 | err = opt.Save("hello") 150 | if err != nil { 151 | t.Fatalf("unexpected error: %s", err) 152 | } 153 | return n 154 | }(), []string{}, nil}, 155 | 156 | {"root option to subcommand", []string{"cmd1", "sub2cmd1", "--rootopt1", "hello"}, Normal, func() *programTree { 157 | tree := setupOpt().programTree 158 | n, err := getNode(tree, "cmd1", "sub2cmd1") 159 | if err != nil { 160 | t.Fatalf("unexpected error: %s, %s", err, stringPT(n)) 161 | } 162 | opt, ok := n.ChildOptions["rootopt1"] 163 | if !ok { 164 | t.Fatalf("not found: %s", stringPT(n)) 165 | } 166 | opt.Called = true 167 | opt.UsedAlias = "rootopt1" 168 | err = opt.Save("hello") 169 | if err != nil { 170 | t.Fatalf("unexpected error: %s", err) 171 | } 172 | return n 173 | }(), []string{}, nil}, 174 | 175 | {"option to subcommand", []string{"cmd1", "sub1cmd1", "--sub1cmd1opt1=hello"}, Normal, func() *programTree { 176 | tree := setupOpt().programTree 177 | n, err := getNode(tree, "cmd1", "sub1cmd1") 178 | if err != nil { 179 | t.Fatalf("unexpected error: %s, %s", err, stringPT(n)) 180 | } 181 | opt, ok := n.ChildOptions["sub1cmd1opt1"] 182 | if !ok { 183 | t.Fatalf("not found: %s", stringPT(n)) 184 | } 185 | opt.Called = true 186 | opt.UsedAlias = "sub1cmd1opt1" 187 | err = opt.Save("hello") 188 | if err != nil { 189 | t.Fatalf("unexpected error: %s", err) 190 | } 191 | return n 192 | }(), []string{}, nil}, 193 | 194 | {"option to subcommand", []string{"cmd1", "sub1cmd1", "--sub1cmd1opt1", "hello"}, Normal, func() *programTree { 195 | tree := setupOpt().programTree 196 | n, err := getNode(tree, "cmd1", "sub1cmd1") 197 | if err != nil { 198 | t.Fatalf("unexpected error: %s, %s", err, stringPT(n)) 199 | } 200 | opt, ok := n.ChildOptions["sub1cmd1opt1"] 201 | if !ok { 202 | t.Fatalf("not found: %s", stringPT(n)) 203 | } 204 | opt.Called = true 205 | opt.UsedAlias = "sub1cmd1opt1" 206 | err = opt.Save("hello") 207 | if err != nil { 208 | t.Fatalf("unexpected error: %s", err) 209 | } 210 | return n 211 | }(), []string{}, nil}, 212 | 213 | {"option argument with dash", []string{"cmd1", "sub1cmd1", "--sub1cmd1opt1", "-hello"}, Normal, func() *programTree { 214 | tree := setupOpt().programTree 215 | n, err := getNode(tree, "cmd1", "sub1cmd1") 216 | if err != nil { 217 | t.Fatalf("unexpected error: %s, %s", err, stringPT(n)) 218 | } 219 | opt, ok := n.ChildOptions["sub1cmd1opt1"] 220 | if !ok { 221 | t.Fatalf("not found: %s", stringPT(n)) 222 | } 223 | opt.Called = true 224 | opt.UsedAlias = "sub1cmd1opt1" 225 | return n 226 | }(), []string{}, ErrorParsing}, 227 | 228 | // {"command", []string{"--opt1", "cmd1", "--cmd1opt1"}, Normal, &programTree{ 229 | // Type: argTypeProgname, 230 | // Name: os.Args[0], 231 | // option: option{Args: []string{"--opt1", "cmd1", "--cmd1opt1"}}, 232 | // Children: []*programTree{ 233 | // { 234 | // Type: argTypeOption, 235 | // Name: "opt1", 236 | // option: option{Args: []string{}}, 237 | // Children: []*programTree{}, 238 | // }, 239 | // { 240 | // Type: argTypeCommand, 241 | // Name: "cmd1", 242 | // option: option{Args: []string{}}, 243 | // Children: []*programTree{ 244 | // { 245 | // Type: argTypeOption, 246 | // Name: "cmd1opt1", 247 | // option: option{Args: []string{}}, 248 | // Children: []*programTree{}, 249 | // }, 250 | // }, 251 | // }, 252 | // }, 253 | // }}, 254 | // {"subcommand", []string{"--opt1", "cmd1", "--cmd1opt1", "sub1cmd1", "--sub1cmd1opt1"}, Normal, &programTree{ 255 | // Type: argTypeProgname, 256 | // Name: os.Args[0], 257 | // option: option{Args: []string{"--opt1", "cmd1", "--cmd1opt1", "sub1cmd1", "--sub1cmd1opt1"}}, 258 | // Children: []*programTree{ 259 | // { 260 | // Type: argTypeOption, 261 | // Name: "opt1", 262 | // option: option{Args: []string{}}, 263 | // Children: []*programTree{}, 264 | // }, 265 | // { 266 | // Type: argTypeCommand, 267 | // Name: "cmd1", 268 | // option: option{Args: []string{}}, 269 | // Children: []*programTree{ 270 | // { 271 | // Type: argTypeOption, 272 | // Name: "cmd1opt1", 273 | // option: option{Args: []string{}}, 274 | // Children: []*programTree{}, 275 | // }, 276 | // { 277 | // Type: argTypeCommand, 278 | // Name: "sub1cmd1", 279 | // option: option{Args: []string{}}, 280 | // Children: []*programTree{ 281 | // { 282 | // Type: argTypeOption, 283 | // Name: "sub1cmd1opt1", 284 | // option: option{Args: []string{}}, 285 | // Children: []*programTree{}, 286 | // }, 287 | // }, 288 | // }, 289 | // }, 290 | // }, 291 | // }, 292 | // }}, 293 | // {"arg", []string{"hello", "world"}, Normal, &programTree{ 294 | // Type: argTypeProgname, 295 | // Name: os.Args[0], 296 | // option: option{Args: []string{"hello", "world"}}, 297 | // Children: []*programTree{ 298 | // { 299 | // Type: argTypeText, 300 | // Name: "hello", 301 | // option: option{Args: []string{}}, 302 | // Children: []*programTree{}, 303 | // }, 304 | // { 305 | // Type: argTypeText, 306 | // Name: "world", 307 | // option: option{Args: []string{}}, 308 | // Children: []*programTree{}, 309 | // }, 310 | // }, 311 | // }}, 312 | } 313 | 314 | for _, test := range tests { 315 | t.Run(test.name, func(t *testing.T) { 316 | logTestOutput := setupTestLogging(t) 317 | defer logTestOutput() 318 | 319 | tree := setupOpt().programTree 320 | argTree, _, err := parseCLIArgs("", tree, test.args, test.mode) 321 | checkError(t, err, test.err) 322 | if !reflect.DeepEqual(test.expected, argTree) { 323 | t.Errorf(spewToFileDiff(t, test.expected, argTree)) 324 | t.Fatalf(programTreeError(test.expected, argTree)) 325 | } 326 | }) 327 | 328 | // This might be too annoying to maintain 329 | // t.Run("completion "+test.name, func(t *testing.T) { 330 | // logTestOutput := setupTestLogging(t) 331 | // defer logTestOutput() 332 | // 333 | // tree := setupOpt().programTree 334 | // _, comps, err := parseCLIArgs(true, tree, test.args, test.mode) 335 | // checkError(t, err, test.err) 336 | // if !reflect.DeepEqual(test.completions, comps) { 337 | // t.Fatalf("expected completions: \n%v\n got: \n%v\n", test.completions, comps) 338 | // } 339 | // }) 340 | } 341 | } 342 | 343 | func TestParseCLIArgsCompletions(t *testing.T) { 344 | tests := []struct { 345 | name string 346 | completionTarget string 347 | args []string 348 | mode Mode 349 | completions completions 350 | err error 351 | }{ 352 | {"empty", "bash", nil, Normal, []string{"cmd1", "cmd2"}, nil}, 353 | 354 | {"empty", "bash", []string{}, Normal, []string{"cmd1", "cmd2"}, nil}, 355 | 356 | {"text", "bash", []string{"txt"}, Normal, []string{}, nil}, 357 | 358 | {"command", "bash", []string{"cmd"}, Normal, []string{"cmd1", "cmd2"}, nil}, 359 | 360 | {"command", "bash", []string{"cmd1"}, Normal, []string{"cmd1 "}, nil}, 361 | 362 | {"command", "bash", []string{"cmd1", ""}, Normal, []string{"sub1cmd1", "sub2cmd1"}, nil}, 363 | 364 | {"command", "bash", []string{"cmd1", "sub"}, Normal, []string{"sub1cmd1", "sub2cmd1"}, nil}, 365 | 366 | {"command", "bash", []string{"cmd1", "sub1"}, Normal, []string{"sub1cmd1 "}, nil}, 367 | 368 | {"text to command", "bash", []string{"cmd1", "txt"}, Normal, []string{}, nil}, 369 | 370 | {"text to sub command", "bash", []string{"cmd1", "sub1cmd1", "txt"}, Normal, []string{}, nil}, 371 | 372 | {"option", "bash", []string{"-"}, Normal, []string{"--rootopt1=", "--rootopt1="}, nil}, 373 | 374 | {"option", "zsh", []string{"-"}, Normal, []string{"--rootopt1=", "--rootopt1="}, nil}, 375 | 376 | {"option", "bash", []string{"--"}, Normal, []string{"--rootopt1=", "--rootopt1="}, nil}, 377 | 378 | {"option", "zsh", []string{"--"}, Normal, []string{"--rootopt1=", "--rootopt1="}, nil}, 379 | 380 | {"option", "bash", []string{"--r"}, Normal, []string{"--rootopt1=", "--rootopt1="}, nil}, 381 | 382 | {"option", "zsh", []string{"--r"}, Normal, []string{"--rootopt1=", "--rootopt1="}, nil}, 383 | 384 | {"option", "bash", []string{"--rootopt1"}, Normal, []string{"--rootopt1=", "--rootopt1="}, nil}, 385 | 386 | {"option", "zsh", []string{"--rootopt1"}, Normal, []string{"--rootopt1=", "--rootopt1="}, nil}, 387 | 388 | {"option with arg", "bash", []string{"--rootopt1=hello"}, Normal, []string{}, nil}, 389 | 390 | {"option with arg", "zsh", []string{"--rootopt1=hello"}, Normal, []string{}, nil}, 391 | 392 | {"option", "bash", []string{"--rootopt1", "hello"}, Normal, []string{}, nil}, 393 | 394 | {"option", "zsh", []string{"--rootopt1", "hello"}, Normal, []string{}, nil}, 395 | 396 | {"terminator", "bash", []string{"--", "--opt1"}, Normal, []string{}, nil}, 397 | 398 | {"terminator", "zsh", []string{"--", "--opt1"}, Normal, []string{}, nil}, 399 | 400 | {"lonesome dash", "bash", []string{"cmd1", "sub2cmd1", "-"}, Normal, []string{"-", "--cmd1opt1=", "--rootopt1="}, nil}, 401 | 402 | {"lonesome dash", "zsh", []string{"cmd1", "sub2cmd1", "-"}, Normal, []string{"-", "--cmd1opt1=", "--rootopt1="}, nil}, 403 | 404 | {"root option to command", "bash", []string{"cmd1", "--rootopt1", "hello"}, Normal, []string{}, nil}, 405 | 406 | {"root option to command", "zsh", []string{"cmd1", "--rootopt1", "hello"}, Normal, []string{}, nil}, 407 | 408 | {"root option to subcommand", "bash", []string{"cmd1", "sub2cmd1", "--rootopt1", "hello"}, Normal, []string{}, nil}, 409 | 410 | {"root option to subcommand", "zsh", []string{"cmd1", "sub2cmd1", "--rootopt1", "hello"}, Normal, []string{}, nil}, 411 | 412 | {"option to subcommand", "bash", []string{"cmd1", "sub1cmd1", "--sub1cmd1opt1=hello"}, Normal, []string{}, nil}, 413 | 414 | {"option to subcommand", "zsh", []string{"cmd1", "sub1cmd1", "--sub1cmd1opt1=hello"}, Normal, []string{}, nil}, 415 | 416 | {"option to subcommand", "bash", []string{"cmd1", "sub1cmd1", "--sub1cmd1opt1", "hello"}, Normal, []string{}, nil}, 417 | 418 | {"option to subcommand", "zsh", []string{"cmd1", "sub1cmd1", "--sub1cmd1opt1", "hello"}, Normal, []string{}, nil}, 419 | 420 | {"option argument with dash", "bash", []string{"cmd1", "sub1cmd1", "--sub1cmd1opt1", "-hello"}, Normal, []string{}, ErrorParsing}, 421 | 422 | {"option argument with dash", "zsh", []string{"cmd1", "sub1cmd1", "--sub1cmd1opt1", "-hello"}, Normal, []string{}, ErrorParsing}, 423 | } 424 | 425 | for _, test := range tests { 426 | t.Run(test.name, func(t *testing.T) { 427 | logTestOutput := setupTestLogging(t) 428 | defer logTestOutput() 429 | 430 | tree := setupOpt().programTree 431 | _, comps, err := parseCLIArgs("bash", tree, test.args, test.mode) 432 | checkError(t, err, test.err) 433 | if !reflect.DeepEqual(test.completions, comps) { 434 | t.Fatalf("expected completions: \n%#v\n got: \n%#v\n", test.completions, comps) 435 | } 436 | }) 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /internal/option/option.go: -------------------------------------------------------------------------------- 1 | // This file is part of go-getoptions. 2 | // 3 | // Copyright (C) 2015-2025 David Gamba Rios 4 | // 5 | // This Source Code Form is subject to the terms of the Mozilla Public 6 | // License, v. 2.0. If a copy of the MPL was not distributed with this 7 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | 9 | // Package option - internal option struct and methods. 10 | package option 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | "io" 16 | "log" 17 | "sort" 18 | "strconv" 19 | "strings" 20 | 21 | "github.com/DavidGamba/go-getoptions/text" 22 | ) 23 | 24 | // Logger instance set to `io.Discard` by default. 25 | // Enable debug logging by setting: `Logger.SetOutput(os.Stderr)`. 26 | var Logger = log.New(io.Discard, "DEBUG: ", log.Ldate|log.Ltime|log.Lshortfile) 27 | 28 | var ErrorMissingRequiredOption = errors.New("") 29 | 30 | // Handler - Signature for the function that handles saving to the option. 31 | type Handler func(optName string, argument string, usedAlias string) error 32 | 33 | // ValueCompletionsFn - Function receiver for custom completions. 34 | // The `target` argument indicates "bash" or "zsh" for the completion targets. 35 | type ValueCompletionsFn func(target string, partialCompletion string) []string 36 | 37 | // Type - Indicates the type of option. 38 | type Type int 39 | 40 | // Option Types 41 | const ( 42 | BoolType Type = iota 43 | IncrementType 44 | 45 | StringType 46 | IntType 47 | Float64Type 48 | 49 | StringOptionalType 50 | IntOptionalType 51 | Float64OptionalType 52 | 53 | StringRepeatType 54 | IntRepeatType 55 | Float64RepeatType 56 | 57 | StringMapType 58 | ) 59 | 60 | // Option - main object 61 | type Option struct { 62 | Name string 63 | Aliases []string 64 | EnvVar string // Env Var that sets the option value 65 | Called bool // Indicates if the option was passed on the command line 66 | UsedAlias string // Alias/Env var used when the option was called 67 | Handler Handler // method used to handle the option 68 | IsOptional bool // Indicates if an option has an optional argument 69 | MapKeysToLower bool // Indicates if the option of map type has it keys set ToLower 70 | OptType Type // Option Type 71 | MinArgs int // minimum args when using multi 72 | MaxArgs int // maximum args when using multi 73 | 74 | IsRequired bool // Indicates if the option is required 75 | IsRequiredErr string // Error message for the required option 76 | 77 | // SuggestedValues used for completions, suggestions don't necessarily limit 78 | // the values you are able to use 79 | SuggestedValues []string 80 | ValidValues []string // ValidValues that can be passed to Save 81 | SuggestedValuesFn ValueCompletionsFn 82 | 83 | // Help 84 | DefaultStr string // String representation of default value 85 | Description string // Optional description used for help 86 | HelpArgName string // Optional arg name used for help 87 | HelpSynopsis string // Help synopsis 88 | 89 | boolDefault bool // copy of bool default value 90 | 91 | // Pointer receivers: 92 | pBool *bool // receiver for bool pointer 93 | pString *string // receiver for string pointer 94 | pInt *int // receiver for int pointer 95 | pFloat64 *float64 // receiver for float64 pointer 96 | pStringS *[]string // receiver for string slice pointer 97 | pIntS *[]int // receiver for int slice pointer 98 | pFloat64S *[]float64 // receiver for float64 slice pointer 99 | pStringM *map[string]string // receiver for string map pointer 100 | 101 | Unknown bool // Temporary marker used during parsing 102 | 103 | // Verbatim text used to generate the option 104 | // Used in cases where the option is unknown and we eventually have to send it to the remaining slice. 105 | Verbatim string 106 | } 107 | 108 | // New - Returns a new option object 109 | func New(name string, optType Type, data interface{}) *Option { 110 | opt := &Option{ 111 | Name: name, 112 | OptType: optType, 113 | Aliases: []string{name}, 114 | } 115 | switch optType { 116 | case StringType: 117 | opt.HelpArgName = "string" 118 | opt.pString = data.(*string) 119 | opt.DefaultStr = fmt.Sprintf("\"%s\"", *data.(*string)) 120 | opt.MinArgs = 1 121 | opt.MaxArgs = 1 122 | case StringOptionalType: 123 | opt.HelpArgName = "string" 124 | opt.pString = data.(*string) 125 | opt.DefaultStr = *data.(*string) 126 | opt.MinArgs = 0 127 | opt.MaxArgs = 1 128 | opt.IsOptional = true 129 | case StringRepeatType: 130 | opt.HelpArgName = "string" 131 | opt.pStringS = data.(*[]string) 132 | opt.DefaultStr = "[]" 133 | opt.MinArgs = 1 134 | opt.MaxArgs = 1 // By default we only allow one argument at a time 135 | case IntType: 136 | opt.HelpArgName = "int" 137 | opt.pInt = data.(*int) 138 | opt.DefaultStr = fmt.Sprintf("%d", *data.(*int)) 139 | opt.MinArgs = 1 140 | opt.MaxArgs = 1 141 | case IntOptionalType: 142 | opt.HelpArgName = "int" 143 | opt.pInt = data.(*int) 144 | opt.DefaultStr = fmt.Sprintf("%d", *data.(*int)) 145 | opt.MinArgs = 0 146 | opt.MaxArgs = 1 147 | opt.IsOptional = true 148 | case IntRepeatType: 149 | opt.HelpArgName = "int" 150 | opt.pIntS = data.(*[]int) 151 | opt.DefaultStr = "[]" 152 | opt.MinArgs = 1 153 | opt.MaxArgs = 1 // By default we only allow one argument at a time 154 | case Float64Type: 155 | opt.HelpArgName = "float64" 156 | opt.pFloat64 = data.(*float64) 157 | opt.DefaultStr = fmt.Sprintf("%f", *data.(*float64)) 158 | opt.MinArgs = 1 159 | opt.MaxArgs = 1 160 | case Float64OptionalType: 161 | opt.HelpArgName = "float64" 162 | opt.pFloat64 = data.(*float64) 163 | opt.DefaultStr = fmt.Sprintf("%f", *data.(*float64)) 164 | opt.MinArgs = 0 165 | opt.MaxArgs = 1 166 | opt.IsOptional = true 167 | case Float64RepeatType: 168 | opt.HelpArgName = "float64" 169 | opt.pFloat64S = data.(*[]float64) 170 | opt.DefaultStr = "[]" 171 | opt.MinArgs = 1 172 | opt.MaxArgs = 1 // By default we only allow one argument at a time 173 | case StringMapType: 174 | opt.HelpArgName = "key=value" 175 | opt.pStringM = data.(*map[string]string) 176 | opt.DefaultStr = "{}" 177 | opt.MinArgs = 1 178 | opt.MaxArgs = 1 // By default we only allow one argument at a time 179 | case IncrementType: 180 | opt.pInt = data.(*int) 181 | opt.DefaultStr = fmt.Sprintf("%d", *data.(*int)) 182 | opt.MinArgs = 0 183 | opt.MaxArgs = 0 184 | case BoolType: 185 | opt.pBool = data.(*bool) 186 | opt.boolDefault = *data.(*bool) 187 | opt.DefaultStr = fmt.Sprintf("%t", *data.(*bool)) 188 | opt.MinArgs = 0 189 | opt.MaxArgs = 0 190 | } 191 | opt.Synopsis() 192 | return opt 193 | } 194 | 195 | // ValidateMinMaxArgs - validates that the min and max make sense. 196 | // 197 | // NOTE: This should only be called to validate Repeat types. 198 | func (opt *Option) ValidateMinMaxArgs() error { 199 | if opt.MinArgs <= 0 { 200 | return fmt.Errorf("min should be > 0") 201 | } 202 | if opt.MaxArgs <= 0 || opt.MaxArgs < opt.MinArgs { 203 | return fmt.Errorf("max should be > 0 and > min") 204 | } 205 | return nil 206 | } 207 | 208 | func (opt *Option) Synopsis() { 209 | aliases := []string{} 210 | for _, e := range opt.Aliases { 211 | if len(e) > 1 { 212 | e = "--" + e 213 | } else { 214 | // Don't add extra dash for lonesome dash 215 | if e != "-" { 216 | e = "-" + e 217 | } 218 | } 219 | aliases = append(aliases, e) 220 | } 221 | opt.HelpSynopsis = strings.Join(aliases, "|") 222 | if opt.OptType != BoolType { 223 | opt.HelpSynopsis += fmt.Sprintf(" <%s>", opt.HelpArgName) 224 | } 225 | if opt.MaxArgs > 1 { 226 | opt.HelpSynopsis += "..." 227 | } 228 | } 229 | 230 | // Value - Get untyped option value 231 | func (opt *Option) Value() interface{} { 232 | switch opt.OptType { 233 | case StringType, StringOptionalType: 234 | return *opt.pString 235 | case StringRepeatType: 236 | return *opt.pStringS 237 | case IncrementType, IntType, IntOptionalType: 238 | return *opt.pInt 239 | case IntRepeatType: 240 | return *opt.pIntS 241 | case Float64Type, Float64OptionalType: 242 | return *opt.pFloat64 243 | case Float64RepeatType: 244 | return *opt.pFloat64S 245 | case StringMapType: 246 | return *opt.pStringM 247 | default: // BoolType: 248 | return *opt.pBool 249 | } 250 | } 251 | 252 | // SetAlias - Adds aliases to an option. 253 | func (opt *Option) SetAlias(alias ...string) *Option { 254 | opt.Aliases = append(opt.Aliases, alias...) 255 | opt.Synopsis() 256 | return opt 257 | } 258 | 259 | // SetDescription - Updates the Description. 260 | func (opt *Option) SetDescription(s string) *Option { 261 | opt.Description = s 262 | return opt 263 | } 264 | 265 | // SetHelpArgName - Updates the HelpArgName. 266 | func (opt *Option) SetHelpArgName(s string) *Option { 267 | opt.HelpArgName = s 268 | opt.Synopsis() 269 | return opt 270 | } 271 | 272 | // SetDefaultStr - Updates the DefaultStr. 273 | func (opt *Option) SetDefaultStr(s string) *Option { 274 | opt.DefaultStr = s 275 | return opt 276 | } 277 | 278 | // SetRequired - Marks an option as required. 279 | func (opt *Option) SetRequired(msg string) *Option { 280 | opt.IsRequired = true 281 | opt.IsRequiredErr = msg 282 | return opt 283 | } 284 | 285 | // SetEnvVar - Sets the name of the Env var that sets the option's value. 286 | func (opt *Option) SetEnvVar(name string) *Option { 287 | opt.EnvVar = name 288 | return opt 289 | } 290 | 291 | // CheckRequired - Returns error if the option is required. 292 | func (opt *Option) CheckRequired() error { 293 | if opt.IsRequired { 294 | if !opt.Called { 295 | if opt.IsRequiredErr != "" { 296 | return fmt.Errorf("%w%s", ErrorMissingRequiredOption, opt.IsRequiredErr) 297 | } 298 | return fmt.Errorf("%w"+text.ErrorMissingRequiredOption, ErrorMissingRequiredOption, opt.Name) 299 | } 300 | } 301 | return nil 302 | } 303 | 304 | // SetCalled - Marks the option as called and records the alias used to call it. 305 | func (opt *Option) SetCalled(usedAlias string) *Option { 306 | opt.Called = true 307 | opt.UsedAlias = usedAlias 308 | return opt 309 | } 310 | 311 | // SetBool - Set the option's data. 312 | func (opt *Option) SetBool(b bool) *Option { 313 | *opt.pBool = b 314 | return opt 315 | } 316 | 317 | func (opt *Option) SetBoolAsOppositeToDefault() *Option { 318 | *opt.pBool = !opt.boolDefault 319 | return opt 320 | } 321 | 322 | // SetString - Set the option's data. 323 | func (opt *Option) SetString(s string) *Option { 324 | *opt.pString = s 325 | return opt 326 | } 327 | 328 | // SetInt - Set the option's data. 329 | func (opt *Option) SetInt(i int) *Option { 330 | *opt.pInt = i 331 | return opt 332 | } 333 | 334 | // Int - Get the option's data. 335 | // Exposed due to handle increment. Maybe there is a better way. 336 | func (opt *Option) Int() int { 337 | return *opt.pInt 338 | } 339 | 340 | // SetFloat64 - Set the option's data. 341 | func (opt *Option) SetFloat64(f float64) *Option { 342 | *opt.pFloat64 = f 343 | return opt 344 | } 345 | 346 | // SetStringSlice - Set the option's data. 347 | func (opt *Option) SetStringSlice(s []string) *Option { 348 | *opt.pStringS = s 349 | return opt 350 | } 351 | 352 | // SetIntSlice - Set the option's data. 353 | func (opt *Option) SetIntSlice(s []int) *Option { 354 | *opt.pIntS = s 355 | return opt 356 | } 357 | 358 | // SetFloat64Slice - Set the option's data. 359 | func (opt *Option) SetFloat64Slice(s []float64) *Option { 360 | *opt.pFloat64S = s 361 | return opt 362 | } 363 | 364 | // SetKeyValueToStringMap - Set the option's data. 365 | func (opt *Option) SetKeyValueToStringMap(k, v string) *Option { 366 | if opt.MapKeysToLower { 367 | (*opt.pStringM)[strings.ToLower(k)] = v 368 | } else { 369 | (*opt.pStringM)[k] = v 370 | } 371 | return opt 372 | } 373 | 374 | // stringSliceIndex - indicates if an element is found in the slice and what its index is 375 | func stringSliceIndex(ss []string, e string) (int, bool) { 376 | for i, s := range ss { 377 | if s == e { 378 | return i, true 379 | } 380 | } 381 | return -1, false 382 | } 383 | 384 | // Save - Saves the data provided into the option 385 | func (opt *Option) Save(a ...string) error { 386 | Logger.Printf("name: %s, optType: %d\n", opt.Name, opt.OptType) 387 | if len(a) < 1 { 388 | switch opt.OptType { 389 | case BoolType: 390 | opt.SetBoolAsOppositeToDefault() 391 | case IncrementType: 392 | opt.SetInt(opt.Int() + 1) 393 | } 394 | return nil 395 | } 396 | for _, e := range a { 397 | if len(opt.ValidValues) > 0 { 398 | _, ok := stringSliceIndex(opt.ValidValues, e) 399 | if !ok { 400 | // TODO: convert to text variable 401 | return fmt.Errorf("wrong value for option '%s', valid values are %q", opt.Name, opt.ValidValues) 402 | } 403 | } 404 | } 405 | 406 | switch opt.OptType { 407 | case StringType, StringOptionalType: 408 | opt.SetString(a[0]) 409 | return nil 410 | case IntType, IntOptionalType: 411 | i, err := strconv.Atoi(a[0]) 412 | if err != nil { 413 | // TODO: Create error type for use in tests with errors.Is 414 | return fmt.Errorf(text.ErrorConvertToInt, opt.UsedAlias, a[0]) 415 | } 416 | opt.SetInt(i) 417 | return nil 418 | case Float64Type, Float64OptionalType: 419 | // TODO: Read the different errors when parsing float 420 | f, err := strconv.ParseFloat(a[0], 64) 421 | if err != nil { 422 | // TODO: Create error type for use in tests with errors.Is 423 | return fmt.Errorf(text.ErrorConvertToFloat64, opt.UsedAlias, a[0]) 424 | } 425 | opt.SetFloat64(f) 426 | return nil 427 | case StringRepeatType: 428 | opt.SetStringSlice(append(*opt.pStringS, a...)) 429 | return nil 430 | case IntRepeatType: 431 | var ii []int 432 | for _, e := range a { 433 | if strings.Contains(e, "..") { 434 | Logger.Printf("e: %s\n", e) 435 | n := strings.SplitN(e, "..", 2) 436 | Logger.Printf("n: %v\n", n) 437 | n1, n2 := n[0], n[1] 438 | in1, err := strconv.Atoi(n1) 439 | if err != nil { 440 | // TODO: Create new error description for this error. 441 | return fmt.Errorf(text.ErrorConvertToInt, opt.UsedAlias, e) 442 | } 443 | in2, err := strconv.Atoi(n2) 444 | if err != nil { 445 | // TODO: Create new error description for this error. 446 | return fmt.Errorf(text.ErrorConvertToInt, opt.UsedAlias, e) 447 | } 448 | if in1 < in2 { 449 | for j := in1; j <= in2; j++ { 450 | ii = append(ii, j) 451 | } 452 | } else { 453 | // TODO: Create new error description for this error. 454 | return fmt.Errorf(text.ErrorConvertToInt, opt.UsedAlias, e) 455 | } 456 | } else { 457 | i, err := strconv.Atoi(e) 458 | if err != nil { 459 | // TODO: Create error type for use in tests with errors.Is 460 | return fmt.Errorf(text.ErrorConvertToInt, opt.UsedAlias, e) 461 | } 462 | ii = append(ii, i) 463 | } 464 | } 465 | opt.SetIntSlice(append(*opt.pIntS, ii...)) 466 | return nil 467 | case Float64RepeatType: 468 | var ff []float64 469 | for _, e := range a { 470 | f, err := strconv.ParseFloat(e, 64) 471 | if err != nil { 472 | // TODO: Create error type for use in tests with errors.Is 473 | return fmt.Errorf(text.ErrorConvertToFloat64, opt.UsedAlias, e) 474 | } 475 | ff = append(ff, f) 476 | } 477 | opt.SetFloat64Slice(append(*opt.pFloat64S, ff...)) 478 | return nil 479 | case StringMapType: 480 | for _, e := range a { 481 | keyValue := strings.Split(e, "=") 482 | if len(keyValue) < 2 { 483 | // TODO: Create error type for use in tests with errors.Is 484 | return fmt.Errorf(text.ErrorArgumentIsNotKeyValue, opt.UsedAlias) 485 | } 486 | opt.SetKeyValueToStringMap(keyValue[0], keyValue[1]) 487 | } 488 | return nil 489 | case IncrementType: 490 | opt.SetInt(opt.Int() + 1) 491 | return nil 492 | default: // BoolType: 493 | if len(a) > 0 && a[0] == "true" { 494 | opt.SetBool(true) 495 | } else if len(a) > 0 && a[0] == "false" { 496 | opt.SetBool(false) 497 | } else { 498 | opt.SetBoolAsOppositeToDefault() 499 | } 500 | return nil 501 | } 502 | } 503 | 504 | // Sort Interface 505 | func Sort(list []*Option) { 506 | sort.Slice(list, func(i, j int) bool { 507 | return list[i].Name < list[j].Name 508 | }) 509 | } 510 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. "Contributor" 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. "Contributor Version" 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the terms of 34 | a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. "Larger Work" 41 | 42 | means a work that combines Covered Software with other material, in a 43 | separate file or files, that is not Covered Software. 44 | 45 | 1.8. "License" 46 | 47 | means this document. 48 | 49 | 1.9. "Licensable" 50 | 51 | means having the right to grant, to the maximum extent possible, whether 52 | at the time of the initial grant or subsequently, any and all of the 53 | rights conveyed by this License. 54 | 55 | 1.10. "Modifications" 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, 60 | deletion from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. "Patent Claims" of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, 67 | process, and apparatus claims, in any patent Licensable by such 68 | Contributor that would be infringed, but for the grant of the License, 69 | by the making, using, selling, offering for sale, having made, import, 70 | or transfer of either its Contributions or its Contributor Version. 71 | 72 | 1.12. "Secondary License" 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. "Source Code Form" 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. "You" (or "Your") 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, "You" includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, "control" means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or 104 | as part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its 108 | Contributions or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution 113 | become effective for each Contribution on the date the Contributor first 114 | distributes such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under 119 | this License. No additional rights or licenses will be implied from the 120 | distribution or licensing of Covered Software under this License. 121 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 122 | Contributor: 123 | 124 | a. for any code that a Contributor has removed from Covered Software; or 125 | 126 | b. for infringements caused by: (i) Your and any other third party's 127 | modifications of Covered Software, or (ii) the combination of its 128 | Contributions with other software (except as part of its Contributor 129 | Version); or 130 | 131 | c. under Patent Claims infringed by Covered Software in the absence of 132 | its Contributions. 133 | 134 | This License does not grant any rights in the trademarks, service marks, 135 | or logos of any Contributor (except as may be necessary to comply with 136 | the notice requirements in Section 3.4). 137 | 138 | 2.4. Subsequent Licenses 139 | 140 | No Contributor makes additional grants as a result of Your choice to 141 | distribute the Covered Software under a subsequent version of this 142 | License (see Section 10.2) or under the terms of a Secondary License (if 143 | permitted under the terms of Section 3.3). 144 | 145 | 2.5. Representation 146 | 147 | Each Contributor represents that the Contributor believes its 148 | Contributions are its original creation(s) or it has sufficient rights to 149 | grant the rights to its Contributions conveyed by this License. 150 | 151 | 2.6. Fair Use 152 | 153 | This License is not intended to limit any rights You have under 154 | applicable copyright doctrines of fair use, fair dealing, or other 155 | equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under 169 | the terms of this License. You must inform recipients that the Source 170 | Code Form of the Covered Software is governed by the terms of this 171 | License, and how they can obtain a copy of this License. You may not 172 | attempt to alter or restrict the recipients' rights in the Source Code 173 | Form. 174 | 175 | 3.2. Distribution of Executable Form 176 | 177 | If You distribute Covered Software in Executable Form then: 178 | 179 | a. such Covered Software must also be made available in Source Code Form, 180 | as described in Section 3.1, and You must inform recipients of the 181 | Executable Form how they can obtain a copy of such Source Code Form by 182 | reasonable means in a timely manner, at a charge no more than the cost 183 | of distribution to the recipient; and 184 | 185 | b. You may distribute such Executable Form under the terms of this 186 | License, or sublicense it under different terms, provided that the 187 | license for the Executable Form does not attempt to limit or alter the 188 | recipients' rights in the Source Code Form under this License. 189 | 190 | 3.3. Distribution of a Larger Work 191 | 192 | You may create and distribute a Larger Work under terms of Your choice, 193 | provided that You also comply with the requirements of this License for 194 | the Covered Software. If the Larger Work is a combination of Covered 195 | Software with a work governed by one or more Secondary Licenses, and the 196 | Covered Software is not Incompatible With Secondary Licenses, this 197 | License permits You to additionally distribute such Covered Software 198 | under the terms of such Secondary License(s), so that the recipient of 199 | the Larger Work may, at their option, further distribute the Covered 200 | Software under the terms of either this License or such Secondary 201 | License(s). 202 | 203 | 3.4. Notices 204 | 205 | You may not remove or alter the substance of any license notices 206 | (including copyright notices, patent notices, disclaimers of warranty, or 207 | limitations of liability) contained within the Source Code Form of the 208 | Covered Software, except that You may alter any license notices to the 209 | extent required to remedy known factual inaccuracies. 210 | 211 | 3.5. Application of Additional Terms 212 | 213 | You may choose to offer, and to charge a fee for, warranty, support, 214 | indemnity or liability obligations to one or more recipients of Covered 215 | Software. However, You may do so only on Your own behalf, and not on 216 | behalf of any Contributor. You must make it absolutely clear that any 217 | such warranty, support, indemnity, or liability obligation is offered by 218 | You alone, and You hereby agree to indemnify every Contributor for any 219 | liability incurred by such Contributor as a result of warranty, support, 220 | indemnity or liability terms You offer. You may include additional 221 | disclaimers of warranty and limitations of liability specific to any 222 | jurisdiction. 223 | 224 | 4. Inability to Comply Due to Statute or Regulation 225 | 226 | If it is impossible for You to comply with any of the terms of this License 227 | with respect to some or all of the Covered Software due to statute, 228 | judicial order, or regulation then You must: (a) comply with the terms of 229 | this License to the maximum extent possible; and (b) describe the 230 | limitations and the code they affect. Such description must be placed in a 231 | text file included with all distributions of the Covered Software under 232 | this License. Except to the extent prohibited by statute or regulation, 233 | such description must be sufficiently detailed for a recipient of ordinary 234 | skill to be able to understand it. 235 | 236 | 5. Termination 237 | 238 | 5.1. The rights granted under this License will terminate automatically if You 239 | fail to comply with any of its terms. However, if You become compliant, 240 | then the rights granted under this License from a particular Contributor 241 | are reinstated (a) provisionally, unless and until such Contributor 242 | explicitly and finally terminates Your grants, and (b) on an ongoing 243 | basis, if such Contributor fails to notify You of the non-compliance by 244 | some reasonable means prior to 60 days after You have come back into 245 | compliance. Moreover, Your grants from a particular Contributor are 246 | reinstated on an ongoing basis if such Contributor notifies You of the 247 | non-compliance by some reasonable means, this is the first time You have 248 | received notice of non-compliance with this License from such 249 | Contributor, and You become compliant prior to 30 days after Your receipt 250 | of the notice. 251 | 252 | 5.2. If You initiate litigation against any entity by asserting a patent 253 | infringement claim (excluding declaratory judgment actions, 254 | counter-claims, and cross-claims) alleging that a Contributor Version 255 | directly or indirectly infringes any patent, then the rights granted to 256 | You by any and all Contributors for the Covered Software under Section 257 | 2.1 of this License shall terminate. 258 | 259 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 260 | license agreements (excluding distributors and resellers) which have been 261 | validly granted by You or Your distributors under this License prior to 262 | termination shall survive termination. 263 | 264 | 6. Disclaimer of Warranty 265 | 266 | Covered Software is provided under this License on an "as is" basis, 267 | without warranty of any kind, either expressed, implied, or statutory, 268 | including, without limitation, warranties that the Covered Software is free 269 | of defects, merchantable, fit for a particular purpose or non-infringing. 270 | The entire risk as to the quality and performance of the Covered Software 271 | is with You. Should any Covered Software prove defective in any respect, 272 | You (not any Contributor) assume the cost of any necessary servicing, 273 | repair, or correction. This disclaimer of warranty constitutes an essential 274 | part of this License. No use of any Covered Software is authorized under 275 | this License except under this disclaimer. 276 | 277 | 7. Limitation of Liability 278 | 279 | Under no circumstances and under no legal theory, whether tort (including 280 | negligence), contract, or otherwise, shall any Contributor, or anyone who 281 | distributes Covered Software as permitted above, be liable to You for any 282 | direct, indirect, special, incidental, or consequential damages of any 283 | character including, without limitation, damages for lost profits, loss of 284 | goodwill, work stoppage, computer failure or malfunction, or any and all 285 | other commercial damages or losses, even if such party shall have been 286 | informed of the possibility of such damages. This limitation of liability 287 | shall not apply to liability for death or personal injury resulting from 288 | such party's negligence to the extent applicable law prohibits such 289 | limitation. Some jurisdictions do not allow the exclusion or limitation of 290 | incidental or consequential damages, so this exclusion and limitation may 291 | not apply to You. 292 | 293 | 8. Litigation 294 | 295 | Any litigation relating to this License may be brought only in the courts 296 | of a jurisdiction where the defendant maintains its principal place of 297 | business and such litigation shall be governed by laws of that 298 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 299 | in this Section shall prevent a party's ability to bring cross-claims or 300 | counter-claims. 301 | 302 | 9. Miscellaneous 303 | 304 | This License represents the complete agreement concerning the subject 305 | matter hereof. If any provision of this License is held to be 306 | unenforceable, such provision shall be reformed only to the extent 307 | necessary to make it enforceable. Any law or regulation which provides that 308 | the language of a contract shall be construed against the drafter shall not 309 | be used to construe this License against a Contributor. 310 | 311 | 312 | 10. Versions of the License 313 | 314 | 10.1. New Versions 315 | 316 | Mozilla Foundation is the license steward. Except as provided in Section 317 | 10.3, no one other than the license steward has the right to modify or 318 | publish new versions of this License. Each version will be given a 319 | distinguishing version number. 320 | 321 | 10.2. Effect of New Versions 322 | 323 | You may distribute the Covered Software under the terms of the version 324 | of the License under which You originally received the Covered Software, 325 | or under the terms of any subsequent version published by the license 326 | steward. 327 | 328 | 10.3. Modified Versions 329 | 330 | If you create software not governed by this License, and you want to 331 | create a new license for such software, you may create and use a 332 | modified version of this License if you rename the license and remove 333 | any references to the name of the license steward (except to note that 334 | such modified license differs from this License). 335 | 336 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 337 | Licenses If You choose to distribute Source Code Form that is 338 | Incompatible With Secondary Licenses under the terms of this version of 339 | the License, the notice described in Exhibit B of this License must be 340 | attached. 341 | 342 | Exhibit A - Source Code Form License Notice 343 | 344 | This Source Code Form is subject to the 345 | terms of the Mozilla Public License, v. 346 | 2.0. If a copy of the MPL was not 347 | distributed with this file, You can 348 | obtain one at 349 | http://mozilla.org/MPL/2.0/. 350 | 351 | If it is not possible or desirable to put the notice in a particular file, 352 | then You may include the notice in a location (such as a LICENSE file in a 353 | relevant directory) where a recipient would be likely to look for such a 354 | notice. 355 | 356 | You may add additional accurate notices of copyright ownership. 357 | 358 | Exhibit B - "Incompatible With Secondary Licenses" Notice 359 | 360 | This Source Code Form is "Incompatible 361 | With Secondary Licenses", as defined by 362 | the Mozilla Public License, v. 2.0. 363 | 364 | --------------------------------------------------------------------------------