commit 0b81160c3ceb42be9a8f7fae0a7e14fc77b51597 Author: jolheiser Date: Mon May 22 16:44:39 2023 -0500 initial commit Signed-off-by: jolheiser diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18ad64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/kv* diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..28142c1 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,25 @@ +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + ldflags: + - "-s -w -X main.Version={{.Version}}" +archives: + - replacements: + 386: i386 + amd64: x86_64 + format_overrides: + - goos: windows + format: zip +checksum: + name_template: 'checksums.txt' +release: + gitea: + owner: jolheiser + name: kv +gitea_urls: + api: https://git.jojodev.com/api/v1/ + download: https://git.jojodev.com diff --git a/.woodpecker/goreleaser.yml b/.woodpecker/goreleaser.yml new file mode 100644 index 0000000..cfeef80 --- /dev/null +++ b/.woodpecker/goreleaser.yml @@ -0,0 +1,38 @@ +clone: + git: + image: woodpeckerci/plugin-git + settings: + tags: true + +pipeline: + compliance: + image: golang:1.18 + commands: + - go test -race ./... + - go vet ./... + when: + event: pull_request + + build: + image: goreleaser/goreleaser + commands: + - goreleaser build --snapshot + when: + event: pull_request + + release: + image: goreleaser/goreleaser + commands: + - goreleaser release + secrets: [ gitea_token ] + when: + event: tag + + prune: + image: jolheiser/drone-gitea-prune + settings: + base: https://git.jojodev.com + token: + from_secret: gitea_token + when: + event: tag diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..56f09ee --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 John Olheiser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..170259d --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module go.jolheiser.com/kv + +go 1.19 + +require ( + github.com/adrg/xdg v0.4.0 + github.com/peterbourgon/ff/v3 v3.3.1 +) + +require ( + github.com/stretchr/testify v1.8.1 // indirect + golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2290383 --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/peterbourgon/ff/v3 v3.3.1 h1:XSWvXxeNdgeppLNGGJEAOiXRdX2YMF/LuZfdnqQ1SNc= +github.com/peterbourgon/ff/v3 v3.3.1/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..3c9005f --- /dev/null +++ b/main.go @@ -0,0 +1,212 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "io/fs" + "os" + "sort" + "strings" + + "github.com/adrg/xdg" + "github.com/peterbourgon/ff/v3" + "github.com/peterbourgon/ff/v3/ffcli" +) + +var Version = "develop" + +func main() { + defaultStore, err := xdg.ConfigFile("kv/store.json") + if err != nil { + defaultStore = "store.json" + } + + fs := flag.NewFlagSet("kv", flag.ContinueOnError) + storeFlag := fs.String("store", defaultStore, "Location of the store on disk") + fs.StringVar(storeFlag, "s", *storeFlag, "--store") + + a := &app{ + storeLocation: storeFlag, + } + + var app *ffcli.Command + app = &ffcli.Command{ + Name: "kv", + ShortUsage: fmt.Sprintf("Key/Value store - version %s", Version), + ShortHelp: "kv [get|set|del|list] ...", + FlagSet: fs, + Subcommands: []*ffcli.Command{ + a.get(), + a.set(), + a.del(), + a.list(), + }, + Options: []ff.Option{ + ff.WithEnvVarPrefix("KV"), + }, + Exec: func(_ context.Context, _ []string) error { + return errors.New(app.UsageFunc(app)) + }, + } + + if err := app.ParseAndRun(context.Background(), os.Args[1:]); err != nil { + if errors.Is(err, flag.ErrHelp) { + return + } + fmt.Println(err) + } +} + +type app struct { + storeLocation *string +} + +func (a *app) get() *ffcli.Command { + fs := flag.NewFlagSet("get", flag.ContinueOnError) + return &ffcli.Command{ + Name: "get", + FlagSet: fs, + Exec: func(ctx context.Context, args []string) error { + if len(args) < 1 { + return errors.New("get requires at least one argument") + } + + data, err := a.load() + if err != nil { + return err + } + + key := strings.ToLower(strings.Join(args, " ")) + val, ok := data[key] + if !ok { + return fmt.Errorf("no value found for %q", key) + } + fmt.Print(val) + return nil + }, + } +} + +func (a *app) set() *ffcli.Command { + fs := flag.NewFlagSet("set", flag.ContinueOnError) + return &ffcli.Command{ + Name: "set", + FlagSet: fs, + Exec: func(ctx context.Context, args []string) error { + if len(args) < 2 { + return errors.New("set requires at least two arguments") + } + + data, err := a.load() + if err != nil { + return err + } + + key := args[0] + data[key] = strings.Join(args[1:], " ") + if err := a.save(data); err != nil { + return err + } + fmt.Printf("set %q\n", key) + return nil + }, + } +} + +func (a *app) del() *ffcli.Command { + fs := flag.NewFlagSet("del", flag.ContinueOnError) + return &ffcli.Command{ + Name: "del", + FlagSet: fs, + Exec: func(ctx context.Context, args []string) error { + if len(args) < 1 { + return errors.New("del requires at least 1 argument") + } + + data, err := a.load() + if err != nil { + return err + } + + key := strings.ToLower(strings.Join(args, " ")) + if _, ok := data[key]; !ok { + return fmt.Errorf("no value found for %q", key) + } + delete(data, key) + if err := a.save(data); err != nil { + return err + } + + fmt.Printf("deleted %q\n", key) + + return nil + }, + } +} + +func (a *app) list() *ffcli.Command { + fs := flag.NewFlagSet("list", flag.ContinueOnError) + return &ffcli.Command{ + Name: "list", + FlagSet: fs, + Exec: func(ctx context.Context, args []string) error { + data, err := a.load() + if err != nil { + return err + } + + var keys []string + prefix := strings.ToLower(strings.Join(args, " ")) + for key := range data { + if strings.HasPrefix(key, prefix) { + keys = append(keys, key) + } + } + sort.Strings(keys) + + for _, key := range keys { + fmt.Println(key) + } + + return nil + }, + } +} + +func (a *app) load() (map[string]string, error) { + var m map[string]string + fi, err := os.Open(*a.storeLocation) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + fi, err = os.Create(*a.storeLocation) + if err != nil { + return m, err + } + if _, err := fi.WriteString("{}"); err != nil { + return m, err + } + if err := fi.Close(); err != nil { + return m, err + } + return a.load() + } + return m, err + } + defer fi.Close() + return m, json.NewDecoder(fi).Decode(&m) +} + +func (a *app) save(m map[string]string) error { + fi, err := os.Create(*a.storeLocation) + if err != nil { + return err + } + defer fi.Close() + + enc := json.NewEncoder(fi) + enc.SetIndent("", "\t") + return enc.Encode(&m) +}