├── comp ├── zsh.txt ├── powershell.txt ├── fish.txt ├── bash.txt ├── zsh.zsh ├── fish.fish ├── powershell.ps1 └── bash.bash ├── go.mod ├── go.go ├── _examples ├── tree │ ├── gen.sh │ └── main.go ├── embedded │ └── main.go ├── bind │ └── main.go ├── reflect │ └── main.go ├── context │ └── main.go └── gen │ └── psql │ └── main.go ├── .golangci.yml ├── glob ├── glob.go └── glob_test.go ├── uuid ├── uuid.go └── uuid_test.go ├── conf.go ├── onerr_string.go ├── color ├── color.go └── color_test.go ├── .github └── workflows │ └── test.yml ├── go.sum ├── .gitignore ├── toml └── toml.go ├── LICENSE ├── yaml └── yaml.go ├── strcase ├── example_test.go ├── initialisms.go ├── strcase.go └── strcase_test.go ├── size_test.go ├── text └── text.go ├── tinygo.go ├── parse.go ├── ox_test.go ├── otx └── otx.go ├── README.md ├── parse_test.go ├── size.go ├── LICENSE.completions.txt ├── type_test.go ├── type.go ├── example_test.go └── value.go /comp/zsh.txt: -------------------------------------------------------------------------------- 1 | Generate the %[1]s completion script for zsh. 2 | 3 | To load completions in your current shell session: 4 | 5 | source <(%[2]s) 6 | 7 | To load completions for every new session, execute once: 8 | 9 | %[2]s > "${fpath[1]}/_%[1]s" 10 | -------------------------------------------------------------------------------- /comp/powershell.txt: -------------------------------------------------------------------------------- 1 | Generate the %[1]s completion script for powershell. 2 | 3 | To load completions in your current shell session: 4 | 5 | PS C:\> %[2]s | Out-String | Invoke-Expression 6 | 7 | To load completions for every new session, add the output of the above command to your powershell profile. 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xo/ox 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/goccy/go-yaml v1.17.1 7 | github.com/google/uuid v1.6.0 8 | github.com/kenshaw/colors v0.2.1 9 | github.com/kenshaw/glob v0.0.0-20250507233341-2ccc24e5a073 10 | github.com/pelletier/go-toml/v2 v2.2.4 11 | ) 12 | -------------------------------------------------------------------------------- /comp/fish.txt: -------------------------------------------------------------------------------- 1 | Generate the %[1]s completion script for fish. 2 | 3 | To load completions in your current shell session: 4 | 5 | %[2]s | source 6 | 7 | To load completions for every new session, execute once: 8 | 9 | %[2]s > ~/.config/fish/completions/%[1]s.fish 10 | 11 | You will need to start a new shell for this setup to take effect. 12 | -------------------------------------------------------------------------------- /comp/bash.txt: -------------------------------------------------------------------------------- 1 | Generate the %[1]s completion script for bash. 2 | 3 | To load completions in your current shell session: 4 | 5 | source <(%[2]s) 6 | 7 | To load completions for every new session, execute once: 8 | - Linux: 9 | 10 | %[2]s > /etc/bash_completion.d/%[1]s 11 | 12 | - MacOS: 13 | 14 | %[2]s > /usr/local/etc/bash_completion.d/%[1]s 15 | -------------------------------------------------------------------------------- /go.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | 3 | package ox 4 | 5 | import ( 6 | "os" 7 | "reflect" 8 | ) 9 | 10 | func overflowComplex(v reflect.Value, c complex128) bool { 11 | return v.OverflowComplex(c) 12 | } 13 | 14 | func userHomeDir() (string, error) { 15 | return os.UserHomeDir() 16 | } 17 | 18 | func userConfigDir() (string, error) { 19 | return os.UserConfigDir() 20 | } 21 | 22 | func userCacheDir() (string, error) { 23 | return os.UserCacheDir() 24 | } 25 | -------------------------------------------------------------------------------- /_examples/tree/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$1" ]; then 4 | echo "usage: $0 " 5 | exit 1 6 | fi 7 | 8 | target=$1 9 | shift 10 | 11 | (set -x 12 | rm -f *.{txt,bash,fish,zsh,ps1} 13 | go build -ldflags "-X main.name=$target" 14 | ) 15 | 16 | for name in bash fish zsh powershell; do 17 | ext=$name 18 | if [ "$ext" = "powershell" ]; then 19 | ext="ps1" 20 | fi 21 | (set -x; 22 | $target completion $name $@ > $target.$ext 23 | ./tree completion $name $@ > tree.$ext 24 | ) 25 | done 26 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: all 4 | disable: 5 | - cyclop 6 | - depguard 7 | - dupl 8 | - err113 9 | - errcheck 10 | - exhaustive 11 | - exhaustruct 12 | - forcetypeassert 13 | - funlen 14 | - gochecknoglobals 15 | - gochecknoinits 16 | - gocognit 17 | - inamedparam 18 | - ineffassign 19 | - ireturn 20 | - lll 21 | - makezero 22 | - mnd 23 | - nilnil 24 | - nlreturn 25 | - testpackage 26 | - varnamelen 27 | - wrapcheck 28 | - wsl 29 | -------------------------------------------------------------------------------- /_examples/embedded/main.go: -------------------------------------------------------------------------------- 1 | // _examples/embedded/main.go 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/xo/ox" 8 | ) 9 | 10 | func main() { 11 | var args struct { 12 | Verbose bool `ox:"enable verbose,short:v"` 13 | More struct { 14 | Foo string `ox:"foo,short:f"` 15 | } `ox:""` 16 | } 17 | ox.Run( 18 | ox.Usage("embedded", "demonstrates using embebbed structs with ox's bind"), 19 | ox.Defaults(), 20 | ox.From(&args), 21 | ox.Exec(func() { 22 | fmt.Println("Verbose:", args.Verbose) 23 | fmt.Println("More.Foo:", args.More.Foo) 24 | }), 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /glob/glob.go: -------------------------------------------------------------------------------- 1 | // Package glob provides a ox type for glob processing. 2 | package glob 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/kenshaw/glob" 8 | "github.com/xo/ox" 9 | "github.com/xo/ox/otx" 10 | ) 11 | 12 | func init() { 13 | ox.RegisterTypeName(ox.GlobT, "*glob.Glob") 14 | ox.RegisterTextType(New) 15 | } 16 | 17 | // New creates a new glob. 18 | func New() (*glob.Glob, error) { 19 | return glob.New(), nil 20 | } 21 | 22 | // Glob returns the glob var from the context. 23 | func Glob(ctx context.Context, name string) *glob.Glob { 24 | return otx.Get[*glob.Glob](ctx, name) 25 | } 26 | -------------------------------------------------------------------------------- /uuid/uuid.go: -------------------------------------------------------------------------------- 1 | // Package uuid provides a ox type for uuid processing. 2 | package uuid 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/google/uuid" 8 | "github.com/xo/ox" 9 | "github.com/xo/ox/otx" 10 | ) 11 | 12 | func init() { 13 | ox.RegisterTypeName(ox.UUIDT, "*uuid.UUID") 14 | ox.RegisterTextType(New) 15 | } 16 | 17 | // New creates a new uuid. 18 | func New() (*uuid.UUID, error) { 19 | return new(uuid.UUID), nil 20 | } 21 | 22 | // UUID returns the uuid var from the context. 23 | func UUID(ctx context.Context, name string) *uuid.UUID { 24 | return otx.Get[*uuid.UUID](ctx, name) 25 | } 26 | -------------------------------------------------------------------------------- /conf.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // ConfigLoader is the interface for configuration decoders. 8 | type ConfigLoader interface{} 9 | 10 | // loaders are config loaders. 11 | var loaders map[string]func(*Context, string) (string, error) 12 | 13 | func init() { 14 | loaders = make(map[string]func(*Context, string) (string, error)) 15 | RegisterConfigLoader("ENV", func(_ *Context, key string) (string, error) { 16 | return os.Getenv(key), nil 17 | }) 18 | } 19 | 20 | // RegisterConfigLoader registers a config file type. 21 | func RegisterConfigLoader(typ string, f func(*Context, string) (string, error)) { 22 | loaders[typ] = f 23 | } 24 | -------------------------------------------------------------------------------- /onerr_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type OnErr"; DO NOT EDIT. 2 | 3 | package ox 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[OnErrExit-0] 12 | _ = x[OnErrContinue-1] 13 | _ = x[OnErrPanic-2] 14 | } 15 | 16 | const _OnErr_name = "OnErrExitOnErrContinueOnErrPanic" 17 | 18 | var _OnErr_index = [...]uint8{0, 9, 22, 32} 19 | 20 | func (i OnErr) String() string { 21 | if i >= OnErr(len(_OnErr_index)-1) { 22 | return "OnErr(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _OnErr_name[_OnErr_index[i]:_OnErr_index[i+1]] 25 | } 26 | -------------------------------------------------------------------------------- /color/color.go: -------------------------------------------------------------------------------- 1 | // Package color provides a color type for ox. 2 | package color 3 | 4 | import ( 5 | "context" 6 | "image/color" 7 | 8 | "github.com/kenshaw/colors" 9 | "github.com/xo/ox" 10 | "github.com/xo/ox/otx" 11 | ) 12 | 13 | func init() { 14 | ox.RegisterTypeName(ox.ColorT, "*colors.Color") 15 | ox.RegisterTextType(New) 16 | } 17 | 18 | // Default is the default color. 19 | var Default color.Color = colors.Transparent 20 | 21 | // New creates a new color. 22 | func New() (*colors.Color, error) { 23 | c := colors.FromColor(Default) 24 | return &c, nil 25 | } 26 | 27 | // Color retrieves a color from the context. 28 | func Color(ctx context.Context, name string) *colors.Color { 29 | return otx.Get[*colors.Color](ctx, name) 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test-go: 5 | name: Test Go 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Install Go 9 | uses: actions/setup-go@v5 10 | with: 11 | go-version: stable 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Test 15 | run: | 16 | go test -v ./... 17 | test-tinygo: 18 | name: Test TinyGo 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Install TinyGo 22 | uses: acifani/setup-tinygo@v2 23 | with: 24 | tinygo-version: "0.34.0" 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | - name: Test 28 | run: | 29 | tinygo test -v ./... 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= 2 | github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/kenshaw/colors v0.2.1 h1:faVXggEiC12dah/CEec8qvbRGbyp6Zx+OwaH3Y/6sB4= 6 | github.com/kenshaw/colors v0.2.1/go.mod h1:Aok7+9KpR+qEwgCxDEoLBS6IGFhY1iRJIzbcv5ijewI= 7 | github.com/kenshaw/glob v0.0.0-20250507233341-2ccc24e5a073 h1:0lmV6JTARQJr8jktooJ2aL3r6gJMPaJEdBfgCTccyig= 8 | github.com/kenshaw/glob v0.0.0-20250507233341-2ccc24e5a073/go.mod h1:ELOE5IWFroNzavhDO6lIJHLGxiifaPRnlf/B0GNM7hs= 9 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 10 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 11 | -------------------------------------------------------------------------------- /glob/glob_test.go: -------------------------------------------------------------------------------- 1 | package glob 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kenshaw/glob" 7 | "github.com/xo/ox" 8 | ) 9 | 10 | func TestGlob(t *testing.T) { 11 | for _, exp := range globTests() { 12 | t.Run(exp, func(t *testing.T) { 13 | v, err := ox.GlobT.New() 14 | if err != nil { 15 | t.Fatalf("expected no error, got: %v", err) 16 | } 17 | if err := v.Set(exp); err != nil { 18 | t.Fatalf("expected no error, got: %v", err) 19 | } 20 | val, err := ox.As[*glob.Glob](v) 21 | if err != nil { 22 | t.Fatalf("expected no error, got: %v", err) 23 | } 24 | if val == nil { 25 | t.Fatalf("expected non-nil value") 26 | } 27 | if s := val.String(); s != exp { 28 | t.Errorf("expected %q, got: %q", exp, s) 29 | } 30 | t.Logf("u: %v", val) 31 | }) 32 | } 33 | } 34 | 35 | func globTests() []string { 36 | return []string{ 37 | "", 38 | "config.{toml,yaml}", 39 | "file.txt", 40 | "[A-Z]*{,_test}.go", 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /ox.test 2 | /ox.test.exe 3 | 4 | /_examples/context/context 5 | /_examples/context/context.exe 6 | /_examples/bind/bind 7 | /_examples/bind/bind.exe 8 | /_examples/reflect/reflect 9 | /_examples/reflect/reflect.exe 10 | /_examples/embedded/embedded 11 | /_examples/embedded/embedded.exe 12 | /_examples/tree/tree 13 | /_examples/tree/tree.exe 14 | 15 | /_examples/gen/docker/docker 16 | /_examples/gen/docker/docker.exe 17 | /_examples/gen/doctl/doctl 18 | /_examples/gen/doctl/doctl.exe 19 | /_examples/gen/gh/gh 20 | /_examples/gen/gh/gh.exe 21 | /_examples/gen/helm/helm 22 | /_examples/gen/helm/helm.exe 23 | /_examples/gen/hugo/hugo 24 | /_examples/gen/hugo/hugo.exe 25 | /_examples/gen/kubectl/kubectl 26 | /_examples/gen/kubectl/kubectl.exe 27 | /_examples/gen/podman/podman 28 | /_examples/gen/podman/podman.exe 29 | /_examples/gen/psql/psql 30 | /_examples/gen/psql/psql.exe 31 | /_examples/gen/rclone/rclone 32 | /_examples/gen/rclone/rclone.exe 33 | 34 | *.txt 35 | *.log 36 | 37 | *.bash 38 | *.fish 39 | *.ps1 40 | *.zsh 41 | -------------------------------------------------------------------------------- /_examples/tree/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/xo/ox" 9 | ) 10 | 11 | var name = "tree" 12 | 13 | func main() { 14 | f, err := os.OpenFile(name+".txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY|os.O_APPEND, 0o644) 15 | if err != nil { 16 | panic(err) 17 | } 18 | defer f.Close() 19 | fmt.Fprintln(f, "---------------------------------------------") 20 | for _, s := range os.Environ() { 21 | fmt.Fprintln(f, s) 22 | } 23 | fmt.Fprintln(f) 24 | fmt.Fprintln(f, os.Args) 25 | fmt.Fprintln(f) 26 | ox.RunContext( 27 | context.Background(), 28 | ox.Defaults(), 29 | ox.Usage(name, "runs a command"), 30 | ox.Flags(). 31 | String("config", "config file"). 32 | String("my-string", "my string on "+name). 33 | Int("int", "an int", ox.Short("i")), 34 | ox.Sub( 35 | ox.Usage("sub1", "a sub command"), 36 | ox.Flags(). 37 | String("my-string", "my string on sub1"), 38 | ), 39 | ox.Sub( 40 | ox.Usage("sub2", "sub2 command"), 41 | ), 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /toml/toml.go: -------------------------------------------------------------------------------- 1 | // Package toml provides a toml config reader for ox. 2 | package toml 3 | 4 | import ( 5 | "context" 6 | "io" 7 | 8 | "github.com/pelletier/go-toml/v2" 9 | "github.com/xo/ox" 10 | ) 11 | 12 | func init() { 13 | ox.RegisterConfigFileType("toml", func(opts ...any) (ox.ConfigDecoder, error) { 14 | d := new(decoder) 15 | for _, opt := range opts { 16 | if o, ok := opt.(func(*toml.Decoder)); ok { 17 | d.opts = append(d.opts, o) 18 | } 19 | } 20 | return d, nil 21 | }) 22 | } 23 | 24 | type decoder struct { 25 | opts []func(*toml.Decoder) 26 | } 27 | 28 | // Decode satisfies the [ox.ConfigLoader] interface. 29 | func (d *decoder) Decode(_ context.Context, r io.Reader, v any) error { 30 | dec := toml.NewDecoder(r) 31 | for _, o := range d.opts { 32 | o(dec) 33 | } 34 | return dec.Decode(v) 35 | } 36 | 37 | func DisallowUnknownFields(dec *toml.Decoder) { 38 | dec.DisallowUnknownFields() 39 | } 40 | 41 | func EnableUnmarshalerInterface(dec *toml.Decoder) { 42 | dec.EnableUnmarshalerInterface() 43 | } 44 | -------------------------------------------------------------------------------- /_examples/bind/main.go: -------------------------------------------------------------------------------- 1 | // _examples/bind/main.go 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "net/url" 7 | 8 | "github.com/xo/ox" 9 | ) 10 | 11 | func main() { 12 | var ( 13 | arg string 14 | u *url.URL 15 | urlSet bool 16 | ints []int 17 | strings []string 18 | m map[int]int 19 | ) 20 | run := func(args []string) { 21 | fmt.Println("arg:", arg) 22 | if urlSet { 23 | fmt.Println("u:", u) 24 | } else { 25 | fmt.Println("u: not set") 26 | } 27 | fmt.Println("ints:", ints) 28 | fmt.Println("strings:", strings) 29 | fmt.Println("map:", m) 30 | } 31 | ox.Run( 32 | ox.Exec(run), 33 | ox.Usage("bind", "demonstrates using ox's binds"), 34 | ox.Defaults(), 35 | ox.Flags(). 36 | String("arg", "an arg", ox.Bind(&arg)). 37 | URL("url", "a url", ox.Short("u"), ox.BindSet(&u, &urlSet)). 38 | Slice("int", "a slice of ints", ox.Short("i"), ox.Uint64T, ox.Bind(&ints), ox.Bind(&strings)). 39 | Map("map", "a map", ox.Short("m"), ox.Bind(&m), ox.IntT, ox.MapKey(ox.IntT)), 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2025 Kenneth Shaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /yaml/yaml.go: -------------------------------------------------------------------------------- 1 | // Package yaml provides a yaml config reader for ox. 2 | package yaml 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/goccy/go-yaml" 8 | "github.com/xo/ox" 9 | ) 10 | 11 | func init() { 12 | ox.RegisterConfigLoader("yaml", func(opts ...any) (ox.ConfigLoader, error) { 13 | /* 14 | d := new(decoder) 15 | for _, opt := range opts { 16 | switch v := opt.(type) { 17 | case func(*decoder) error: 18 | if err := v(d); err != nil { 19 | return nil, err 20 | } 21 | case yaml.DecodeOption: 22 | d.opts = append(d.opts, v) 23 | } 24 | } 25 | return d, nil 26 | */ 27 | return nil, nil 28 | }) 29 | } 30 | 31 | type decoder struct { 32 | opts []yaml.DecodeOption 33 | once sync.Once 34 | } 35 | 36 | // func From(r io.Reader) 37 | 38 | func File(name string) func(*decoder) error { 39 | return func(d *decoder) error { 40 | return nil 41 | } 42 | } 43 | 44 | /* 45 | // decoder is a yaml decoder. 46 | type decoder struct{} 47 | 48 | // Decode satisfies the [ox.ConfigLoader] interface. 49 | func (d *decoder) Decode(_ context.Context, r io.Reader, v any) error { 50 | return yaml.NewDecoder(r, d.opts...).Decode(v) 51 | } 52 | */ 53 | -------------------------------------------------------------------------------- /color/color_test.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kenshaw/colors" 7 | "github.com/xo/ox" 8 | ) 9 | 10 | func TestColor(t *testing.T) { 11 | for _, test := range colorTests() { 12 | t.Run(test.s, func(t *testing.T) { 13 | v, err := ox.ColorT.New() 14 | if err != nil { 15 | t.Fatalf("expected no error, got: %v", err) 16 | } 17 | if err := v.Set(test.s); err != nil { 18 | t.Fatalf("expected no error, got: %v", err) 19 | } 20 | val, err := ox.As[*colors.Color](v) 21 | if err != nil { 22 | t.Fatalf("expected no error, got: %v", err) 23 | } 24 | if s := val.AsText(); s != test.exp { 25 | t.Errorf("expected %s, got: %s", test.exp, s) 26 | } 27 | t.Logf("c: %v", val) 28 | }) 29 | } 30 | } 31 | 32 | type colorTest struct { 33 | s string 34 | exp string 35 | } 36 | 37 | func colorTests() []colorTest { 38 | return []colorTest{ 39 | {"", "transparent"}, 40 | {"red", "red"}, 41 | {"BLACK", "black"}, 42 | {"rgb(0,255,0)", "lime"}, 43 | {"rgba( 0, 255, 0, 255)", "lime"}, 44 | {"hex(0,ff,0)", "lime"}, 45 | {"hex(0,ff,0,ff)", "lime"}, 46 | {"#00ff00", "lime"}, 47 | {"#00ff00ff", "lime"}, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /strcase/example_test.go: -------------------------------------------------------------------------------- 1 | package strcase_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/xo/ox/strcase" 7 | ) 8 | 9 | func Example() { 10 | fmt.Println("Change CamelCase -> snake_case:", strcase.CamelToSnake("AnIdentifier")) 11 | fmt.Println("Change CamelCase -> snake_case (2):", strcase.CamelToSnake("XMLHTTPACL")) 12 | fmt.Println("Change snake_case -> CamelCase:", strcase.SnakeToCamel("an_identifier")) 13 | fmt.Println("Force CamelCase:", strcase.ForceCamelIdentifier("APoorly_named_httpMethod")) 14 | fmt.Println("Force lower camelCase:", strcase.ForceLowerCamelIdentifier("APoorly_named_httpMethod")) 15 | fmt.Println("Force lower camelCase (2):", strcase.ForceLowerCamelIdentifier("XmlHttpACL")) 16 | fmt.Println("Change snake_case identifier -> CamelCase:", strcase.SnakeToCamelIdentifier("__2__xml___thing---")) 17 | // Output: 18 | // Change CamelCase -> snake_case: an_identifier 19 | // Change CamelCase -> snake_case (2): xml_http_acl 20 | // Change snake_case -> CamelCase: AnIdentifier 21 | // Force CamelCase: APoorlyNamedHTTPMethod 22 | // Force lower camelCase: aPoorlyNamedHTTPMethod 23 | // Force lower camelCase (2): xmlHTTPACL 24 | // Change snake_case identifier -> CamelCase: XMLThing 25 | } 26 | -------------------------------------------------------------------------------- /_examples/reflect/main.go: -------------------------------------------------------------------------------- 1 | // _examples/reflect/main.go 2 | package main 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "net/url" 8 | 9 | "github.com/xo/ox" 10 | ) 11 | 12 | type args struct { 13 | Arg string `ox:"an argument,short:a"` 14 | URL []*url.URL `ox:"a url,short:u,set:URLSet"` 15 | URLSet bool `` 16 | Ints []int `ox:"a slice of ints,short:i"` 17 | Strings []string `ox:"a slice of strings,short:s"` 18 | Map map[int]int `ox:"a map of ints,short:m"` 19 | Other []*url.URL `ox:"a slice of urls,short:z"` 20 | } 21 | 22 | func main() { 23 | args := new(args) 24 | ox.Run( 25 | ox.Exec(run(args)), 26 | ox.Usage("reflect", "demonstrates using ox's From with struct tags"), 27 | ox.Defaults(), 28 | ox.From(args), 29 | ) 30 | } 31 | 32 | func run(args *args) func(context.Context, []string) error { 33 | return func(ctx context.Context, v []string) error { 34 | fmt.Println("args:", v) 35 | fmt.Println("arg:", args.Arg) 36 | fmt.Println("url set:", args.URLSet) 37 | if args.URL != nil { 38 | fmt.Println("u:", args.URL) 39 | } else { 40 | fmt.Println("u: not set") 41 | } 42 | fmt.Println("ints:", args.Ints) 43 | fmt.Println("strings:", args.Strings) 44 | fmt.Println("map:", args.Map) 45 | fmt.Println("other:", args.Other) 46 | return nil 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /uuid/uuid_test.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | "github.com/xo/ox" 8 | ) 9 | 10 | func TestUUID(t *testing.T) { 11 | for _, exp := range uuidTests() { 12 | t.Run(exp, func(t *testing.T) { 13 | v, err := ox.UUIDT.New() 14 | if err != nil { 15 | t.Fatalf("expected no error, got: %v", err) 16 | } 17 | if err := v.Set(exp); err != nil { 18 | t.Fatalf("expected no error, got: %v", err) 19 | } 20 | val, err := ox.As[*uuid.UUID](v) 21 | if err != nil { 22 | t.Fatalf("expected no error, got: %v", err) 23 | } 24 | if val == nil { 25 | t.Fatalf("expected non-nil value") 26 | } 27 | if exp == "" { 28 | exp = "00000000-0000-0000-0000-000000000000" 29 | } 30 | if s := val.String(); s != exp { 31 | t.Errorf("expected %s, got: %s", exp, s) 32 | } 33 | t.Logf("u: %v", val) 34 | }) 35 | } 36 | } 37 | 38 | func uuidTests() []string { 39 | return []string{ 40 | "", 41 | "00000000-0000-0000-0000-000000000000", 42 | "f47ac10b-58cc-0372-8567-0e02b2c3d479", 43 | "f47ac10b-58cc-1372-8567-0e02b2c3d479", 44 | "f47ac10b-58cc-2372-8567-0e02b2c3d479", 45 | "f47ac10b-58cc-3372-8567-0e02b2c3d479", 46 | "f47ac10b-58cc-4372-8567-0e02b2c3d479", 47 | "f47ac10b-58cc-5372-8567-0e02b2c3d479", 48 | "f47ac10b-58cc-6372-8567-0e02b2c3d479", 49 | "f47ac10b-58cc-7372-8567-0e02b2c3d479", 50 | "f47ac10b-58cc-8372-8567-0e02b2c3d479", 51 | "f47ac10b-58cc-9372-8567-0e02b2c3d479", 52 | "f47ac10b-58cc-a372-8567-0e02b2c3d479", 53 | "f47ac10b-58cc-b372-8567-0e02b2c3d479", 54 | "f47ac10b-58cc-c372-8567-0e02b2c3d479", 55 | "f47ac10b-58cc-d372-8567-0e02b2c3d479", 56 | "f47ac10b-58cc-e372-8567-0e02b2c3d479", 57 | "f47ac10b-58cc-f372-8567-0e02b2c3d479", 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /size_test.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | func TestSize(t *testing.T) { 10 | tests := []struct { 11 | v Size 12 | as string 13 | exp string 14 | parse bool 15 | }{ 16 | {1000, "%d", "1000", true}, 17 | {1000, "%s", "1000 B", true}, 18 | {1000, "%S", "1 kB", true}, 19 | {-1000, "%s", "-1000 B", true}, 20 | {-1000, "%S", "-1 kB", true}, 21 | {1000, "%f", "1000 B", true}, 22 | {1024, "%d", "1024", true}, 23 | {1024, "%s", "1 KiB", true}, 24 | {1024, "%f", "1 KiB", true}, 25 | {1024, "%.6z", "1.000000KiB", true}, 26 | {1024, "%.0z", "1KiB", true}, 27 | {1024, "%.1z", "1.0KiB", true}, 28 | {1024, "%.2z", "1.00KiB", true}, 29 | {1024, "%.3z", "1.000KiB", true}, 30 | {12345678, "%d", "12345678", true}, 31 | {12345678, "%D", "12345678", true}, 32 | {12345678, "%s", "11.77 MiB", false}, 33 | {12345678, "%m", "11.77MiB", false}, 34 | {12345678, "%.3m", "11.774MiB", false}, 35 | {12345678, "% .3m", "11.774 MiB", false}, 36 | {12345678, "%d", "12345678", true}, 37 | {12345678, "%s", "11.77 MiB", false}, 38 | {12345678, "%f", "11.77 MiB", false}, 39 | {3*MiB + 100*KiB, "%d", "3248128", true}, 40 | {3*MiB + 100*KiB, "%s", "3.1 MiB", false}, 41 | {3*MiB + 100*KiB, "%F", "3.25 MB", false}, 42 | {2 * GiB, "%d", "2147483648", true}, 43 | {2 * GiB, "%s", "2 GiB", true}, 44 | {2 * GiB, "%f", "2 GiB", true}, 45 | {4 * TiB, "%d", "4398046511104", true}, 46 | {4 * TiB, "%s", "4 TiB", true}, 47 | {5*PiB + 200*TiB, "%d", "5849401859768320", true}, 48 | {5*PiB + 200*TiB, "% m", "5578424320 MiB", true}, 49 | {5*PiB + 200*TiB, "% g", "5447680 GiB", true}, 50 | {5*PiB + 200*TiB, "% t", "5320 TiB", true}, 51 | {5*PiB + 200*TiB, "% b", "5.2 PiB", false}, 52 | {5*PiB + 200*TiB, "%v", "5.2 PiB", false}, 53 | {5*PiB + 200*TiB, "%V", "5.85 PB", false}, 54 | {5*PiB + 200*TiB, "% x", "%x(error=unknown size verb)", false}, 55 | } 56 | for i, test := range tests { 57 | t.Run(strconv.Itoa(i), func(t *testing.T) { 58 | s := fmt.Sprintf(test.as, test.v) 59 | t.Logf("%d %q :: %q == %q", test.v, test.as, test.exp, s) 60 | if s != test.exp { 61 | t.Errorf("expected %q, got: %q", test.exp, s) 62 | } 63 | if !test.parse { 64 | return 65 | } 66 | sz, err := ParseSize(s) 67 | if err != nil { 68 | t.Fatalf("expected no error, got: %v", err) 69 | } 70 | if sz != test.v { 71 | t.Errorf("expected %s, got: %s", test.v, sz) 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /_examples/context/main.go: -------------------------------------------------------------------------------- 1 | // _examples/context/main.go 2 | package main 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "math/big" 8 | "net/url" 9 | "reflect" 10 | 11 | "github.com/xo/ox" 12 | "github.com/xo/ox/otx" 13 | ) 14 | 15 | func main() { 16 | ox.Run( 17 | ox.Exec(run), 18 | ox.Usage("context", "a context demo of the ox api"), 19 | ox.Defaults(), 20 | ox.UserConfigFile(), 21 | ox.Flags(). 22 | Var("param-a", "parameter a", ox.Short("a"), ox.Key("", "param-a"), ox.Key("yaml", "my_param_a"), ox.Key("toml", "paramA")). 23 | Int("param-b", "parameter b", ox.Short("b"), ox.Default(125)). 24 | Slice("floats", "a slice of float64", ox.Float64T, ox.Short("f")). 25 | URL("url", "a url", ox.Aliases("my-url"), ox.Short("u")). 26 | Count("verbose", "verbosity", ox.Short("v")), 27 | ox.Sub( 28 | ox.Exec(sub), 29 | ox.Usage("sub", "a sub command"), 30 | ox.Aliases("subCommand"), 31 | ox.Flags(). 32 | Var("sub", "sub param"). 33 | Slice("strings", "a slice of strings", ox.Short("s")). 34 | Slice("bigint", "a slice of big ints", ox.BigIntT, ox.Short("t")). 35 | Map("ints", "a map of integers", ox.IntT, ox.Short("i")), 36 | ox.ValidArgs(0, 10), 37 | ox.Sub( 38 | ox.Usage("dub", "a dub command"), 39 | ), 40 | ), 41 | ) 42 | } 43 | 44 | func run(ctx context.Context, args []string) error { 45 | fmt.Println("run args:", args) 46 | 47 | // get param-a 48 | paramA := otx.Get[string](ctx, "param-a") 49 | fmt.Println("paramA:", paramA) 50 | 51 | // convert param-b (int) into a string 52 | paramB := otx.String(ctx, "param-b") 53 | fmt.Println("paramB:", paramB) 54 | 55 | // a slice 56 | floats := otx.Slice[float64](ctx, "floats") 57 | fmt.Println("floats:", floats) 58 | 59 | // convert a slice's values to strings 60 | floatStrings := otx.Slice[string](ctx, "floats") 61 | fmt.Println("floatStrings:", floatStrings) 62 | 63 | // sub param is not available in this command, as it was defined on a sub 64 | // command and not on the root command 65 | sub := otx.Get[string](ctx, "sub") 66 | fmt.Println("sub:", sub) 67 | 68 | // a url 69 | if u := otx.URL(ctx, "url"); u != nil { 70 | // NOTE: this is wrapped in a if block, because when no flag has been 71 | // NOTE: passed, tinygo's fmt.Println will panic with a *url.URL(nil), 72 | // NOTE: however Go's fmt.Println does not 73 | fmt.Println("url:", u) 74 | // url alternate 75 | urlAlt := otx.Get[*url.URL](ctx, "url") 76 | fmt.Println("urlAlt:", urlAlt) 77 | } 78 | 79 | // verbose as its own type 80 | type Verbosity int64 81 | v := otx.Get[Verbosity](ctx, "verbose") 82 | fmt.Println("verbosity:", v, reflect.TypeOf(v)) 83 | 84 | return nil 85 | } 86 | 87 | func sub(ctx context.Context, args []string) error { 88 | fmt.Println("sub args:", args) 89 | 90 | // get param-a, as any parent's 91 | paramA := otx.Get[string](ctx, "param-a") 92 | fmt.Println("paramA:", paramA) 93 | 94 | // convert param-b (int) into a uint32 95 | paramB := otx.Uint32(ctx, "param-b") 96 | fmt.Println("paramB:", paramB) 97 | 98 | // the floats param is available, as this is a sub command 99 | floats := otx.Slice[float64](ctx, "floats") 100 | fmt.Println("floats:", floats) 101 | 102 | // sub is available here 103 | sub := otx.Get[string](ctx, "sub") 104 | fmt.Println("subParam:", sub) 105 | 106 | // get strings 107 | slice := otx.Slice[string](ctx, "strings") 108 | fmt.Println("slice:", slice) 109 | 110 | // slice of *big.Int 111 | bigint := otx.Slice[*big.Int](ctx, "bigint") 112 | fmt.Println("bigint:", bigint) 113 | 114 | // map of ints, converted to int64 115 | ints := otx.Map[string, int64](ctx, "ints") 116 | fmt.Println("ints:", ints) 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /text/text.go: -------------------------------------------------------------------------------- 1 | // Package text contains the text strings for xo/ox. 2 | package text 3 | 4 | var ( 5 | // ErrorMessage is the 'error: ' message. 6 | ErrorMessage = "error: %v\n" 7 | // SuggestionErrorMessage is the suggestion error message. 8 | SuggestionErrorMessage = `%s %q for %q` 9 | // SuggestionErrorDetails is the suggestion error details. 10 | SuggestionErrorDetails = "\nDid you mean this?\n %s\n\n" 11 | // CommandFlagsSpec is the spec for command flags. 12 | CommandFlagsSpec = `[flags]` 13 | // CommandSubSpec is the spec for sub command names. 14 | CommandSubSpec = `[command]` 15 | // CommandArgsSpec is the spec for command args. 16 | CommandArgsSpec = `[args]` 17 | // FlagSpecSpacer is the spacer between the flag and the displayed flag 18 | // spec. 19 | FlagSpecSpacer = ` ` 20 | // FlagDefault is the flag `(default: ...)` text. 21 | FlagDefault = `(default: %s)` 22 | 23 | // Usage is the `Usage:` section name. 24 | Usage = `Usage` 25 | // Aliases is the `Aliases:` section name. 26 | Aliases = `Aliases` 27 | // Examples is the `Examples:` section name. 28 | Examples = `Examples` 29 | // Commands is the `Available Commands:` section name. 30 | Commands = `Available Commands` 31 | // GlobalFlags is the `Global Flags:` section name. 32 | GlobalFlags = `Global Flags` 33 | // Flags is the `Flags:` section name. 34 | Flags = `Flags` 35 | 36 | // VersionCommandName is the `version` command name. 37 | VersionCommandName = `version` 38 | // VersionCommandUsage is the `version` command description. 39 | VersionCommandUsage = `show %s version information` 40 | // VersionCommandBanner is the `version` command banner. 41 | VersionCommandBanner = `Show %s version information.` 42 | // VersionFlagName is the `--version` flag name. 43 | VersionFlagName = `version` 44 | // VersionFlagUsage is the `--version` flag description. 45 | VersionFlagUsage = `show version, then exit` 46 | // VersionFlagShort is the `--version` short flag name. 47 | VersionFlagShort = `v` 48 | 49 | // HelpCommandName is the `help` command name. 50 | HelpCommandName = `help` 51 | // HelpCommandUsage is the `help` command description. 52 | HelpCommandUsage = `show help for any command` 53 | // HelpCommandBanner is the `help` command banner. 54 | HelpCommandBanner = "Help provides help for any %[1]s command.\n\nSimply type %[1]s help [path to command] for full details." 55 | // HelpFlagName is the `--help` flag name. 56 | HelpFlagName = `help` 57 | // HelpFlagUsage is the `--help` flag description. 58 | HelpFlagUsage = `show help, then exit` 59 | // HelpFlagShort is the `--help` short flag name. 60 | HelpFlagShort = `h` 61 | 62 | // CompCommandName is the `completion` command name. 63 | CompCommandName = `completion` 64 | // CompCommandUsage is the `completion` command description. 65 | CompCommandUsage = `generate completion script for %s` 66 | // CompCommandBanner is the `completion` command banner. 67 | CompCommandBanner = "Generate %s completion script for %s.\n\nSee each sub-command's help for details on using the generated completion script." 68 | // CompFlagName is the `--completion-script-` flag name. 69 | CompFlagName = `completion-script-%s` 70 | // CompFlagUsage is the `--completion-script-` flag description. 71 | CompFlagUsage = `generate completion script for %s` 72 | // CompCommandFlagNoDescriptionsName is the `completion` command 73 | // `no-descriptions` flag name. 74 | CompCommandFlagNoDescriptionsName = `no-descriptions` 75 | // CompCommandFlagNoDescriptionsUsage is the `completion` command 76 | // `no-descriptions` flag description. 77 | CompCommandFlagNoDescriptionsUsage = `disable completion descriptions` 78 | // CompCommandAnyShellDesc is the `completion` command `a specified shell` 79 | // description. 80 | CompCommandAnyShellDesc = `a specified shell` 81 | 82 | // Footer is the default footer for commands with sub commands. 83 | Footer = `Use "%s [command] --help" for more information about a command.` 84 | ) 85 | -------------------------------------------------------------------------------- /_examples/gen/psql/main.go: -------------------------------------------------------------------------------- 1 | // Command psql is a xo/ox version of `psql`. 2 | // 3 | // Generated from _examples/gen.go. 4 | package main 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/xo/ox" 10 | "github.com/xo/ox/text" 11 | ) 12 | 13 | func init() { 14 | text.FlagSpecSpacer = "=" 15 | } 16 | 17 | func main() { 18 | ox.RunContext( 19 | context.Background(), 20 | ox.Defaults(), 21 | ox.Usage(`psql`, ``), 22 | ox.Banner(`psql is the PostgreSQL interactive terminal.`), 23 | ox.Spec(`[OPTION]... [DBNAME [USERNAME]]`), 24 | ox.Help(ox.Sections( 25 | "General options", 26 | "Input and output options", 27 | "Output format options", 28 | "Connection options", 29 | )), 30 | ox.Footer(`For more information, type "\?" (for internal commands) or "\help" (for SQL 31 | commands) from within psql, or consult the psql section in the PostgreSQL 32 | documentation. 33 | 34 | Report bugs to . 35 | PostgreSQL home page - `), 36 | ox.Flags(). 37 | String(`command`, `run only single command (SQL or internal) and exit`, ox.Spec(`COMMAND`), ox.Short("c"), ox.Section(0)). 38 | String(`dbname`, `database name to connect to`, ox.Spec(`DBNAME`), ox.Short("d"), ox.Section(0)). 39 | String(`file`, `execute commands from file, then exit`, ox.Spec(`FILENAME`), ox.Short("f"), ox.Section(0)). 40 | Bool(`list`, `list available databases, then exit`, ox.Short("l"), ox.Section(0)). 41 | String(`set`, `--variable=NAME=VALUE`, ox.Spec(`,`), ox.Short("v"), ox.Section(0)). 42 | Bool(`no-psqlrc`, `do not read startup file (~/.psqlrc)`, ox.Short("X"), ox.Section(0)). 43 | String(`help[`, `show this help, then exit`, ox.Spec(`options]`), ox.Short("?"), ox.Section(0)). 44 | Bool(`echo-all`, `echo all input from script`, ox.Short("a"), ox.Section(1)). 45 | Bool(`echo-errors`, `echo failed commands`, ox.Short("b"), ox.Section(1)). 46 | Bool(`echo-queries`, `echo commands sent to server`, ox.Short("e"), ox.Section(1)). 47 | Bool(`echo-hidden`, `display queries that internal commands generate`, ox.Short("E"), ox.Section(1)). 48 | String(`log-file`, `send session log to file`, ox.Spec(`FILENAME`), ox.Short("L"), ox.Section(1)). 49 | Bool(`no-readline`, `disable enhanced command line editing (readline)`, ox.Short("n"), ox.Section(1)). 50 | String(`output`, `send query results to file (or |pipe)`, ox.Spec(`FILENAME`), ox.Short("o"), ox.Section(1)). 51 | Bool(`quiet`, `run quietly (no messages, only query output)`, ox.Short("q"), ox.Section(1)). 52 | Bool(`single-step`, `single-step mode (confirm each query)`, ox.Short("s"), ox.Section(1)). 53 | Bool(`single-line`, `single-line mode (end of line terminates SQL command)`, ox.Short("S"), ox.Section(1)). 54 | Bool(`no-align`, `unaligned table output mode`, ox.Short("A"), ox.Section(2)). 55 | Bool(`csv`, `CSV (Comma-Separated Values) table output mode`, ox.Section(2)). 56 | String(`field-separator`, `field separator for unaligned output (default: "|")`, ox.Spec(`STRING`), ox.Short("F"), ox.Section(2)). 57 | Bool(`html`, `HTML table output mode`, ox.Short("H"), ox.Section(2)). 58 | Map(`pset`, `set printing option VAR to ARG (see \pset command)`, ox.Spec(`VAR[=ARG]`), ox.Short("P"), ox.Section(2)). 59 | String(`record-separator`, `record separator for unaligned output (default: newline)`, ox.Spec(`STRING`), ox.Short("R"), ox.Section(2)). 60 | Bool(`tuples-only`, `print rows only`, ox.Short("t"), ox.Section(2)). 61 | String(`table-attr`, `set HTML table tag attributes (e.g., width, border)`, ox.Spec(`TEXT`), ox.Short("T"), ox.Section(2)). 62 | Bool(`expanded`, `turn on expanded table output`, ox.Short("x"), ox.Section(2)). 63 | String(`host`, `database server host or socket directory`, ox.Spec(`HOSTNAME`), ox.Short("h"), ox.Section(3)). 64 | String(`port`, `database server port`, ox.Spec(`PORT`), ox.Short("p"), ox.Section(3)). 65 | String(`username`, `database user name`, ox.Spec(`USERNAME`), ox.Short("U"), ox.Section(3)). 66 | Bool(`no-password`, `never prompt for password`, ox.Short("w"), ox.Section(3)). 67 | Bool(`password`, `force password prompt (should happen automatically)`, ox.Short("W"), ox.Section(3)), 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /tinygo.go: -------------------------------------------------------------------------------- 1 | //go:build tinygo 2 | 3 | package ox 4 | 5 | // copied from the go source tree 6 | 7 | import ( 8 | "errors" 9 | "os" 10 | "reflect" 11 | "runtime" 12 | ) 13 | 14 | func overflowComplex(v reflect.Value, c complex128) bool { 15 | // tinygo doesn't have reflect.Value.OverflowComplex 16 | return true 17 | } 18 | 19 | // userCacheDir returns the default root directory to use for user-specific 20 | // cached data. Users should create their own application-specific subdirectory 21 | // within this one and use that. 22 | // 23 | // On Unix systems, it returns $XDG_CACHE_HOME as specified by 24 | // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if 25 | // non-empty, else $HOME/.cache. 26 | // On Darwin, it returns $HOME/Library/Caches. 27 | // On Windows, it returns %LocalAppData%. 28 | // On Plan 9, it returns $home/lib/cache. 29 | // 30 | // If the location cannot be determined (for example, $HOME is not defined), 31 | // then it will return an error. 32 | func userCacheDir() (string, error) { 33 | var dir string 34 | 35 | switch runtime.GOOS { 36 | case "windows": 37 | dir = os.Getenv("LocalAppData") 38 | if dir == "" { 39 | return "", errors.New("%LocalAppData% is not defined") 40 | } 41 | 42 | case "darwin", "ios": 43 | dir = os.Getenv("HOME") 44 | if dir == "" { 45 | return "", errors.New("$HOME is not defined") 46 | } 47 | dir += "/Library/Caches" 48 | 49 | case "plan9": 50 | dir = os.Getenv("home") 51 | if dir == "" { 52 | return "", errors.New("$home is not defined") 53 | } 54 | dir += "/lib/cache" 55 | 56 | default: // Unix 57 | dir = os.Getenv("XDG_CACHE_HOME") 58 | if dir == "" { 59 | dir = os.Getenv("HOME") 60 | if dir == "" { 61 | return "", errors.New("neither $XDG_CACHE_HOME nor $HOME are defined") 62 | } 63 | dir += "/.cache" 64 | } 65 | } 66 | 67 | return dir, nil 68 | } 69 | 70 | // userConfigDir returns the default root directory to use for user-specific 71 | // configuration data. Users should create their own application-specific 72 | // subdirectory within this one and use that. 73 | // 74 | // On Unix systems, it returns $XDG_CONFIG_HOME as specified by 75 | // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if 76 | // non-empty, else $HOME/.config. 77 | // On Darwin, it returns $HOME/Library/Application Support. 78 | // On Windows, it returns %AppData%. 79 | // On Plan 9, it returns $home/lib. 80 | // 81 | // If the location cannot be determined (for example, $HOME is not defined), 82 | // then it will return an error. 83 | func userConfigDir() (string, error) { 84 | var dir string 85 | 86 | switch runtime.GOOS { 87 | case "windows": 88 | dir = os.Getenv("AppData") 89 | if dir == "" { 90 | return "", errors.New("%AppData% is not defined") 91 | } 92 | 93 | case "darwin", "ios": 94 | dir = os.Getenv("HOME") 95 | if dir == "" { 96 | return "", errors.New("$HOME is not defined") 97 | } 98 | dir += "/Library/Application Support" 99 | 100 | case "plan9": 101 | dir = os.Getenv("home") 102 | if dir == "" { 103 | return "", errors.New("$home is not defined") 104 | } 105 | dir += "/lib" 106 | 107 | default: // Unix 108 | dir = os.Getenv("XDG_CONFIG_HOME") 109 | if dir == "" { 110 | dir = os.Getenv("HOME") 111 | if dir == "" { 112 | return "", errors.New("neither $XDG_CONFIG_HOME nor $HOME are defined") 113 | } 114 | dir += "/.config" 115 | } 116 | } 117 | 118 | return dir, nil 119 | } 120 | 121 | // userHomeDir returns the current user's home directory. 122 | // 123 | // On Unix, including macOS, it returns the $HOME environment variable. 124 | // On Windows, it returns %USERPROFILE%. 125 | // On Plan 9, it returns the $home environment variable. 126 | // 127 | // If the expected variable is not set in the environment, UserHomeDir 128 | // returns either a platform-specific default value or a non-nil error. 129 | func userHomeDir() (string, error) { 130 | env, enverr := "HOME", "$HOME" 131 | switch runtime.GOOS { 132 | case "windows": 133 | env, enverr = "USERPROFILE", "%userprofile%" 134 | case "plan9": 135 | env, enverr = "home", "$home" 136 | } 137 | if v := os.Getenv(env); v != "" { 138 | return v, nil 139 | } 140 | // On some geese the home directory is not always defined. 141 | switch runtime.GOOS { 142 | case "android": 143 | return "/sdcard", nil 144 | case "ios": 145 | return "/", nil 146 | } 147 | return "", errors.New(enverr + " is not defined") 148 | } 149 | -------------------------------------------------------------------------------- /strcase/initialisms.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode" 7 | ) 8 | 9 | // Initialisms is a set of initialisms. 10 | type Initialisms struct { 11 | known map[string]string 12 | post map[string]string 13 | max int 14 | } 15 | 16 | // New creates a new set of initialisms. 17 | func New(initialisms ...string) (*Initialisms, error) { 18 | ini := &Initialisms{ 19 | known: make(map[string]string), 20 | post: make(map[string]string), 21 | } 22 | if err := ini.Add(initialisms...); err != nil { 23 | return nil, err 24 | } 25 | return ini, nil 26 | } 27 | 28 | // NewDefaults creates a default set of known, common initialisms. 29 | func NewDefaults() (*Initialisms, error) { 30 | ini, err := New(CommonInitialisms()...) 31 | if err != nil { 32 | return nil, err 33 | } 34 | var pairs []string 35 | for _, s := range CommonPlurals() { 36 | pairs = append(pairs, s+"S", s+"s") 37 | } 38 | if err := ini.Post(pairs...); err != nil { 39 | return nil, err 40 | } 41 | return ini, nil 42 | } 43 | 44 | // Add adds a known initialisms. 45 | func (ini *Initialisms) Add(initialisms ...string) error { 46 | for _, s := range initialisms { 47 | s = strings.ToUpper(s) 48 | if len(s) < 2 { 49 | return fmt.Errorf("invalid initialism %q", s) 50 | } 51 | ini.known[s], ini.max = s, max(ini.max, len(s)) 52 | } 53 | return nil 54 | } 55 | 56 | // Post adds a key, value pair to the initialisms and post map. 57 | func (ini *Initialisms) Post(pairs ...string) error { 58 | if len(pairs)%2 != 0 { 59 | return fmt.Errorf("invalid pairs length %d", len(pairs)) 60 | } 61 | for i := 0; i < len(pairs); i += 2 { 62 | s := strings.ToUpper(pairs[i]) 63 | if s != strings.ToUpper(pairs[i+1]) { 64 | return fmt.Errorf("invalid pair %q, %q", pairs[i], pairs[i+1]) 65 | } 66 | ini.known[s] = pairs[i+1] 67 | ini.post[pairs[i+1]] = pairs[i+1] 68 | ini.max = max(ini.max, len(s)) 69 | } 70 | return nil 71 | } 72 | 73 | // CamelToSnake converts name from camel case ("AnIdentifier") to snake case 74 | // ("an_identifier"). 75 | func (ini *Initialisms) CamelToSnake(name string) string { 76 | if name == "" { 77 | return "" 78 | } 79 | var s string 80 | var wasUpper, wasLetter, wasIsm, isUpper, isLetter bool 81 | for i, r, next := 0, []rune(name), ""; i < len(r); i, s = i+len(next), s+next { 82 | isUpper, isLetter = unicode.IsUpper(r[i]), unicode.IsLetter(r[i]) 83 | // append _ when last was not upper and not letter 84 | if (wasLetter && isUpper) || (wasIsm && isLetter) { 85 | s += "_" 86 | } 87 | // determine next to append to r 88 | if ism := ini.Peek(r[i:]); ism != "" && (!wasUpper || wasIsm) { 89 | next = ism 90 | } else { 91 | next = string(r[i]) 92 | } 93 | // save for next iteration 94 | wasIsm, wasUpper, wasLetter = 1 < len(next), isUpper, isLetter 95 | } 96 | return strings.ToLower(s) 97 | } 98 | 99 | // CamelToSnakeIdentifier converts name from camel case to a snake case 100 | // identifier. 101 | func (ini *Initialisms) CamelToSnakeIdentifier(name string) string { 102 | return ToIdentifier(ini.CamelToSnake(name)) 103 | } 104 | 105 | // SnakeToCamel converts name to CamelCase. 106 | func (ini *Initialisms) SnakeToCamel(name string) string { 107 | var s string 108 | for word := range strings.SplitSeq(name, "_") { 109 | if word == "" { 110 | continue 111 | } 112 | if u, ok := ini.known[strings.ToUpper(word)]; ok { 113 | s += u 114 | } else { 115 | s += strings.ToUpper(word[:1]) + strings.ToLower(word[1:]) 116 | } 117 | } 118 | return s 119 | } 120 | 121 | // SnakeToCamelIdentifier converts name to its CamelCase identifier (first 122 | // letter is capitalized). 123 | func (ini *Initialisms) SnakeToCamelIdentifier(name string) string { 124 | return ini.SnakeToCamel(ToIdentifier(name)) 125 | } 126 | 127 | // ForceCamelIdentifier forces name to its CamelCase specific to Go 128 | // ("AnIdentifier"). 129 | func (ini *Initialisms) ForceCamelIdentifier(name string) string { 130 | if name == "" { 131 | return "" 132 | } 133 | return ini.SnakeToCamelIdentifier(ini.CamelToSnake(name)) 134 | } 135 | 136 | // ForceLowerCamelIdentifier forces the first portion of an identifier to be 137 | // lower case ("anIdentifier"). 138 | func (ini *Initialisms) ForceLowerCamelIdentifier(name string) string { 139 | if name == "" { 140 | return "" 141 | } 142 | name = ini.CamelToSnake(name) 143 | first := strings.Split(name, "_")[0] 144 | name = ini.SnakeToCamelIdentifier(name) 145 | return strings.ToLower(first) + name[len(first):] 146 | } 147 | 148 | // Peek returns the next longest possible initialism in v. 149 | func (ini *Initialisms) Peek(r []rune) string { 150 | // do no work 151 | if len(r) < 2 { 152 | return "" 153 | } 154 | // peek next few runes, up to max length of the largest known initialism 155 | var i int 156 | for n := min(len(r), ini.max); i < n && unicode.IsLetter(r[i]); i++ { 157 | } 158 | // bail if not enough letters 159 | if i < 2 { 160 | return "" 161 | } 162 | // determine if common initialism 163 | var k string 164 | for i = min(ini.max, i+1, len(r)); i >= 2; i-- { 165 | k = string(r[:i]) 166 | if s, ok := ini.known[k]; ok { 167 | return s 168 | } 169 | if s, ok := ini.post[k]; ok { 170 | return s 171 | } 172 | } 173 | return "" 174 | } 175 | 176 | // Is indicates whether or not s is a registered initialism. 177 | func (ini *Initialisms) Is(s string) bool { 178 | _, ok := ini.known[strings.ToUpper(s)] 179 | return ok 180 | } 181 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "fmt" 5 | "maps" 6 | "slices" 7 | "strings" 8 | ) 9 | 10 | // Parse parses the args into context. 11 | func Parse(ctx *Context, cmd *Command, args []string) (*Command, []string, error) { 12 | var v []string 13 | var s string 14 | var n int 15 | var err error 16 | for len(args) != 0 { 17 | switch s, n, args = args[0], len(args[0]), args[1:]; { 18 | case n == 0, n == 1, s[0] != '-': 19 | // lookup sub command 20 | var c *Command 21 | if len(v) == 0 { 22 | c = cmd.Command(s) 23 | } 24 | if c != nil { 25 | if err := ctx.Populate(c, false, false); err != nil { 26 | return nil, nil, newCommandError(c.Name, err) 27 | } 28 | cmd = c 29 | } else { 30 | v = append(v, s) 31 | } 32 | case s == "--": 33 | return cmd, append(v, args...), nil 34 | case s[1] == '-': 35 | if args, err = ParseFlagLong(ctx, cmd, s, args); err != nil { 36 | if !ctx.Continue(cmd, err) { 37 | return nil, nil, err 38 | } 39 | } 40 | default: 41 | if args, err = ParseFlagShort(ctx, cmd, s, args); err != nil { 42 | if !ctx.Continue(cmd, err) { 43 | return nil, nil, err 44 | } 45 | } 46 | } 47 | } 48 | return cmd, v, nil 49 | } 50 | 51 | // ParseFlagLong parses a long flag ('--arg' '--arg v' '--arg k=v' '--arg=' '--arg=v'). 52 | func ParseFlagLong(ctx *Context, cmd *Command, s string, args []string) ([]string, error) { 53 | arg, value, ok := strings.Cut(strings.TrimPrefix(s, "--"), "=") 54 | g := cmd.Flag(arg, true, false) 55 | switch { 56 | case g == nil && ctx.Continue(cmd, newFlagError(arg, ErrUnknownFlag)): 57 | return args, nil 58 | case g == nil: 59 | return nil, newFlagError(arg, ErrUnknownFlag) 60 | case ok: // --arg=v 61 | case g.NoArg: // --arg 62 | var err error 63 | if value, err = asString[string](g.NoArgDef); err != nil { 64 | return nil, newFlagError(arg, err) 65 | } 66 | case len(args) != 0: // --arg v 67 | value, args = args[0], args[1:] 68 | default: // missing argument to --arg 69 | return nil, newFlagError(arg, ErrMissingArgument) 70 | } 71 | if err := ctx.Vars.Set(ctx, g, value, true); err != nil { 72 | return nil, newFlagError(arg, err) 73 | } 74 | return args, nil 75 | } 76 | 77 | // ParseFlagShort parses short flags ('-a' '-aaa' '-av' '-a v' '-a=' '-a=v'). 78 | func ParseFlagShort(ctx *Context, cmd *Command, s string, args []string) ([]string, error) { 79 | for v := []rune(s[1:]); len(v) != 0; v = v[1:] { 80 | arg := string(v[0]) 81 | switch g, n := cmd.Flag(arg, true, true), len(v[1:]); { 82 | case g == nil && ctx.Continue(cmd, newFlagError(arg, ErrUnknownFlag)): 83 | return args, nil 84 | case g == nil: 85 | return nil, newFlagError(arg, ErrUnknownFlag) 86 | case g.NoArg: // -a 87 | var value string 88 | var err error 89 | if slices.Index(v, '=') == 1 { 90 | value, v = string(v[2:]), v[len(v)-1:] 91 | } else if value, err = asString[string](g.NoArgDef); err != nil { 92 | return nil, newFlagError(arg, err) 93 | } 94 | if err := ctx.Vars.Set(ctx, g, value, true); err != nil { 95 | return nil, newFlagError(arg, err) 96 | } 97 | case n == 0 && len(args) == 0: // missing argument to -a 98 | return nil, newFlagError(arg, ErrMissingArgument) 99 | case n != 0: // -a=, -a=v, -av 100 | if slices.Index(v, '=') == 1 { 101 | v = v[1:] 102 | } 103 | if err := ctx.Vars.Set(ctx, g, string(v[1:]), true); err != nil { 104 | return nil, newFlagError(arg, err) 105 | } 106 | return args, nil 107 | default: // -a v 108 | if err := ctx.Vars.Set(ctx, g, args[0], true); err != nil { 109 | return nil, newFlagError(arg, err) 110 | } 111 | return args[1:], nil 112 | } 113 | } 114 | return args, nil 115 | } 116 | 117 | // Vars is a map of argument variables. 118 | type Vars map[string]Value 119 | 120 | // String satisfies the [fmt.Stringer] interface. 121 | func (vars Vars) String() string { 122 | var v []string 123 | for _, k := range slices.Sorted(maps.Keys(vars)) { 124 | if s, err := vars[k].Get(); err == nil { 125 | v = append(v, k+":"+s) 126 | } 127 | } 128 | return "[" + strings.Join(v, " ") + "]" 129 | } 130 | 131 | // Set sets a variable in the vars. 132 | func (vars Vars) Set(ctx *Context, g *Flag, s string, set bool) error { 133 | name := g.Name 134 | if name == "" { 135 | return ErrInvalidFlagName 136 | } 137 | // instantiate 138 | v, ok := vars[name] 139 | if !ok { 140 | var err error 141 | if v, err = g.New(ctx); err != nil { 142 | return err 143 | } 144 | setValid(v, g.Validate) 145 | setSplit(v, g.Split) 146 | } 147 | // set 148 | if err := v.Set(s); err != nil { 149 | return err 150 | } 151 | v.SetSet(set) 152 | // bind 153 | for i, bind := range g.Binds { 154 | setSplit(bind, g.Split) 155 | if err := bind.Bind(s); err != nil { 156 | return fmt.Errorf("bind %d (%s): cannot set %q: %w", i, bind, s, err) 157 | } 158 | bind.SetSet(set) 159 | } 160 | vars[name] = v 161 | return nil 162 | } 163 | 164 | // setValid sets the valid func on v. 165 | func setValid(v any, valid func(any) error) { 166 | if valid == nil { 167 | return 168 | } 169 | if z, ok := v.(interface{ SetValid(func(any) error) }); ok { 170 | z.SetValid(valid) 171 | } 172 | } 173 | 174 | // setSplit sets the split func on v. 175 | func setSplit(v any, split string) { 176 | if split == "" { 177 | return 178 | } 179 | z, ok := v.(interface{ SetSplit(func(string) []string) }) 180 | if !ok { 181 | return 182 | } 183 | var f func(string) []string 184 | switch r := []rune(split); len(r) { 185 | case 1: 186 | f = func(s string) []string { 187 | return SplitBy(s, r[0]) 188 | } 189 | default: 190 | f = func(s string) []string { 191 | return strings.Split(s, split) 192 | } 193 | } 194 | z.SetSplit(f) 195 | } 196 | -------------------------------------------------------------------------------- /strcase/strcase.go: -------------------------------------------------------------------------------- 1 | // Package strcase provides methods to convert CamelCase to and from snake_case. 2 | // 3 | // Correctly recognizes common (Go idiomatic) initialisms (HTTP, XML, etc) with 4 | // the ability to define/override/add initialisms. 5 | package strcase 6 | 7 | import ( 8 | "regexp" 9 | "strings" 10 | "unicode" 11 | ) 12 | 13 | // Defaults is the set of default (common) initialisms used by the package 14 | // level conversions funcs. 15 | var Defaults *Initialisms 16 | 17 | func init() { 18 | // initialize common default initialisms 19 | var err error 20 | if Defaults, err = NewDefaults(); err != nil { 21 | panic(err) 22 | } 23 | } 24 | 25 | // CamelToSnake converts name from camel case ("AnIdentifier") to snake case 26 | // ("an_identifier"). 27 | func CamelToSnake(name string) string { 28 | return Defaults.CamelToSnake(name) 29 | } 30 | 31 | // CamelToSnakeIdentifier converts name from camel case to a snake case 32 | // identifier. 33 | func CamelToSnakeIdentifier(name string) string { 34 | return Defaults.CamelToSnakeIdentifier(name) 35 | } 36 | 37 | // SnakeToCamel converts name to CamelCase. 38 | func SnakeToCamel(name string) string { 39 | return Defaults.SnakeToCamel(name) 40 | } 41 | 42 | // SnakeToCamelIdentifier converts name to its CamelCase identifier (first 43 | // letter is capitalized). 44 | func SnakeToCamelIdentifier(name string) string { 45 | return Defaults.SnakeToCamelIdentifier(name) 46 | } 47 | 48 | // ForceCamelIdentifier forces name to its CamelCase specific to Go 49 | // ("AnIdentifier"). 50 | func ForceCamelIdentifier(name string) string { 51 | return Defaults.ForceCamelIdentifier(name) 52 | } 53 | 54 | // ForceLowerCamelIdentifier forces the first portion of an identifier to be 55 | // lower case ("anIdentifier"). 56 | func ForceLowerCamelIdentifier(name string) string { 57 | return Defaults.ForceLowerCamelIdentifier(name) 58 | } 59 | 60 | // IsInitialism returns true when s is a registered default initialism. 61 | func IsInitialism(s string) bool { 62 | return Defaults.Is(s) 63 | } 64 | 65 | // ToIdentifier cleans s so that it is usable as an identifier. 66 | // 67 | // Substitutes invalid characters with an underscore, removing leading 68 | // numbers/underscores and trailing underscores. 69 | // 70 | // Additionally collapses multiple underscores to a single underscore. 71 | // 72 | // Makes no changes to case. 73 | func ToIdentifier(s string) string { 74 | return toIdent(s, '_') 75 | } 76 | 77 | // ToSnake cleans s to snake_case. 78 | // 79 | // Substitutes invalid characters with an underscore, removing leading 80 | // numbers/underscores and trailing underscores. 81 | // 82 | // Additionally collapses multiple underscores to a single underscore. 83 | // 84 | // Converts entire string to lower case. 85 | func ToSnake(s string) string { 86 | return strings.ToLower(toIdent(s, '_')) 87 | } 88 | 89 | // ToKebab changes s to kebab-case. 90 | // 91 | // Substitutes invalid characters with a hyphen, removing leading 92 | // numbers/hyphens and trailing hyphens. 93 | // 94 | // Additionally collapses multiple hyphens to a single hyphen. 95 | // 96 | // Converts entire string to lower case. 97 | func ToKebab(s string) string { 98 | return strings.ToLower(toIdent(s, '-')) 99 | } 100 | 101 | // CommonInitialisms returns the set of common initialisms. 102 | // 103 | // Originally built from the list in golang.org/x/lint @ 738671d. 104 | // 105 | // Note: golang.org/x/lint has since been deprecated, and some additional 106 | // initialisms have since been added. 107 | func CommonInitialisms() []string { 108 | return []string{ 109 | "ACL", 110 | "API", 111 | "ASCII", 112 | "CPU", 113 | "CSS", 114 | "CSV", 115 | "DNS", 116 | "EOF", 117 | "GPU", 118 | "GUID", 119 | "HTML", 120 | "HTTP", 121 | "HTTPS", 122 | "ID", 123 | "IP", 124 | "JSON", 125 | "LHS", 126 | "QPS", 127 | "RAM", 128 | "RHS", 129 | "RPC", 130 | "SLA", 131 | "SMTP", 132 | "SQL", 133 | "SSH", 134 | "TCP", 135 | "TLS", 136 | "TSV", 137 | "TTL", 138 | "UDP", 139 | "UI", 140 | "UID", 141 | "URI", 142 | "URL", 143 | "UTC", 144 | "UTF8", 145 | "UUID", 146 | "VM", 147 | "XML", 148 | "XMPP", 149 | "XSRF", 150 | "XSS", 151 | "YAML", 152 | } 153 | } 154 | 155 | // CommonPlurals returns initialisms that have a common plural of s. 156 | func CommonPlurals() []string { 157 | return []string{ 158 | "ACL", 159 | "API", 160 | "CPU", 161 | "CSV", 162 | "GPU", 163 | "GUID", 164 | "ID", 165 | "IP", 166 | "TSV", 167 | "UID", 168 | "UID", 169 | "URI", 170 | "URL", 171 | "UUID", 172 | "VM", 173 | } 174 | } 175 | 176 | // toIdent converts s to a identifier. 177 | func toIdent(s string, repl rune) string { 178 | // replace bad chars with c 179 | s = sub(strings.TrimSpace(s), repl) 180 | // compact multiple c to single c 181 | s = regexp.MustCompile(`\Q`+string(repl)+`\E{2,}`).ReplaceAllString(s, string(repl)) 182 | // remove leading numbers and c 183 | s = strings.TrimLeftFunc(s, func(r rune) bool { 184 | return unicode.IsNumber(r) || r == repl 185 | }) 186 | // remove trailing c 187 | s = strings.TrimRightFunc(s, func(r rune) bool { 188 | return r == repl 189 | }) 190 | return s 191 | } 192 | 193 | // sub substitutes all non-valid identifier characters in s with repl. 194 | func sub(s string, repl rune) string { 195 | r := []rune(s) 196 | for i, ch := range r { 197 | if !isIdentifierChar(ch, repl) { 198 | r[i] = repl 199 | } 200 | } 201 | return string(r) 202 | } 203 | 204 | // isIdentifierChar determines if ch is a valid character for a Go identifier. 205 | // 206 | // See: go/src/go/scanner/scanner.go. 207 | func isIdentifierChar(ch, repl rune) bool { 208 | return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == repl || ch >= 0x80 && unicode.IsLetter(ch) || 209 | '0' <= ch && ch <= '9' || ch >= 0x80 && unicode.IsDigit(ch) 210 | } 211 | -------------------------------------------------------------------------------- /ox_test.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "strconv" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func TestSuggestions(t *testing.T) { 16 | for i, test := range []struct { 17 | args []string 18 | exp string 19 | }{ 20 | { 21 | ss(`subfoo`), `error: unknown command "subfoo" for "cmd"`, 22 | }, 23 | { 24 | ss(`on`), `error: unknown command "on" for "cmd" 25 | 26 | Did you mean this? 27 | one 28 | `, 29 | }, 30 | { 31 | ss(`remove`), `error: unknown command "remove" for "cmd" 32 | 33 | Did you mean this? 34 | one 35 | `, 36 | }, 37 | { 38 | ss(`rmove`), `error: unknown command "rmove" for "cmd" 39 | 40 | Did you mean this? 41 | one 42 | `, 43 | }, 44 | } { 45 | t.Run(strconv.Itoa(i), func(t *testing.T) { 46 | var code int 47 | c := testContext(t, &code, test.args...) 48 | err := c.Run(context.Background()) 49 | if err == nil { 50 | t.Fatalf("expected non-nil error") 51 | } 52 | if !c.Handler(err) { 53 | t.Fatalf("expected Handler to return true") 54 | } 55 | if code == 0 { 56 | t.Fatalf("expected Handler to set non-zero code") 57 | } 58 | if s := strings.TrimSuffix(c.Stderr.(*bytes.Buffer).String(), "\n"); s != test.exp { 59 | t.Errorf("\nexpected:\n%s\ngot:\n%s", test.exp, s) 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func TestContextExpand(t *testing.T) { 66 | root := &Command{ 67 | Name: "myApp", 68 | } 69 | configDir, err := userConfigDir() 70 | if err != nil { 71 | t.Fatalf("expected no error, got: %v", err) 72 | } 73 | cacheDir, err := userCacheDir() 74 | if err != nil { 75 | t.Fatalf("expected no error, got: %v", err) 76 | } 77 | tests := []struct { 78 | v string 79 | exp string 80 | err error 81 | }{ 82 | {``, ``, nil}, 83 | {`a`, `a`, nil}, 84 | {`$ABC`, `$ABC`, nil}, 85 | {`$HOME`, os.Getenv(`HOME`), nil}, 86 | {`$USER`, os.Getenv(`USER`), nil}, 87 | {`$APPNAME`, root.Name, nil}, 88 | {`$CONFIG`, configDir, nil}, 89 | {`$APPCONFIG`, filepath.Join(configDir, root.Name), nil}, 90 | {`$CACHE`, cacheDir, nil}, 91 | {`$APPCACHE`, filepath.Join(cacheDir, root.Name), nil}, 92 | {`$NUMCPU`, strconv.Itoa(runtime.NumCPU()), nil}, 93 | {`$NUMCPU2`, strconv.Itoa(runtime.NumCPU() + 2), nil}, 94 | {`$NUMCPU2X`, strconv.Itoa(runtime.NumCPU() * 2), nil}, 95 | {`$ARCH`, runtime.GOARCH, nil}, 96 | {`$OS`, runtime.GOOS, nil}, 97 | {`$ENV{HOME}`, os.Getenv(`HOME`), nil}, 98 | {`$ENV{NOT_DEFINED}`, ``, nil}, 99 | {`$MY_OVERRIDE{foo}`, `bar`, nil}, 100 | {`$HOME/$USER/$APPNAME`, os.Getenv("HOME") + "/" + os.Getenv("USER") + "/" + root.Name, nil}, 101 | {`$HOME$USER$APPNAME`, os.Getenv("HOME") + os.Getenv("USER") + root.Name, nil}, 102 | {`//$OS//`, "//" + runtime.GOOS + "//", nil}, 103 | } 104 | for i, test := range tests { 105 | t.Run(strconv.Itoa(i), func(t *testing.T) { 106 | t.Logf("test: %q", test.v) 107 | ctx := &Context{ 108 | Root: root, 109 | Loader: DefaultLoader, 110 | Override: func(typ, key string) (string, bool) { 111 | if typ == "MY_OVERRIDE" { 112 | switch strings.ToLower(key) { 113 | case "foo": 114 | return "bar", true 115 | } 116 | } 117 | return "", false 118 | }, 119 | } 120 | v, err := ctx.Expand(test.v) 121 | switch { 122 | case err != nil && !errors.Is(err, test.err): 123 | t.Fatalf("expected error %v, got: %v", test.err, err) 124 | case err == nil && test.err != nil: 125 | t.Fatalf("expected error %v, got nil", test.err) 126 | } 127 | t.Logf("v: %v (%T)", v, v) 128 | s, ok := v.(string) 129 | if !ok { 130 | t.Fatalf("expected string, got: %T", v) 131 | } 132 | if s != test.exp { 133 | t.Errorf("expected %q, got: %q", test.exp, s) 134 | } 135 | }) 136 | } 137 | } 138 | 139 | func TestLdist(t *testing.T) { 140 | tests := []struct { 141 | a string 142 | b string 143 | exp int 144 | }{ 145 | {"", "", 0}, 146 | {"", "a", 1}, 147 | {"ab", "aa", 1}, 148 | {"ab", "aaa", 2}, 149 | {"ab", "ba", 2}, 150 | {"a very long string that is meant to exceed", "another very long string that is meant to exceed", 6}, 151 | {"bar", "br", 1}, 152 | {"bbb", "a", 3}, 153 | {"book", "back", 2}, 154 | {"br", "bar", 1}, 155 | {"distance", "difference", 5}, 156 | {"fod", "Food", 1}, 157 | {"foo", "", 3}, 158 | {"foo", "bar", 3}, 159 | {"foo", "Food", 1}, 160 | {"Hafþór Júlíus Björnsson", "Hafþor Julius Bjornsson", 4}, 161 | {"", "hello", 5}, 162 | {"hello", "hello", 0}, 163 | {"kitten", "sitting", 3}, 164 | {"levenshtein", "frankenstein", 6}, 165 | {"mississippi", "swiss miss", 8}, 166 | {"resume and cafe", "resumé and café", 2}, 167 | {"resume and cafe", "resumes and cafes", 2}, 168 | {"resumé and café", "resumés and cafés", 2}, 169 | {"rosettacode", "raisethysword", 8}, 170 | {"saturday", "sunday", 3}, 171 | {"sitten", "sittin", 1}, 172 | {"sittin", "sitting", 1}, 173 | {"sleep", "fleeting", 5}, 174 | {"stop", "tops", 2}, 175 | {"test", "t", 3}, 176 | {"།་གམ་འས་པ་་མ།", "།་གམའས་པ་་མ", 2}, 177 | {"👀😀", "😀", 1}, 178 | } 179 | for _, test := range tests { 180 | t.Run(test.a+"::"+test.b, func(t *testing.T) { 181 | a, b := []rune(strings.ToLower(test.a)), []rune(strings.ToLower(test.b)) 182 | if i := Ldist(a, b); i != test.exp { 183 | t.Errorf("expected %d, got: %d", test.exp, i) 184 | } 185 | if i := Ldist(b, a); i != test.exp { 186 | t.Errorf("expected %d, got: %d", test.exp, i) 187 | } 188 | }) 189 | } 190 | } 191 | 192 | func testContext(t *testing.T, code *int, args ...string) *Context { 193 | t.Helper() 194 | cmd := testCommand(t) 195 | cmd.Exec = nil 196 | c := &Context{ 197 | Exit: func(c int) { 198 | *code = c 199 | }, 200 | Panic: func(v any) { 201 | t.Fatalf("context panic: %v", v) 202 | }, 203 | Root: cmd, 204 | Stderr: new(bytes.Buffer), 205 | Args: args, 206 | Vars: make(Vars), 207 | } 208 | c.Handler = DefaultErrorHandler(c) 209 | if err := c.Parse(); err != nil { 210 | t.Fatalf("expected no error, got: %v", err) 211 | } 212 | return c 213 | } 214 | -------------------------------------------------------------------------------- /otx/otx.go: -------------------------------------------------------------------------------- 1 | // Package otx provides [context.Context] access methods for xo/ox. 2 | package otx 3 | 4 | import ( 5 | "cmp" 6 | "context" 7 | "math/big" 8 | "net/netip" 9 | "net/url" 10 | "time" 11 | 12 | "github.com/xo/ox" 13 | ) 14 | 15 | // Vars returns all variables from the context. 16 | func Vars(ctx context.Context) (ox.Vars, bool) { 17 | if c, ok := ox.Ctx(ctx); ok && c != nil { 18 | return c.Vars, true 19 | } 20 | return nil, false 21 | } 22 | 23 | // Any returns a variable, its set status, and if it was defined from the 24 | // context. 25 | func Any(ctx context.Context, name string) (ox.Value, bool) { 26 | if vars, ok := Vars(ctx); ok { 27 | if val, ok := vars[name]; ok { 28 | return val, true 29 | } 30 | } 31 | return nil, false 32 | } 33 | 34 | // Get returns a variable. 35 | func Get[T any](ctx context.Context, name string) T { 36 | if val, ok := Any(ctx, name); ok { 37 | if v, err := ox.As[T](val); err == nil { 38 | return v 39 | } 40 | } 41 | var v T 42 | return v 43 | } 44 | 45 | // Slice returns the slice variable from the context as a slice of type E. 46 | func Slice[E any](ctx context.Context, name string) []E { 47 | if val, ok := Any(ctx, name); ok { 48 | if v, err := ox.AsSlice[E](val); err == nil { 49 | return v 50 | } 51 | } 52 | return nil 53 | } 54 | 55 | // Map returns the map variable from the context. 56 | func Map[K cmp.Ordered, T any](ctx context.Context, name string) map[K]T { 57 | if val, ok := Any(ctx, name); ok { 58 | if m, err := ox.AsMap[K, T](val); err == nil { 59 | return m 60 | } 61 | } 62 | return make(map[K]T) 63 | } 64 | 65 | // Bytes returns a variable as []byte from the context. 66 | func Bytes(ctx context.Context, name string) []byte { 67 | return Get[[]byte](ctx, name) 68 | } 69 | 70 | // String returns the string variable from the context. 71 | func String(ctx context.Context, name string) string { 72 | return Get[string](ctx, name) 73 | } 74 | 75 | // Runes returns a variable as []rune from the context. 76 | func Runes(ctx context.Context, name string) []rune { 77 | return Get[[]rune](ctx, name) 78 | } 79 | 80 | // Bool returns the bool variable from the context. 81 | func Bool(ctx context.Context, name string) bool { 82 | return Get[bool](ctx, name) 83 | } 84 | 85 | // Byte returns the byte variable from the context. 86 | func Byte(ctx context.Context, name string) byte { 87 | if b := Get[[]byte](ctx, name); len(b) != 0 { 88 | return b[0] 89 | } 90 | return 0 91 | } 92 | 93 | // Rune returns the rune variable from the context. 94 | func Rune(ctx context.Context, name string) rune { 95 | if r := Get[[]rune](ctx, name); len(r) != 0 { 96 | return r[0] 97 | } 98 | return 0 99 | } 100 | 101 | // Int64 returns the int64 variable from the context. 102 | func Int64(ctx context.Context, name string) int64 { 103 | return Get[int64](ctx, name) 104 | } 105 | 106 | // Int32 returns the int32 variable from the context. 107 | func Int32(ctx context.Context, name string) int32 { 108 | return Get[int32](ctx, name) 109 | } 110 | 111 | // Int16 returns the int16 variable from the context. 112 | func Int16(ctx context.Context, name string) int16 { 113 | return Get[int16](ctx, name) 114 | } 115 | 116 | // Int returns the int variable from the context. 117 | func Int(ctx context.Context, name string) int { 118 | return Get[int](ctx, name) 119 | } 120 | 121 | // Uint64 returns the uint64 variable from the context. 122 | func Uint64(ctx context.Context, name string) uint64 { 123 | return Get[uint64](ctx, name) 124 | } 125 | 126 | // Uint32 returns the uint32 variable from the context. 127 | func Uint32(ctx context.Context, name string) uint32 { 128 | return Get[uint32](ctx, name) 129 | } 130 | 131 | // Uint16 returns the uint16 variable from the context. 132 | func Uint16(ctx context.Context, name string) uint16 { 133 | return Get[uint16](ctx, name) 134 | } 135 | 136 | // Uint8 returns the uint8 variable from the context. 137 | func Uint8(ctx context.Context, name string) uint8 { 138 | return Get[uint8](ctx, name) 139 | } 140 | 141 | // Uint returns the uint variable from the context. 142 | func Uint(ctx context.Context, name string) uint { 143 | return Get[uint](ctx, name) 144 | } 145 | 146 | // Float64 returns the float64 variable from the context. 147 | func Float64(ctx context.Context, name string) float64 { 148 | return Get[float64](ctx, name) 149 | } 150 | 151 | // Float32 returns the float32 variable from the context. 152 | func Float32(ctx context.Context, name string) float32 { 153 | return Get[float32](ctx, name) 154 | } 155 | 156 | // Complex128 returns the complex128 variable from the context. 157 | func Complex128(ctx context.Context, name string) complex128 { 158 | return Get[complex128](ctx, name) 159 | } 160 | 161 | // Complex64 returns the complex64 variable from the context. 162 | func Complex64(ctx context.Context, name string) complex64 { 163 | return Get[complex64](ctx, name) 164 | } 165 | 166 | // BigInt returns the [big.Int] variable from the context. 167 | func BigInt(ctx context.Context, name string) *big.Int { 168 | return Get[*big.Int](ctx, name) 169 | } 170 | 171 | // BigFloat returns the [big.Float] variable from the context. 172 | func BigFloat(ctx context.Context, name string) *big.Float { 173 | return Get[*big.Float](ctx, name) 174 | } 175 | 176 | // BigRat returns the [big.Rat] variable from the context. 177 | func BigRat(ctx context.Context, name string) *big.Rat { 178 | return Get[*big.Rat](ctx, name) 179 | } 180 | 181 | // Time returns the [ox.Time] variable from the context. 182 | func Time(ctx context.Context, name string) ox.FormattedTime { 183 | return Get[ox.FormattedTime](ctx, name) 184 | } 185 | 186 | // TimeTime returns the [time.Time] variable from the context. 187 | func TimeTime(ctx context.Context, name string) time.Time { 188 | return Get[ox.FormattedTime](ctx, name).Time() 189 | } 190 | 191 | // Duration returns the [time.Duration] variable from the context. 192 | func Duration(ctx context.Context, name string) time.Duration { 193 | return Get[time.Duration](ctx, name) 194 | } 195 | 196 | // URL returns the [url.URL] variable from the context. 197 | func URL(ctx context.Context, name string) *url.URL { 198 | return Get[*url.URL](ctx, name) 199 | } 200 | 201 | // Addr returns the [netip.Addr] variable from the context. 202 | func Addr(ctx context.Context, name string) *netip.Addr { 203 | return Get[*netip.Addr](ctx, name) 204 | } 205 | 206 | // AddrPort returns the [netip.AddrPort] variable from the context. 207 | func AddrPort(ctx context.Context, name string) *netip.AddrPort { 208 | return Get[*netip.AddrPort](ctx, name) 209 | } 210 | 211 | // CIDR returns the [netip.Prefix] variable from the context. 212 | func CIDR(ctx context.Context, name string) *netip.Prefix { 213 | return Get[*netip.Prefix](ctx, name) 214 | } 215 | 216 | // Path returns the path variable from the context. 217 | func Path(ctx context.Context, name string) string { 218 | return Get[string](ctx, name) 219 | } 220 | 221 | // Count returns the count variable from the context. 222 | func Count(ctx context.Context, name string) int { 223 | return Get[int](ctx, name) 224 | } 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xo/ox 2 | 3 | `xo/ox` is a Go and TinyGo package for command-line argument and flag parsing. 4 | 5 | [Using][] | [Example][] | [Applications][] | [About][] | [License][] 6 | 7 | [Using]: #using "Using" 8 | [Example]: #example "Example" 9 | [Applications]: #applications "Applications" 10 | [About]: #about "About" 11 | [License]: #license "License" 12 | 13 | [![Unit Tests][ox-ci-status]][ox-ci] 14 | [![Go Reference][goref-ox-status]][goref-ox] 15 | [![Discord Discussion][discord-status]][discord] 16 | 17 | [ox-ci]: https://github.com/xo/ox/actions/workflows/test.yml 18 | [ox-ci-status]: https://github.com/xo/ox/actions/workflows/test.yml/badge.svg 19 | [goref-ox]: https://pkg.go.dev/github.com/xo/ox 20 | [goref-ox-status]: https://pkg.go.dev/badge/github.com/xo/ox.svg 21 | [discord]: https://discord.gg/yJKEzc7prt "Discord Discussion" 22 | [discord-status]: https://img.shields.io/discord/829150509658013727.svg?label=Discord&logo=Discord&colorB=7289da&style=flat-square "Discord Discussion" 23 | 24 | ## Features 25 | 26 | - Long (`--arg`, `--arg val`) and Short (`-a`, `-a val`) flag parsing 27 | - POSIX-style/compatible flag parsing (`-vvv`, `-mfoo=bar` `-m foo=bar`, `--map=foo=bar`) 28 | - Flags can have optional arguments (via `NoArg`) and type specific defaults 29 | - Full command tree and sub command heirarchy 30 | - Support for `builtin` types: 31 | - `[]byte`, `string`, `[]rune`, `byte`, `rune` 32 | - `int64`, `int32`, `int16`, `int8`, `int` 33 | - `uint64`, `uint32`, `uint16`, `uint8`, `uint` 34 | - `float64`, `float32` 35 | - `complex128`, `complex64` 36 | - Support for standard library types: 37 | - `time.Time`, `time.Duration` 38 | - `*big.Int`, `*big.Float`, `*big.Rat` 39 | - `*url.URL`, `*regexp.Regexp` 40 | - `*netip.Addr`, `*netip.AddrPort`, `*netip.Prefix` 41 | - Non-standard types: 42 | - `ox.Size` - a byte size (`15 MiB`, `1 GB`, ...) 43 | - `ox.Rate` - a byte rate (`15 MiB/s`, `1 GB/h`, ...) 44 | - Support for compound types of all above (slices/maps): 45 | - `[]int`, `[][]byte`, `[]string`, `[]float64`, `[]*big.Int`, etc. 46 | - `map[string]string`, `map[int]string`, `map[float64]*url.URL`, etc. 47 | - Additional type support: 48 | - `ox.DateTimeT`, `ox.DateT`, `ox.TimeT` / `type:datetime`, `type:date`, `type:time` - standard dates and times 49 | - `ox.FormattedTime` - any `time.Time` value using any `time.Layout` format 50 | - `ox.CountT` / `type:count` - incrementing counter, such as for verbosity `-vvvv` 51 | - `ox.Base64T` - a base64 encoded string 52 | - `ox.HexT` - a hex encoded string 53 | - `ox.PathT` / `type:path` - a file system path 54 | - `ox.HookT` - argument `func` hook, for hooking flags 55 | - Optional, common types, available with optional import: 56 | - `*github.com/google/uuid.UUID` - standard UUID's 57 | - `*github.com/kenshaw/colors.Color` - named and css style colors (`white`, `black`, `#ffffff`, `RGBA(...)`, ...) 58 | - `*github.com/kenshaw/glob.Glob` - a file path globbing type 59 | - Registerable user defined types, which work with all API styles 60 | - Testable commands/sub-commands 61 | - Simple/flexible APIs for Reflection, Bind, and Context style use cases 62 | - Generics used where it makes sense 63 | - Fast 64 | - Environment, YAML, TOML, HCL config loading 65 | - Deferred default value expansion 66 | - Standard help, version and shell completion 67 | - Command, argument, and flag completion 68 | - Suggestions for command names, aliases, and suggested names 69 | - Argument validation and advanced shell completion support 70 | - TinyGo compatible 71 | 72 | ## Using 73 | 74 | Add to a Go project in the usual way: 75 | 76 | ```sh 77 | $ go get -u github.com/xo/ox@latest 78 | ``` 79 | 80 | ## Example 81 | 82 | Examples are available in [the package overview examples][pkg-overview], as 83 | well as [in the `_examples` directory](_examples). 84 | 85 | [pkg-overview]: https://pkg.go.dev/github.com/xo/ox#pkg-overview 86 | 87 | ## Applications 88 | 89 | The following applications make use of the `xo/ox` package for command-line 90 | parsing: 91 | 92 | - [`usql`][usql] - a universal command-line interface for SQL databases 93 | - [`xo`][xo] - a templated code generator for databases 94 | - [`iv`][iv] - a command-line terminal graphics image viewer 95 | - [`fv`][fv] - a command-line terminal graphics font viewer 96 | - [`wallgrab`][wallgrab] - a Apple Aerial wallpaper downloader 97 | 98 | [usql]: https://github.com/xo/usql 99 | [xo]: https://github.com/xo/xo 100 | [iv]: https://github.com/xo/iv 101 | [fv]: https://github.com/xo/fv 102 | [wallgrab]: https://github.com/kenshaw/wallgrab 103 | 104 | ## About 105 | 106 | `ox` aims to provide a robust and simple command-line package for the most 107 | common command-line use-cases in [Go][golang]. 108 | 109 | `ox` was built to streamline/simplify complexity found in the powerful (and 110 | popular!) [`cobra`][cobra]/ [`pflag`][pflag]/[`viper`][viper] combo. `ox` is 111 | written in pure Go, with no non-standard package dependencies, and provides a 112 | robust, extensible type system, as well as configuration loaders for 113 | [YAML][yaml], [TOML][toml], [HCL][hcl] that can be optionally enabled/disabled 114 | through imports. 115 | 116 | `ox` avoids "magic", and has sane, sensible defaults. No interfaces, type 117 | members or other internal logic is hidden or obscured. When using `ox`, the 118 | user can manually build commands and flags however they see fit. 119 | 120 | Wherever a non-standard package has been used, such as for the [YAML][yaml], 121 | [TOML][toml], or [HCL][hcl] loaders, or for the built-in support for 122 | [colors](color), [globs](glob), and [UUIDs](uuid), the external dependencies 123 | are optional, requiring a import of a `xo/ox` subpackage, for example: 124 | 125 | ```go 126 | import ( 127 | // base package 128 | "github.com/xo/ox" 129 | 130 | // import config loaders 131 | _ "github.com/xo/ox/hcl" 132 | _ "github.com/xo/ox/toml" 133 | _ "github.com/xo/ox/yaml" 134 | 135 | // well-known types 136 | _ "github.com/xo/ox/color" 137 | _ "github.com/xo/ox/glob" 138 | _ "github.com/xo/ox/uuid" 139 | ) 140 | ``` 141 | 142 | `ox` has been designed to use generics, and is built with Go 1.23+ applications 143 | in mind and works with [TinyGo][tinygo]. 144 | 145 | Specific design considerations of the `ox` package: 146 | 147 | - Constrained "good enough" feature set, no ambition to support every use 148 | case/scenario 149 | - No magic, sane defaults, overrideable defaults 150 | - Functional option and interface smuggling 151 | - Use generics, iterators and other go1.23+ features where prudent 152 | - Work with TinyGo out of the box 153 | - Minimal use of reflection (unless TinyGo supports it) 154 | - Case sensitive 155 | - Enable registration for config file loaders, types with minimal hassle 156 | - Man page generation 157 | - **Optional** support for common use-cases, via package dependencies 158 | 159 | Other command-line packages: 160 | 161 | - [spf13/cobra][cobra] + [spf13/viper][viper] + [spf13/pflag][pflag] 162 | - [urfave/cli][urfave] 163 | - [alecthomas/kong][kong] 164 | - [alecthomas/kingpin][kingpin] 165 | - [jessevdk/go-flags][go-flags] 166 | - [mow.cli][mowcli] 167 | - [peterbourgon/ff][pbff] 168 | 169 | [cobra]: https://github.com/spf13/cobra 170 | [go-flags]: https://github.com/jessevdk/go-flags 171 | [golang]: https://go.dev 172 | [kingpin]: https://github.com/alecthomas/kingpin 173 | [kong]: https://github.com/alecthomas/kong 174 | [mowcli]: https://github.com/jawher/mow.cli 175 | [pbff]: https://github.com/peterbourgon/ff 176 | [pflag]: https://github.com/spf13/pflag 177 | [tinygo]: https://tinygo.org 178 | [urfave]: https://github.com/urfave/cli 179 | [viper]: https://github.com/spf13/viper 180 | 181 | Articles: 182 | 183 | - [Matt Turner, Choosing a Go CLI Library][mtgocli] 184 | 185 | [mtgocli]: https://mt165.co.uk/blog/golang-cli-library/ 186 | 187 | ## License 188 | 189 | `xo/ox` is licensed under the [MIT License](LICENSE). `ox`'s completion scripts 190 | are originally cribbed from the [cobra][cobra] project, and are made available 191 | under the [Apache License](LICENSE.completions.txt). 192 | 193 | [hcl]: https://github.com/hashicorp/hcl 194 | [toml]: https://toml.io 195 | [yaml]: https://yaml.org 196 | -------------------------------------------------------------------------------- /strcase/strcase_test.go: -------------------------------------------------------------------------------- 1 | package strcase 2 | 3 | import "testing" 4 | 5 | func TestCamelToSnake(t *testing.T) { 6 | tests := []struct { 7 | s, exp string 8 | }{ 9 | {"", ""}, 10 | {"0", "0"}, 11 | {"_", "_"}, 12 | {"-X-", "-x-"}, 13 | {"-X_", "-x_"}, 14 | {"AReallyLongName", "a_really_long_name"}, 15 | {"SomethingID", "something_id"}, 16 | {"SomethingID_", "something_id_"}, 17 | {"_SomethingID_", "_something_id_"}, 18 | {"_Something-ID_", "_something-id_"}, 19 | {"_Something-IDS_", "_something-ids_"}, 20 | {"_Something-IDs_", "_something-ids_"}, 21 | {"ACL", "acl"}, 22 | {"GPU", "gpu"}, 23 | {"zGPU", "z_gpu"}, 24 | {"GPUs", "gpus"}, 25 | {"!GPU*", "!gpu*"}, 26 | {"GpuInfo", "gpu_info"}, 27 | {"GPUInfo", "gpu_info"}, 28 | {"gpUInfo", "gp_ui_nfo"}, 29 | {"gpUIDNfo", "gp_uid_nfo"}, 30 | {"gpUIDnfo", "gp_uid_nfo"}, 31 | {"HTTPWriter", "http_writer"}, 32 | {"uHTTPWriter", "u_http_writer"}, 33 | {"UHTTPWriter", "u_h_t_t_p_writer"}, 34 | {"UHTTP_Writer", "u_h_t_t_p_writer"}, 35 | {"UHTTP-Writer", "u_h_t_t_p-writer"}, 36 | {"HTTPHTTP", "http_http"}, 37 | {"uHTTPHTTP", "u_http_http"}, 38 | {"uHTTPHTTPS", "u_http_https"}, 39 | {"uHTTPHTTPS*", "u_http_https*"}, 40 | {"uHTTPSUID*", "u_https_uid*"}, 41 | {"UIDuuidUIDIDUUID", "uid_uuid_uid_id_uuid"}, 42 | {"UID-uuidUIDIDUUID", "uid-uuid_uid_id_uuid"}, 43 | {"UIDzuuidUIDIDUUID", "uid_zuuid_uid_id_uuid"}, 44 | {"UIDzUUIDUIDidUUID", "uid_z_uuid_uid_id_uuid"}, 45 | {"UIDzUUID-UIDidUUID", "uid_z_uuid-uid_id_uuid"}, 46 | {"sampleIDIDS", "sample_id_ids"}, 47 | } 48 | for _, test := range tests { 49 | t.Run(test.s, func(t *testing.T) { 50 | if v := CamelToSnake(test.s); v != test.exp { 51 | t.Errorf("%q expected %q, got: %q", test.s, test.exp, v) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func TestCamelToSnakeIdentifier(t *testing.T) { 58 | tests := []struct { 59 | s, exp string 60 | }{ 61 | {"", ""}, 62 | {"0", ""}, 63 | {"_", ""}, 64 | {"-X-", "x"}, 65 | {"-X_", "x"}, 66 | {"AReallyLongName", "a_really_long_name"}, 67 | {"SomethingID", "something_id"}, 68 | {"SomethingID_", "something_id"}, 69 | {"_SomethingID_", "something_id"}, 70 | {"_Something-ID_", "something_id"}, 71 | {"_Something-IDS_", "something_ids"}, 72 | {"_Something-IDs_", "something_ids"}, 73 | {"ACL", "acl"}, 74 | {"GPU", "gpu"}, 75 | {"zGPU", "z_gpu"}, 76 | {"!GPU*", "gpu"}, 77 | {"GpuInfo", "gpu_info"}, 78 | {"GPUInfo", "gpu_info"}, 79 | {"gpUInfo", "gp_ui_nfo"}, 80 | {"gpUIDNfo", "gp_uid_nfo"}, 81 | {"gpUIDnfo", "gp_uid_nfo"}, 82 | {"HTTPWriter", "http_writer"}, 83 | {"uHTTPWriter", "u_http_writer"}, 84 | {"UHTTPWriter", "u_h_t_t_p_writer"}, 85 | {"UHTTP_Writer", "u_h_t_t_p_writer"}, 86 | {"UHTTP-Writer", "u_h_t_t_p_writer"}, 87 | {"HTTPHTTP", "http_http"}, 88 | {"uHTTPHTTP", "u_http_http"}, 89 | {"uHTTPHTTPS", "u_http_https"}, 90 | {"uHTTPHTTPS*", "u_http_https"}, 91 | {"uHTTPSUID*", "u_https_uid"}, 92 | {"UIDuuidUIDIDUUID", "uid_uuid_uid_id_uuid"}, 93 | {"UID-uuidUIDIDUUID", "uid_uuid_uid_id_uuid"}, 94 | {"UIDzuuidUIDIDUUID", "uid_zuuid_uid_id_uuid"}, 95 | {"UIDzUUIDUIDidUUID", "uid_z_uuid_uid_id_uuid"}, 96 | {"UIDzUUID-UIDidUUID", "uid_z_uuid_uid_id_uuid"}, 97 | {"SampleIDs", "sample_ids"}, 98 | {"SampleIDS", "sample_ids"}, 99 | {"SampleIDIDs", "sample_id_ids"}, 100 | } 101 | for _, test := range tests { 102 | t.Run(test.s, func(t *testing.T) { 103 | if v := CamelToSnakeIdentifier(test.s); v != test.exp { 104 | t.Errorf("CamelToSnake(%q) expected %q, got: %q", test.s, test.exp, v) 105 | } 106 | }) 107 | } 108 | } 109 | 110 | func TestSnakeToCamel(t *testing.T) { 111 | tests := []struct { 112 | s, exp string 113 | }{ 114 | {"", ""}, 115 | {"0", "0"}, 116 | {"_", ""}, 117 | {"x_", "X"}, 118 | {"_x", "X"}, 119 | {"_x_", "X"}, 120 | {"a_really_long_name", "AReallyLongName"}, 121 | {"a_really__long_name", "AReallyLongName"}, 122 | {"something_id", "SomethingID"}, 123 | {"something_ids", "SomethingIDs"}, 124 | {"acl", "ACL"}, 125 | {"acl_", "ACL"}, 126 | {"_acl", "ACL"}, 127 | {"_acl_", "ACL"}, 128 | {"_a_c_l_", "ACL"}, 129 | {"gpu_info", "GPUInfo"}, 130 | {"gpu_______info", "GPUInfo"}, 131 | {"GPU_info", "GPUInfo"}, 132 | {"gPU_info", "GPUInfo"}, 133 | {"g_p_u_info", "GPUInfo"}, 134 | {"uuid_id_uuid", "UUIDIDUUID"}, 135 | {"sample_id_ids", "SampleIDIDs"}, 136 | } 137 | for _, test := range tests { 138 | t.Run(test.s, func(t *testing.T) { 139 | if v := SnakeToCamel(test.s); v != test.exp { 140 | t.Errorf("SnakeToCamel(%q) expected %q, got: %q", test.s, test.exp, v) 141 | } 142 | }) 143 | } 144 | } 145 | 146 | func TestSnakeToCamelIdentifier(t *testing.T) { 147 | tests := []struct { 148 | s, exp string 149 | }{ 150 | {"", ""}, 151 | {"_", ""}, 152 | {"0", ""}, 153 | {"000", ""}, 154 | {"_000", ""}, 155 | {"_000", ""}, 156 | {"000_", ""}, 157 | {"_000_", ""}, 158 | {"___0--00_", ""}, 159 | {"A0", "A0"}, 160 | {"a_0", "A0"}, 161 | {"a-0", "A0"}, 162 | {"x_", "X"}, 163 | {"_x", "X"}, 164 | {"_x_", "X"}, 165 | {"a_really_long_name", "AReallyLongName"}, 166 | {"_a_really_long_name", "AReallyLongName"}, 167 | {"a_really_long_name_", "AReallyLongName"}, 168 | {"_a_really_long_name_", "AReallyLongName"}, 169 | {"_a_really___long_name_", "AReallyLongName"}, 170 | {"something_id", "SomethingID"}, 171 | {"something-id", "SomethingID"}, 172 | {"-something-id", "SomethingID"}, 173 | {"something-id-", "SomethingID"}, 174 | {"-something-id-", "SomethingID"}, 175 | {"-something_ids-", "SomethingIDs"}, 176 | {"-something_id_s-", "SomethingIDS"}, 177 | {"g_p_u_s", "GPUS"}, 178 | {"gpus", "GPUs"}, 179 | {"acl", "ACL"}, 180 | {"acls", "ACLs"}, 181 | {"acl_", "ACL"}, 182 | {"_acl", "ACL"}, 183 | {"_acl_", "ACL"}, 184 | {"_a_c_l_", "ACL"}, 185 | {"gpu_info", "GPUInfo"}, 186 | {"g_p_u_info", "GPUInfo"}, 187 | {"uuid_id_uuid", "UUIDIDUUID"}, 188 | {"sample_id_ids", "SampleIDIDs"}, 189 | } 190 | for _, test := range tests { 191 | t.Run(test.s, func(t *testing.T) { 192 | if v := SnakeToCamelIdentifier(test.s); v != test.exp { 193 | t.Errorf("SnakeToCamelIdentifier(%q) expected %q, got: %q", test.s, test.exp, v) 194 | } 195 | }) 196 | } 197 | } 198 | 199 | func TestToSnake(t *testing.T) { 200 | tests := []struct { 201 | s, exp string 202 | }{ 203 | {"", ""}, 204 | {"0", ""}, 205 | {"_", ""}, 206 | {"x_", "x"}, 207 | {"_x", "x"}, 208 | {"_x_", "x"}, 209 | {"a_really_long_name", "a_really_long_name"}, 210 | {"a_really__long_name", "a_really_long_name"}, 211 | {"something_id", "something_id"}, 212 | {"something_ids", "something_ids"}, 213 | {"acl", "acl"}, 214 | {"acl_", "acl"}, 215 | {"_acl", "acl"}, 216 | {"_acl_", "acl"}, 217 | {"_a_c_l_", "a_c_l"}, 218 | {"gpu_info", "gpu_info"}, 219 | {"gpu_______info", "gpu_info"}, 220 | {"GPU_info", "gpu_info"}, 221 | {"gPU_info", "gpu_info"}, 222 | {"g_p_u_info", "g_p_u_info"}, 223 | {"uuid_id_uuid", "uuid_id_uuid"}, 224 | {"sample_id_ids", "sample_id_ids"}, 225 | } 226 | for _, test := range tests { 227 | t.Run(test.s, func(t *testing.T) { 228 | if v := ToSnake(test.s); v != test.exp { 229 | t.Errorf("ToSnake(%q) expected %q, got: %q", test.s, test.exp, v) 230 | } 231 | }) 232 | } 233 | } 234 | 235 | func TestToKebab(t *testing.T) { 236 | tests := []struct { 237 | s, exp string 238 | }{ 239 | {"", ""}, 240 | {"0", ""}, 241 | {"_", ""}, 242 | {"x_", "x"}, 243 | {"_x", "x"}, 244 | {"_x_", "x"}, 245 | {"a_really_long_name", "a-really-long-name"}, 246 | {"a_really__long_name", "a-really-long-name"}, 247 | {"something_id", "something-id"}, 248 | {"something_ids", "something-ids"}, 249 | {"acl", "acl"}, 250 | {"acl_", "acl"}, 251 | {"_acl", "acl"}, 252 | {"_acl_", "acl"}, 253 | {"_a_c_l_", "a-c-l"}, 254 | {"gpu_info", "gpu-info"}, 255 | {"gpu_______info", "gpu-info"}, 256 | {"GPU_info", "gpu-info"}, 257 | {"gPU_info", "gpu-info"}, 258 | {"g_p_u_info", "g-p-u-info"}, 259 | {"uuid_id_uuid", "uuid-id-uuid"}, 260 | {"sample_id_ids", "sample-id-ids"}, 261 | } 262 | for _, test := range tests { 263 | t.Run(test.s, func(t *testing.T) { 264 | if v := ToKebab(test.s); v != test.exp { 265 | t.Errorf("ToKebab(%q) expected %q, got: %q", test.s, test.exp, v) 266 | } 267 | }) 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /comp/zsh.zsh: -------------------------------------------------------------------------------- 1 | #compdef %[1]s 2 | compdef _%[1]s %[1]s 3 | 4 | # zsh completion for %[1]s 5 | 6 | __%[1]s_debug() 7 | { 8 | local file="$BASH_COMP_DEBUG_FILE" 9 | if [[ -n ${file} ]]; then 10 | echo "$*" >> "${file}" 11 | fi 12 | } 13 | 14 | _%[1]s() 15 | { 16 | local shellCompDirectiveError=1 17 | local shellCompDirectiveNoSpace=2 18 | local shellCompDirectiveNoFileComp=4 19 | local shellCompDirectiveFilterFileExt=8 20 | local shellCompDirectiveFilterDirs=16 21 | local shellCompDirectiveKeepOrder=32 22 | 23 | local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder 24 | local -a completions 25 | 26 | __%[1]s_debug "\n========= starting completion logic ==========" 27 | __%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" 28 | 29 | # The user could have moved the cursor backwards on the command-line. 30 | # We need to trigger completion from the $CURRENT location, so we need 31 | # to truncate the command-line ($words) up to the $CURRENT location. 32 | # (We cannot use $CURSOR as its value does not work when a command is an alias.) 33 | words=("${=words[1,CURRENT]}") 34 | __%[1]s_debug "Truncated words[*]: ${words[*]}," 35 | 36 | lastParam=${words[-1]} 37 | lastChar=${lastParam[-1]} 38 | __%[1]s_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" 39 | 40 | # For zsh, when completing a flag with an = (e.g., %[1]s -n=) 41 | # completions must be prefixed with the flag 42 | setopt local_options BASH_REMATCH 43 | if [[ "${lastParam}" =~ '-.*=' ]]; then 44 | # We are dealing with a flag with an = 45 | flagPrefix="-P ${BASH_REMATCH}" 46 | fi 47 | 48 | # Prepare the command to obtain completions 49 | requestComp="${words[1]} %[3]s ${words[2,-1]}" 50 | if [ "${lastChar}" = "" ]; then 51 | # If the last parameter is complete (there is a space following it) 52 | # We add an extra empty parameter so we can indicate this to the go completion code. 53 | __%[1]s_debug "Adding extra empty parameter" 54 | requestComp="${requestComp} \"\"" 55 | fi 56 | 57 | __%[1]s_debug "About to call: eval ${requestComp}" 58 | 59 | # Use eval to handle any environment variables and such 60 | out=$(eval ${requestComp} 2>/dev/null) 61 | __%[1]s_debug "completion output: ${out}" 62 | 63 | # Extract the directive integer following a : from the last line 64 | local lastLine 65 | while IFS='\n' read -r line; do 66 | lastLine=${line} 67 | done < <(printf "%%s\n" "${out[@]}") 68 | __%[1]s_debug "last line: ${lastLine}" 69 | 70 | if [ "${lastLine[1]}" = : ]; then 71 | directive=${lastLine[2,-1]} 72 | # Remove the directive including the : and the newline 73 | local suffix 74 | (( suffix=${#lastLine}+2)) 75 | out=${out[1,-$suffix]} 76 | else 77 | # There is no directive specified. Leave $out as is. 78 | __%[1]s_debug "No directive found. Setting do default" 79 | directive=0 80 | fi 81 | 82 | __%[1]s_debug "directive: ${directive}" 83 | __%[1]s_debug "completions: ${out}" 84 | __%[1]s_debug "flagPrefix: ${flagPrefix}" 85 | 86 | if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then 87 | __%[1]s_debug "Completion received error. Ignoring completions." 88 | return 89 | fi 90 | 91 | local activeHelpMarker="_activeHelp_ " 92 | local endIndex=${#activeHelpMarker} 93 | local startIndex=$((${#activeHelpMarker}+1)) 94 | local hasActiveHelp=0 95 | while IFS='\n' read -r comp; do 96 | # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) 97 | if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then 98 | __%[1]s_debug "ActiveHelp found: $comp" 99 | comp="${comp[$startIndex,-1]}" 100 | if [ -n "$comp" ]; then 101 | compadd -x "${comp}" 102 | __%[1]s_debug "ActiveHelp will need delimiter" 103 | hasActiveHelp=1 104 | fi 105 | 106 | continue 107 | fi 108 | 109 | if [ -n "$comp" ]; then 110 | # If requested, completions are returned with a description. 111 | # The description is preceded by a TAB character. 112 | # For zsh's _describe, we need to use a : instead of a TAB. 113 | # We first need to escape any : as part of the completion itself. 114 | comp=${comp//:/\\:} 115 | 116 | local tab="$(printf '\t')" 117 | comp=${comp//$tab/:} 118 | 119 | __%[1]s_debug "Adding completion: ${comp}" 120 | completions+=${comp} 121 | lastComp=$comp 122 | fi 123 | done < <(printf "%%s\n" "${out[@]}") 124 | 125 | # Add a delimiter after the activeHelp statements, but only if: 126 | # - there are completions following the activeHelp statements, or 127 | # - file completion will be performed (so there will be choices after the activeHelp) 128 | if [ $hasActiveHelp -eq 1 ]; then 129 | if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then 130 | __%[1]s_debug "Adding activeHelp delimiter" 131 | compadd -x "--" 132 | hasActiveHelp=0 133 | fi 134 | fi 135 | 136 | if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then 137 | __%[1]s_debug "Activating nospace." 138 | noSpace="-S ''" 139 | fi 140 | 141 | if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then 142 | __%[1]s_debug "Activating keep order." 143 | keepOrder="-V" 144 | fi 145 | 146 | if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then 147 | # File extension filtering 148 | local filteringCmd 149 | filteringCmd='_files' 150 | for filter in ${completions[@]}; do 151 | if [ ${filter[1]} != '*' ]; then 152 | # zsh requires a glob pattern to do file filtering 153 | filter="\*.$filter" 154 | fi 155 | filteringCmd+=" -g $filter" 156 | done 157 | filteringCmd+=" ${flagPrefix}" 158 | 159 | __%[1]s_debug "File filtering command: $filteringCmd" 160 | _arguments '*:filename:'"$filteringCmd" 161 | elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then 162 | # File completion for directories only 163 | local subdir 164 | subdir="${completions[1]}" 165 | if [ -n "$subdir" ]; then 166 | __%[1]s_debug "Listing directories in $subdir" 167 | pushd "${subdir}" >/dev/null 2>&1 168 | else 169 | __%[1]s_debug "Listing directories in ." 170 | fi 171 | 172 | local result 173 | _arguments '*:dirname:_files -/'" ${flagPrefix}" 174 | result=$? 175 | if [ -n "$subdir" ]; then 176 | popd >/dev/null 2>&1 177 | fi 178 | return $result 179 | else 180 | __%[1]s_debug "Calling _describe" 181 | if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then 182 | __%[1]s_debug "_describe found some completions" 183 | 184 | # Return the success of having called _describe 185 | return 0 186 | else 187 | __%[1]s_debug "_describe did not find completions." 188 | __%[1]s_debug "Checking if we should do file completion." 189 | if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then 190 | __%[1]s_debug "deactivating file completion" 191 | 192 | # We must return an error code here to let zsh know that there were no 193 | # completions found by _describe; this is what will trigger other 194 | # matching algorithms to attempt to find completions. 195 | # For example zsh can match letters in the middle of words. 196 | return 1 197 | else 198 | # Perform file completion 199 | __%[1]s_debug "Activating file completion" 200 | 201 | # We must return the result of this command, so it must be the 202 | # last command, or else we must store its result to return it. 203 | _arguments '*:filename:_files'" ${flagPrefix}" 204 | fi 205 | fi 206 | fi 207 | } 208 | 209 | # don't run the completion function when being source-ed or eval-ed 210 | if [ "$funcstack[1]" = "_%[1]s" ]; then 211 | _%[1]s 212 | fi 213 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestParse(t *testing.T) { 13 | for i, test := range parseTests() { 14 | t.Run(strconv.Itoa(i), func(t *testing.T) { 15 | t.Logf("invoked: %v", test.v) 16 | c := &Context{ 17 | Root: testCommand(t), 18 | Stdout: new(bytes.Buffer), 19 | Continue: func(*Command, error) bool { 20 | return false 21 | }, 22 | Args: test.v[1:], 23 | Vars: make(Vars), 24 | } 25 | t.Logf("cmd: %s", c.Root.Name) 26 | t.Logf("args: %q", c.Args) 27 | switch err := c.Parse(); { 28 | case err != nil: 29 | fmt.Fprintln(c.Stdout, "error:", err) 30 | } 31 | if c.Exec != nil { 32 | t.Logf("exec: %s", c.Exec.Name) 33 | } 34 | t.Logf("vars: %s", c.Vars) 35 | if c.Exec != nil { 36 | ctx := WithContext(context.Background(), c) 37 | if err := c.Exec.Exec(ctx, c.Args); err != nil { 38 | t.Fatalf("expected no error, got: %v", err) 39 | } 40 | } 41 | s := strings.TrimSpace(c.Stdout.(*bytes.Buffer).String()) 42 | if exp := strings.Join(test.exp, "\n"); s != exp { 43 | t.Errorf("\nexpected:\n%s\ngot:\n%s", exp, s) 44 | } 45 | }) 46 | } 47 | } 48 | 49 | type parseTest struct { 50 | v []string 51 | exp []string 52 | } 53 | 54 | func parseTests() []parseTest { 55 | return []parseTest{ 56 | { 57 | ss(``), 58 | []string{ 59 | `exec: cmd`, 60 | `name: cmd`, 61 | `path: []`, 62 | `args: []`, 63 | `vars: [int:15]`, 64 | }, 65 | }, 66 | { 67 | ss(`a//`), 68 | []string{ 69 | `exec: cmd`, 70 | `name: cmd`, 71 | `path: []`, 72 | `args: []`, 73 | `vars: [int:15]`, 74 | }, 75 | }, 76 | { 77 | ss(`a//--foo=a//one////--foo//b//two//--foo//c//three//four//blah//yay`), 78 | []string{ 79 | `exec: one`, 80 | `name: one`, 81 | `path: [one]`, 82 | `args: [ two three four blah yay]`, 83 | `vars: [foo:[a b c] int:15]`, 84 | }, 85 | }, 86 | { 87 | ss(`a//--foo=a,b//--foo//c,d//-f=e,f`), 88 | []string{ 89 | `exec: cmd`, 90 | `name: cmd`, 91 | `path: []`, 92 | `args: []`, 93 | `vars: [foo:[a b c d e f] int:15]`, 94 | }, 95 | }, 96 | { 97 | ss(`a//-m=A=100//-m//FOO=200//one//two`), 98 | []string{ 99 | `exec: two`, 100 | `name: two`, 101 | `path: [one two]`, 102 | `args: []`, 103 | `vars: [int:15 map:[A:100 FOO:200]]`, 104 | }, 105 | }, 106 | { 107 | ss(`a//-b//one//-b=false//two//-b=t//three//-b=1`), 108 | []string{ 109 | `exec: three`, 110 | `name: three`, 111 | `path: [one two three]`, 112 | `args: []`, 113 | `vars: [bvar:true int:15]`, 114 | }, 115 | }, 116 | { 117 | ss(`a//--inc//one//--inc//--inc//two//--inc//three`), 118 | []string{ 119 | `exec: three`, 120 | `name: three`, 121 | `path: [one two three]`, 122 | `args: []`, 123 | `vars: [inc:4 int:15]`, 124 | }, 125 | }, 126 | { 127 | ss(`a//-fa=b//-iiib//one//two//-bbb//three`), 128 | []string{ 129 | `exec: three`, 130 | `name: three`, 131 | `path: [one two three]`, 132 | `args: []`, 133 | `vars: [bvar:true foo:[a=b] inc:3 int:15]`, 134 | }, 135 | }, 136 | { 137 | ss(`a//one//two//four//-b`), 138 | []string{ 139 | `exec: two`, 140 | `name: two`, 141 | `path: [one two]`, 142 | `args: [four]`, 143 | `vars: [bvar:true int:15]`, 144 | }, 145 | }, 146 | { 147 | ss(`a//one//four//-iii//--inc//fun`), 148 | []string{ 149 | `exec: four`, 150 | `name: four`, 151 | `path: [one four]`, 152 | `args: [fun]`, 153 | `vars: [inc:4 int:15]`, 154 | }, 155 | }, 156 | { 157 | ss(`a//five//-u//file:a//-ufile:b//-c=1.2.3.4/24//--cidr//2.4.6.8/0//foo//bar`), 158 | []string{ 159 | `exec: five`, 160 | `name: five`, 161 | `path: [five]`, 162 | `args: [foo bar]`, 163 | `vars: [cidr:[1.2.3.4/24 2.4.6.8/0] int:15 url:[file:a file:b] val:125]`, 164 | }, 165 | }, 166 | { 167 | ss(`a//five//--time//A=07:15:13//a//-d2001-12-25//-d=2002-01-15//--time=B=12:15:32//b`), 168 | []string{ 169 | `exec: five`, 170 | `name: five`, 171 | `path: [five]`, 172 | `args: [a b]`, 173 | `vars: [date:[2001-12-25 2002-01-15] int:15 time:[A:07:15:13 B:12:15:32] val:125]`, 174 | }, 175 | }, 176 | { 177 | ss(`a//--//five//--a=b`), 178 | []string{ 179 | `exec: cmd`, 180 | `name: cmd`, 181 | `path: []`, 182 | `args: [five --a=b]`, 183 | `vars: [int:15]`, 184 | }, 185 | }, 186 | { 187 | ss(`a//five//-T//255=07:15:32//-T//128=12:15:20//-C=16.1=A//-iiiiC25=J//-C//16.100=C//-C//17.0=1//-C17.0000=//-C//17=//--//--//-a//-b=c`), 188 | []string{ 189 | `exec: five`, 190 | `name: five`, 191 | `path: [five]`, 192 | `args: [-- -a -b=c]`, 193 | `vars: [countmap:[16.1:2 17:3 25:1] inc:4 int:15 timemap:[128:12:15:20 255:07:15:32] val:125]`, 194 | }, 195 | }, 196 | { 197 | ss(`a//six//--map//a=b,c=d//--slice//e,f`), 198 | []string{ 199 | `exec: six`, 200 | `name: six`, 201 | `path: [six]`, 202 | `args: []`, 203 | `vars: [int:15 map:[a:b c:d] slice:[e f]]`, 204 | }, 205 | }, 206 | { 207 | ss(`a//six//-s//15mib//-r//186mb/s//-d//15us`), 208 | []string{ 209 | `exec: six`, 210 | `name: six`, 211 | `path: [six]`, 212 | `args: []`, 213 | "vars: [duration:15µs int:15 rate:177.38 MiB/s size:15 MiB]", 214 | }, 215 | }, 216 | // parse errors 217 | { 218 | ss(`a//-n//16`), 219 | []string{ 220 | `error: -n: invalid value: 16: allowed values: 1, 5, 10, 15`, 221 | }, 222 | }, 223 | { 224 | ss(`a//--int=16`), 225 | []string{ 226 | `error: --int: invalid value: 16: allowed values: 1, 5, 10, 15`, 227 | }, 228 | }, 229 | { 230 | ss(`a//-b//one//-b=fal//two//-b=t//three//-b=1`), 231 | []string{ 232 | `error: -b: invalid value: strconv.ParseBool: parsing "fal": invalid syntax`, 233 | }, 234 | }, 235 | { 236 | ss(`a//six//--map//a=b,c=d//--slice//e,g`), 237 | []string{ 238 | `error: --slice: set 2: invalid value: "g": allowed values: "foo", "bar", "e", "f"`, 239 | }, 240 | }, 241 | } 242 | } 243 | 244 | func ss(s string) []string { 245 | return strings.Split(s, "//") 246 | } 247 | 248 | func testCommand(t *testing.T) *Command { 249 | t.Helper() 250 | cmd, err := NewCommand( 251 | Exec(testDump(t, "cmd")), 252 | Usage("cmd", ""), 253 | Flags(). 254 | String("avar", "", Short("a")). 255 | Bool("bvar", "", Short("b")). 256 | Count("inc", "", Short("i")). 257 | Map("map", "", RunesT, Short("m")). 258 | Slice("foo", "", Short("f")). 259 | Int("int", "", Short("n"), Default(float64(15.0)), Valid(1, 5, 10, 15)), 260 | Sub( 261 | Exec(testDump(t, "one")), 262 | Usage("one", ""), 263 | Sub( 264 | Exec(testDump(t, "two")), 265 | Usage("two", ""), 266 | Sub( 267 | Exec(testDump(t, "three")), 268 | Usage("three", ""), 269 | ), 270 | ), 271 | Sub( 272 | Exec(testDump(t, "four")), 273 | Usage("four", ""), 274 | ), 275 | Suggested("remove"), 276 | ), 277 | Sub( 278 | Exec(testDump(t, "five")), 279 | Usage("five", ""), 280 | Flags(). 281 | String("val", "", Short("l"), Default(125)). 282 | Slice("cidr", "", Short("c"), CIDRT). 283 | Slice("url", "", Short("u"), URLT). 284 | Slice("date", "", Short("d"), DateT). 285 | Map("time", "", Short("t"), TimeT). 286 | Map("timemap", "", Short("T"), MapKey(Uint64T), TimeT). 287 | Map("countmap", "", Short("C"), MapKey(Float64T), CountT), 288 | ), 289 | Sub( 290 | Exec(testDump(t, "six")), 291 | Usage("six", ""), 292 | Flags(). 293 | Duration("duration", "", Short("d")). 294 | Size("size", "", Short("s")). 295 | Rate("rate", "", Short("r")). 296 | Slice("slice", "", Short("S"), Valid("foo", "bar", "e", "f")). 297 | Array("array", "", Short("a")). 298 | Map("map", "", Short("m")), 299 | ), 300 | ) 301 | if err != nil { 302 | t.Fatalf("expected no error, got: %v", err) 303 | } 304 | return cmd 305 | } 306 | 307 | func testDump(t *testing.T, name string) func(context.Context, []string) { 308 | t.Helper() 309 | return func(ctx context.Context, args []string) { 310 | c, ok := Ctx(ctx) 311 | if !ok { 312 | t.Fatalf("expected non-nil context") 313 | } 314 | _, _ = fmt.Fprintln(c.Stdout, "exec:", name) 315 | _, _ = fmt.Fprintln(c.Stdout, "name:", c.Exec.Name) 316 | _, _ = fmt.Fprintln(c.Stdout, "path:", c.Exec.Path()) 317 | _, _ = fmt.Fprintln(c.Stdout, "args:", args) 318 | _, _ = fmt.Fprintln(c.Stdout, "vars:", c.Vars) 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /size.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // ParseSize parses a byte size string. 13 | func ParseSize(s string) (Size, error) { 14 | m := sizeRE.FindStringSubmatch(s) 15 | if m == nil { 16 | return 0, fmt.Errorf("%w %q", ErrInvalidSize, s) 17 | } 18 | f, err := strconv.ParseFloat(m[2], 64) 19 | switch { 20 | case err != nil: 21 | return 0, fmt.Errorf("%w: %w", ErrInvalidSize, err) 22 | case m[1] == "-": 23 | f = -f 24 | } 25 | sz, err := parseSize(m[3]) 26 | if err != nil { 27 | return 0, fmt.Errorf("%w: %w", ErrInvalidSize, err) 28 | } 29 | return Size(f * float64(sz)), nil 30 | } 31 | 32 | // ParseRate parses a byte rate string. 33 | func ParseRate(s string) (Rate, error) { 34 | unit := time.Second 35 | if i := strings.LastIndexByte(s, '/'); i != -1 { 36 | switch strings.ToLower(s[i+1:]) { 37 | case "ns": 38 | unit = time.Nanosecond 39 | case "us", "µs", "μs": // U+00B5 = micro symbol, U+03BC = Greek letter mu 40 | unit = time.Microsecond 41 | case "ms": 42 | unit = time.Millisecond 43 | case "s": 44 | unit = time.Second 45 | case "m": 46 | unit = time.Minute 47 | case "h": 48 | unit = time.Hour 49 | default: 50 | return Rate{}, fmt.Errorf("%w %q", ErrInvalidRate, s) 51 | } 52 | s = s[:i] 53 | } 54 | sz, err := ParseSize(s) 55 | if err != nil { 56 | return Rate{}, err 57 | } 58 | return Rate{sz, unit}, nil 59 | } 60 | 61 | // AppendSize appends the formatted size to b. A precision below -1 will format 62 | // the value to the fixed precision then trims any trailing '.' / '0'. Formats 63 | // the value based on the verb (see below). When space is true, a space will be 64 | // appended between the formatted size and the suffix. 65 | // 66 | // Supported verbs: 67 | // 68 | // d/D - size in bytes (ex: 12345678) 69 | // f/F - size in best fitting *B/*iB (ex: 999 B, 1.1 KiB) and always with a space 70 | // z/Z - size in best fitting *B/*iB 71 | // k/K - size in KB/KiB (ex: 0.9 kB, 2.3 KiB) 72 | // m/M - size in MB/MiB (ex: 1.2345 MB) 73 | // g/G - size in GB/GiB (ex: 1 GiB) 74 | // t/T - size in TB/TiB (ex: 4.5 TiB) 75 | // b/P - size in PB/PiB (ex: 4.5 PiB) -- must use b, as p is reserved for pointers 76 | // s/S - same as f/F 77 | // v/V - same as f/F 78 | func AppendSize(b []byte, size int64, verb rune, prec int, space bool) []byte { 79 | neg, sz, unit := "", float64(size), "B" 80 | switch verb { 81 | case 'd', 'D': 82 | return strconv.AppendInt(b, size, 10) 83 | case 'k': 84 | sz, unit = sz/KiB, "KiB" 85 | case 'K': 86 | sz, unit = sz/KB, "kB" 87 | case 'm': 88 | sz, unit = sz/MiB, "MiB" 89 | case 'M': 90 | sz, unit = sz/MB, "MB" 91 | case 'g': 92 | sz, unit = sz/GiB, "GiB" 93 | case 'G': 94 | sz, unit = sz/GB, "GB" 95 | case 't': 96 | sz, unit = sz/TiB, "TiB" 97 | case 'T': 98 | sz, unit = sz/TB, "TB" 99 | case 'b': 100 | sz, unit = sz/PiB, "PiB" 101 | case 'P': 102 | sz, unit = sz/PB, "PB" 103 | case 'z': 104 | neg, sz, unit = bestSize(size, true) 105 | case 'Z': 106 | neg, sz, unit = bestSize(size, false) 107 | case 'f', 's', 'v': 108 | neg, sz, unit = bestSize(size, true) 109 | prec, space = DefaultSizePrec, true 110 | case 'F', 'S', 'V': 111 | neg, sz, unit = bestSize(size, false) 112 | prec, space = DefaultSizePrec, true 113 | default: 114 | return append(b, "%"+string(verb)+"(error=unknown size verb)"...) 115 | } 116 | aprec := prec 117 | if aprec < -1 { 118 | aprec = -aprec 119 | } 120 | b = append(b, neg...) 121 | b = strconv.AppendFloat(b, sz, 'f', aprec, 64) 122 | // trim right {.,0} 123 | if prec < -1 { 124 | b = bytes.TrimRightFunc(b, func(r rune) bool { 125 | return r == '0' 126 | }) 127 | b = bytes.TrimRightFunc(b, func(r rune) bool { 128 | return r == '.' 129 | }) 130 | } 131 | if space { 132 | b = append(b, ' ') 133 | } 134 | return append(b, unit...) 135 | } 136 | 137 | // FormatSize formats a byte size. 138 | func FormatSize(size int64, verb rune, prec int, space bool) string { 139 | return string(AppendSize(make([]byte, 0, 28), size, verb, prec, space)) 140 | } 141 | 142 | // AppendRate appends the formatted rate to b. 143 | func AppendRate(b []byte, rate Rate, verb rune, prec int, space bool) []byte { 144 | return append(append(AppendSize(b, int64(rate.Size), verb, prec, space), '/'), unitString(rate.Unit)...) 145 | } 146 | 147 | // FormatRate formats a byte rate. 148 | func FormatRate(rate Rate, verb rune, prec int, space bool) string { 149 | return string(AppendRate(make([]byte, 0, 31), rate, verb, prec, space)) 150 | } 151 | 152 | // Size is a byte size. 153 | type Size int64 154 | 155 | // NewSize creates a byte size. 156 | func NewSize[T ~int64 | ~int32 | ~int16 | ~int8 | ~int | ~uint64 | ~uint32 | ~uint16 | ~uint8 | ~uint](size T) Size { 157 | return Size(size) 158 | } 159 | 160 | // Format satisfies the [fmt.Formatter] interface. See [AppendSize] for 161 | // recognized verbs. 162 | func (size Size) Format(f fmt.State, verb rune) { 163 | prec := DefaultSizePrec 164 | if p, ok := f.Precision(); ok { 165 | prec = p 166 | } 167 | _, _ = f.Write(AppendSize(make([]byte, 0, 28), int64(size), verb, prec, f.Flag(' '))) 168 | } 169 | 170 | // MarshalText satisfies the [BinaryMarshalUnmarshaler] interface. 171 | func (size Size) MarshalText() ([]byte, error) { 172 | return AppendSize(make([]byte, 0, 28), int64(size), 'z', -2, true), nil 173 | } 174 | 175 | // UnmarshalText satisfies the [BinaryMarshalUnmarshaler] interface. 176 | func (size *Size) UnmarshalText(b []byte) error { 177 | i, err := ParseSize(string(b)) 178 | if err != nil { 179 | return err 180 | } 181 | *size = Size(i) 182 | return nil 183 | } 184 | 185 | // Rate is a byte rate. 186 | type Rate struct { 187 | Size Size 188 | Unit time.Duration 189 | } 190 | 191 | // NewRate creates a byte rate. 192 | func NewRate[T ~int64 | ~int32 | ~int16 | ~int8 | ~int | ~uint64 | ~uint32 | ~uint16 | ~uint8 | ~uint](size T, unit time.Duration) Rate { 193 | return Rate{ 194 | Size: NewSize(size), 195 | Unit: unit, 196 | } 197 | } 198 | 199 | // Format satisfies the [fmt.Formatter] interface. See [AppendRate] for 200 | // recognized verbs. 201 | func (rate Rate) Format(f fmt.State, verb rune) { 202 | prec := DefaultRatePrec 203 | if p, ok := f.Precision(); ok { 204 | prec = p 205 | } 206 | _, _ = f.Write(AppendRate(make([]byte, 0, 31), rate, verb, prec, f.Flag(' '))) 207 | } 208 | 209 | // Int64 returns the bytes as an int64. 210 | func (rate Rate) Int64() int64 { 211 | return int64(rate.Size) 212 | } 213 | 214 | // UnmarshalText satisfies the [BinaryMarshalUnmarshaler] interface. 215 | func (rate *Rate) UnmarshalText(b []byte) error { 216 | var err error 217 | *rate, err = ParseRate(string(b)) 218 | return err 219 | } 220 | 221 | // MarshalText satisfies the [BinaryMarshalUnmarshaler] interface. 222 | func (rate *Rate) MarshalText() ([]byte, error) { 223 | return AppendRate(make([]byte, 0, 31), *rate, 'z', -2, true), nil 224 | } 225 | 226 | // Byte sizes. 227 | const ( 228 | B = 1 229 | KB = 1_000 230 | MB = 1_000_000 231 | GB = 1_000_000_000 232 | TB = 1_000_000_000_000 233 | PB = 1_000_000_000_000_000 234 | EB = 1_000_000_000_000_000_000 235 | KiB = 1_024 236 | MiB = 1_048_576 237 | GiB = 1_073_741_824 238 | TiB = 1_099_511_627_776 239 | PiB = 1_125_899_906_842_624 240 | EiB = 1_152_921_504_606_846_976 241 | ) 242 | 243 | // bestSize returns the best size. 244 | func bestSize(size int64, iec bool) (string, float64, string) { 245 | n, units, suffix := int64(KB), "kMGTPE", "B" 246 | if iec { 247 | n, units, suffix = KiB, "KMGTPE", "iB" 248 | } 249 | var neg string 250 | if size < 0 { 251 | neg, size = "-", -size 252 | } 253 | if size < n { 254 | return neg, float64(size), "B" 255 | } 256 | e, d := 0, n 257 | for i := size / n; n <= i; i /= n { 258 | d *= n 259 | e++ 260 | } 261 | return neg, float64(size) / float64(d), string(units[e]) + suffix 262 | } 263 | 264 | // parseSize returns the byte size of s. 265 | func parseSize(s string) (int64, error) { 266 | switch strings.ToLower(s) { 267 | case "", "b": 268 | return B, nil 269 | case "kb": 270 | return KB, nil 271 | case "mb": 272 | return MB, nil 273 | case "gb": 274 | return GB, nil 275 | case "tb": 276 | return TB, nil 277 | case "pb": 278 | return PB, nil 279 | case "eb": 280 | return EB, nil 281 | case "kib": 282 | return KiB, nil 283 | case "mib": 284 | return MiB, nil 285 | case "gib": 286 | return GiB, nil 287 | case "tib": 288 | return TiB, nil 289 | case "pib": 290 | return PiB, nil 291 | case "eib": 292 | return EiB, nil 293 | } 294 | return 0, fmt.Errorf("%w %q", ErrUnknownSize, s) 295 | } 296 | 297 | // unitString returns the string for a time unit (duration). 298 | func unitString(unit time.Duration) string { 299 | switch { 300 | case unit == 0, unit > time.Hour: 301 | return unitString(DefaultRateUnit) 302 | case unit > time.Minute: 303 | return "h" 304 | case unit > time.Second: 305 | return "m" 306 | case unit > time.Millisecond: 307 | return "s" 308 | case unit > time.Microsecond: 309 | return "ms" 310 | case unit > time.Nanosecond: 311 | return "µs" 312 | } 313 | return "ns" 314 | } 315 | 316 | // sizeRE matches sizes. 317 | var sizeRE = regexp.MustCompile(`(?i)^([-+])?([0-9]+(?:\.[0-9]*)?)(?: ?([a-z]+))?$`) 318 | -------------------------------------------------------------------------------- /comp/fish.fish: -------------------------------------------------------------------------------- 1 | # fish completion for %[1]s 2 | 3 | function __%[1]s_debug 4 | set -l file "$BASH_COMP_DEBUG_FILE" 5 | if test -n "$file" 6 | echo "$argv" >> $file 7 | end 8 | end 9 | 10 | function __%[1]s_perform_completion 11 | __%[1]s_debug "Starting __%[1]s_perform_completion" 12 | 13 | # Extract all args except the last one 14 | set -l args (commandline -opc) 15 | # Extract the last arg and escape it in case it is a space 16 | set -l lastArg (string escape -- (commandline -ct)) 17 | 18 | __%[1]s_debug "args: $args" 19 | __%[1]s_debug "last arg: $lastArg" 20 | 21 | # Disable ActiveHelp which is not supported for fish shell 22 | set -l requestComp "%[4]s=0 $args[1] %[3]s $args[2..-1] $lastArg" 23 | 24 | __%[1]s_debug "Calling $requestComp" 25 | set -l results (eval $requestComp 2> /dev/null) 26 | 27 | # Some programs may output extra empty lines after the directive. 28 | # Let's ignore them or else it will break completion. 29 | # Ref: https://github.com/spf13/cobra/issues/1279 30 | for line in $results[-1..1] 31 | if test (string trim -- $line) = "" 32 | # Found an empty line, remove it 33 | set results $results[1..-2] 34 | else 35 | # Found non-empty line, we have our proper output 36 | break 37 | end 38 | end 39 | 40 | set -l comps $results[1..-2] 41 | set -l directiveLine $results[-1] 42 | 43 | # For Fish, when completing a flag with an = (e.g., -n=) 44 | # completions must be prefixed with the flag 45 | set -l flagPrefix (string match -r -- '-.*=' "$lastArg") 46 | 47 | __%[1]s_debug "Comps: $comps" 48 | __%[1]s_debug "DirectiveLine: $directiveLine" 49 | __%[1]s_debug "flagPrefix: $flagPrefix" 50 | 51 | for comp in $comps 52 | printf "%%s%%s\n" "$flagPrefix" "$comp" 53 | end 54 | 55 | printf "%%s\n" "$directiveLine" 56 | end 57 | 58 | # this function limits calls to __%[1]s_perform_completion, by caching the result behind $__%[1]s_perform_completion_once_result 59 | function __%[1]s_perform_completion_once 60 | __%[1]s_debug "Starting __%[1]s_perform_completion_once" 61 | 62 | if test -n "$__%[1]s_perform_completion_once_result" 63 | __%[1]s_debug "Seems like a valid result already exists, skipping __%[1]s_perform_completion" 64 | return 0 65 | end 66 | 67 | set --global __%[1]s_perform_completion_once_result (__%[1]s_perform_completion) 68 | if test -z "$__%[1]s_perform_completion_once_result" 69 | __%[1]s_debug "No completions, probably due to a failure" 70 | return 1 71 | end 72 | 73 | __%[1]s_debug "Performed completions and set __%[1]s_perform_completion_once_result" 74 | return 0 75 | end 76 | 77 | # this function is used to clear the $__%[1]s_perform_completion_once_result variable after completions are run 78 | function __%[1]s_clear_perform_completion_once_result 79 | __%[1]s_debug "" 80 | __%[1]s_debug "========= clearing previously set __%[1]s_perform_completion_once_result variable ==========" 81 | set --erase __%[1]s_perform_completion_once_result 82 | __%[1]s_debug "Successfully erased the variable __%[1]s_perform_completion_once_result" 83 | end 84 | 85 | function __%[1]s_requires_order_preservation 86 | __%[1]s_debug "" 87 | __%[1]s_debug "========= checking if order preservation is required ==========" 88 | 89 | __%[1]s_perform_completion_once 90 | if test -z "$__%[1]s_perform_completion_once_result" 91 | __%[1]s_debug "Error determining if order preservation is required" 92 | return 1 93 | end 94 | 95 | set -l directive (string sub --start 2 $__%[1]s_perform_completion_once_result[-1]) 96 | __%[1]s_debug "Directive is: $directive" 97 | 98 | set -l shellCompDirectiveKeepOrder 32 99 | set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) %% 2) 100 | __%[1]s_debug "Keeporder is: $keeporder" 101 | 102 | if test $keeporder -ne 0 103 | __%[1]s_debug "This does require order preservation" 104 | return 0 105 | end 106 | 107 | __%[1]s_debug "This doesn't require order preservation" 108 | return 1 109 | end 110 | 111 | 112 | # This function does two things: 113 | # - Obtain the completions and store them in the global __%[1]s_comp_results 114 | # - Return false if file completion should be performed 115 | function __%[1]s_prepare_completions 116 | __%[1]s_debug "" 117 | __%[1]s_debug "========= starting completion logic ==========" 118 | 119 | # Start fresh 120 | set --erase __%[1]s_comp_results 121 | 122 | __%[1]s_perform_completion_once 123 | __%[1]s_debug "Completion results: $__%[1]s_perform_completion_once_result" 124 | 125 | if test -z "$__%[1]s_perform_completion_once_result" 126 | __%[1]s_debug "No completion, probably due to a failure" 127 | # Might as well do file completion, in case it helps 128 | return 1 129 | end 130 | 131 | set -l directive (string sub --start 2 $__%[1]s_perform_completion_once_result[-1]) 132 | set --global __%[1]s_comp_results $__%[1]s_perform_completion_once_result[1..-2] 133 | 134 | __%[1]s_debug "Completions are: $__%[1]s_comp_results" 135 | __%[1]s_debug "Directive is: $directive" 136 | 137 | set -l shellCompDirectiveError 1 138 | set -l shellCompDirectiveNoSpace 2 139 | set -l shellCompDirectiveNoFileComp 4 140 | set -l shellCompDirectiveFilterFileExt 8 141 | set -l shellCompDirectiveFilterDirs 16 142 | 143 | if test -z "$directive" 144 | set directive 0 145 | end 146 | 147 | set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) %% 2) 148 | if test $compErr -eq 1 149 | __%[1]s_debug "Received error directive: aborting." 150 | # Might as well do file completion, in case it helps 151 | return 1 152 | end 153 | 154 | set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) %% 2) 155 | set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2) 156 | if test $filefilter -eq 1; or test $dirfilter -eq 1 157 | __%[1]s_debug "File extension filtering or directory filtering not supported" 158 | # Do full file completion instead 159 | return 1 160 | end 161 | 162 | set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) %% 2) 163 | set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2) 164 | 165 | __%[1]s_debug "nospace: $nospace, nofiles: $nofiles" 166 | 167 | # If we want to prevent a space, or if file completion is NOT disabled, 168 | # we need to count the number of valid completions. 169 | # To do so, we will filter on prefix as the completions we have received 170 | # may not already be filtered so as to allow fish to match on different 171 | # criteria than the prefix. 172 | if test $nospace -ne 0; or test $nofiles -eq 0 173 | set -l prefix (commandline -t | string escape --style=regex) 174 | __%[1]s_debug "prefix: $prefix" 175 | 176 | set -l completions (string match -r -- "^.*" $__%[1]s_comp_results) 177 | #set -l completions (string match -r -- "^$prefix.*" $__%[1]s_comp_results) 178 | set --global __%[1]s_comp_results $completions 179 | __%[1]s_debug "Filtered completions are: $__%[1]s_comp_results" 180 | 181 | # Important not to quote the variable for count to work 182 | set -l numComps (count $__%[1]s_comp_results) 183 | __%[1]s_debug "numComps: $numComps" 184 | 185 | if test $numComps -eq 1; and test $nospace -ne 0 186 | # We must first split on \t to get rid of the descriptions to be 187 | # able to check what the actual completion will be. 188 | # We don't need descriptions anyway since there is only a single 189 | # real completion which the shell will expand immediately. 190 | set -l split (string split --max 1 \t $__%[1]s_comp_results[1]) 191 | 192 | # Fish won't add a space if the completion ends with any 193 | # of the following characters: @=/:., 194 | set -l lastChar (string sub -s -1 -- $split) 195 | if not string match -r -q "[@=/:.,]" -- "$lastChar" 196 | # In other cases, to support the "nospace" directive we trick the shell 197 | # by outputting an extra, longer completion. 198 | __%[1]s_debug "Adding second completion to perform nospace directive" 199 | set --global __%[1]s_comp_results $split[1] $split[1]. 200 | __%[1]s_debug "Completions are now: $__%[1]s_comp_results" 201 | end 202 | end 203 | 204 | if test $numComps -eq 0; and test $nofiles -eq 0 205 | # To be consistent with bash and zsh, we only trigger file 206 | # completion when there are no other completions 207 | __%[1]s_debug "Requesting file completion" 208 | return 1 209 | end 210 | end 211 | 212 | return 0 213 | end 214 | 215 | # Since Fish completions are only loaded once the user triggers them, we trigger them ourselves 216 | # so we can properly delete any completions provided by another script. 217 | # Only do this if the program can be found, or else fish may print some errors; besides, 218 | # the existing completions will only be loaded if the program can be found. 219 | if type -q "%[2]s" 220 | # The space after the program name is essential to trigger completion for the program 221 | # and not completion of the program name itself. 222 | # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. 223 | complete --do-complete "%[2]s " > /dev/null 2>&1 224 | end 225 | 226 | # Remove any pre-existing completions for the program since we will be handling all of them. 227 | complete -c %[2]s -e 228 | 229 | # this will get called after the two calls below and clear the $__%[1]s_perform_completion_once_result global 230 | complete -c %[2]s -n '__%[1]s_clear_perform_completion_once_result' 231 | # The call to __%[1]s_prepare_completions will setup __%[1]s_comp_results 232 | # which provides the program's completion choices. 233 | # If this doesn't require order preservation, we don't use the -k flag 234 | complete -c %[2]s -n 'not __%[1]s_requires_order_preservation && __%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' 235 | # otherwise we use the -k flag 236 | complete -k -c %[2]s -n '__%[1]s_requires_order_preservation && __%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' 237 | -------------------------------------------------------------------------------- /LICENSE.completions.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | -------------------------------------------------------------------------------- /comp/powershell.ps1: -------------------------------------------------------------------------------- 1 | # powershell completion for %[1]s 2 | 3 | function __%[1]s_debug { 4 | if ($env:BASH_COMP_DEBUG_FILE) { 5 | "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE" 6 | } 7 | } 8 | 9 | filter __%[1]s_escapeStringWithSpecialChars { 10 | $_ -replace '\s|#|@|\$|;|,|''|\{|\}|\(|\)|"|`|\||<|>|&','`$&' 11 | } 12 | 13 | [scriptblock]${__%[2]sCompleterBlock} = { 14 | param( 15 | $WordToComplete, 16 | $CommandAst, 17 | $CursorPosition 18 | ) 19 | 20 | # Get the current command line and convert into a string 21 | $Command = $CommandAst.CommandElements 22 | $Command = "$Command" 23 | 24 | __%[1]s_debug "" 25 | __%[1]s_debug "========= starting completion logic ==========" 26 | __%[1]s_debug "WordToComplete: $WordToComplete Command: $Command CursorPosition: $CursorPosition" 27 | 28 | # The user could have moved the cursor backwards on the command-line. 29 | # We need to trigger completion from the $CursorPosition location, so we need 30 | # to truncate the command-line ($Command) up to the $CursorPosition location. 31 | # Make sure the $Command is longer then the $CursorPosition before we truncate. 32 | # This happens because the $Command does not include the last space. 33 | if ($Command.Length -gt $CursorPosition) { 34 | $Command=$Command.Substring(0,$CursorPosition) 35 | } 36 | __%[1]s_debug "Truncated command: $Command" 37 | 38 | $ShellCompDirectiveError=1 39 | $ShellCompDirectiveNoSpace=2 40 | $ShellCompDirectiveNoFileComp=4 41 | $ShellCompDirectiveFilterFileExt=8 42 | $ShellCompDirectiveFilterDirs=16 43 | $ShellCompDirectiveKeepOrder=32 44 | 45 | # Prepare the command to request completions for the program. 46 | # Split the command at the first space to separate the program and arguments. 47 | $Program,$Arguments = $Command.Split(" ",2) 48 | 49 | $RequestComp="$Program %[3]s $Arguments" 50 | __%[1]s_debug "RequestComp: $RequestComp" 51 | 52 | # we cannot use $WordToComplete because it 53 | # has the wrong values if the cursor was moved 54 | # so use the last argument 55 | if ($WordToComplete -ne "" ) { 56 | $WordToComplete = $Arguments.Split(" ")[-1] 57 | } 58 | __%[1]s_debug "New WordToComplete: $WordToComplete" 59 | 60 | 61 | # Check for flag with equal sign 62 | $IsEqualFlag = ($WordToComplete -Like "--*=*" ) 63 | if ( $IsEqualFlag ) { 64 | __%[1]s_debug "Completing equal sign flag" 65 | # Remove the flag part 66 | $Flag,$WordToComplete = $WordToComplete.Split("=",2) 67 | } 68 | 69 | if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) { 70 | # If the last parameter is complete (there is a space following it) 71 | # We add an extra empty parameter so we can indicate this to the go method. 72 | __%[1]s_debug "Adding extra empty parameter" 73 | # PowerShell 7.2+ changed the way how the arguments are passed to executables, 74 | # so for pre-7.2 or when Legacy argument passing is enabled we need to use 75 | # `"`" to pass an empty argument, a "" or '' does not work!!! 76 | if ($PSVersionTable.PsVersion -lt [version]'7.2.0' -or 77 | ($PSVersionTable.PsVersion -lt [version]'7.3.0' -and -not [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -or 78 | (($PSVersionTable.PsVersion -ge [version]'7.3.0' -or [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -and 79 | $PSNativeCommandArgumentPassing -eq 'Legacy')) { 80 | $RequestComp="$RequestComp" + ' `"`"' 81 | } else { 82 | $RequestComp="$RequestComp" + ' ""' 83 | } 84 | } 85 | 86 | __%[1]s_debug "Calling $RequestComp" 87 | # First disable ActiveHelp which is not supported for Powershell 88 | ${env:%[4]s}=0 89 | 90 | #call the command store the output in $out and redirect stderr and stdout to null 91 | # $Out is an array contains each line per element 92 | Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null 93 | 94 | # get directive from last line 95 | [int]$Directive = $Out[-1].TrimStart(':') 96 | if ($Directive -eq "") { 97 | # There is no directive specified 98 | $Directive = 0 99 | } 100 | __%[1]s_debug "The completion directive is: $Directive" 101 | 102 | # remove directive (last element) from out 103 | $Out = $Out | Where-Object { $_ -ne $Out[-1] } 104 | __%[1]s_debug "The completions are: $Out" 105 | 106 | if (($Directive -band $ShellCompDirectiveError) -ne 0 ) { 107 | # Error code. No completion. 108 | __%[1]s_debug "Received error from custom completion go code" 109 | return 110 | } 111 | 112 | $Longest = 0 113 | [Array]$Values = $Out | ForEach-Object { 114 | #Split the output in name and description 115 | $Name, $Description = $_.Split("`t",2) 116 | __%[1]s_debug "Name: $Name Description: $Description" 117 | 118 | # Look for the longest completion so that we can format things nicely 119 | if ($Longest -lt $Name.Length) { 120 | $Longest = $Name.Length 121 | } 122 | 123 | # Set the description to a one space string if there is none set. 124 | # This is needed because the CompletionResult does not accept an empty string as argument 125 | if (-Not $Description) { 126 | $Description = " " 127 | } 128 | @{Name="$Name";Description="$Description"} 129 | } 130 | 131 | 132 | $Space = " " 133 | if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) { 134 | # remove the space here 135 | __%[1]s_debug "ShellCompDirectiveNoSpace is called" 136 | $Space = "" 137 | } 138 | 139 | if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or 140 | (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 )) { 141 | __%[1]s_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported" 142 | 143 | # return here to prevent the completion of the extensions 144 | return 145 | } 146 | 147 | $Values = $Values | Where-Object { 148 | # filter the result 149 | $_.Name -like "*" 150 | #$_.Name -like "$WordToComplete*" 151 | 152 | # Join the flag back if we have an equal sign flag 153 | if ( $IsEqualFlag ) { 154 | __%[1]s_debug "Join the equal sign flag back to the completion value" 155 | $_.Name = $Flag + "=" + $_.Name 156 | } 157 | } 158 | 159 | # we sort the values in ascending order by name if keep order isn't passed 160 | if (($Directive -band $ShellCompDirectiveKeepOrder) -eq 0 ) { 161 | $Values = $Values | Sort-Object -Property Name 162 | } 163 | 164 | if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) { 165 | __%[1]s_debug "ShellCompDirectiveNoFileComp is called" 166 | 167 | if ($Values.Length -eq 0) { 168 | # Just print an empty string here so the 169 | # shell does not start to complete paths. 170 | # We cannot use CompletionResult here because 171 | # it does not accept an empty string as argument. 172 | "" 173 | return 174 | } 175 | } 176 | 177 | # Get the current mode 178 | $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function 179 | __%[1]s_debug "Mode: $Mode" 180 | 181 | $Values | ForEach-Object { 182 | 183 | # store temporary because switch will overwrite $_ 184 | $comp = $_ 185 | 186 | # PowerShell supports three different completion modes 187 | # - TabCompleteNext (default windows style - on each key press the next option is displayed) 188 | # - Complete (works like bash) 189 | # - MenuComplete (works like zsh) 190 | # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function 191 | 192 | # CompletionResult Arguments: 193 | # 1) CompletionText text to be used as the auto completion result 194 | # 2) ListItemText text to be displayed in the suggestion list 195 | # 3) ResultType type of completion result 196 | # 4) ToolTip text for the tooltip with details about the object 197 | 198 | switch ($Mode) { 199 | 200 | # bash like 201 | "Complete" { 202 | 203 | if ($Values.Length -eq 1) { 204 | __%[1]s_debug "Only one completion left" 205 | 206 | # insert space after value 207 | [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") 208 | 209 | } else { 210 | # Add the proper number of spaces to align the descriptions 211 | while($comp.Name.Length -lt $Longest) { 212 | $comp.Name = $comp.Name + " " 213 | } 214 | 215 | # Check for empty description and only add parentheses if needed 216 | if ($($comp.Description) -eq " " ) { 217 | $Description = "" 218 | } else { 219 | $Description = " ($($comp.Description))" 220 | } 221 | 222 | [System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") 223 | } 224 | } 225 | 226 | # zsh like 227 | "MenuComplete" { 228 | # insert space after value 229 | # MenuComplete will automatically show the ToolTip of 230 | # the highlighted value at the bottom of the suggestions. 231 | [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") 232 | } 233 | 234 | # TabCompleteNext and in case we get something unknown 235 | Default { 236 | # Like MenuComplete but we don't want to add a space here because 237 | # the user need to press space anyway to get the completion. 238 | # Description will not be shown because that's not possible with TabCompleteNext 239 | [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)") 240 | } 241 | } 242 | 243 | } 244 | } 245 | 246 | Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock ${__%[2]sCompleterBlock} 247 | -------------------------------------------------------------------------------- /type_test.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "errors" 5 | "net/netip" 6 | "net/url" 7 | "regexp" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestTypeNew(t *testing.T) { 13 | for _, tt := range typeTests(t) { 14 | for _, test := range tt.tests { 15 | t.Run(tt.typ.String()+"/"+toString[string](test.v), func(t *testing.T) { 16 | expErr, ok := test.exp.(error) 17 | switch v, err := tt.typ.New(); { 18 | case err != nil: 19 | t.Fatalf("expected no error, got: %v", err) 20 | default: 21 | switch err := v.Set(toString[string](test.v)); { 22 | case err != nil && ok && !errors.Is(err, expErr): 23 | t.Errorf("expected error %v, got: %v", expErr, err) 24 | case err != nil && !ok: 25 | t.Errorf("expected no error, got: %v", err) 26 | case err == nil && ok: 27 | t.Errorf("expected error %v, got: nil", expErr) 28 | case !ok: 29 | t.Logf("type: %T", v) 30 | s, err := v.Get() 31 | if err != nil { 32 | t.Fatalf("expected no error, got: %v", err) 33 | } 34 | t.Logf("val: %s", s) 35 | if exp := toString[string](test.exp); s != exp { 36 | t.Errorf("expected %q, got: %q", exp, s) 37 | } 38 | } 39 | } 40 | }) 41 | } 42 | } 43 | } 44 | 45 | func typeTests(t *testing.T) []typeTest { 46 | t.Helper() 47 | return []typeTest{ 48 | { 49 | BytesT, []test{ 50 | {"", []byte("")}, 51 | {"a", []byte("a")}, 52 | {15, []byte("15")}, 53 | {int64(20), []byte("20")}, 54 | {[]rune("foo"), []byte("foo")}, 55 | {[]byte("bar"), []byte("bar")}, 56 | {float64(0.0), []byte("0")}, 57 | {float64(0.127), []byte("0.127")}, 58 | }, 59 | }, 60 | { 61 | StringT, []test{ 62 | {"", ""}, 63 | {"a", "a"}, 64 | {15, "15"}, 65 | {int64(20), "20"}, 66 | {[]rune("foo"), "foo"}, 67 | {[]byte("bar"), "bar"}, 68 | {float64(0.0), "0"}, 69 | {float64(0.127), "0.127"}, 70 | }, 71 | }, 72 | { 73 | RunesT, []test{ 74 | {"", []rune("")}, 75 | {"a", []rune("a")}, 76 | {15, []rune("15")}, 77 | {int64(20), []rune("20")}, 78 | {[]rune("foo"), []rune("foo")}, 79 | {[]byte("bar"), []rune("bar")}, 80 | {float64(0.0), []rune("0")}, 81 | {float64(0.127), []rune("0.127")}, 82 | }, 83 | }, 84 | { 85 | Base64T, []test{ 86 | {"", ""}, 87 | {"Zm9v", "foo"}, 88 | {"Zm9vCg==", "foo\n"}, 89 | {"---", ErrInvalidValue}, 90 | }, 91 | }, 92 | { 93 | HexT, []test{ 94 | {"", ""}, 95 | {"666f6f", "foo"}, 96 | {"666f6f0a", "foo\n"}, 97 | {"---", ErrInvalidValue}, 98 | }, 99 | }, 100 | { 101 | BoolT, []test{ 102 | {"", false}, 103 | {"true", true}, 104 | {"t", true}, 105 | {"T", true}, 106 | {"f", false}, 107 | {"false", false}, 108 | {"0", false}, 109 | {true, true}, 110 | {false, false}, 111 | {"1", true}, 112 | {float64(1.0), true}, 113 | {uint(128), true}, 114 | {int64(124), true}, 115 | {int64(-124), false}, 116 | {int64(0), false}, 117 | {"foo", ErrInvalidValue}, 118 | }, 119 | }, 120 | { 121 | ByteT, []test{ 122 | {"", ""}, 123 | {"a", "a"}, 124 | {"😀", ErrInvalidValue}, 125 | {"foo", ErrInvalidValue}, 126 | }, 127 | }, 128 | { 129 | RuneT, []test{ 130 | {"", ""}, 131 | {"a", "a"}, 132 | {"😀", "😀"}, 133 | {"😀😀", ErrInvalidValue}, 134 | {"foo", ErrInvalidValue}, 135 | }, 136 | }, 137 | { 138 | Int64T, []test{ 139 | {"", int8(0)}, 140 | {"0", int8(0)}, 141 | {0, int8(0)}, 142 | {21551, "21551"}, 143 | {float64(1.0), int(1)}, 144 | {"57", int8(57)}, 145 | {"-10", int8(-10)}, 146 | {"foo", ErrInvalidValue}, 147 | }, 148 | }, 149 | { 150 | Int8T, []test{ 151 | {"", int8(0)}, 152 | {"0", int8(0)}, 153 | {0, int8(0)}, 154 | {21551, ErrInvalidValue}, 155 | {float64(1.0), int(1)}, 156 | {"57", int8(57)}, 157 | {"-10", int8(-10)}, 158 | {"foo", ErrInvalidValue}, 159 | }, 160 | }, 161 | { 162 | IntT, []test{ 163 | {"", int(0)}, 164 | {"0", int(0)}, 165 | {0, int(0)}, 166 | {21551, int(21551)}, 167 | {float64(1.0), int(1)}, 168 | {"57", int(57)}, 169 | {"-10", int(-10)}, 170 | {"foo", ErrInvalidValue}, 171 | }, 172 | }, 173 | { 174 | Uint64T, []test{ 175 | {"", uint(0)}, 176 | {"0", uint(0)}, 177 | {0, uint(0)}, 178 | {21551, uint(21551)}, 179 | {-25555, ErrInvalidValue}, 180 | {float64(1.0), uint(1)}, 181 | {"foo", ErrInvalidValue}, 182 | {"-10", ErrInvalidValue}, 183 | }, 184 | }, 185 | { 186 | Uint8T, []test{ 187 | {"", uint(0)}, 188 | {"0", uint(0)}, 189 | {0, uint(0)}, 190 | {21551, ErrInvalidValue}, 191 | {-25555, ErrInvalidValue}, 192 | {float64(1.0), uint(1)}, 193 | {"foo", ErrInvalidValue}, 194 | {"-10", ErrInvalidValue}, 195 | }, 196 | }, 197 | { 198 | UintT, []test{ 199 | {"", uint(0)}, 200 | {"0", uint(0)}, 201 | {0, uint(0)}, 202 | {21551, uint(21551)}, 203 | {-25555, ErrInvalidValue}, 204 | {float64(1.0), uint(1)}, 205 | {"foo", ErrInvalidValue}, 206 | {"-10", ErrInvalidValue}, 207 | }, 208 | }, 209 | { 210 | Float64T, []test{ 211 | {"", float64(0.0)}, 212 | {"0.0", float64(0.0)}, 213 | {"79.99", float64(79.99)}, 214 | {float64(57.33), float64(57.33)}, 215 | {"foo", ErrInvalidValue}, 216 | }, 217 | }, 218 | { 219 | Float32T, []test{ 220 | {"", float32(0.0)}, 221 | {"0.0", float32(0.0)}, 222 | {"79.99", float32(79.99)}, 223 | {float64(57.33), float32(57.33)}, 224 | {"foo", ErrInvalidValue}, 225 | }, 226 | }, 227 | { 228 | Complex128T, []test{ 229 | {"", complex128(0.0)}, 230 | {"0.0", complex128(0.0)}, 231 | {"79.99", complex128(79.99)}, 232 | {complex128(57.33), complex128(57.33)}, 233 | {float64(54.33), complex128(54.33)}, 234 | {"foo", ErrInvalidValue}, 235 | }, 236 | }, 237 | { 238 | Complex64T, []test{ 239 | {"", complex64(0.0)}, 240 | {"0.0", complex64(0.0)}, 241 | {"79.99", complex64(79.99)}, 242 | {complex128(57.33), complex64(57.33)}, 243 | {float64(54.33), complex64(54.33)}, 244 | {"foo", ErrInvalidValue}, 245 | }, 246 | }, 247 | { 248 | TimestampT, []test{ 249 | {"", ""}, 250 | {"2024-11-24T07:41:36+07:00", mustTime(t, "2024-11-24T07:41:36+07:00", time.RFC3339)}, 251 | {"foo", ErrInvalidValue}, 252 | }, 253 | }, 254 | { 255 | DateTimeT, []test{ 256 | {"", ""}, 257 | {"2024-11-24 7:41:36", mustTime(t, "2024-11-24 7:41:36", time.DateTime)}, 258 | {"foo", ErrInvalidValue}, 259 | }, 260 | }, 261 | { 262 | DateT, []test{ 263 | {"", ""}, 264 | {"2004-01-02", mustTime(t, "2004-01-02", time.DateOnly)}, 265 | {"foo", ErrInvalidValue}, 266 | }, 267 | }, 268 | { 269 | TimeT, []test{ 270 | {"", ""}, 271 | {"7:41:36", mustTime(t, "7:41:36", time.TimeOnly)}, 272 | {"foo", ErrInvalidValue}, 273 | }, 274 | }, 275 | { 276 | DurationT, []test{ 277 | {"", ""}, 278 | {"0", time.Duration(0)}, 279 | {"1", 1 * time.Second}, 280 | {"1s", 1 * time.Second}, 281 | {int(125), 125 * time.Second}, 282 | {uint(65), "1m5s"}, 283 | {float64(2.0), 2 * time.Second}, 284 | {"foo", ErrInvalidValue}, 285 | }, 286 | }, 287 | { 288 | CountT, []test{ 289 | {"", 1}, 290 | {"a", 1}, 291 | {"foo", 1}, 292 | {"bar", "1"}, 293 | }, 294 | }, 295 | { 296 | AddrT, 297 | []test{ 298 | {"", ""}, 299 | {"0.0.0.0", mustAddr(t, "0.0.0.0")}, 300 | {"127.0.0.1", mustAddr(t, "127.0.0.1")}, 301 | {"::ffff:192.168.140.255", mustAddr(t, "::ffff:192.168.140.255")}, 302 | {"foo", ErrInvalidValue}, 303 | }, 304 | }, 305 | { 306 | AddrPortT, 307 | []test{ 308 | {"", ""}, 309 | {"1.2.3.4:80", mustAddrPort(t, "1.2.3.4:80")}, 310 | {"[::]:80", mustAddrPort(t, "[::]:80")}, 311 | {"[1::CAFE]:80", mustAddrPort(t, "[1::cafe]:80")}, 312 | {"[1::CAFE%en0]:80", mustAddrPort(t, "[1::cafe%en0]:80")}, 313 | {"[::FFFF:192.168.140.255]:80", mustAddrPort(t, "[::ffff:192.168.140.255]:80")}, 314 | {"[::FFFF:192.168.140.255%en0]:80", mustAddrPort(t, "[::ffff:192.168.140.255%en0]:80")}, 315 | {"foo", ErrInvalidValue}, 316 | }, 317 | }, 318 | { 319 | CIDRT, 320 | []test{ 321 | {"", ""}, 322 | {"1.2.3.4/24", mustPrefix(t, "1.2.3.4/24")}, 323 | {"fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118", mustPrefix(t, "fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118")}, 324 | {"::ffff:c000:0280/96", mustPrefix(t, "::ffff:192.0.2.128/96")}, 325 | {"::ffff:192.168.140.255/8", mustPrefix(t, "::ffff:192.168.140.255/8")}, 326 | {"1.2.3.4/24", mustPrefix(t, "1.2.3.4/24")}, 327 | {"foo", ErrInvalidValue}, 328 | }, 329 | }, 330 | { 331 | RegexpT, 332 | []test{ 333 | {"", ""}, 334 | {"^[0-9]+", mustRegexp(t, "^[0-9]+")}, 335 | {"([]{-1,}", ErrInvalidValue}, 336 | }, 337 | }, 338 | { 339 | URLT, []test{ 340 | {"", mustURL(t, "")}, 341 | {"https://www.google.com", mustURL(t, "https://www.google.com")}, 342 | {"file:test", mustURL(t, "file:test")}, 343 | {":foo", ErrInvalidValue}, 344 | }, 345 | }, 346 | { 347 | SizeT, []test{ 348 | {"", "0 B"}, 349 | {"1", "1 B"}, 350 | {"1 mib", Size(1 * MiB)}, 351 | {"1.5GB", Size(int64(1.5 * float64(GB)))}, 352 | {"1.5GiB", Size(int64(1.5 * float64(GiB)))}, 353 | {"15 MiB", Size(int64(15 * float64(MiB)))}, 354 | {"foo", ErrInvalidValue}, 355 | }, 356 | }, 357 | { 358 | RateT, []test{ 359 | {"", "0 B/s"}, 360 | {"1", "1 B/s"}, 361 | {"1 mib", "1 MiB/s"}, 362 | {"1.5GB", "1.4 GiB/s"}, 363 | {"-15.4 MB", "-14.69 MiB/s"}, 364 | {"-15.4 mib", "-15.4 MiB/s"}, 365 | {"1/s", "1 B/s"}, 366 | {"1 mib/h", "1 MiB/h"}, 367 | {"1.5GB/m", "1.4 GiB/m"}, 368 | {"-15.4 MiB/s", "-15.4 MiB/s"}, 369 | {"1 gib/µs", "1 GiB/µs"}, 370 | {"foo", ErrInvalidValue}, 371 | }, 372 | }, 373 | { 374 | SliceT, []test{ 375 | {"", "[]"}, 376 | {"a", "[a]"}, 377 | {"a,b", "[a b]"}, 378 | {"a,b,c", "[a b c]"}, 379 | }, 380 | }, 381 | { 382 | ArrayT, []test{ 383 | {"", "[]"}, 384 | {"a", "[a]"}, 385 | {"a,b", "[a,b]"}, 386 | {"a,b,c", "[a,b,c]"}, 387 | }, 388 | }, 389 | { 390 | MapT, []test{ 391 | {"", ErrInvalidValue}, 392 | {"a=", "[a:]"}, 393 | {"a=b", "[a:b]"}, 394 | {"a=b,c=d", "[a:b c:d]"}, 395 | }, 396 | }, 397 | } 398 | } 399 | 400 | func mustTime(t *testing.T, s, layout string) FormattedTime { 401 | t.Helper() 402 | v, err := time.Parse(layout, s) 403 | if err != nil { 404 | t.Fatalf("expected no error, got: %v", err) 405 | } 406 | return FormattedTime{layout: layout, v: v} 407 | } 408 | 409 | func mustURL(t *testing.T, s string) *url.URL { 410 | t.Helper() 411 | u, err := url.Parse(s) 412 | if err != nil { 413 | t.Fatalf("expected no error, got: %v", err) 414 | } 415 | return u 416 | } 417 | 418 | func mustAddr(t *testing.T, s string) netip.Addr { 419 | t.Helper() 420 | a, err := netip.ParseAddr(s) 421 | if err != nil { 422 | t.Fatalf("expected no error, got: %v", err) 423 | } 424 | return a 425 | } 426 | 427 | func mustAddrPort(t *testing.T, s string) netip.AddrPort { 428 | t.Helper() 429 | a, err := netip.ParseAddrPort(s) 430 | if err != nil { 431 | t.Fatalf("expected no error, got: %v", err) 432 | } 433 | return a 434 | } 435 | 436 | func mustPrefix(t *testing.T, s string) netip.Prefix { 437 | t.Helper() 438 | a, err := netip.ParsePrefix(s) 439 | if err != nil { 440 | t.Fatalf("expected no error, got: %v", err) 441 | } 442 | return a 443 | } 444 | 445 | func mustRegexp(t *testing.T, s string) *regexp.Regexp { 446 | t.Helper() 447 | re, err := regexp.Compile(s) 448 | if err != nil { 449 | t.Fatalf("expected no error, got: %v", err) 450 | } 451 | return re 452 | } 453 | 454 | type typeTest struct { 455 | typ Type 456 | tests []test 457 | } 458 | 459 | type test struct { 460 | v any 461 | exp any 462 | } 463 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "encoding" 5 | "fmt" 6 | "math/big" 7 | "net/netip" 8 | "net/url" 9 | "reflect" 10 | "regexp" 11 | "time" 12 | ) 13 | 14 | // Type is a variable type. 15 | type Type string 16 | 17 | // Types. 18 | const ( 19 | BytesT Type = "bytes" 20 | StringT Type = "string" 21 | RunesT Type = "runes" 22 | Base64T Type = "base64" 23 | HexT Type = "hex" 24 | BoolT Type = "bool" 25 | ByteT Type = "byte" 26 | RuneT Type = "rune" 27 | Int64T Type = "int64" 28 | Int32T Type = "int32" 29 | Int16T Type = "int16" 30 | Int8T Type = "int8" 31 | IntT Type = "int" 32 | Uint64T Type = "uint64" 33 | Uint32T Type = "uint32" 34 | Uint16T Type = "uint16" 35 | Uint8T Type = "uint8" 36 | UintT Type = "uint" 37 | Float64T Type = "float64" 38 | Float32T Type = "float32" 39 | Complex128T Type = "complex128" 40 | Complex64T Type = "complex64" 41 | SizeT Type = "size" 42 | RateT Type = "rate" 43 | 44 | TimestampT Type = "timestamp" 45 | DateTimeT Type = "datetime" 46 | DateT Type = "date" 47 | TimeT Type = "time" 48 | 49 | DurationT Type = "duration" 50 | 51 | CountT Type = "count" 52 | PathT Type = "path" 53 | 54 | BigIntT Type = "bigint" 55 | BigFloatT Type = "bigfloat" 56 | BigRatT Type = "bigrat" 57 | AddrT Type = "addr" 58 | AddrPortT Type = "addrport" 59 | CIDRT Type = "cidr" 60 | RegexpT Type = "regexp" 61 | URLT Type = "url" 62 | 63 | UUIDT Type = "uuid" 64 | ColorT Type = "color" 65 | GlobT Type = "glob" 66 | 67 | SliceT Type = "slice" 68 | ArrayT Type = "array" 69 | MapT Type = "map" 70 | 71 | HookT Type = "hook" 72 | ) 73 | 74 | // Option returns an [option] for the type. 75 | func (typ Type) Option() option { 76 | return option{ 77 | name: "Type(" + typ.String() + ")", 78 | flag: func(g *Flag) error { 79 | if g.Type != SliceT && g.Type != ArrayT && g.Type != MapT && g.Type != HookT { 80 | g.Type = typ 81 | } else if g.Type != HookT { 82 | g.Elem = typ 83 | } 84 | return nil 85 | }, 86 | typ: func(t interface{ SetType(Type) }) error { 87 | t.SetType(typ) 88 | return nil 89 | }, 90 | } 91 | } 92 | 93 | // String satisfies the [fmt.Stringer] interface. 94 | func (typ Type) String() string { 95 | return string(typ) 96 | } 97 | 98 | // Formatter returns the value format string for the type. 99 | func (typ Type) Formatter() string { 100 | switch typ { 101 | case BytesT, StringT, RunesT, ByteT, RuneT: 102 | return `%q` 103 | } 104 | return `%v` 105 | } 106 | 107 | // New creates a new [Value] for the registered type. 108 | func (typ Type) New() (Value, error) { 109 | var v Value 110 | var err error 111 | switch typ { 112 | case HookT: 113 | return nil, fmt.Errorf("%w (hook)", ErrCouldNotCreateValue) 114 | case SliceT: 115 | v = NewSlice(StringT) 116 | case ArrayT: 117 | v = NewArray(StringT) 118 | case MapT: 119 | v, err = NewMap(StringT, StringT) 120 | default: 121 | f, ok := typeNews[typ] 122 | if !ok { 123 | return nil, fmt.Errorf("%w: type not registered (%q)", ErrCouldNotCreateValue, string(typ)) 124 | } 125 | v, err = f() 126 | } 127 | if err != nil { 128 | return nil, fmt.Errorf("%w: %w", ErrCouldNotCreateValue, err) 129 | } 130 | return v, nil 131 | } 132 | 133 | // typeNews are registered type creation funcs. 134 | var typeNews map[Type]func() (Value, error) 135 | 136 | // typeFlagOpts are registered type flag options. 137 | var typeFlagOpts map[Type][]Option 138 | 139 | // reflectTypes are reflect type name lookups for registered types. 140 | var reflectTypes map[string]Type 141 | 142 | // typeTextNews are registered type creation funcs for text marshalable types. 143 | var typeTextNews map[Type]func() (any, error) 144 | 145 | // typeBinaryNews are registered type creation funcs for binary marshalable 146 | // types. 147 | var typeBinaryNews map[Type]func() (any, error) 148 | 149 | func init() { 150 | typeNews = make(map[Type]func() (Value, error)) 151 | typeFlagOpts = make(map[Type][]Option) 152 | reflectTypes = make(map[string]Type) 153 | typeTextNews = make(map[Type]func() (any, error)) 154 | typeBinaryNews = make(map[Type]func() (any, error)) 155 | // register basic types 156 | RegisterType(BytesT, NewVal[[]byte]()) 157 | RegisterType(StringT, NewVal[string]()) 158 | RegisterType(RunesT, NewVal[[]rune]()) 159 | RegisterType(Base64T, NewVal[[]byte](Base64T)) 160 | RegisterType(HexT, NewVal[[]byte](HexT)) 161 | RegisterType(BoolT, NewVal[bool](), NoArg(true, true)) 162 | RegisterType(ByteT, NewVal[string](ByteT)) 163 | RegisterType(RuneT, NewVal[string](RuneT)) 164 | RegisterType(Int64T, NewVal[int64]()) 165 | RegisterType(Int32T, NewVal[int32]()) 166 | RegisterType(Int16T, NewVal[int16]()) 167 | RegisterType(Int8T, NewVal[int8]()) 168 | RegisterType(IntT, NewVal[int]()) 169 | RegisterType(Uint64T, NewVal[uint64]()) 170 | RegisterType(Uint32T, NewVal[uint32]()) 171 | RegisterType(Uint16T, NewVal[uint16]()) 172 | RegisterType(Uint8T, NewVal[uint8]()) 173 | RegisterType(UintT, NewVal[uint]()) 174 | RegisterType(Float64T, NewVal[float64]()) 175 | RegisterType(Float32T, NewVal[float32]()) 176 | RegisterType(Complex128T, NewVal[complex128]()) 177 | RegisterType(Complex64T, NewVal[complex64]()) 178 | RegisterType(RateT, NewVal[Rate]()) 179 | RegisterType(SizeT, NewVal[Size]()) 180 | RegisterType(TimestampT, NewTime(TimestampT, "")) 181 | RegisterType(DateTimeT, NewTime(DateTimeT, time.DateTime)) 182 | RegisterType(DateT, NewTime(DateT, time.DateOnly)) 183 | RegisterType(TimeT, NewTime(TimeT, time.TimeOnly)) 184 | RegisterType(DurationT, NewVal[time.Duration]()) 185 | RegisterType(CountT, NewVal[uint64](CountT), NoArg(true, "")) 186 | RegisterType(PathT, NewVal[string](PathT)) 187 | // register text marshal types 188 | RegisterTextType(func() (*big.Int, error) { 189 | return big.NewInt(0), nil 190 | }) 191 | RegisterTextType(func() (*big.Float, error) { 192 | return big.NewFloat(0), nil 193 | }) 194 | RegisterTextType(func() (*big.Rat, error) { 195 | return big.NewRat(0, 1), nil 196 | }) 197 | RegisterTextType(func() (*netip.Addr, error) { 198 | return new(netip.Addr), nil 199 | }) 200 | RegisterTextType(func() (*netip.AddrPort, error) { 201 | return new(netip.AddrPort), nil 202 | }) 203 | RegisterTextType(func() (*netip.Prefix, error) { 204 | return new(netip.Prefix), nil 205 | }) 206 | RegisterTextType(func() (*regexp.Regexp, error) { 207 | return new(regexp.Regexp), nil 208 | }) 209 | // register binary marshal types 210 | RegisterBinaryType(func() (*url.URL, error) { 211 | return new(url.URL), nil 212 | }) 213 | } 214 | 215 | // RegisterType registers a type. 216 | func RegisterType(typ Type, f func() (Value, error), opts ...Option) { 217 | typeNews[typ], typeFlagOpts[typ] = f, opts 218 | } 219 | 220 | // RegisterTypeName registers a type name. 221 | func RegisterTypeName(typ Type, names ...string) { 222 | for _, name := range names { 223 | reflectTypes[name] = typ 224 | } 225 | } 226 | 227 | // RegisterTextType registers a new text type. 228 | func RegisterTextType[T TextMarshalUnmarshaler](f func() (T, error)) { 229 | registerMarshaler[T](func() (any, error) { return f() }, typeTextNews) 230 | } 231 | 232 | // RegisterBinaryType registers a new binary type. 233 | func RegisterBinaryType[T BinaryMarshalUnmarshaler](f func() (T, error)) { 234 | registerMarshaler[T](func() (any, error) { return f() }, typeBinaryNews) 235 | } 236 | 237 | // registerMarshaler registers a type marshaler. 238 | func registerMarshaler[T any](f func() (any, error), m map[Type]func() (any, error)) { 239 | typ := typeType[T]() 240 | if _, ok := typeNews[typ]; ok { 241 | return 242 | } 243 | if _, ok := m[typ]; ok { 244 | return 245 | } 246 | typeNews[typ], m[typ] = NewVal[T](typ), f 247 | } 248 | 249 | // TextMarshalUnmarshaler is the text marshal interface. 250 | type TextMarshalUnmarshaler interface { 251 | encoding.TextMarshaler 252 | encoding.TextUnmarshaler 253 | } 254 | 255 | // BinaryMarshalUnmarshaler is the binary marshal interface. 256 | type BinaryMarshalUnmarshaler interface { 257 | encoding.BinaryMarshaler 258 | encoding.BinaryUnmarshaler 259 | } 260 | 261 | // typeType returns the type for T. 262 | func typeType[T any]() Type { 263 | var v T 264 | return typeRef(v) 265 | } 266 | 267 | // typeRef returns the type for val. 268 | func typeRef(val any) Type { 269 | switch val.(type) { 270 | case []byte: 271 | return BytesT 272 | case string: 273 | return StringT 274 | case []rune: 275 | return RunesT 276 | case bool: 277 | return BoolT 278 | case int64: 279 | return Int64T 280 | case int32: 281 | return Int32T 282 | case int16: 283 | return Int16T 284 | case int8: 285 | return Int8T 286 | case int: 287 | return IntT 288 | case uint64: 289 | return Uint64T 290 | case uint32: 291 | return Uint32T 292 | case uint16: 293 | return Uint16T 294 | case uint8: 295 | return Uint8T 296 | case uint: 297 | return UintT 298 | case float64: 299 | return Float64T 300 | case float32: 301 | return Float32T 302 | case complex128: 303 | return Complex128T 304 | case complex64: 305 | return Complex64T 306 | case time.Time: 307 | return TimestampT 308 | case time.Duration: 309 | return DurationT 310 | case Size: 311 | return SizeT 312 | case Rate: 313 | return RateT 314 | case *big.Int: 315 | return BigIntT 316 | case *big.Float: 317 | return BigFloatT 318 | case *big.Rat: 319 | return BigRatT 320 | case *netip.Addr: 321 | return AddrT 322 | case *netip.AddrPort: 323 | return AddrPortT 324 | case *netip.Prefix: 325 | return CIDRT 326 | case *regexp.Regexp: 327 | return RegexpT 328 | case *url.URL: 329 | return URLT 330 | } 331 | typ := reflect.TypeOf(val) 332 | if typ != nil { 333 | s := typ.String() 334 | if typ, ok := reflectTypes[s]; ok { 335 | return typ 336 | } 337 | return Type(s) 338 | } 339 | return "" 340 | } 341 | 342 | // defaultType returns the type, map key type, and element type of v. 343 | func defaultType(refType reflect.Type) (Type, Type, Type, error) { 344 | switch refType.Kind() { 345 | case reflect.String: 346 | return StringT, StringT, StringT, nil 347 | case reflect.Bool: 348 | return BoolT, StringT, StringT, nil 349 | case reflect.Int64: 350 | return Int64T, StringT, StringT, nil 351 | case reflect.Int32: 352 | return Int32T, StringT, StringT, nil 353 | case reflect.Int16: 354 | return Int16T, StringT, StringT, nil 355 | case reflect.Int8: 356 | return Int8T, StringT, StringT, nil 357 | case reflect.Int: 358 | return IntT, StringT, StringT, nil 359 | case reflect.Uint64: 360 | return Uint64T, StringT, StringT, nil 361 | case reflect.Uint32: 362 | return Uint32T, StringT, StringT, nil 363 | case reflect.Uint16: 364 | return Uint16T, StringT, StringT, nil 365 | case reflect.Uint8: 366 | return Uint8T, StringT, StringT, nil 367 | case reflect.Uint: 368 | return UintT, StringT, StringT, nil 369 | case reflect.Float64: 370 | return Float64T, StringT, StringT, nil 371 | case reflect.Float32: 372 | return Float32T, StringT, StringT, nil 373 | case reflect.Complex128: 374 | return Complex128T, StringT, StringT, nil 375 | case reflect.Complex64: 376 | return Complex128T, StringT, StringT, nil 377 | case reflect.Pointer: 378 | if typ := reflectType(refType); typ != "" { 379 | return typ, StringT, StringT, nil 380 | } 381 | case reflect.Slice: 382 | elem := refType.Elem() 383 | if elem.Kind() == reflect.Slice && elem.Elem().Kind() == reflect.Uint8 { 384 | return SliceT, StringT, BytesT, nil 385 | } 386 | if typ, _, _, err := defaultType(elem); err == nil { 387 | return SliceT, StringT, typ, nil 388 | } 389 | case reflect.Map: 390 | if mapKey, _, _, err := defaultType(refType.Key()); err == nil { 391 | if elem, _, _, err := defaultType(refType.Elem()); err == nil { 392 | return MapT, mapKey, elem, nil 393 | } 394 | } 395 | } 396 | if refType == timeType { 397 | return TimestampT, StringT, StringT, nil 398 | } 399 | return "", "", "", ErrInvalidType 400 | } 401 | 402 | // reflectType returns the [Type] for the reflect type. 403 | func reflectType(refType reflect.Type) Type { 404 | s := refType.String() 405 | switch s { 406 | case "*big.Int": 407 | return BigIntT 408 | case "*big.Float": 409 | return BigFloatT 410 | case "*big.Rat": 411 | return BigRatT 412 | case "*netip.Addr": 413 | return AddrT 414 | case "*netip.AddrPort": 415 | return AddrPortT 416 | case "*netip.Prefix": 417 | return CIDRT 418 | case "*regexp.Regexp": 419 | return RegexpT 420 | case "*url.URL": 421 | return URLT 422 | } 423 | if typ, ok := reflectTypes[s]; ok { 424 | return typ 425 | } 426 | return "" 427 | } 428 | 429 | var timeType = reflect.TypeOf(time.Time{}) 430 | -------------------------------------------------------------------------------- /comp/bash.bash: -------------------------------------------------------------------------------- 1 | # bash completion for %[1]s 2 | 3 | __%[1]s_debug() 4 | { 5 | if [[ -n ${BASH_COMP_DEBUG_FILE-} ]]; then 6 | echo "$*" >> "${BASH_COMP_DEBUG_FILE}" 7 | fi 8 | } 9 | 10 | # Macs have bash3 for which the bash-completion package doesn't include 11 | # _init_completion. This is a minimal version of that function. 12 | __%[1]s_init_completion() 13 | { 14 | COMPREPLY=() 15 | _get_comp_words_by_ref "$@" cur prev words cword 16 | } 17 | 18 | # This function calls the %[1]s program to obtain the completion 19 | # results and the directive. It fills the 'out' and 'directive' vars. 20 | __%[1]s_get_completion_results() { 21 | local requestComp lastParam lastChar args 22 | 23 | # Prepare the command to request completions for the program. 24 | # Calling ${words[0]} instead of directly %[1]s allows handling aliases 25 | args=("${words[@]:1}") 26 | requestComp="${words[0]} %[3]s ${args[*]}" 27 | 28 | lastParam=${words[$((${#words[@]}-1))]} 29 | lastChar=${lastParam:$((${#lastParam}-1)):1} 30 | __%[1]s_debug "lastParam ${lastParam}, lastChar ${lastChar}" 31 | 32 | if [[ -z ${cur} && ${lastChar} != = ]]; then 33 | # If the last parameter is complete (there is a space following it) 34 | # We add an extra empty parameter so we can indicate this to the go method. 35 | __%[1]s_debug "Adding extra empty parameter" 36 | requestComp="${requestComp} ''" 37 | fi 38 | 39 | # When completing a flag with an = (e.g., %[1]s -n=) 40 | # bash focuses on the part after the =, so we need to remove 41 | # the flag part from $cur 42 | if [[ ${cur} == -*=* ]]; then 43 | cur="${cur#*=}" 44 | fi 45 | 46 | __%[1]s_debug "Calling ${requestComp}" 47 | # Use eval to handle any environment variables and such 48 | out=$(eval "${requestComp}" 2>/dev/null) 49 | 50 | # Extract the directive integer at the very end of the output following a colon (:) 51 | directive=${out##*:} 52 | # Remove the directive 53 | out=${out%%:*} 54 | if [[ ${directive} == "${out}" ]]; then 55 | # There is not directive specified 56 | directive=0 57 | fi 58 | __%[1]s_debug "The completion directive is: ${directive}" 59 | __%[1]s_debug "The completions are: ${out}" 60 | } 61 | 62 | __%[1]s_process_completion_results() { 63 | local shellCompDirectiveError=1 64 | local shellCompDirectiveNoSpace=2 65 | local shellCompDirectiveNoFileComp=4 66 | local shellCompDirectiveFilterFileExt=8 67 | local shellCompDirectiveFilterDirs=16 68 | local shellCompDirectiveKeepOrder=32 69 | 70 | if (((directive & shellCompDirectiveError) != 0)); then 71 | # Error code. No completion. 72 | __%[1]s_debug "Received error from custom completion go code" 73 | return 74 | else 75 | if (((directive & shellCompDirectiveNoSpace) != 0)); then 76 | if [[ $(type -t compopt) == builtin ]]; then 77 | __%[1]s_debug "Activating no space" 78 | compopt -o nospace 79 | else 80 | __%[1]s_debug "No space directive not supported in this version of bash" 81 | fi 82 | fi 83 | if (((directive & shellCompDirectiveKeepOrder) != 0)); then 84 | if [[ $(type -t compopt) == builtin ]]; then 85 | # no sort isn't supported for bash less than < 4.4 86 | if [[ ${BASH_VERSINFO[0]} -lt 4 || ( ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 4 ) ]]; then 87 | __%[1]s_debug "No sort directive not supported in this version of bash" 88 | else 89 | __%[1]s_debug "Activating keep order" 90 | compopt -o nosort 91 | fi 92 | else 93 | __%[1]s_debug "No sort directive not supported in this version of bash" 94 | fi 95 | fi 96 | if (((directive & shellCompDirectiveNoFileComp) != 0)); then 97 | if [[ $(type -t compopt) == builtin ]]; then 98 | __%[1]s_debug "Activating no file completion" 99 | compopt +o default 100 | else 101 | __%[1]s_debug "No file completion directive not supported in this version of bash" 102 | fi 103 | fi 104 | fi 105 | 106 | # Separate activeHelp from normal completions 107 | local completions=() 108 | local activeHelp=() 109 | __%[1]s_extract_activeHelp 110 | 111 | if (((directive & shellCompDirectiveFilterFileExt) != 0)); then 112 | # File extension filtering 113 | local fullFilter filter filteringCmd 114 | 115 | # Do not use quotes around the $completions variable or else newline 116 | # characters will be kept. 117 | for filter in ${completions[*]}; do 118 | fullFilter+="$filter|" 119 | done 120 | 121 | filteringCmd="_filedir $fullFilter" 122 | __%[1]s_debug "File filtering command: $filteringCmd" 123 | $filteringCmd 124 | elif (((directive & shellCompDirectiveFilterDirs) != 0)); then 125 | # File completion for directories only 126 | 127 | local subdir 128 | subdir=${completions[0]} 129 | if [[ -n $subdir ]]; then 130 | __%[1]s_debug "Listing directories in $subdir" 131 | pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return 132 | else 133 | __%[1]s_debug "Listing directories in ." 134 | _filedir -d 135 | fi 136 | else 137 | __%[1]s_handle_completion_types 138 | fi 139 | 140 | __%[1]s_handle_special_char "$cur" : 141 | __%[1]s_handle_special_char "$cur" = 142 | 143 | # Print the activeHelp statements before we finish 144 | if ((${#activeHelp[*]} != 0)); then 145 | printf "\n"; 146 | printf "%%s\n" "${activeHelp[@]}" 147 | printf "\n" 148 | 149 | # The prompt format is only available from bash 4.4. 150 | # We test if it is available before using it. 151 | if (x=${PS1@P}) 2> /dev/null; then 152 | printf "%%s" "${PS1@P}${COMP_LINE[@]}" 153 | else 154 | # Can't print the prompt. Just print the 155 | # text the user had typed, it is workable enough. 156 | printf "%%s" "${COMP_LINE[@]}" 157 | fi 158 | fi 159 | } 160 | 161 | # Separate activeHelp lines from real completions. 162 | # Fills the $activeHelp and $completions arrays. 163 | __%[1]s_extract_activeHelp() { 164 | local activeHelpMarker="_activeHelp_ " 165 | local endIndex=${#activeHelpMarker} 166 | 167 | while IFS='' read -r comp; do 168 | if [[ ${comp:0:endIndex} == $activeHelpMarker ]]; then 169 | comp=${comp:endIndex} 170 | __%[1]s_debug "ActiveHelp found: $comp" 171 | if [[ -n $comp ]]; then 172 | activeHelp+=("$comp") 173 | fi 174 | else 175 | # Not an activeHelp line but a normal completion 176 | completions+=("$comp") 177 | fi 178 | done <<<"${out}" 179 | } 180 | 181 | __%[1]s_handle_completion_types() { 182 | __%[1]s_debug "__%[1]s_handle_completion_types: COMP_TYPE is $COMP_TYPE" 183 | 184 | case $COMP_TYPE in 185 | 37|42) 186 | # Type: menu-complete/menu-complete-backward and insert-completions 187 | # If the user requested inserting one completion at a time, or all 188 | # completions at once on the command-line we must remove the descriptions. 189 | # https://github.com/spf13/cobra/issues/1508 190 | local tab=$'\t' comp 191 | while IFS='' read -r comp; do 192 | [[ -z $comp ]] && continue 193 | # Strip any description 194 | comp=${comp%%%%$tab*} 195 | # Only consider the completions that match 196 | #if [[ $comp == "$cur"* ]]; then 197 | COMPREPLY+=("$comp") 198 | #fi 199 | done < <(printf "%%s\n" "${completions[@]}") 200 | ;; 201 | 202 | *) 203 | # Type: complete (normal completion) 204 | __%[1]s_handle_standard_completion_case 205 | ;; 206 | esac 207 | } 208 | 209 | __%[1]s_handle_standard_completion_case() { 210 | local tab=$'\t' comp 211 | 212 | # Short circuit to optimize if we don't have descriptions 213 | if [[ "${completions[*]}" != *$tab* ]]; then 214 | IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur") 215 | return 0 216 | fi 217 | 218 | local longest=0 219 | local compline 220 | # Look for the longest completion so that we can format things nicely 221 | while IFS='' read -r compline; do 222 | [[ -z $compline ]] && continue 223 | # Strip any description before checking the length 224 | comp=${compline%%%%$tab*} 225 | # Only consider the completions that match 226 | #[[ $comp == "$cur"* ]] || continue 227 | COMPREPLY+=("$compline") 228 | if ((${#comp}>longest)); then 229 | longest=${#comp} 230 | fi 231 | done < <(printf "%%s\n" "${completions[@]}") 232 | 233 | # If there is a single completion left, remove the description text 234 | if ((${#COMPREPLY[*]} == 1)); then 235 | __%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}" 236 | comp="${COMPREPLY[0]%%%%$tab*}" 237 | __%[1]s_debug "Removed description from single completion, which is now: ${comp}" 238 | COMPREPLY[0]=$comp 239 | else # Format the descriptions 240 | __%[1]s_format_comp_descriptions $longest 241 | fi 242 | } 243 | 244 | __%[1]s_handle_special_char() 245 | { 246 | local comp="$1" 247 | local char=$2 248 | if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then 249 | local word=${comp%%"${comp##*${char}}"} 250 | local idx=${#COMPREPLY[*]} 251 | while ((--idx >= 0)); do 252 | COMPREPLY[idx]=${COMPREPLY[idx]#"$word"} 253 | done 254 | fi 255 | } 256 | 257 | __%[1]s_format_comp_descriptions() 258 | { 259 | local tab=$'\t' 260 | local comp desc maxdesclength 261 | local longest=$1 262 | 263 | local i ci 264 | for ci in ${!COMPREPLY[*]}; do 265 | comp=${COMPREPLY[ci]} 266 | # Properly format the description string which follows a tab character if there is one 267 | if [[ "$comp" == *$tab* ]]; then 268 | __%[1]s_debug "Original comp: $comp" 269 | desc=${comp#*$tab} 270 | comp=${comp%%%%$tab*} 271 | 272 | # $COLUMNS stores the current shell width. 273 | # Remove an extra 4 because we add 2 spaces and 2 parentheses. 274 | maxdesclength=$(( COLUMNS - longest - 4 )) 275 | 276 | # Make sure we can fit a description of at least 8 characters 277 | # if we are to align the descriptions. 278 | if ((maxdesclength > 8)); then 279 | # Add the proper number of spaces to align the descriptions 280 | for ((i = ${#comp} ; i < longest ; i++)); do 281 | comp+=" " 282 | done 283 | else 284 | # Don't pad the descriptions so we can fit more text after the completion 285 | maxdesclength=$(( COLUMNS - ${#comp} - 4 )) 286 | fi 287 | 288 | # If there is enough space for any description text, 289 | # truncate the descriptions that are too long for the shell width 290 | if ((maxdesclength > 0)); then 291 | if ((${#desc} > maxdesclength)); then 292 | desc=${desc:0:$(( maxdesclength - 1 ))} 293 | desc+="…" 294 | fi 295 | comp+=" ($desc)" 296 | fi 297 | COMPREPLY[ci]=$comp 298 | __%[1]s_debug "Final comp: $comp" 299 | fi 300 | done 301 | } 302 | 303 | __start_%[1]s() 304 | { 305 | local cur prev words cword split 306 | 307 | COMPREPLY=() 308 | 309 | # Call _init_completion from the bash-completion package 310 | # to prepare the arguments properly 311 | if declare -F _init_completion >/dev/null 2>&1; then 312 | _init_completion -n =: || return 313 | else 314 | __%[1]s_init_completion -n =: || return 315 | fi 316 | 317 | __%[1]s_debug 318 | __%[1]s_debug "========= starting completion logic ==========" 319 | __%[1]s_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" 320 | 321 | # The user could have moved the cursor backwards on the command-line. 322 | # We need to trigger completion from the $cword location, so we need 323 | # to truncate the command-line ($words) up to the $cword location. 324 | words=("${words[@]:0:$cword+1}") 325 | __%[1]s_debug "Truncated words[*]: ${words[*]}," 326 | 327 | local out directive 328 | __%[1]s_get_completion_results 329 | __%[1]s_process_completion_results 330 | } 331 | 332 | if [[ $(type -t compopt) = "builtin" ]]; then 333 | complete -o default -F __start_%[1]s %[1]s 334 | else 335 | complete -o default -o nospace -F __start_%[1]s %[1]s 336 | fi 337 | 338 | # ex: ts=4 sw=4 et filetype=sh 339 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package ox_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/netip" 7 | "net/url" 8 | "time" 9 | 10 | "github.com/xo/ox" 11 | ) 12 | 13 | // Example demonstrates quickly using the xo/ox package. 14 | func Example() { 15 | type Verbosity int 16 | args := struct { 17 | MyString string `ox:"a string,short:s"` 18 | MyBool bool `ox:"a bool,short:b"` 19 | Ints []int `ox:"a slice of ints,short:i"` 20 | Date time.Time `ox:"formatted date,type:date,short:d"` 21 | MyURL *url.URL `ox:"a url,name:url,short:u"` 22 | Verbose Verbosity `ox:"enable verbose,short:v,type:count"` 23 | Sub struct { 24 | Bools map[string]bool `ox:"bool map,short:y"` 25 | } `ox:""` 26 | Extra struct { 27 | SomeOtherString string `ox:"another string,short:S"` 28 | AReallyLongName string `ox:"long arg"` 29 | } `ox:"x"` 30 | }{} 31 | ox.Run( 32 | // ox.Exec(myFunc), 33 | ox.Usage("myApp", "my app"), 34 | ox.Defaults( 35 | ox.Banner(`an example ox app.`), 36 | ox.Footer(`See: https://github.com/xo/ox for more information.`), 37 | ), 38 | ox.From(&args), 39 | ) 40 | // Output: 41 | // an example ox app. 42 | // 43 | // Usage: 44 | // myApp [flags] [args] 45 | // 46 | // Flags: 47 | // -s, --my-string string a string 48 | // -b, --my-bool a bool 49 | // -i, --ints int a slice of ints 50 | // -d, --date date formatted date 51 | // -u, --url url a url 52 | // -v, --verbose enable verbose 53 | // -y, --sub-bools string=bool bool map 54 | // -S, --x-some-other-string string another string 55 | // --x-a-really-long-name string long arg 56 | // --version show version, then exit 57 | // -h, --help show help, then exit 58 | // 59 | // See: https://github.com/xo/ox for more information. 60 | } 61 | 62 | // Example_argsTest demonstrates testing arbitrary command-line invocations by 63 | // setting the arguments to [Parse] with [ox.Args]. 64 | func Example_argsTest() { 65 | args := struct { 66 | Number float64 `ox:"a number"` 67 | }{} 68 | subArgs := struct { 69 | URL *url.URL `ox:"a url,short:u"` 70 | Addr *netip.Addr `ox:"an ip address"` 71 | }{} 72 | ox.RunContext( 73 | context.Background(), 74 | // ox.Exec(myFunc), 75 | ox.Usage("extest", "test example"), 76 | ox.Defaults(), 77 | ox.From(&args), 78 | ox.Sub( 79 | ox.Exec(func() error { 80 | // return an error to show that this func is not called 81 | return errors.New("oops!") 82 | }), 83 | ox.Usage("sub", "a sub command to test"), 84 | ox.From(&subArgs), 85 | ox.Sort(true), 86 | ), 87 | ox.Sort(true), 88 | // the command line args to test 89 | ox.Args("help", "sub"), 90 | ) 91 | // Output: 92 | // sub a sub command to test 93 | // 94 | // Usage: 95 | // extest sub [flags] [args] 96 | // 97 | // Flags: 98 | // -u, --url url a url 99 | // --addr addr an ip address 100 | // -h, --help show help, then exit 101 | } 102 | 103 | // Example_psql demonstrates building complex help output, based on original 104 | // output of `psql --help`. The output formatting has been slightly changed, as 105 | // the generated help output alters the column formatting, and the output 106 | // cannot be duplicated perfectly -- that said, a faithful attempt has been 107 | // made to stick to the original help output wherever possible. 108 | func Example_psql() { 109 | args := struct { 110 | Command string `ox:"run only single command (SQL or internal) and exit,short:c,section:0"` 111 | Dbname string `ox:"database name to connect to,short:d,default:$USER,section:0"` 112 | File string `ox:"execute commands from file\\, then exit,short:f,spec:FILENAME,section:0"` 113 | List bool `ox:"list databases\\, then exit,short:l,section:0"` 114 | Variable map[string]string `ox:"set psql variable NAME to VALUE,short:v,alias:set,spec:NAME=VALUE,section:0"` 115 | Version bool `ox:"output version information\\, then exit,hook:version,short:V,section:0"` 116 | NoPsqlrc bool `ox:"do not read startup file (~/.psqlrc),short:X,section:0"` 117 | SingleTransaction bool `ox:"execute as a single transaction (if non-interactive),short:1,section:0"` 118 | Help bool `ox:"show this help\\, then exit,short:?,hook:help,section:0"` 119 | 120 | EchoAll bool `ox:"echo all input from script,short:a,section:1"` 121 | EchoErrors bool `ox:"echo failed commands,short:b,section:1"` 122 | EchoQueries bool `ox:"echo commands sent to server,short:e,section:1"` 123 | EchoHidden bool `ox:"disply queries that internal commands generate,short:E,section:1"` 124 | LogFile string `ox:"send session log to file,spec:FILENAME,short:L,section:1"` 125 | NoReadline bool `ox:"display enhanced command line editing (readline),short:L,section:1"` 126 | Output string `ox:"send query results to file (or |pipe),short:o,section:1"` 127 | Quiet bool `ox:"run quietly (no messages\\, only query output),short:q,section:1"` 128 | SingleStep bool `ox:"single-step mode (confirm each query),short:s,section:1"` 129 | SingleLine bool `ox:"single-line mode (end of line terminates SQL command),short:S,section:1"` 130 | 131 | NoAlign bool `ox:"unaligned table output mode,short:A,section:2"` 132 | CSV bool `ox:"CSV (Comma-Separated Values) table output mode,section:2"` 133 | FieldSeparator string `ox:"field separator for unaligned output,default:|,short:F,section:2"` 134 | HTML bool `ox:"HTML table output mode,short:H,section:2"` 135 | Pset map[string]string `ox:"set printing option VAR to ARG (see \\pset command),short:P,spec:VAR[=ARG],section:2"` 136 | RecordSeparator string `ox:"record separator for unaligned output,default:newline,short:R,section:2"` 137 | TuplesOnly bool `ox:"print rows only,short:t,section:2"` 138 | TableAttr string `ox:"set HTML table tag attributes (e.g.\\, width\\, border),short:T,section:2"` 139 | Expanded bool `ox:"turn on expanded table output,short:x,section:2"` 140 | FieldSeparatorZero string `ox:"set field separator for unaligned output to zero byte,short:z,section:2"` 141 | RecordSeparatorZero string `ox:"set record separator for unaligned output to zero byte,short:0,section:2"` 142 | 143 | Host string `ox:"database server host or socket directory,default:local socket,spec:HOSTNAME,short:h,section:3"` 144 | Port uint `ox:"database server port,default:5432,spec:PORT,short:p,section:3"` 145 | Username string `ox:"database user name,default:$USER,spec:USERNAME,short:U,section:3"` 146 | NoPassword bool `ox:"never prompt for password,short:w,section:3"` 147 | Password bool `ox:"force password prompt (should happen automatically),short:W,section:3"` 148 | }{} 149 | ox.Run( 150 | // ox.Exec(myFunc), 151 | ox.Usage("psql", "the PostgreSQL interactive terminal"), 152 | ox.Help( 153 | ox.Banner("psql is the PostgreSQL interactive terminal."), 154 | ox.Spec("[OPTION]... [DBNAME [USERNAME]]"), 155 | ox.Sections( 156 | "General options", 157 | "Input and output options", 158 | "Output format options", 159 | "Connection options", 160 | ), 161 | ox.Footer(`For more information, type "\?" (for internal commands) or "\help" (for SQL 162 | commands) from within psql, or consult the psql section in the PostgreSQL 163 | documentation. 164 | 165 | Report bugs to . 166 | PostgreSQL home page: `), 167 | ), 168 | ox.From(&args), 169 | // override replacement key expansion 170 | ox.OverrideMap(map[string]string{ 171 | "USER": "fuser", 172 | }), 173 | ) 174 | // Output: 175 | // psql is the PostgreSQL interactive terminal. 176 | // 177 | // Usage: 178 | // psql [OPTION]... [DBNAME [USERNAME]] 179 | // 180 | // General options: 181 | // -c, --command string run only single command (SQL or internal) and exit 182 | // -d, --dbname string database name to connect to (default: fuser) 183 | // -f, --file FILENAME execute commands from file, then exit 184 | // -l, --list list databases, then exit 185 | // -v, --variable NAME=VALUE set psql variable NAME to VALUE 186 | // -V, --version output version information, then exit 187 | // -X, --no-psqlrc do not read startup file (~/.psqlrc) 188 | // -1, --single-transaction execute as a single transaction (if non-interactive) 189 | // -?, --help show this help, then exit 190 | // 191 | // Input and output options: 192 | // -a, --echo-all echo all input from script 193 | // -b, --echo-errors echo failed commands 194 | // -e, --echo-queries echo commands sent to server 195 | // -E, --echo-hidden disply queries that internal commands generate 196 | // -L, --log-file FILENAME send session log to file 197 | // -L, --no-readline display enhanced command line editing (readline) 198 | // -o, --output string send query results to file (or |pipe) 199 | // -q, --quiet run quietly (no messages, only query output) 200 | // -s, --single-step single-step mode (confirm each query) 201 | // -S, --single-line single-line mode (end of line terminates SQL command) 202 | // 203 | // Output format options: 204 | // -A, --no-align unaligned table output mode 205 | // --csv CSV (Comma-Separated Values) table output mode 206 | // -F, --field-separator string field separator for unaligned output (default: |) 207 | // -H, --html HTML table output mode 208 | // -P, --pset VAR[=ARG] set printing option VAR to ARG (see pset command) 209 | // -R, --record-separator string record separator for unaligned output (default: newline) 210 | // -t, --tuples-only print rows only 211 | // -T, --table-attr string set HTML table tag attributes (e.g., width, border) 212 | // -x, --expanded turn on expanded table output 213 | // -z, --field-separator-zero string set field separator for unaligned output to zero byte 214 | // -0, --record-separator-zero string set record separator for unaligned output to zero byte 215 | // 216 | // Connection options: 217 | // -h, --host HOSTNAME database server host or socket directory (default: local 218 | // socket) 219 | // -p, --port PORT database server port (default: 5432) 220 | // -U, --username USERNAME database user name (default: fuser) 221 | // -w, --no-password never prompt for password 222 | // -W, --password force password prompt (should happen automatically) 223 | // 224 | // For more information, type "\?" (for internal commands) or "\help" (for SQL 225 | // commands) from within psql, or consult the psql section in the PostgreSQL 226 | // documentation. 227 | // 228 | // Report bugs to . 229 | // PostgreSQL home page: 230 | } 231 | 232 | // Example_sections demonstrates setting the help section for commands and 233 | // flags, including default `--help` flag and `help` command. 234 | func Example_sections() { 235 | args := struct { 236 | Config string `ox:"config file,spec:FILE,section:1"` 237 | MyInts []int `ox:"a integer slice,short:i"` 238 | URLMap map[int]*url.URL `ox:"urls,short:U"` 239 | }{} 240 | ox.Run( 241 | ox.Usage("tree", "a command tree"), 242 | ox.Defaults(ox.Sections( 243 | "Normal flags", 244 | "More flags", 245 | "Other flags", 246 | )), 247 | ox.Sub( 248 | ox.Usage("sub1", "the sub1 command"), 249 | ox.Section(0), 250 | ), 251 | ox.Sub( 252 | ox.Usage("sub2.b", "the sub2.b command"), 253 | ), 254 | ox.Sub( 255 | ox.Usage("sub2.a", "the sub2.a command"), 256 | ox.Section(1), 257 | ), 258 | ox.Sections( 259 | "Primary commands", 260 | "Secondary commands", 261 | ), 262 | ox.SectionMap{ 263 | "help": 0, 264 | "sub2.b": 1, 265 | "flag:help": 0, 266 | "flag:my-ints": 2, 267 | }, 268 | ox.From(&args), 269 | ) 270 | // Output: 271 | // tree a command tree 272 | // 273 | // Usage: 274 | // tree [flags] [command] [args] 275 | // 276 | // Available Commands: 277 | // completion generate completion script for a specified shell 278 | // version show tree version information 279 | // 280 | // Primary commands: 281 | // help show help for any command 282 | // sub1 the sub1 command 283 | // 284 | // Secondary commands: 285 | // sub2.a the sub2.a command 286 | // sub2.b the sub2.b command 287 | // 288 | // Flags: 289 | // -U, --url-map int=url urls 290 | // 291 | // Normal flags: 292 | // -h, --help show help, then exit 293 | // 294 | // More flags: 295 | // --config FILE config file 296 | // 297 | // Other flags: 298 | // -i, --my-ints int a integer slice 299 | // 300 | // Use "tree [command] --help" for more information about a command. 301 | } 302 | 303 | // Example_help demonstrates configuring help output. 304 | func Example_help() { 305 | ox.Run( 306 | ox.Usage("cmdtree", "help command tree"), 307 | ox.Defaults(ox.Sort(true)), 308 | ox.Sub( 309 | ox.Usage("sub1", "sub1 tree"), 310 | ox.Sub( 311 | ox.Usage("sub2", "sub2 tree"), 312 | ox.Aliases("subcommand2", "subby2"), 313 | ox.Flags(). 314 | String("my-flag", "my flag"). 315 | BigInt("big-int", "big int", ox.Short("B")). 316 | Int("a", "the a int"), 317 | ox.Sub(ox.Usage("sub3", "sub3 tree")), 318 | ox.Sub(ox.Usage("a", "another command")), 319 | ), 320 | ), 321 | ox.Args("help", "sub1", "subcommand2", "--bad-flag", "-b"), 322 | ) 323 | // Output: 324 | // sub2 sub2 tree 325 | // 326 | // Usage: 327 | // cmdtree sub1 sub2 [flags] [command] [args] 328 | // 329 | // Aliases: 330 | // sub2, subcommand2, subby2 331 | // 332 | // Available Commands: 333 | // a another command 334 | // sub3 sub3 tree 335 | // 336 | // Flags: 337 | // --a int the a int 338 | // -B, --big-int bigint big int 339 | // -h, --help show help, then exit 340 | // --my-flag string my flag 341 | // 342 | // Use "cmdtree sub1 sub2 [command] --help" for more information about a command. 343 | } 344 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "cmp" 5 | "encoding/base64" 6 | "encoding/hex" 7 | "fmt" 8 | "maps" 9 | "reflect" 10 | "slices" 11 | "strings" 12 | "sync/atomic" 13 | "time" 14 | ) 15 | 16 | // Value is the value interface. 17 | type Value interface { 18 | Type() Type 19 | Val() any 20 | SetSet(bool) 21 | WasSet() bool 22 | Set(string) error 23 | Get() (string, error) 24 | String() string 25 | } 26 | 27 | // anyVal wraps a value. 28 | type anyVal[T any] struct { 29 | typ Type 30 | valid func(any) error 31 | set bool 32 | v T 33 | } 34 | 35 | // NewVal returns a [Value] func storing the value as type T. 36 | func NewVal[T any](opts ...Option) func() (Value, error) { 37 | return func() (Value, error) { 38 | val := &anyVal[T]{ 39 | typ: typeType[T](), 40 | } 41 | if err := Apply(val, opts...); err != nil { 42 | return nil, err 43 | } 44 | return val, nil 45 | } 46 | } 47 | 48 | func (val *anyVal[T]) Type() Type { 49 | return val.typ 50 | } 51 | 52 | func (val *anyVal[T]) Val() any { 53 | return val.v 54 | } 55 | 56 | func (val *anyVal[T]) SetType(typ Type) { 57 | val.typ = typ 58 | } 59 | 60 | func (val *anyVal[T]) SetSet(set bool) { 61 | val.set = set 62 | } 63 | 64 | func (val *anyVal[T]) WasSet() bool { 65 | return val.set 66 | } 67 | 68 | func (val *anyVal[T]) SetValid(valid func(any) error) { 69 | val.valid = valid 70 | } 71 | 72 | func (val *anyVal[T]) Set(s string) error { 73 | var value any = s 74 | switch val.typ { 75 | case Base64T: 76 | b, err := base64.StdEncoding.DecodeString(s) 77 | if err != nil { 78 | return fmt.Errorf("%w: %w", ErrInvalidValue, err) 79 | } 80 | value = b 81 | case HexT: 82 | b, err := hex.DecodeString(s) 83 | if err != nil { 84 | return fmt.Errorf("%w: %w", ErrInvalidValue, err) 85 | } 86 | value = b 87 | case ByteT: 88 | s, err := asByte(s) 89 | if err != nil { 90 | return fmt.Errorf("%w: %w", ErrInvalidValue, err) 91 | } 92 | value = s 93 | case RuneT: 94 | s, err := asRune(s) 95 | if err != nil { 96 | return fmt.Errorf("%w: %w", ErrInvalidValue, err) 97 | } 98 | value = s 99 | case CountT: 100 | inc(&val.v, 1) 101 | return nil 102 | } 103 | // convert 104 | z, err := as[T](value, layout(val.v)) 105 | if err != nil { 106 | return fmt.Errorf("%w: %w", ErrInvalidValue, err) 107 | } 108 | // validate 109 | v, ok := z.(T) 110 | switch { 111 | case !ok: 112 | return fmt.Errorf("%w: %T->%T", ErrInvalidConversion, value, val.v) 113 | case val.valid != nil: 114 | if err := val.valid(v); err != nil { 115 | return err 116 | } 117 | } 118 | val.v = v 119 | return nil 120 | } 121 | 122 | func (val *anyVal[T]) Get() (string, error) { 123 | if invalid(val.v) { 124 | return "", nil 125 | } 126 | v, err := as[string](val.v, layout(val.v)) 127 | if err != nil { 128 | return "", fmt.Errorf("%w: %w", ErrInvalidValue, err) 129 | } 130 | if s, ok := v.(string); ok { 131 | return s, nil 132 | } 133 | return "", fmt.Errorf("%w: %T->string", ErrInvalidConversion, v) 134 | } 135 | 136 | func (val *anyVal[T]) String() string { 137 | s, _ := val.Get() 138 | return s 139 | } 140 | 141 | // NewTime returns a Value func for a [FormattedTime] value. 142 | func NewTime(typ Type, layout string) func() (Value, error) { 143 | return func() (Value, error) { 144 | val := &anyVal[FormattedTime]{ 145 | typ: typ, 146 | v: FormattedTime{layout: layout}, 147 | } 148 | if val.typ == "" { 149 | val.typ = TimestampT 150 | } 151 | if val.v.layout == "" { 152 | val.v.layout = DefaultLayout 153 | } 154 | return val, nil 155 | } 156 | } 157 | 158 | // sliceVal is a slice value. 159 | type sliceVal struct { 160 | typ Type 161 | v []Value 162 | set bool 163 | valid func(any) error 164 | split func(string) []string 165 | } 166 | 167 | // NewSlice creates a slice value of type. 168 | func NewSlice(typ Type) Value { 169 | return &sliceVal{ 170 | typ: typ, 171 | split: func(s string) []string { 172 | return SplitBy(s, ',') 173 | }, 174 | } 175 | } 176 | 177 | // NewArray creates a slice value of type. 178 | func NewArray(typ Type) Value { 179 | return &sliceVal{ 180 | typ: typ, 181 | } 182 | } 183 | 184 | func (val *sliceVal) Type() Type { 185 | return "[]" + val.typ 186 | } 187 | 188 | func (val *sliceVal) Val() any { 189 | return val.v 190 | } 191 | 192 | func (val *sliceVal) Get() (string, error) { 193 | return val.String(), nil 194 | } 195 | 196 | func (val *sliceVal) SetSet(set bool) { 197 | val.set = set 198 | } 199 | 200 | func (val *sliceVal) WasSet() bool { 201 | return val.set 202 | } 203 | 204 | func (val *sliceVal) Set(s string) error { 205 | var strs []string 206 | if val.split != nil { 207 | strs = val.split(s) 208 | } else { 209 | strs = []string{s} 210 | } 211 | for i, str := range strs { 212 | v, err := val.typ.New() 213 | if err != nil { 214 | return err 215 | } 216 | setValid(v, val.valid) 217 | switch err := v.Set(str); { 218 | case err != nil && 1 < len(strs): 219 | return fmt.Errorf("set %d: %w", i+1, err) 220 | case err != nil: 221 | return err 222 | } 223 | val.v = append(val.v, v) 224 | } 225 | return nil 226 | } 227 | 228 | func (val *sliceVal) String() string { 229 | s := make([]string, len(val.v)) 230 | for i, v := range val.v { 231 | s[i] = toString[string](v) 232 | } 233 | return "[" + strings.Join(s, " ") + "]" 234 | } 235 | 236 | // SetValid sets the valid func. 237 | func (val *sliceVal) SetValid(valid func(any) error) { 238 | val.valid = valid 239 | } 240 | 241 | // SetSplit sets the split func. 242 | func (val *sliceVal) SetSplit(split func(string) []string) { 243 | val.split = split 244 | } 245 | 246 | // Index returns the i'th variable from the slice. 247 | func (val *sliceVal) Index(i int) Value { 248 | return val.v[i] 249 | } 250 | 251 | // Len returns the slice length. 252 | func (val *sliceVal) Len() int { 253 | return len(val.v) 254 | } 255 | 256 | // mapVal is a map value. 257 | type mapVal struct { 258 | key Type 259 | typ Type 260 | set bool 261 | v valueMap 262 | valid func(string) error 263 | split func(string) []string 264 | } 265 | 266 | // NewMap creates a map value of type. 267 | func NewMap(key, typ Type) (Value, error) { 268 | val := &mapVal{ 269 | key: key, 270 | typ: typ, 271 | split: func(s string) []string { 272 | return SplitBy(s, ',') 273 | }, 274 | } 275 | var err error 276 | if val.v, err = makeMap(val.key, val.typ); err != nil { 277 | return nil, err 278 | } 279 | return val, nil 280 | } 281 | 282 | func (val *mapVal) Type() Type { 283 | return "map[" + val.key + "]" + val.typ 284 | } 285 | 286 | func (val *mapVal) Val() any { 287 | return val.v 288 | } 289 | 290 | func (val *mapVal) SetSet(set bool) { 291 | val.set = set 292 | } 293 | 294 | func (val *mapVal) WasSet() bool { 295 | return val.set 296 | } 297 | 298 | func (val *mapVal) Set(s string) error { 299 | var strs []string 300 | if val.split != nil { 301 | strs = val.split(s) 302 | } else { 303 | strs = []string{s} 304 | } 305 | for i, str := range strs { 306 | switch err := val.v.Set(str); { 307 | case err != nil && 1 < len(strs): 308 | return fmt.Errorf("set %d: %w", i, err) 309 | case err != nil: 310 | return err 311 | } 312 | } 313 | return nil 314 | } 315 | 316 | func (val *mapVal) String() string { 317 | return val.v.String() 318 | } 319 | 320 | func (val *mapVal) Get() (string, error) { 321 | return val.v.String(), nil 322 | } 323 | 324 | // SetValid sets the valid func. 325 | func (val *mapVal) SetValid(valid func(string) error) { 326 | val.valid = valid 327 | } 328 | 329 | // SetSplit sets the split func. 330 | func (val *mapVal) SetSplit(split func(string) []string) { 331 | val.split = split 332 | } 333 | 334 | type valueMap interface { 335 | Set(s string) error 336 | String() string 337 | Keys() []any 338 | Get(any) Value 339 | } 340 | 341 | // makeMap makes a map for the key and type. 342 | func makeMap(key, typ Type) (valueMap, error) { 343 | switch key { 344 | case StringT: 345 | return newValueMap[string](typ), nil 346 | case Int64T: 347 | return newValueMap[int64](typ), nil 348 | case Int32T: 349 | return newValueMap[int32](typ), nil 350 | case Int16T: 351 | return newValueMap[int16](typ), nil 352 | case Int8T: 353 | return newValueMap[int8](typ), nil 354 | case IntT: 355 | return newValueMap[int](typ), nil 356 | case Uint64T: 357 | return newValueMap[uint64](typ), nil 358 | case Uint32T: 359 | return newValueMap[uint32](typ), nil 360 | case Uint16T: 361 | return newValueMap[uint16](typ), nil 362 | case Uint8T: 363 | return newValueMap[uint8](typ), nil 364 | case UintT: 365 | return newValueMap[uint](typ), nil 366 | case Float64T: 367 | return newValueMap[float64](typ), nil 368 | case Float32T: 369 | return newValueMap[float32](typ), nil 370 | } 371 | return nil, fmt.Errorf("%w: bad map key type %s", ErrInvalidType, key) 372 | } 373 | 374 | // valMap wraps a map. 375 | type valMap[K cmp.Ordered] struct { 376 | typ Type 377 | valid func(any) error 378 | v map[K]Value 379 | } 380 | 381 | // newValueMap creates a new value map. 382 | func newValueMap[K cmp.Ordered](typ Type) valueMap { 383 | return &valMap[K]{ 384 | typ: typ, 385 | } 386 | } 387 | 388 | // SetValid sets the valid func for the value map. 389 | func (val *valMap[K]) SetValid(valid func(any) error) { 390 | val.valid = valid 391 | } 392 | 393 | func (val *valMap[K]) Set(s string) error { 394 | if val.v == nil { 395 | val.v = make(map[K]Value) 396 | } 397 | keystr, value, ok := strings.Cut(s, "=") 398 | if !ok || keystr == "" { 399 | return fmt.Errorf("%w %q: %s", ErrInvalidValue, s, "missing map key") 400 | } 401 | key, err := as[K](keystr, "") 402 | if err != nil { 403 | return fmt.Errorf("bad map key: %q: %w", keystr, err) 404 | } 405 | k, ok := key.(K) 406 | if !ok { 407 | return fmt.Errorf("%w: string->%T", ErrInvalidConversion, k) 408 | } 409 | v, ok := val.v[k] 410 | if !ok { 411 | if v, err = val.typ.New(); err != nil { 412 | return err 413 | } 414 | setValid(v, val.valid) 415 | } 416 | if err := v.Set(value); err != nil { 417 | return err 418 | } 419 | val.v[k] = v 420 | return nil 421 | } 422 | 423 | func (val valMap[K]) String() string { 424 | s := make([]string, len(val.v)) 425 | for i, k := range slices.Sorted(maps.Keys(val.v)) { 426 | value, _ := val.v[k].Get() 427 | s[i] = toString[string](k) + ":" + value 428 | } 429 | return "[" + strings.Join(s, " ") + "]" 430 | } 431 | 432 | func (val valMap[K]) Keys() []any { 433 | keys := make([]any, len(val.v)) 434 | for i, k := range slices.Sorted(maps.Keys(val.v)) { 435 | keys[i] = k 436 | } 437 | return keys 438 | } 439 | 440 | func (val valMap[K]) Get(key any) Value { 441 | if k, ok := key.(K); ok { 442 | return val.v[k] 443 | } 444 | return nil 445 | } 446 | 447 | // Binder is the interface for binding values. 448 | type Binder interface { 449 | Bind(string) error 450 | SetSet(bool) 451 | } 452 | 453 | // bindVal is a reflection bound value. 454 | type bindVal struct { 455 | v reflect.Value 456 | set *bool 457 | split func(string) []string 458 | } 459 | 460 | // NewBindRef binds [reflect.Value] value and its set flag. 461 | func NewBindRef(value reflect.Value, set *bool) (Binder, error) { 462 | switch { 463 | case value.Kind() != reflect.Pointer, value.IsNil(): 464 | return nil, fmt.Errorf("%w: not a pointer or is nil", ErrInvalidValue) 465 | } 466 | return &bindVal{ 467 | v: value, 468 | set: set, 469 | }, nil 470 | } 471 | 472 | // NewBind binds a value and its set flag. 473 | func NewBind[T *E, E any](v T, set *bool) (Binder, error) { 474 | return NewBindRef(reflect.ValueOf(v), set) 475 | } 476 | 477 | // SetSplit sets the split func. 478 | func (val *bindVal) SetSplit(split func(string) []string) { 479 | val.split = split 480 | } 481 | 482 | func (val *bindVal) Get() any { 483 | return val.v.Elem().Interface() 484 | } 485 | 486 | // String satisfies the [fmt.Formatter] interface. 487 | // 488 | // Helps with friendlier error messages. 489 | func (val *bindVal) String() string { 490 | return val.v.Type().String() 491 | } 492 | 493 | // SetSet satisfies the [Binder] interface. 494 | func (val *bindVal) SetSet(set bool) { 495 | if val.set != nil { 496 | *val.set = set 497 | } 498 | } 499 | 500 | // Bind satisfies the [Binder] interface. 501 | func (val *bindVal) Bind(s string) error { 502 | typ := val.v.Elem().Type() 503 | switch typ.Kind() { 504 | case reflect.Slice: 505 | return val.sliceSet(s) 506 | case reflect.Map: 507 | return val.mapSet(s) 508 | case reflect.Pointer: 509 | v, err := asUnmarshal(reflectType(typ), s) 510 | if err != nil { 511 | return err 512 | } 513 | reflect.Indirect(val.v).Set(reflect.ValueOf(v)) 514 | return nil 515 | } 516 | return asValue(val.v, s) 517 | } 518 | 519 | // sliceSet sets value on slice. 520 | func (val *bindVal) sliceSet(s string) error { 521 | var values []string 522 | if val.split != nil { 523 | values = val.split(s) 524 | } else { 525 | values = []string{s} 526 | } 527 | typ := val.v.Elem().Type().Elem() 528 | for _, str := range values { 529 | v := reflect.New(typ) 530 | if err := asValue(v, str); err != nil { 531 | return err 532 | } 533 | reflect.Indirect(val.v).Set(reflect.Append(val.v.Elem(), reflect.Indirect(v))) 534 | } 535 | return nil 536 | } 537 | 538 | // mapSet sets value on map. 539 | func (val *bindVal) mapSet(s string) error { 540 | var values []string 541 | if val.split != nil { 542 | values = val.split(s) 543 | } else { 544 | values = []string{s} 545 | } 546 | typ := val.v.Elem().Type() 547 | for _, str := range values { 548 | key, value, ok := strings.Cut(str, "=") 549 | if !ok || key == "" { 550 | return ErrInvalidValue 551 | } 552 | // create map if nil 553 | if val.v.Elem().IsNil() { 554 | reflect.Indirect(val.v).Set(reflect.MakeMap(typ)) 555 | } 556 | // create key 557 | k := reflect.New(typ.Key()) 558 | if err := asValue(k, key); err != nil { 559 | return err 560 | } 561 | // create value 562 | v := reflect.New(typ.Elem()) 563 | if err := asValue(v, value); err != nil { 564 | return err 565 | } 566 | reflect.Indirect(val.v).SetMapIndex(reflect.Indirect(k), reflect.Indirect(v)) 567 | } 568 | return nil 569 | } 570 | 571 | // hookVal is a hook func. 572 | type hookVal struct { 573 | typ Type 574 | ctx *Context 575 | set bool 576 | v func(*Context, string) error 577 | } 578 | 579 | // newHook creates a new hook for the func f in v. 580 | func newHook(ctx *Context, v any) (Value, error) { 581 | val := &hookVal{ 582 | typ: HookT, 583 | ctx: ctx, 584 | } 585 | switch f := v.(type) { 586 | case func(*Context, string) error: 587 | val.v = f 588 | case func(*Context, string): 589 | val.v = func(ctx *Context, s string) error { 590 | f(ctx, s) 591 | return nil 592 | } 593 | case func(*Context) error: 594 | val.v = func(ctx *Context, _ string) error { 595 | return f(ctx) 596 | } 597 | case func(*Context): 598 | val.v = func(ctx *Context, _ string) error { 599 | f(ctx) 600 | return nil 601 | } 602 | case func(string) error: 603 | val.v = func(_ *Context, s string) error { 604 | return f(s) 605 | } 606 | case func(string): 607 | val.v = func(_ *Context, s string) error { 608 | f(s) 609 | return nil 610 | } 611 | case func() error: 612 | val.v = func(*Context, string) error { 613 | return f() 614 | } 615 | case func(): 616 | val.v = func(*Context, string) error { 617 | f() 618 | return nil 619 | } 620 | default: 621 | return nil, fmt.Errorf("%w: invalid hook func %T", ErrInvalidValue, v) 622 | } 623 | return val, nil 624 | } 625 | 626 | func (val *hookVal) Type() Type { 627 | return val.typ 628 | } 629 | 630 | func (val *hookVal) Val() any { 631 | return val.v 632 | } 633 | 634 | func (val *hookVal) SetSet(set bool) { 635 | val.set = set 636 | } 637 | 638 | func (val *hookVal) WasSet() bool { 639 | return val.set 640 | } 641 | 642 | func (val *hookVal) Set(s string) error { 643 | return val.v(val.ctx, s) 644 | } 645 | 646 | func (val *hookVal) Get() (string, error) { 647 | return "(hook)", nil 648 | } 649 | 650 | func (val *hookVal) String() string { 651 | return "(hook)" 652 | } 653 | 654 | // FormattedTime wraps a time value with a specific layout. 655 | type FormattedTime struct { 656 | layout string 657 | v time.Time 658 | } 659 | 660 | // Layout returns the layout. 661 | func (val FormattedTime) Layout() string { 662 | return val.layout 663 | } 664 | 665 | // Time returns the time value. 666 | func (val FormattedTime) Time() time.Time { 667 | return val.v 668 | } 669 | 670 | // Set sets parses the time value from the string. 671 | func (val *FormattedTime) Set(s string) error { 672 | var err error 673 | val.v, err = time.Parse(val.layout, s) 674 | return err 675 | } 676 | 677 | // Format formats the time value as the provided layout. 678 | func (val FormattedTime) Format(layout string) string { 679 | return val.v.Format(layout) 680 | } 681 | 682 | // String satisfies the [fmt.Stringer] interface. 683 | func (val FormattedTime) String() string { 684 | return val.v.Format(val.layout) 685 | } 686 | 687 | // IsValid returns true when the time is not zero. 688 | func (val FormattedTime) IsValid() bool { 689 | return !val.v.IsZero() 690 | } 691 | 692 | // inc increments the value. 693 | func inc(val any, delta uint64) { 694 | if v, ok := val.(*uint64); ok { 695 | atomic.AddUint64(v, delta) 696 | } 697 | } 698 | 699 | // invalid returns true if the value is invalid. 700 | func invalid(val any) bool { 701 | switch v := val.(type) { 702 | case interface{ IsValid() bool }: 703 | // netip.{Addr,AddrPort,Prefix} and FormattedTime 704 | return !v.IsValid() 705 | case interface{ IsZero() bool }: 706 | return v.IsZero() 707 | } 708 | return false 709 | } 710 | 711 | // valid creates a validator func for the provided values. 712 | func valid[T cmp.Ordered](values ...T) func(any) error { 713 | return func(val any) error { 714 | switch v, err := as[T](val, layout(val)); { 715 | case err != nil: 716 | return err 717 | case !slices.Contains(values, v.(T)): 718 | return ErrInvalidValue 719 | } 720 | return nil 721 | } 722 | } 723 | 724 | // layout returns the layout for the value. 725 | func layout(val any) string { 726 | if v, ok := val.(interface{ Layout() string }); ok { 727 | return v.Layout() 728 | } 729 | return "" 730 | } 731 | --------------------------------------------------------------------------------