1
Fork 0
mirror of https://github.com/RGBCube/nu_scripts synced 2025-08-01 06:37:46 +00:00

key-value module for std-rfc (#965)

`std-rfc/kv` is a straightforward but flexible interface for setting and
getting key-value pairs.

## Use-cases

* Ergonomically assign the result of a pipeline to a "variable". Just
"Up Arrow" and append `| kv set foo`.
* Use mid-pipeline to assign a "variable" and yet still continue the
pipeline
* Use mid-pipeline to inspect the state (like the `inspect` command) and
examine the results via `kv list` or `kv get` after the command
completes.
* Chaining assignments/setters
* Set universal variables once and access them even after the shell
exits (or in other simultaneously running shells).

## Features

* Values can be any Nushell type other than a closure. Values are
converted to and from nuons that are stored in a SQLite database.
* The module's commands can operate on either an in-memory database
(using `stor`) or on-disk (`into sqlite`).
* Includes a hook that enables "universal variables" similar to that of
the Fish shell. Universal variables are environment variables that are
immediately updated and available in *all* Nushell sessions that are
running the hook. Since they are stored in an on-disk SQLite database,
they also persist when the shell exits.
* Because kv pairs are stored as rows in a database, they can be
removed, unlike normal variables.
* kv pairs are easily converted to a record using `| transpose -dr`. The
resulting record is, of course, easily converted to environment
variables using `load-env`.
* Assignment can come from either pipeline input or a positional
parameter. When both are provided, the positional parameter is preferred
so that `$in` can be used.
* A closure can be used to modify the pipeline input before storing.
* Can optionally return either the pipeline input (default), the value
that was set, or the entire store back to continue the pipeline.
This commit is contained in:
Douglas 2025-01-29 12:57:48 -05:00 committed by GitHub
parent e6da07f512
commit dbcecf2653
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 450 additions and 0 deletions

View file

@ -0,0 +1,210 @@
# kv module
#
# use std-rfc/kv *
#
# Easily store and retrieve key-value pairs
# in a pipeline.
#
# A common request is to be able to assign a
# pipeline result to a variable. While it's
# not currently possible to use a "let" statement
# within a pipeline, this module provides an
# alternative. Think of each key as a variable
# that can be set and retrieved.
# Stores the pipeline value for later use
#
# If the key already exists, it is updated
# to the new value provided.
#
# Usage:
# <input> | kv set <key> <value?>
#
# Example:
# ls ~ | kv set "home snapshot"
# kv set foo 5
export def "kv set" [
key: string
value_or_closure?: any
--return (-r): string # Whether and what to return to the pipeline output
--universal (-u)
] {
# Pipeline input is preferred, but prioritize
# parameter if present. This allows $in to be
# used in the parameter if needed.
let input = $in
# If passed a closure, execute it
let arg_type = ($value_or_closure | describe)
let value = match $arg_type {
closure => { $input | do $value_or_closure }
_ => ($value_or_closure | default $input)
}
# Store values as nuons for type-integrity
let kv_pair = {
session: '' # Placeholder
key: $key
value: ($value | to nuon)
}
let db_open = (db_setup --universal=$universal)
try {
# Delete the existing key if it does exist
do $db_open | query db $"DELETE FROM std_kv_store WHERE key = '($key)'"
}
match $universal {
true => { $kv_pair | into sqlite (universal_db_path) -t std_kv_store }
false => { $kv_pair | stor insert -t std_kv_store }
}
# The value that should be returned from `kv set`
# By default, this is the input to `kv set`, even if
# overridden by a positional parameter.
# This can also be:
# input: (Default) The pipeline input to `kv set`, even if
# overridden by a positional parameter. `null` if no
# pipeline input was used.
# ---
# value: If a positional parameter was used for the value, then
# return it, otherwise return the input (whatever was set).
# If the positional was a closure, return the result of the
# closure on the pipeline input.
# ---
# all: The entire contents of the existing kv table are returned
match ($return | default 'input') {
'all' => (kv list --universal=$universal)
'a' => (kv list --universal=$universal)
'value' => $value
'v' => $value
'input' => $input
'in' => $input
'i' => $input
_ => {
error make {
msg: "Invalid --return option"
label: {
text: "Must be 'all'/'a', 'value'/'v', or 'input'/'in'/'i'"
span: (metadata $return).span
}
}
}
}
}
# Retrieves a stored value by key
#
# Counterpart of "kv set". Returns null
# if the key is not found.
#
# Usage:
# kv get <key> | <pipeline>
export def "kv get" [
key: string # Key of the kv-pair to retrieve
--universal (-u)
] {
let db_open = (db_setup --universal=$universal)
do $db_open
# Hack to turn a SQLiteDatabase into a table
| $in.std_kv_store | wrap temp | get temp
| where key == $key
# Should only be one occurence of each key in the stor
| get -i value.0
| match $in {
# Key not found
null => null
# Key found
_ => { from nuon }
}
}
# List the currently stored key-value pairs
#
# Returns results as the Nushell value rather
# than the stored nuon.
export def "kv list" [
--universal (-u)
] {
let db_open = (db_setup --universal=$universal)
do $db_open | $in.std_kv_store? | each {|kv_pair|
{
key: $kv_pair.key
value: ($kv_pair.value | from nuon )
}
}
}
# Returns and removes a key-value pair
export def --env "kv drop" [
key: string # Key of the kv-pair to drop
--universal (-u)
] {
let db_open = (db_setup --universal=$universal)
let value = (kv get --universal=$universal $key)
try {
do $db_open
# Hack to turn a SQLiteDatabase into a table
| query db $"DELETE FROM std_kv_store WHERE key = '($key)'"
}
if $universal and ($env.NU_KV_UNIVERSALS? | default false) {
hide-env $key
}
$value
}
def universal_db_path [] {
$env.NU_UNIVERSAL_KV_PATH?
| default (
$nu.data-dir | path join "std_kv_variables.sqlite3"
)
}
def db_setup [
--universal
] : nothing -> closure {
try {
match $universal {
true => {
# Ensure universal sqlite db and table exists
let uuid = (random uuid)
let dummy_record = {
session: ''
key: $uuid
value: ''
}
$dummy_record | into sqlite (universal_db_path) -t std_kv_store
open (universal_db_path) | query db $"DELETE FROM std_kv_store WHERE key = '($uuid)'"
}
false => {
# Create the stor table if it doesn't exist
stor create -t std_kv_store -c {session: str, key: str, value: str} | ignore
}
}
}
# Return the correct closure for opening on-disk vs. in-memory
match $universal {
true => {|| {|| open (universal_db_path)}}
false => {|| {|| stor open}}
}
}
# This hook can be added to $env.config.hooks.pre_execution to enable
# "universal variables" similar to the Fish shell. Adding, changing, or
# removing a universal variable will immediately update the corresponding
# environment variable in all running Nushell sessions.
export def "kv universal-variable-hook" [] {
{||
kv list --universal
| transpose -dr
| load-env
$env.NU_KV_UNIVERSALS = true
}
}

View file

@ -0,0 +1,240 @@
use std/assert
use ../std-rfc/kv *
# Important to use random keys and clean-up
# since the user running these tests may have
# either an existing local stor or universal db.
#[test]
def simple-local-set [] {
let key = (random uuid)
kv set $key 42
let actual = (kv get $key)
let expected = 42
assert equal $actual $expected
kv drop $key | ignore
}
#[test]
def local-pipeline_set_returns_value [] {
let key = (random uuid)
let actual = (42 | kv set $key)
let expected = 42
assert equal $actual $expected
let actual = (kv get $key)
let expected = 42
assert equal $actual $expected
kv drop $key | ignore
}
#[test]
def local-multiple_assignment [] {
let key1 = (random uuid)
let key2 = (random uuid)
let key3 = (random uuid)
"test value" | kv set $key1 | kv set $key2 | kv set $key3
let expected = "test value"
assert equal (kv get $key1) $expected
assert equal (kv get $key2) $expected
assert equal (kv get $key3) $expected
assert equal (kv get $key3) (kv get $key1)
kv drop $key1
kv drop $key2
kv drop $key3
}
#[test]
def local-transpose_to_record [] {
let key1 = (random uuid)
let key2 = (random uuid)
let key3 = (random uuid)
"test value" | kv set $key1 | kv set $key2 | kv set $key3
let record = (kv list | transpose -dr)
let actual = ($record | select $key1)
let expected = { $key1: "test value" }
assert equal $actual $expected
kv drop $key1
kv drop $key2
kv drop $key3
}
#[test]
def local-using_closure [] {
let name_key = (random uuid)
let size_key = (random uuid)
ls
| kv set $name_key { get name }
| kv set $size_key { get size }
let expected = "list<string>"
let actual = (kv get $name_key | describe)
assert equal $actual $expected
let expected = "list<filesize>"
let actual = (kv get $size_key | describe)
assert equal $actual $expected
kv drop $name_key
kv drop $size_key
}
#[test]
def local-return-entire-list [] {
let key1 = (random uuid)
let key2 = (random uuid)
let expected = 'value1'
$expected | kv set $key1
let actual = (
'value2'
| kv set --return all $key2 # Set $key2, but return the entire kv store
| transpose -dr # Convert to record for easier retrieval
| get $key1 # Attempt to retrieve key1 (set previously)
)
assert equal $actual $expected
kv drop $key1
kv drop $key2
}
#[test]
def local-return_value_only [] {
let key = (random uuid)
let expected = 'VALUE'
let actual = ('value' | kv set -r v $key {str upcase})
assert equal $actual $expected
kv drop $key
}
#[test]
def universal-simple_set [] {
let key = (random uuid)
kv set -u $key 42
let actual = (kv get -u $key)
let expected = 42
assert equal $actual $expected
kv drop -u $key | ignore
}
#[test]
def universal-pipeline_set_returns_value [] {
let key = (random uuid)
let actual = (42 | kv set -u $key)
let expected = 42
assert equal $actual $expected
let actual = (kv get -u $key)
let expected = 42
assert equal $actual $expected
kv drop -u $key | ignore
}
#[test]
def universal-multiple_assignment [] {
let key1 = (random uuid)
let key2 = (random uuid)
let key3 = (random uuid)
"test value" | kv set -u $key1 | kv set -u $key2 | kv set -u $key3
let expected = "test value"
assert equal (kv get -u $key1) $expected
assert equal (kv get -u $key2) $expected
assert equal (kv get -u $key3) $expected
assert equal (kv get $key3) (kv get $key1)
kv drop -u $key1
kv drop -u $key2
kv drop -u $key3
}
#[test]
def universal-transpose_to_record [] {
let key1 = (random uuid)
let key2 = (random uuid)
let key3 = (random uuid)
"test value" | kv set -u $key1 | kv set -u $key2 | kv set -u $key3
let record = (kv list -u | transpose -dr)
let actual = ($record | select $key1)
let expected = { $key1: "test value" }
assert equal $actual $expected
kv drop -u $key1
kv drop -u $key2
kv drop -u $key3
}
#[test]
def universal-using_closure [] {
let name_key = (random uuid)
let size_key = (random uuid)
ls
| kv set -u $name_key { get name }
| kv set -u $size_key { get size }
let expected = "list<string>"
let actual = (kv get -u $name_key | describe)
assert equal $actual $expected
let expected = "list<filesize>"
let actual = (kv get -u $size_key | describe)
assert equal $actual $expected
kv drop -u $name_key
kv drop -u $size_key
}
#[test]
def universal-return-entire-list [] {
let key1 = (random uuid)
let key2 = (random uuid)
let expected = 'value1'
$expected | kv set -u $key1
let actual = (
'value2'
| kv set -u --return all $key2 # Set $key2, but return the entire kv store
| transpose -dr # Convert to record for easier retrieval
| get $key1 # Attempt to retrieve key1 (set previously)
)
assert equal $actual $expected
kv drop --universal $key1
kv drop --universal $key2
}
#[test]
def universal-return_value_only [] {
let key = (random uuid)
let expected = 'VALUE'
let actual = ('value' | kv set --universal -r v $key {str upcase})
assert equal $actual $expected
kv drop --universal $key
}