mirror of
https://github.com/RGBCube/nu_scripts
synced 2025-07-31 14:17:45 +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:
parent
e6da07f512
commit
dbcecf2653
2 changed files with 450 additions and 0 deletions
210
stdlib-candidate/std-rfc/kv/mod.nu
Normal file
210
stdlib-candidate/std-rfc/kv/mod.nu
Normal 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
|
||||
}
|
||||
}
|
240
stdlib-candidate/tests/kv.nu
Normal file
240
stdlib-candidate/tests/kv.nu
Normal 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
|
||||
}
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue