├── .github ├── CODEOWNERS ├── workflows │ ├── lint-sync.yml │ ├── build.yml │ ├── dependabot-sync.yml │ ├── lint.yml │ └── goreleaser.yml └── dependabot.yml ├── .gitignore ├── .goreleaser.yml ├── .golangci.yml ├── LICENSE ├── go.mod ├── README.md ├── go.sum └── main.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @charmbracelet/everyone 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | skate 2 | dist/ 3 | completions/ 4 | manpages/ -------------------------------------------------------------------------------- /.github/workflows/lint-sync.yml: -------------------------------------------------------------------------------- 1 | name: lint-sync 2 | on: 3 | schedule: 4 | - cron: "0 0 * * 0" # every sunday at midnight 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | lint: 13 | uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | uses: charmbracelet/meta/.github/workflows/build.yml@main 8 | 9 | snapshot: 10 | uses: charmbracelet/meta/.github/workflows/snapshot.yml@main 11 | secrets: 12 | goreleaser_key: ${{ secrets.GORELEASER_KEY }} -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json 2 | 3 | version: 2 4 | 5 | includes: 6 | - from_url: 7 | url: charmbracelet/meta/main/goreleaser-simple.yaml 8 | 9 | variables: 10 | binary_name: skate 11 | description: "A personal key value store 🛼" 12 | github_url: "https://github.com/charmbracelet/skate" 13 | maintainer: "Ayman Bagabas " 14 | brew_commit_author_name: "Ayman Bagabas" 15 | brew_commit_author_email: "ayman@charm.sh" 16 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-sync.yml: -------------------------------------------------------------------------------- 1 | name: dependabot-sync 2 | on: 3 | schedule: 4 | - cron: "0 0 * * 0" # every Sunday at midnight 5 | workflow_dispatch: # allows manual triggering 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | dependabot-sync: 13 | uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main 14 | with: 15 | repo_name: ${{ github.event.repository.name }} 16 | secrets: 17 | gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | golangci: 8 | name: lint 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v6 12 | - name: golangci-lint 13 | uses: golangci/golangci-lint-action@v9.2.0 14 | with: 15 | # Optional: golangci-lint command line arguments. 16 | args: --issues-exit-code=0 17 | # Optional: show only new issues if it's a pull request. The default value is `false`. 18 | only-new-issues: true 19 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | concurrency: 9 | group: goreleaser 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | goreleaser: 14 | uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main 15 | secrets: 16 | docker_username: ${{ secrets.DOCKERHUB_USERNAME }} 17 | docker_token: ${{ secrets.DOCKERHUB_TOKEN }} 18 | gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 19 | goreleaser_key: ${{ secrets.GORELEASER_KEY }} 20 | fury_token: ${{ secrets.FURY_TOKEN }} 21 | nfpm_gpg_key: ${{ secrets.NFPM_GPG_KEY }} 22 | nfpm_passphrase: ${{ secrets.NFPM_PASSPHRASE }} 23 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | tests: false 4 | linters: 5 | enable: 6 | - bodyclose 7 | - exhaustive 8 | - goconst 9 | - godot 10 | - gomoddirectives 11 | - goprintffuncname 12 | - gosec 13 | - misspell 14 | - nakedret 15 | - nestif 16 | - nilerr 17 | - noctx 18 | - nolintlint 19 | - prealloc 20 | - revive 21 | - rowserrcheck 22 | - sqlclosecheck 23 | - tparallel 24 | - unconvert 25 | - unparam 26 | - whitespace 27 | - wrapcheck 28 | exclusions: 29 | rules: 30 | - text: '(slog|log)\.\w+' 31 | linters: 32 | - noctx 33 | generated: lax 34 | presets: 35 | - common-false-positives 36 | issues: 37 | max-issues-per-linter: 0 38 | max-same-issues: 0 39 | formatters: 40 | enable: 41 | - gofumpt 42 | - goimports 43 | exclusions: 44 | generated: lax 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Charmbracelet, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "monday" 9 | time: "05:00" 10 | timezone: "America/New_York" 11 | labels: 12 | - "dependencies" 13 | commit-message: 14 | prefix: "chore" 15 | include: "scope" 16 | groups: 17 | all: 18 | patterns: 19 | - "*" 20 | ignore: 21 | - dependency-name: github.com/charmbracelet/bubbletea/v2 22 | versions: 23 | - v2.0.0-beta1 24 | 25 | - package-ecosystem: "github-actions" 26 | directory: "/" 27 | schedule: 28 | interval: "weekly" 29 | day: "monday" 30 | time: "05:00" 31 | timezone: "America/New_York" 32 | labels: 33 | - "dependencies" 34 | commit-message: 35 | prefix: "chore" 36 | include: "scope" 37 | groups: 38 | all: 39 | patterns: 40 | - "*" 41 | 42 | - package-ecosystem: "docker" 43 | directory: "/" 44 | schedule: 45 | interval: "weekly" 46 | day: "monday" 47 | time: "05:00" 48 | timezone: "America/New_York" 49 | labels: 50 | - "dependencies" 51 | commit-message: 52 | prefix: "chore" 53 | include: "scope" 54 | groups: 55 | all: 56 | patterns: 57 | - "*" 58 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charmbracelet/skate 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/agnivade/levenshtein v1.2.1 7 | github.com/charmbracelet/fang v0.4.3 8 | github.com/charmbracelet/lipgloss v1.1.0 9 | github.com/dgraph-io/badger/v4 v4.8.0 10 | github.com/muesli/go-app-paths v0.2.2 11 | github.com/spf13/cobra v1.10.1 12 | golang.org/x/term v0.36.0 13 | ) 14 | 15 | require ( 16 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 17 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 18 | github.com/charmbracelet/colorprofile v0.3.2 // indirect 19 | github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea // indirect 20 | github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef // indirect 21 | github.com/charmbracelet/x/ansi v0.10.1 // indirect 22 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 23 | github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect 24 | github.com/charmbracelet/x/term v0.2.1 // indirect 25 | github.com/charmbracelet/x/termios v0.1.1 // indirect 26 | github.com/charmbracelet/x/windows v0.2.2 // indirect 27 | github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect 28 | github.com/dustin/go-humanize v1.0.1 // indirect 29 | github.com/go-logr/logr v1.4.3 // indirect 30 | github.com/go-logr/stdr v1.2.2 // indirect 31 | github.com/google/flatbuffers v25.2.10+incompatible // indirect 32 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 33 | github.com/klauspost/compress v1.18.0 // indirect 34 | github.com/lucasb-eyer/go-colorful v1.3.0 // indirect 35 | github.com/mattn/go-isatty v0.0.20 // indirect 36 | github.com/mattn/go-runewidth v0.0.16 // indirect 37 | github.com/mitchellh/go-homedir v1.1.0 // indirect 38 | github.com/muesli/cancelreader v0.2.2 // indirect 39 | github.com/muesli/mango v0.1.0 // indirect 40 | github.com/muesli/mango-cobra v1.2.0 // indirect 41 | github.com/muesli/mango-pflag v0.1.0 // indirect 42 | github.com/muesli/roff v0.1.0 // indirect 43 | github.com/muesli/termenv v0.16.0 // indirect 44 | github.com/rivo/uniseg v0.4.7 // indirect 45 | github.com/spf13/pflag v1.0.9 // indirect 46 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 47 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 48 | go.opentelemetry.io/otel v1.37.0 // indirect 49 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 50 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 51 | golang.org/x/net v0.41.0 // indirect 52 | golang.org/x/sync v0.17.0 // indirect 53 | golang.org/x/sys v0.37.0 // indirect 54 | golang.org/x/text v0.26.0 // indirect 55 | google.golang.org/protobuf v1.36.6 // indirect 56 | ) 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skate 2 | 3 |

4 | A nice rendering of a roller skate with the words ‘Charm Skate’ next to it
5 | Latest Release 6 | Build Status 7 |

8 | 9 | A personal key-value store. 🛼 10 | 11 | *** 12 | ⚠️ As of v1.0.0 Skate operates locally and no longer syncs to the Charm Cloud. [Read more about why][sunset] and see [the v1.0.0 release notes](https://github.com/charmbracelet/skate/releases/tag/v1.0.0) for a migration guide. The Charm Cloud [sunsets][sunset] on 29 November 2024. 13 | 14 | [sunset]: https://github.com/charmbracelet/charm?tab=readme-ov-file#sunsetting-charm-cloud 15 | *** 16 | 17 | Skate is simple and powerful. Use it to save and retrieve anything you’d 18 | like—even binary data. 19 | 20 | ```bash 21 | # Store something (and sync it to the network) 22 | skate set kitty meow 23 | 24 | # Fetch something (from the local cache) 25 | skate get kitty 26 | 27 | # What’s in the store? 28 | skate list 29 | 30 | # Spaces are fine 31 | skate set "kitty litter" "smells great" 32 | 33 | # You can store binary data, too 34 | skate set profile-pic < my-cute-pic.jpg 35 | skate get profile-pic > here-it-is.jpg 36 | 37 | # Unicode also works, of course 38 | skate set 猫咪 喵 39 | skate get 猫咪 40 | 41 | # For more info 42 | skate --help 43 | 44 | # Do creative things with skate list 45 | skate set penelope marmalade 46 | skate set christian tacos 47 | skate set muesli muesli 48 | 49 | skate list | xargs -n 2 printf '%s loves %s.\n' 50 | ``` 51 | 52 | ## Installation 53 | 54 | Use a package manager: 55 | 56 | ```bash 57 | # macOS or Linux 58 | brew tap charmbracelet/tap && brew install charmbracelet/tap/skate 59 | 60 | # Arch Linux (btw) 61 | pacman -S skate 62 | 63 | # Nix 64 | nix-env -iA nixpkgs.skate 65 | 66 | # Debian/Ubuntu 67 | sudo mkdir -p /etc/apt/keyrings 68 | curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg 69 | echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list 70 | sudo apt update && sudo apt install skate 71 | 72 | # Fedora/RHEL 73 | echo '[charm] 74 | name=Charm 75 | baseurl=https://repo.charm.sh/yum/ 76 | enabled=1 77 | gpgcheck=1 78 | gpgkey=https://repo.charm.sh/yum/gpg.key' | sudo tee /etc/yum.repos.d/charm.repo 79 | sudo yum install skate 80 | ``` 81 | 82 | Or download it: 83 | 84 | - [Packages][releases] are available in Debian and RPM formats 85 | - [Binaries][releases] are available for Linux, macOS, and Windows 86 | 87 | Or just install it with `go`: 88 | 89 | ```bash 90 | go install github.com/charmbracelet/skate@latest 91 | ``` 92 | 93 | [releases]: https://github.com/charmbracelet/skate/releases 94 | 95 | ## Other Features 96 | 97 | ### List Filters 98 | 99 | ```bash 100 | # list keys only 101 | skate list -k 102 | 103 | # list values only 104 | skate list -v 105 | 106 | # reverse lexicographic order 107 | skate list -r 108 | 109 | # add a custom delimeter between keys and values; default is a tab 110 | skate list -d "\t" 111 | 112 | # show binary values 113 | skate list -b 114 | ``` 115 | 116 | ### Databases 117 | 118 | Sometimes you’ll want to separate your data into different databases: 119 | 120 | ```bash 121 | # Database are automatically created on demand 122 | skate set secret-boss-key@work-stuff password123 123 | 124 | # Most commands accept a @db argument 125 | skate set "office rumor"@work-stuff "penelope likes marmalade" 126 | skate get "office rumor"@work-stuff 127 | skate list @work-stuff 128 | 129 | # Wait, what was that db named? 130 | skate list-dbs 131 | ``` 132 | 133 | ## Examples 134 | 135 | Here are some of our favorite ways to use `skate`. 136 | 137 | ### Keep secrets out of your scripts 138 | 139 | ```bash 140 | skate set gh_token GITHUB_TOKEN 141 | 142 | #!/bin/bash 143 | curl -su "$1:$(skate get gh_token)" \ 144 | https://api.github.com/users/$1 \ 145 | | jq -r '"\(.login) has \(.total_private_repos) private repos"' 146 | ``` 147 | 148 | ### Keep passwords in their own database 149 | 150 | ```bash 151 | skate set github@password.db PASSWORD 152 | skate get github@password.db 153 | ``` 154 | 155 | ### Use scripts to manage data 156 | 157 | ```bash 158 | #!/bin/bash 159 | skate set "$(date)@bookmarks.db" $1 160 | skate list @bookmarks.db 161 | ``` 162 | 163 | What do you use `skate` for? [Let us know](mailto:vt100@charm.sh). 164 | 165 | ## Feedback 166 | 167 | We’d love to hear your thoughts on this project. Feel free to drop us a note! 168 | 169 | - [Twitter](https://twitter.com/charmcli) 170 | - [The Fediverse](https://mastodon.social/@charmcli) 171 | - [Discord](https://charm.sh/chat) 172 | 173 | ## License 174 | 175 | [MIT](https://github.com/charmbracelet/skate/raw/main/LICENSE) 176 | 177 | --- 178 | 179 | Part of [Charm](https://charm.sh). 180 | 181 | The Charm logo 182 | 183 | Charm热爱开源 • Charm loves open source 184 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= 2 | github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= 3 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= 4 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= 5 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 6 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 7 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 8 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 9 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 10 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 | github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= 12 | github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= 13 | github.com/charmbracelet/fang v0.4.3 h1:qXeMxnL4H6mSKBUhDefHu8NfikFbP/MBNTfqTrXvzmY= 14 | github.com/charmbracelet/fang v0.4.3/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg= 15 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 16 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 17 | github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea h1:g1HfUgSMvye8mgecMD1mPscpt+pzJoDEiSA+p2QXzdQ= 18 | github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea/go.mod h1:ngHerf1JLJXBrDXdphn5gFrBPriCL437uwukd5c93pM= 19 | github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef h1:VrWaUi2LXYLjfjCHowdSOEc6dQ9Ro14KY7Bw4IWd19M= 20 | github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef/go.mod h1:AThRsQH1t+dfyOKIwXRoJBniYFQUkUpQq4paheHMc2o= 21 | github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= 22 | github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 23 | github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= 24 | github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 25 | github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= 26 | github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= 27 | github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= 28 | github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 29 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 30 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 31 | github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= 32 | github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= 33 | github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= 34 | github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= 35 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 36 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 37 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= 39 | github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= 40 | github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= 41 | github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= 42 | github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= 43 | github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 44 | github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= 45 | github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= 46 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 47 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 48 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 49 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 50 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 51 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 52 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 53 | github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= 54 | github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 55 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 56 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 57 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 58 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 59 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 60 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 61 | github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= 62 | github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 63 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 64 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 65 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 66 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 67 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 68 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 69 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 70 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 71 | github.com/muesli/go-app-paths v0.2.2 h1:NqG4EEZwNIhBq/pREgfBmgDmt3h1Smr1MjZiXbpZUnI= 72 | github.com/muesli/go-app-paths v0.2.2/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho= 73 | github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= 74 | github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= 75 | github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= 76 | github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= 77 | github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= 78 | github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= 79 | github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= 80 | github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= 81 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 82 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 83 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 84 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 85 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 86 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 87 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 88 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 89 | github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= 90 | github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 91 | github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 92 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 93 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 94 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 95 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 96 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 97 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 98 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 99 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 100 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 101 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 102 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 103 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 104 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 105 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 106 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 107 | golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= 108 | golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= 109 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 110 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 111 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 112 | golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= 113 | golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 114 | golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= 115 | golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= 116 | golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= 117 | golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= 118 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 119 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 120 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 121 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 122 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 123 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package main provides the skate CLI. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "math" 10 | "os" 11 | "path/filepath" 12 | "strconv" 13 | "strings" 14 | "unicode/utf8" 15 | 16 | "github.com/agnivade/levenshtein" 17 | "github.com/charmbracelet/fang" 18 | "github.com/charmbracelet/lipgloss" 19 | "github.com/dgraph-io/badger/v4" 20 | gap "github.com/muesli/go-app-paths" 21 | "github.com/spf13/cobra" 22 | "golang.org/x/term" 23 | ) 24 | 25 | var ( 26 | reverseIterate bool 27 | keysIterate bool 28 | valuesIterate bool 29 | showBinary bool 30 | delimiterIterate string 31 | 32 | warningStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("204")).Bold(true) 33 | 34 | rootCmd = &cobra.Command{ 35 | Use: "skate", 36 | Short: "Skate, a personal key value store.", 37 | Args: cobra.NoArgs, 38 | RunE: func(cmd *cobra.Command, _ []string) error { 39 | return cmd.Help() 40 | }, 41 | } 42 | 43 | setCmd = &cobra.Command{ 44 | Use: "set KEY[@DB] [VALUE]", 45 | Short: "Set a value for a key with an optional @ db. If VALUE is omitted, read value from the standard input.", 46 | Example: " skate set foo bar\n skate set foo <./bar.txt", 47 | Args: cobra.RangeArgs(1, 2), 48 | RunE: set, 49 | } 50 | 51 | getCmd = &cobra.Command{ 52 | Use: "get KEY[@DB]", 53 | Short: "Get a value for a key with an optional @ db.", 54 | SilenceUsage: true, 55 | SilenceErrors: true, 56 | Args: cobra.ExactArgs(1), 57 | RunE: get, 58 | } 59 | 60 | deleteCmd = &cobra.Command{ 61 | Use: "delete KEY[@DB]", 62 | Short: "Delete a key with an optional @ db.", 63 | Aliases: []string{"del", "rm"}, 64 | Args: cobra.ExactArgs(1), 65 | RunE: del, 66 | } 67 | 68 | listCmd = &cobra.Command{ 69 | Use: "list [@DB]", 70 | Short: "List key value pairs with an optional @ db.", 71 | Aliases: []string{"ls"}, 72 | Args: cobra.MaximumNArgs(1), 73 | RunE: list, 74 | } 75 | 76 | listDbsCmd = &cobra.Command{ 77 | Use: "list-dbs", 78 | Short: "List databases.", 79 | Aliases: []string{"ls-db"}, 80 | Args: cobra.NoArgs, 81 | RunE: listDbs, 82 | } 83 | 84 | deleteDbCmd = &cobra.Command{ 85 | Use: "delete-db [@DB]", 86 | Hidden: false, 87 | Short: "Delete a database", 88 | Aliases: []string{"del-db", "rm-db"}, 89 | Args: cobra.MinimumNArgs(1), 90 | RunE: deleteDb, 91 | } 92 | ) 93 | 94 | type errDBNotFound struct { 95 | suggestions []string 96 | } 97 | 98 | func (err errDBNotFound) Error() string { 99 | if len(err.suggestions) == 0 { 100 | return "no suggestions found" 101 | } 102 | return fmt.Sprintf("did you mean %q", strings.Join(err.suggestions, ", ")) 103 | } 104 | 105 | //nolint:wrapcheck 106 | func set(cmd *cobra.Command, args []string) error { 107 | k, n, err := keyParser(args[0]) 108 | if err != nil { 109 | return err 110 | } 111 | db, err := openKV(n) 112 | if err != nil { 113 | return err 114 | } 115 | defer db.Close() //nolint:errcheck 116 | if len(args) == 2 { 117 | return wrap(db, false, func(tx *badger.Txn) error { 118 | return tx.Set(k, []byte(args[1])) 119 | }) 120 | } 121 | bts, err := io.ReadAll(cmd.InOrStdin()) 122 | if err != nil { 123 | return err 124 | } 125 | return wrap(db, false, func(tx *badger.Txn) error { 126 | return tx.Set(k, bts) 127 | }) 128 | } 129 | 130 | //nolint:wrapcheck 131 | func get(_ *cobra.Command, args []string) error { 132 | k, n, err := keyParser(args[0]) 133 | if err != nil { 134 | return err 135 | } 136 | db, err := openKV(n) 137 | if err != nil { 138 | return err 139 | } 140 | defer db.Close() //nolint:errcheck 141 | var v []byte 142 | if err := wrap(db, true, func(tx *badger.Txn) error { 143 | item, err := tx.Get(k) 144 | if err != nil { 145 | return err 146 | } 147 | v, err = item.ValueCopy(nil) 148 | return err 149 | }); err != nil { 150 | return err 151 | } 152 | printFromKV("%s", v) 153 | return nil 154 | } 155 | 156 | func del(_ *cobra.Command, args []string) error { 157 | k, n, err := keyParser(args[0]) 158 | if err != nil { 159 | return err 160 | } 161 | db, err := openKV(n) 162 | if err != nil { 163 | return err 164 | } 165 | defer db.Close() //nolint:errcheck 166 | 167 | return wrap(db, false, func(tx *badger.Txn) error { 168 | return tx.Delete(k) 169 | }) 170 | } 171 | 172 | // TODO: use lists/tables/trees for this? 173 | func listDbs(*cobra.Command, []string) error { 174 | dbs, err := getDbs() 175 | for _, db := range dbs { 176 | fmt.Println(db) 177 | } 178 | return err 179 | } 180 | 181 | // getDbs: returns a formatted list of available Skate DBs. 182 | // 183 | //nolint:wrapcheck 184 | func getDbs() ([]string, error) { 185 | filepath, err := getFilePath() 186 | if err != nil { 187 | return nil, err 188 | } 189 | entries, err := os.ReadDir(filepath) 190 | if err != nil { 191 | return nil, err 192 | } 193 | var dbList []string 194 | for _, e := range entries { 195 | if e.IsDir() { 196 | dbList = append(dbList, e.Name()) 197 | } 198 | } 199 | return formatDbs(dbList), nil 200 | } 201 | 202 | func formatDbs(dbs []string) []string { 203 | out := make([]string, 0, len(dbs)) 204 | for _, db := range dbs { 205 | out = append(out, "@"+db) 206 | } 207 | return out 208 | } 209 | 210 | // getFilePath: get the file path to the skate databases. 211 | // 212 | //nolint:wrapcheck 213 | func getFilePath(args ...string) (string, error) { 214 | scope := gap.NewScope(gap.User, "charm") 215 | dd, pathErr := scope.DataPath("") 216 | if pathErr != nil { 217 | return "", pathErr 218 | } 219 | dir := filepath.Join(dd, "kv") 220 | if err := os.MkdirAll(dir, 0o750); err != nil { 221 | return "", err 222 | } 223 | return filepath.Join(append([]string{dir}, args...)...), nil 224 | } 225 | 226 | // deleteDb: delete a Skate database. 227 | // 228 | //nolint:wrapcheck 229 | func deleteDb(_ *cobra.Command, args []string) error { 230 | path, err := findDb(args[0]) 231 | var errNotFound errDBNotFound 232 | if errors.As(err, &errNotFound) { 233 | fmt.Fprintf(os.Stderr, "%q does not exist, %s\n", args[0], err.Error()) 234 | os.Exit(1) 235 | } 236 | if err != nil { 237 | fmt.Fprintf(os.Stderr, "unexpected error: %s", err.Error()) 238 | os.Exit(1) 239 | } 240 | var confirmation string 241 | 242 | home, err := os.UserHomeDir() 243 | showpath := path 244 | if err == nil && strings.HasPrefix(path, home) { 245 | showpath = filepath.Join("~", strings.TrimPrefix(showpath, home)) 246 | } 247 | message := fmt.Sprintf("Are you sure you want to delete '%s' and all its contents? (y/n)", warningStyle.Render(showpath)) 248 | message = lipgloss.NewStyle().Width(78).Render(message) 249 | fmt.Println(message) 250 | 251 | // TODO: use huh 252 | if _, err := fmt.Scanln(&confirmation); err != nil { 253 | return err 254 | } 255 | if confirmation == "y" { 256 | if err := os.RemoveAll(path); err != nil { 257 | return err 258 | } 259 | fmt.Fprintf(os.Stderr, "Deleted %q\n", showpath) 260 | return nil 261 | } 262 | fmt.Fprintf(os.Stderr, "Did not delete %q\n", showpath) 263 | return nil 264 | } 265 | 266 | // findDb: returns the path to the named db or an errDBNotFound if no 267 | // match is found. 268 | func findDb(name string) (string, error) { 269 | sName, err := nameFromArgs([]string{name}) 270 | if err != nil { 271 | return "", err 272 | } 273 | path, err := getFilePath(sName) 274 | if err != nil { 275 | return "", err 276 | } 277 | _, err = os.Stat(path) 278 | if sName == "" || os.IsNotExist(err) { 279 | dbs, err := getDbs() 280 | if err != nil { 281 | return "", err 282 | } 283 | var suggestions []string 284 | for _, db := range dbs { 285 | diff := int(math.Abs(float64(len(db) - len(name)))) 286 | levenshteinDistance := levenshtein.ComputeDistance(name, db) 287 | suggestByLevenshtein := levenshteinDistance <= diff 288 | if suggestByLevenshtein { 289 | suggestions = append(suggestions, db) 290 | } 291 | } 292 | return "", errDBNotFound{suggestions: suggestions} 293 | } 294 | return path, nil 295 | } 296 | 297 | //nolint:wrapcheck 298 | func list(_ *cobra.Command, args []string) error { 299 | var k string 300 | var pf string 301 | if keysIterate || valuesIterate { 302 | pf = "%s\n" 303 | } else { 304 | var err error 305 | pf, err = strconv.Unquote(fmt.Sprintf(`"%%s%s%%s\n"`, delimiterIterate)) 306 | if err != nil { 307 | return err 308 | } 309 | } 310 | if len(args) == 1 { 311 | k = args[0] 312 | } 313 | _, n, err := keyParser(k) 314 | if err != nil { 315 | return err 316 | } 317 | db, err := openKV(n) 318 | if err != nil { 319 | return err 320 | } 321 | err = db.Sync() 322 | if err != nil { 323 | return err 324 | } 325 | return db.View(func(txn *badger.Txn) error { 326 | opts := badger.DefaultIteratorOptions 327 | opts.PrefetchSize = 10 328 | opts.Reverse = reverseIterate 329 | if keysIterate { 330 | opts.PrefetchValues = false 331 | } 332 | it := txn.NewIterator(opts) 333 | defer it.Close() 334 | for it.Rewind(); it.Valid(); it.Next() { 335 | item := it.Item() 336 | k := item.Key() 337 | if keysIterate { 338 | printFromKV(pf, k) 339 | continue 340 | } 341 | err := item.Value(func(v []byte) error { 342 | if valuesIterate { 343 | printFromKV(pf, v) 344 | } else { 345 | printFromKV(pf, k, v) 346 | } 347 | return nil 348 | }) 349 | if err != nil { 350 | return err 351 | } 352 | } 353 | return nil 354 | }) 355 | } 356 | 357 | func nameFromArgs(args []string) (string, error) { 358 | if len(args) == 0 { 359 | return "", nil 360 | } 361 | _, n, err := keyParser(args[0]) 362 | if err != nil { 363 | return "", err 364 | } 365 | return n, nil 366 | } 367 | 368 | func printFromKV(pf string, vs ...[]byte) { 369 | nb := "(omitted binary data)" 370 | fvs := make([]any, 0) 371 | isatty := term.IsTerminal(int(os.Stdin.Fd())) 372 | for _, v := range vs { 373 | if isatty && !showBinary && !utf8.Valid(v) { 374 | fvs = append(fvs, nb) 375 | } else { 376 | fvs = append(fvs, string(v)) 377 | } 378 | } 379 | fmt.Printf(pf, fvs...) 380 | if isatty && !strings.HasSuffix(pf, "\n") { 381 | fmt.Println() 382 | } 383 | } 384 | 385 | func keyParser(k string) ([]byte, string, error) { 386 | var key, db string 387 | ps := strings.Split(k, "@") 388 | switch len(ps) { 389 | case 1: 390 | key = strings.ToLower(ps[0]) 391 | case 2: 392 | key = strings.ToLower(ps[0]) 393 | db = strings.ToLower(ps[1]) 394 | default: 395 | return nil, "", fmt.Errorf("bad key format, use KEY@DB") 396 | } 397 | return []byte(key), db, nil 398 | } 399 | 400 | func openKV(name string) (*badger.DB, error) { 401 | if name == "" { 402 | name = "default" 403 | } 404 | path, err := getFilePath(name) 405 | if err != nil { 406 | return nil, err 407 | } 408 | return badger.Open(badger.DefaultOptions(path).WithLoggingLevel(badger.ERROR)) //nolint:wrapcheck 409 | } 410 | 411 | func init() { 412 | listCmd.Flags().BoolVarP(&reverseIterate, "reverse", "r", false, "list in reverse lexicographic order") 413 | listCmd.Flags().BoolVarP(&keysIterate, "keys-only", "k", false, "only print keys and don't fetch values from the db") 414 | listCmd.Flags().BoolVarP(&valuesIterate, "values-only", "v", false, "only print values") 415 | listCmd.Flags().StringVarP(&delimiterIterate, "delimiter", "d", "\t", "delimiter to separate keys and values") 416 | listCmd.Flags().BoolVarP(&showBinary, "show-binary", "b", false, "print binary values") 417 | getCmd.Flags().BoolVarP(&showBinary, "show-binary", "b", false, "print binary values") 418 | 419 | rootCmd.AddCommand( 420 | getCmd, 421 | setCmd, 422 | deleteCmd, 423 | listCmd, 424 | listDbsCmd, 425 | deleteDbCmd, 426 | ) 427 | } 428 | 429 | func main() { 430 | if err := fang.Execute(context.Background(), rootCmd); err != nil { 431 | fmt.Fprint(os.Stderr, err) 432 | os.Exit(1) 433 | } 434 | } 435 | 436 | func wrap(db *badger.DB, readonly bool, fn func(tx *badger.Txn) error) error { 437 | tx := db.NewTransaction(!readonly) 438 | if err := fn(tx); err != nil { 439 | tx.Discard() 440 | return err 441 | } 442 | return tx.Commit() //nolint:wrapcheck 443 | } 444 | --------------------------------------------------------------------------------