Move std-rfc into Nushell (#15042)

Move `std-rfc` into Nushell.  `use std-rfc/<submodule>` now works "out-of-the-box"
This commit is contained in:
Douglas
2025-02-09 09:03:37 -05:00
committed by GitHub
parent bfe398ca36
commit 5b4dd775d4
16 changed files with 1967 additions and 17 deletions

View File

@ -0,0 +1,71 @@
# Commands for interacting with the system clipboard
#
# > These commands require your terminal to support OSC 52
# > Terminal multiplexers such as screen, tmux, zellij etc may interfere with this command
# Copy input to system clipboard
#
# # Example
# ```nushell
# >_ "Hello" | clip copy
# ```
export def copy [
--ansi (-a) # Copy ansi formatting
]: any -> nothing {
let input = $in | collect
let text = match ($input | describe -d | get type) {
$type if $type in [ table, record, list ] => {
$input | table -e
}
_ => {$input}
}
let do_strip_ansi = match $ansi {
true => {{||}}
false => {{|| ansi strip }}
}
let output = (
$text
| do $do_strip_ansi
| encode base64
)
print -n $'(ansi osc)52;c;($output)(ansi st)'
}
# Paste contents of system clipboard
#
# # Example
# ```nushell
# >_ clip paste
# "Hello"
# ```
export def paste []: [nothing -> string] {
try {
term query $'(ansi osc)52;c;?(ansi st)' -p $'(ansi osc)52;c;' -t (ansi st)
} catch {
error make -u {
msg: "Terminal did not responds to OSC 52 paste request."
help: $"Check if your terminal supports OSC 52."
}
}
| decode
| decode base64
| decode
}
# Add a prefix to each line of the content to be copied
#
# # Example: Format output for Nushell doc
# ls | clip prefix '# => ' | clip copy
export def prefix [prefix: string]: any -> string {
let input = $in | collect
match ($input | describe -d | get type) {
$type if $type in [ table, record, list ] => {
$input | table -e
}
_ => {$input}
}
| str replace -r --all '(?m)(.*)' $'($prefix)$1'
}

View File

@ -0,0 +1,121 @@
# Convert a Nushell value to a list
#
# Primary useful for range-to-list, but other types are accepted as well.
#
# Example:
#
# 1..10 | into list
export def "into list" []: any -> list {
let input = $in
let type = ($input | describe --detailed | get type)
match $type {
range => {$input | each {||}}
list => $input
table => $input
_ => [ $input ]
}
}
# Convert a list of columns into a table
#
# Examples:
#
# [
# ([ 1 2 3 ] | wrap a)
# ([ 4 5 6 ] | wrap b)
# ([ 7 8 9 ] | wrap c)
# ] | columns-into-table
# => ╭───┬───┬───┬───╮
# => │ # │ a │ b │ c │
# => ├───┼───┼───┼───┤
# => │ 0 │ 1 │ 4 │ 7 │
# => │ 1 │ 2 │ 5 │ 8 │
# => │ 2 │ 3 │ 6 │ 9 │
# => ╰───┴───┴───┴───╯
#
# Can roundtrip with `table-into-columns`
#
# ls | table-into-columns | columns-into-table
# => ╭───┬────────────────────────┬──────┬────────┬────────────────╮
# => │ # │ name │ type │ size │ modified │
# => ├───┼────────────────────────┼──────┼────────┼────────────────┤
# => │ 0 │ into-list.nu │ file │ 378 B │ 40 minutes ago │
# => │ 1 │ mod.nu │ file │ 28 B │ 41 minutes ago │
# => │ 2 │ name-values.nu │ file │ 394 B │ 34 minutes ago │
# => │ 3 │ record-into-columns.nu │ file │ 1.3 kB │ 27 minutes ago │
# => ╰───┴────────────────────────┴──────┴────────┴────────────────╯
export def columns-into-table []: [list<table> -> table] {
reduce {|it| merge $it}
}
# Convert a record, where each value is a list, into a list of columns.
# { a: [ 1 2 3 ], b: [ 4 5 6 ] } | record-into-columns
# => ╭───┬───────────╮
# => │ 0 │ ╭───┬───╮ │
# => │ │ │ # │ a │ │
# => │ │ ├───┼───┤ │
# => │ │ │ 0 │ 1 │ │
# => │ │ │ 1 │ 2 │ │
# => │ │ │ 2 │ 3 │ │
# => │ │ ╰───┴───╯ │
# => │ 1 │ ╭───┬───╮ │
# => │ │ │ # │ b │ │
# => │ │ ├───┼───┤ │
# => │ │ │ 0 │ 4 │ │
# => │ │ │ 1 │ 5 │ │
# => │ │ │ 2 │ 6 │ │
# => │ │ ╰───┴───╯ │
# => ╰───┴───────────╯
# =>
# This can be especially useful when combined with `columns-into-table`, as in:
#
# { a: [ 1 2 3 ], b: [ 4 5 6 ] } | record-into-columns
# | columns-into-table
# => ╭───┬───┬───╮
# => │ # │ a │ b │
# => ├───┼───┼───┤
# => │ 0 │ 1 │ 4 │
# => │ 1 │ 2 │ 5 │
# => │ 2 │ 3 │ 6 │
# => ╰───┴───┴───╯
# =>
export def record-into-columns []: [record -> list] {
items {|key, val| $val | wrap $key}
}
# Convert/split a table into a list of columns
#
# Examples:
# ls | table-into-columns
# => Returns a list of 4 tables, one for each of the `ls` columns
#
# Can be roundtripped with `columns-into-table`
#
# ls | table-into-columns | columns-into-table
# => ╭───┬────────────────────────┬──────┬────────┬────────────────╮
# => │ # │ name │ type │ size │ modified │
# => ├───┼────────────────────────┼──────┼────────┼────────────────┤
# => │ 0 │ into-list.nu │ file │ 378 B │ 40 minutes ago │
# => │ 1 │ mod.nu │ file │ 28 B │ 41 minutes ago │
# => │ 2 │ name-values.nu │ file │ 394 B │ 34 minutes ago │
# => │ 3 │ record-into-columns.nu │ file │ 1.3 kB │ 27 minutes ago │
# => ╰───┴────────────────────────┴──────┴────────┴────────────────╯
export def table-into-columns []: [table -> list<table>] {
let IN = $in
$IN | columns | each {|col| $IN | select $col}
}
# Assign keynames to a list of values, effectively converting the list to a record.
#
# Example:
#
# [ 1 2 3 ] | name-values a b c
# => ╭───┬───╮
# => │ a │ 1 │
# => │ b │ 2 │
# => │ c │ 3 │
# => ╰───┴───╯
export def name-values [...names: string]: [list -> record] {
let IN = $in
0.. | zip $IN | into record | rename ...$names
}

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 occurrence 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,9 @@
export use conversions *
export use tables *
export use path *
export module clip
export module str
# kv module depends on sqlite feature, which may not be available in some builds
const kv_module = if ("sqlite" in (version).features) { "std-rfc/kv" } else { null }
export use $kv_module *

View File

@ -0,0 +1,71 @@
# Helper function for `path with` commands
def with-field [field: string, value: string] {
path parse
| update $field $value
| path join
}
# Replace extension of input file paths.
#
# Note that it doesn't change the file name locally.
#
# # Example
# - setting path ext to `rs`
# ```nushell
# > "ab.txt" | path with-extension "rs"
# ab.rs
# > "ab.txt" | path with-extension ".rs"
# ab.rs
#
# - setting a list of input path ext to `rs`
# > ["ab.txt", "cd.exe"] | path with-extension "rs"
# ╭───┬──────────╮
# │ 0 │ ab.rs │
# │ 1 │ cd.rs │
# ╰───┴──────────╯
# ```
export def with-extension [ext: string] {
let path = $in
let ext_trim = if $ext starts-with "." {
$ext | str substring 1..
} else {
$ext
}
$path | with-field extension $ext_trim
}
# Replace stem of input file paths.
#
# Note that it doesn't change the file name locally.
#
# # Example
# - replace stem with "share"
# ```nushell
# > "/usr/bin" | path with-stem "share"
# /usr/share
#
# - replace stem with "nushell"
# > ["/home/alice/", "/home/bob/secret.txt"] | path with-stem "nushell"
# ╭───┬───────────────────────╮
# │ 0 │ /home/nushell │
# │ 1 │ /home/bob/nushell.txt │
# ╰───┴───────────────────────╯
# ```
export def with-stem [stem: string] { with-field stem $stem }
# Replace parent field of input file paths.
#
# # Example
# - replace parent path with `/usr/share`
# ```nushell
# > "/etc/foobar" | path with-parent "/usr/share/"
# /usr/share/foobar
#
# - replace parent path with `/root/` for all filenames in list
# > ["/home/rose/meow", "/home/fdncred/"] | path with-parent "/root/"
# ╭───┬───────────────╮
# │ 0 │ /root/meow │
# │ 1 │ /root/fdncred │
# ╰───┴───────────────╯
# ```
export def with-parent [parent: string] { with-field parent $parent }

View File

@ -0,0 +1,136 @@
# Removes common indent from a multi-line string based on the number of spaces on the last line.
#
# Example - Two leading spaces are removed from all lines:
#
# > let s = "
# Heading
# Indented Line
# Another Indented Line
#
# Another Heading
# "
# > $a | str dedent
#
# # => Heading
# # => Indented Line
# # => Another Indented Line
# # =>
# # => Another Heading
export def dedent [
--tabs (-t)
]: string -> string {
let string = $in
if ($string !~ $'^\s*(char lsep)') {
return (error make {
msg: 'First line must be empty'
})
}
if ($string !~ $'(char lsep)[ \t]*$') {
return (error make {
msg: 'Last line must contain only whitespace indicating the dedent'
})
}
# Get indent characters from the last line
let indent_chars = $string
| str replace -r $"\(?s\).*(char lsep)\([ \t]*\)$" '$1'
# Skip the first and last lines
let lines = (
$string
| lines
| skip
| # Only drop if there is whitespace. Otherwise, `lines`
| # drops a 0-length line anyway
| if ($indent_chars | str length) > 0 { drop } else {}
| enumerate
| rename lineNumber text
)
# Has to be done outside the replacement block or the error
# is converted to text. This is probably a Nushell bug, and
# this code can be recombined with the next iterator when
# the Nushell behavior is fixed.
for line in $lines {
# Skip lines with whitespace-only
if $line.text like '^\s*$' { continue }
# Error if any line doesn't start with enough indentation
if ($line.text | parse -r $"^\(($indent_chars)\)" | get capture0?.0?) != $indent_chars {
error make {
msg: $"Line ($line.lineNumber + 1) must have an indent of ($indent_chars | str length) or more."
}
}
}
$lines
| each {|line|
# Don't operate on lines containing only whitespace
if ($line.text not-like '^\s*$') {
$line.text | str replace $indent_chars ''
} else {
$line.text
}
}
| str join (char line_sep)
}
# Remove common indent from a multi-line string based on the line with the smallest indent
#
# Example - Two leading spaces are removed from all lines:
#
# > let s = "
# Heading
# Indented Line
# Another Indented Line
#
# Another Heading
# "
# > $a | str dedent
#
# # => Heading
# # => Indented Line
# # => Another Indented Line
# # =>
# # => Another Heading
#
export def unindent [
--tabs (-t) # String uses tabs instead of spaces for indentation
]: string -> string {
let indent_char = match $tabs {
true => '\t'
false => ' '
}
let text = (
$in
| # Remove the first line if it is only whitespace (tabs or spaces)
| str replace -r $'^[ \t]*(char lsep)' ''
| # Remove the last line if it is only whitespace (tabs or spaces)
| str replace -r $'(char lsep)[ \t]*$' ''
)
# Early return if there is only a single, empty (other than whitespace) line
if ($text like '^[ \t]*$') {
return $text
}
let minimumIndent = (
$text
| lines
| # Ignore indentation in any line that is only whitespace
| where $it not-like '^[ \t]*$'
| # Replaces the text with its indentation
| each {
str replace -r $"^\(($indent_char)*\).*" '$1'
| str length
}
| math min
)
let indent_chars = ('' | fill -c $indent_char -w $minimumIndent)
$text
| str replace -r --all $"\(?m\)^($indent_chars)" ''
}

View File

@ -0,0 +1,221 @@
# Run aggregate operations on output of `group-by --to-table`.
#
# # Example
#
# - group files by type and extension, and get stats about their sizes
# ```nushell
# >_ ls | group-by type { get name | path parse | get extension } --to-table | aggregate size
# ```
#
# - group data by multiple columns, and run custom aggregate operations
# ```nushell
# >_ open movies.csv
# | group-by Lead_Studio Genre --to-table
# | aggregate Worldwide_Gross Profitability --ops {avg: {math avg}, std: {math stddev}}
# ```
#
# - run aggregate operations without grouping the input
# ```nushell
# >_ open movies.csv | aggregate Year
# ```
export def aggregate [
--ops: record, # default = {min: {math min}, avg: {math avg}, max: {math max}, sum: {math sum}}
...columns: cell-path, # columns to perform aggregations on
]: [
table -> table<count: int>,
record -> error,
] {
def aggregate-default-ops [] {
{
min: {math min},
avg: {math avg},
max: {math max},
sum: {math sum},
}
}
def aggregate-col-name [col: cell-path, op_name: string]: [nothing -> string] {
$col | split cell-path | get value | str join "." | $"($in)_($op_name)"
}
def get-item-with-error [
col: cell-path,
opts: record<span: record<start: int, end: int>, items: bool>
]: [table -> any] {
try {
get $col
} catch {
let full_cellpath = if $opts.items {
$col
| split cell-path
| prepend {value: items, optional: false}
| into cell-path
} else {
$col
}
error make {
msg: $"Cannot find column '($full_cellpath)'",
label: {
text: "value originates here",
span: $opts.span
},
}
}
}
def "error not-a-table" [span: record<start: int, end:int>] {
error make {
msg: "input must be a table",
label: {
text: "from here",
span: $span
},
help: "Are you using `group-by`? Make sure to use its `--to-table` flag."
}
}
let IN = $in
let md = metadata $in
let first = try { $IN | first } catch { error not-a-table $md.span }
if not (($first | describe) starts-with record) {
error not-a-table $md.span
}
let grouped = "items" in $first
let IN = if $grouped {
$IN
} else {
[{items: $IN}]
}
let agg_ops = $ops | default (aggregate-default-ops)
let results = $IN
| update items {|group|
let column_results = $columns
| each {|col| # col: cell-path
let column = $group.items | get-item-with-error $col {span: $md.span, items: $grouped}
let agg_results = $agg_ops | items {|op_name, op| # op_name: string, op: closure
$column | do $op | wrap (aggregate-col-name $col $op_name)
}
for r in $agg_results {
if ($r | describe) == error {
return $r
}
}
$agg_results
| reduce {|it| merge $it}
}
# Manually propagate errors
for r in $column_results {
if ($r | describe) == error {
return $r
}
}
$column_results
| reduce --fold {} {|it| merge $it}
| insert count ($group.items | length)
| roll right # put count as the first column
}
# Manually propagate errors
for r in $results {
if ($r.items | describe) == error {
return $r.items
}
}
$results | flatten items
}
# Used in reject-column-slices and select-column-slices
def col-indices [ ...slices ] {
use std-rfc/conversions *
let indices = (
$slices
| reduce -f [] {|slice,indices|
$indices ++ ($slice | into list)
}
)
$in | columns
| select slices $indices
| get item
}
# Used in select-row-slices and reject-row-slices
def row-indices [ ...slices ] {
use std-rfc/conversions *
$slices
| reduce -f [] {|slice,indices|
$indices ++ ($slice | into list)
}
}
# Selects one or more rows while keeping the original indices.
#
# Example - Selects the first, fifth, and sixth rows from the table:
#
# ls / | select slices 0 4..5
#
# Example - Select the 4th row.
#
# Note that the difference between this and `select 3` is that the index (#) column shows the *original* (pre-select) position in the table.
#
# ls | select slices 3
export def "select slices" [ ...slices ] {
enumerate
| flatten
| select ...(row-indices ...$slices)
}
# Rejects one or more rows while keeping the original indices.
#
# Example - Rejects the first, fifth, and sixth rows from the table:
#
# ls / | reject slices 0 4..5
export def "reject slices" [ ...slices ] {
enumerate
| flatten
| collect
| reject ...(row-indices ...$slices)
}
# Select one or more columns by their indices
#
# Example:
#
# ls -l | select column-slices 0 10..12 | first 3
# # => ╭───┬────────────────────┬──────────────┬─────────────┬──────────────╮
# # => │ # │ name │ created │ accessed │ modified │
# # => ├───┼────────────────────┼──────────────┼─────────────┼──────────────┤
# # => │ 0 │ CITATION.cff │ 3 months ago │ 4 hours ago │ 3 months ago │
# # => │ 1 │ CODE_OF_CONDUCT.md │ 7 months ago │ 4 hours ago │ 7 months ago │
# # => │ 2 │ CONTRIBUTING.md │ 3 months ago │ 4 hours ago │ 3 months ago │
# # => ╰───┴────────────────────┴──────────────┴─────────────┴──────────────╯
export def "select column-slices" [
...slices
] {
let column_selector = ($in | col-indices ...$slices)
$in | select ...$column_selector
}
# Reject one or more columns by their indices
#
# Example:
#
# ls | reject column-slices 0 4 5 | first 3
export def "reject column-slices" [
...slices
] {
let column_selector = ($in | col-indices ...$slices)
$in | reject ...$column_selector
}