diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 12d46820062b..30570ac96f80 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,7 +18,7 @@ Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass -- `cargo run -- crates/nu-std/tests.nu` to run the tests for the standard library +- `cargo run -- crates/nu-std/tests/run.nu` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3af32c7821a7..2016bc95cad6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,7 +116,7 @@ jobs: run: cargo install --path . --locked --no-default-features - name: Standard library tests - run: nu crates/nu-std/tests.nu + run: nu crates/nu-std/tests/run.nu - name: Setup Python uses: actions/setup-python@v4 diff --git a/crates/nu-std/examples.nu b/crates/nu-std/examples.nu new file mode 100644 index 000000000000..b08fedfbbbf7 --- /dev/null +++ b/crates/nu-std/examples.nu @@ -0,0 +1,13 @@ +use std 'log debug' +use std 'log info' +use std 'log warning' +use std 'log error' +use std 'log critical' + +export def log [] { + log debug "Debug message" + log info "Info message" + log warning "Warning message" + log error "Error message" + log critical "Critical message" +} diff --git a/crates/nu-std/lib/assert.nu b/crates/nu-std/lib/assert.nu new file mode 100644 index 000000000000..fee1bab62f04 --- /dev/null +++ b/crates/nu-std/lib/assert.nu @@ -0,0 +1,208 @@ +# Universal assert command +# +# If the condition is not true, it generates an error. +# +# # Example +# +# ```nushell +# >_ assert (3 == 3) +# >_ assert (42 == 3) +# Error: +# × Assertion failed: +# ╭─[myscript.nu:11:1] +# 11 │ assert (3 == 3) +# 12 │ assert (42 == 3) +# · ───┬──── +# · ╰── It is not true. +# 13 │ +# ╰──── +# ``` +# +# The --error-label flag can be used if you want to create a custom assert command: +# ``` +# def "assert even" [number: int] { +# assert ($number mod 2 == 0) --error-label { +# start: (metadata $number).span.start, +# end: (metadata $number).span.end, +# text: $"($number) is not an even number", +# } +# } +# ``` +export def main [ + condition: bool, # Condition, which should be true + message?: string, # Optional error message + --error-label: record # Label for `error make` if you want to create a custom assert +] { + if $condition { return } + let span = (metadata $condition).span + error make { + msg: ($message | default "Assertion failed."), + label: ($error_label | default { + text: "It is not true.", + start: (metadata $condition).span.start, + end: (metadata $condition).span.end + }) + } +} + +# Assert that executing the code generates an error +# +# For more documentation see the assert command +# +# # Examples +# +# > assert error {|| missing_command} # passes +# > assert error {|| 12} # fails +export def "assert error" [ + code: closure, + message?: string +] { + let error_raised = (try { do $code; false } catch { true }) + main ($error_raised) $message --error-label { + start: (metadata $code).span.start + end: (metadata $code).span.end + text: $"There were no error during code execution: (view source $code)" + } +} + +# Skip the current test case +# +# # Examples +# +# if $condition { assert skip } +export def "assert skip" [] { + error make {msg: "ASSERT:SKIP"} +} + + +# Assert $left == $right +# +# For more documentation see the assert command +# +# # Examples +# +# > assert equal 1 1 # passes +# > assert equal (0.1 + 0.2) 0.3 +# > assert equal 1 2 # fails +export def "assert equal" [left: any, right: any, message?: string] { + main ($left == $right) $message --error-label { + start: (metadata $left).span.start + end: (metadata $right).span.end + text: $"They are not equal. Left = ($left). Right = ($right)." + } +} + +# Assert $left != $right +# +# For more documentation see the assert command +# +# # Examples +# +# > assert not equal 1 2 # passes +# > assert not equal 1 "apple" # passes +# > assert not equal 7 7 # fails +export def "assert not equal" [left: any, right: any, message?: string] { + main ($left != $right) $message --error-label { + start: (metadata $left).span.start + end: (metadata $right).span.end + text: $"They both are ($left)." + } +} + +# Assert $left <= $right +# +# For more documentation see the assert command +# +# # Examples +# +# > assert less or equal 1 2 # passes +# > assert less or equal 1 1 # passes +# > assert less or equal 1 0 # fails +export def "assert less or equal" [left: any, right: any, message?: string] { + main ($left <= $right) $message --error-label { + start: (metadata $left).span.start + end: (metadata $right).span.end + text: $"Left: ($left), Right: ($right)" + } +} + +# Assert $left < $right +# +# For more documentation see the assert command +# +# # Examples +# +# > assert less 1 2 # passes +# > assert less 1 1 # fails +export def "assert less" [left: any, right: any, message?: string] { + main ($left < $right) $message --error-label { + start: (metadata $left).span.start + end: (metadata $right).span.end + text: $"Left: ($left), Right: ($right)" + } +} + +# Assert $left > $right +# +# For more documentation see the assert command +# +# # Examples +# +# > assert greater 2 1 # passes +# > assert greater 2 2 # fails +export def "assert greater" [left: any, right: any, message?: string] { + main ($left > $right) $message --error-label { + start: (metadata $left).span.start + end: (metadata $right).span.end + text: $"Left: ($left), Right: ($right)" + } +} + +# Assert $left >= $right +# +# For more documentation see the assert command +# +# # Examples +# +# > assert greater or equal 2 1 # passes +# > assert greater or equal 2 2 # passes +# > assert greater or equal 1 2 # fails +export def "assert greater or equal" [left: any, right: any, message?: string] { + main ($left >= $right) $message --error-label { + start: (metadata $left).span.start + end: (metadata $right).span.end + text: $"Left: ($left), Right: ($right)" + } +} + +# Assert length of $left is $right +# +# For more documentation see the assert command +# +# # Examples +# +# > assert length [0, 0] 2 # passes +# > assert length [0] 3 # fails +export def "assert length" [left: list, right: int, message?: string] { + main (($left | length) == $right) $message --error-label { + start: (metadata $left).span.start + end: (metadata $right).span.end + text: $"Length of ($left) is ($left | length), not ($right)" + } +} + +# Assert that ($left | str contains $right) +# +# For more documentation see the assert command +# +# # Examples +# +# > assert str contains "arst" "rs" # passes +# > assert str contains "arst" "k" # fails +export def "assert str contains" [left: string, right: string, message?: string] { + main ($left | str contains $right) $message --error-label { + start: (metadata $left).span.start + end: (metadata $right).span.end + text: $"'($left)' does not contain '($right)'." + } +} diff --git a/crates/nu-std/lib/dirs.nu b/crates/nu-std/lib/dirs.nu new file mode 100644 index 000000000000..d2e9664461e6 --- /dev/null +++ b/crates/nu-std/lib/dirs.nu @@ -0,0 +1,83 @@ +# Maintain a list of working directories and navigates them + +# the directory stack +export-env { + let-env DIRS_POSITION = 0 + let-env DIRS_LIST = [($env.PWD | path expand)] +} + +# Add one or more directories to the list. +# PWD becomes first of the newly added directories. +export def-env "dirs add" [ + ...paths: string # directory or directories to add to working list + ] { + mut abspaths = [] + for p in $paths { + let exp = ($p | path expand) + if ($exp | path type) != 'dir' { + let span = (metadata $p).span + error make {msg: "not a directory", label: {text: "not a directory", start: $span.start, end: $span.end } } + } + $abspaths = ($abspaths | append $exp) + + } + let-env DIRS_LIST = ($env.DIRS_LIST | insert ($env.DIRS_POSITION + 1) $abspaths | flatten) + let-env DIRS_POSITION = $env.DIRS_POSITION + 1 + + _fetch 0 +} + +# Advance to the next directory in the list or wrap to beginning. +export def-env "dirs next" [ + N:int = 1 # number of positions to move. +] { + _fetch $N +} + +# Back up to the previous directory or wrap to the end. +export def-env "dirs prev" [ + N:int = 1 # number of positions to move. +] { + _fetch (-1 * $N) +} + +# Drop the current directory from the list, if it's not the only one. +# PWD becomes the next working directory +export def-env "dirs drop" [] { + if ($env.DIRS_LIST | length) > 1 { + let-env DIRS_LIST = ( + ($env.DIRS_LIST | take $env.DIRS_POSITION) + | append ($env.DIRS_LIST | skip ($env.DIRS_POSITION + 1)) + ) + } + + _fetch 0 +} + +# Display current working directories. +export def-env "dirs show" [] { + mut out = [] + for $p in ($env.DIRS_LIST | enumerate) { + $out = ($out | append [ + [active, path]; + [($p.index == $env.DIRS_POSITION), $p.item] + ]) + } + + $out +} + +# fetch item helper +def-env _fetch [ + offset: int, # signed change to position +] { + # nushell 'mod' operator is really 'remainder', can return negative values. + # see: https://stackoverflow.com/questions/13683563/whats-the-difference-between-mod-and-remainder + let pos = ($env.DIRS_POSITION + + $offset + + ($env.DIRS_LIST | length) + ) mod ($env.DIRS_LIST | length) + let-env DIRS_POSITION = $pos + + cd ($env.DIRS_LIST | get $pos ) +} diff --git a/crates/nu-std/std.nu b/crates/nu-std/lib/help.nu similarity index 53% rename from crates/nu-std/std.nu rename to crates/nu-std/lib/help.nu index c2dbda3fcb39..f7fa8b39d875 100644 --- a/crates/nu-std/std.nu +++ b/crates/nu-std/lib/help.nu @@ -1,716 +1,3 @@ -# std.nu, `used` to load all standard library components - -# Universal assert command -# -# If the condition is not true, it generates an error. -# -# # Example -# -# ```nushell -# >_ assert (3 == 3) -# >_ assert (42 == 3) -# Error: -# × Assertion failed: -# ╭─[myscript.nu:11:1] -# 11 │ assert (3 == 3) -# 12 │ assert (42 == 3) -# · ───┬──── -# · ╰── It is not true. -# 13 │ -# ╰──── -# ``` -# -# The --error-label flag can be used if you want to create a custom assert command: -# ``` -# def "assert even" [number: int] { -# assert ($number mod 2 == 0) --error-label { -# start: (metadata $number).span.start, -# end: (metadata $number).span.end, -# text: $"($number) is not an even number", -# } -# } -# ``` -export def assert [ - condition: bool, # Condition, which should be true - message?: string, # Optional error message - --error-label: record # Label for `error make` if you want to create a custom assert -] { - if $condition { return } - let span = (metadata $condition).span - error make { - msg: ($message | default "Assertion failed."), - label: ($error_label | default { - text: "It is not true.", - start: (metadata $condition).span.start, - end: (metadata $condition).span.end - }) - } -} - -# Assert that executing the code generates an error -# -# For more documentation see the assert command -# -# # Examples -# -# > assert error {|| missing_command} # passes -# > assert error {|| 12} # fails -export def "assert error" [ - code: closure, - message?: string -] { - let error_raised = (try { do $code; false } catch { true }) - assert ($error_raised) $message --error-label { - start: (metadata $code).span.start - end: (metadata $code).span.end - text: $"There were no error during code execution: (view source $code)" - } -} - -# Skip the current test case -# -# # Examples -# -# if $condition { assert skip } -export def "assert skip" [] { - error make {msg: "ASSERT:SKIP"} -} - - -# Assert $left == $right -# -# For more documentation see the assert command -# -# # Examples -# -# > assert equal 1 1 # passes -# > assert equal (0.1 + 0.2) 0.3 -# > assert equal 1 2 # fails -export def "assert equal" [left: any, right: any, message?: string] { - assert ($left == $right) $message --error-label { - start: (metadata $left).span.start - end: (metadata $right).span.end - text: $"They are not equal. Left = ($left). Right = ($right)." - } -} - -# Assert $left != $right -# -# For more documentation see the assert command -# -# # Examples -# -# > assert not equal 1 2 # passes -# > assert not equal 1 "apple" # passes -# > assert not equal 7 7 # fails -export def "assert not equal" [left: any, right: any, message?: string] { - assert ($left != $right) $message --error-label { - start: (metadata $left).span.start - end: (metadata $right).span.end - text: $"They both are ($left)." - } -} - -# Assert $left <= $right -# -# For more documentation see the assert command -# -# # Examples -# -# > assert less or equal 1 2 # passes -# > assert less or equal 1 1 # passes -# > assert less or equal 1 0 # fails -export def "assert less or equal" [left: any, right: any, message?: string] { - assert ($left <= $right) $message --error-label { - start: (metadata $left).span.start - end: (metadata $right).span.end - text: $"Left: ($left), Right: ($right)" - } -} - -# Assert $left < $right -# -# For more documentation see the assert command -# -# # Examples -# -# > assert less 1 2 # passes -# > assert less 1 1 # fails -export def "assert less" [left: any, right: any, message?: string] { - assert ($left < $right) $message --error-label { - start: (metadata $left).span.start - end: (metadata $right).span.end - text: $"Left: ($left), Right: ($right)" - } -} - -# Assert $left > $right -# -# For more documentation see the assert command -# -# # Examples -# -# > assert greater 2 1 # passes -# > assert greater 2 2 # fails -export def "assert greater" [left: any, right: any, message?: string] { - assert ($left > $right) $message --error-label { - start: (metadata $left).span.start - end: (metadata $right).span.end - text: $"Left: ($left), Right: ($right)" - } -} - -# Assert $left >= $right -# -# For more documentation see the assert command -# -# # Examples -# -# > assert greater or equal 2 1 # passes -# > assert greater or equal 2 2 # passes -# > assert greater or equal 1 2 # fails -export def "assert greater or equal" [left: any, right: any, message?: string] { - assert ($left >= $right) $message --error-label { - start: (metadata $left).span.start - end: (metadata $right).span.end - text: $"Left: ($left), Right: ($right)" - } -} - -# Assert length of $left is $right -# -# For more documentation see the assert command -# -# # Examples -# -# > assert length [0, 0] 2 # passes -# > assert length [0] 3 # fails -export def "assert length" [left: list, right: int, message?: string] { - assert (($left | length) == $right) $message --error-label { - start: (metadata $left).span.start - end: (metadata $right).span.end - text: $"Length of ($left) is ($left | length), not ($right)" - } -} - -# Assert that ($left | str contains $right) -# -# For more documentation see the assert command -# -# # Examples -# -# > assert str contains "arst" "rs" # passes -# > assert str contains "arst" "k" # fails -export def "assert str contains" [left: string, right: string, message?: string] { - assert ($left | str contains $right) $message --error-label { - start: (metadata $left).span.start - end: (metadata $right).span.end - text: $"'($left)' does not contain '($right)'." - } -} - -# Add the given paths to the PATH. -# -# # Example -# - adding some dummy paths to an empty PATH -# ```nushell -# >_ with-env [PATH []] { -# std path add "foo" -# std path add "bar" "baz" -# std path add "fooo" --append -# -# assert equal $env.PATH ["bar" "baz" "foo" "fooo"] -# -# print (std path add "returned" --ret) -# } -# ╭───┬──────────╮ -# │ 0 │ returned │ -# │ 1 │ bar │ -# │ 2 │ baz │ -# │ 3 │ foo │ -# │ 4 │ fooo │ -# ╰───┴──────────╯ -# ``` -export def-env "path add" [ - --ret (-r) # return $env.PATH, useful in pipelines to avoid scoping. - --append (-a) # append to $env.PATH instead of prepending to. - ...paths # the paths to add to $env.PATH. -] { - let-env PATH = ( - $env.PATH - | if $append { append $paths } - else { prepend $paths } - ) - - if $ret { - $env.PATH - } -} - -# Maintain a list of working directories and navigates them - -# the directory stack -export-env { - let-env DIRS_POSITION = 0 - let-env DIRS_LIST = [($env.PWD | path expand)] -} - -# Add one or more directories to the list. -# PWD becomes first of the newly added directories. -export def-env "dirs add" [ - ...paths: string # directory or directories to add to working list - ] { - mut abspaths = [] - for p in $paths { - let exp = ($p | path expand) - if ($exp | path type) != 'dir' { - let span = (metadata $p).span - error make {msg: "not a directory", label: {text: "not a directory", start: $span.start, end: $span.end } } - } - $abspaths = ($abspaths | append $exp) - - } - let-env DIRS_LIST = ($env.DIRS_LIST | insert ($env.DIRS_POSITION + 1) $abspaths | flatten) - let-env DIRS_POSITION = $env.DIRS_POSITION + 1 - - _fetch 0 -} - -# Advance to the next directory in the list or wrap to beginning. -export def-env "dirs next" [ - N:int = 1 # number of positions to move. -] { - _fetch $N -} - -# Back up to the previous directory or wrap to the end. -export def-env "dirs prev" [ - N:int = 1 # number of positions to move. -] { - _fetch (-1 * $N) -} - -# Drop the current directory from the list, if it's not the only one. -# PWD becomes the next working directory -export def-env "dirs drop" [] { - if ($env.DIRS_LIST | length) > 1 { - let-env DIRS_LIST = ( - ($env.DIRS_LIST | take $env.DIRS_POSITION) - | append ($env.DIRS_LIST | skip ($env.DIRS_POSITION + 1)) - ) - } - - _fetch 0 -} - -# Display current working directories. -export def-env "dirs show" [] { - mut out = [] - for $p in ($env.DIRS_LIST | enumerate) { - $out = ($out | append [ - [active, path]; - [($p.index == $env.DIRS_POSITION), $p.item] - ]) - } - - $out -} - -# fetch item helper -def-env _fetch [ - offset: int, # signed change to position -] { - # nushell 'mod' operator is really 'remainder', can return negative values. - # see: https://stackoverflow.com/questions/13683563/whats-the-difference-between-mod-and-remainder - let pos = ($env.DIRS_POSITION - + $offset - + ($env.DIRS_LIST | length) - ) mod ($env.DIRS_LIST | length) - let-env DIRS_POSITION = $pos - - cd ($env.DIRS_LIST | get $pos ) -} - -def CRITICAL_LEVEL [] { 50 } -def ERROR_LEVEL [] { 40 } -def WARNING_LEVEL [] { 30 } -def INFO_LEVEL [] { 20 } -def DEBUG_LEVEL [] { 10 } - -def parse-string-level [level: string] { - ( - if $level == "CRITICAL" { (CRITICAL_LEVEL)} - else if $level == "CRIT" { (CRITICAL_LEVEL)} - else if $level == "ERROR" { (ERROR_LEVEL) } - else if $level == "WARNING" { (WARNING_LEVEL) } - else if $level == "WARN" { (WARNING_LEVEL) } - else if $level == "INFO" { (INFO_LEVEL) } - else if $level == "DEBUG" { (DEBUG_LEVEL) } - else { (INFO_LEVEL) } - ) -} - -def current-log-level [] { - let env_level = ($env | get -i NU_LOG_LEVEL | default (INFO_LEVEL)) - - try { - ($env_level | into int) - } catch { - parse-string-level $env_level - } -} - -def now [] { - date now | date format "%Y-%m-%dT%H:%M:%S%.3f" -} - -# Log critical message -export def "log critical" [message: string] { - if (current-log-level) > (CRITICAL_LEVEL) { return } - - print --stderr $"(ansi red_bold)CRT|(now)|($message)(ansi reset)" -} -# Log error message -export def "log error" [message: string] { - if (current-log-level) > (ERROR_LEVEL) { return } - - print --stderr $"(ansi red)ERR|(now)|($message)(ansi reset)" -} -# Log warning message -export def "log warning" [message: string] { - if (current-log-level) > (WARNING_LEVEL) { return } - - print --stderr $"(ansi yellow)WRN|(now)|($message)(ansi reset)" -} -# Log info message -export def "log info" [message: string] { - if (current-log-level) > (INFO_LEVEL) { return } - - print --stderr $"(ansi default)INF|(now)|($message)(ansi reset)" -} -# Log debug message -export def "log debug" [message: string] { - if (current-log-level) > (DEBUG_LEVEL) { return } - - print --stderr $"(ansi default_dimmed)DBG|(now)|($message)(ansi reset)" -} - -# Utility functions to read, change and create XML data in format supported -# by `to xml` and `from xml` commands - -# Get all xml entries matching simple xpath-inspired query -export def xaccess [ - path: list # List of steps. Each step can be a - # 1. String with tag name. Finds all children with specified name. Equivalent to `child::A` in xpath - # 2. `*` string. Get all children without any filter. Equivalent to `descendant` in xpath - # 3. Int. Select n-th among nodes selected by previous path. Equivalent to `(...)[1]` in xpath, but is indexed from 0. - # 4. Closure. Predicate accepting entry. Selects all entries among nodes selected by previous path for which predicate returns true. -] { - let input = $in - if ($path | is-empty) { - let path_span = (metadata $path).span - error make {msg: 'Empty path provided' - label: {text: 'Use a non-empty list of path steps' - start: $path_span.start end: $path_span.end}} - } - # In xpath first element in path is applied to root element - # this way it is possible to apply first step to root element - # of nu xml without unrolling one step of loop - mut values = () - $values = {content: [ { content: $input } ] } - for $step in ($path) { - match ($step | describe) { - 'string' => { - if $step == '*' { - $values = ($values.content | flatten) - } else { - $values = ($values.content | flatten | where tag == $step) - } - }, - 'int' => { - $values = [ ($values | get $step) ] - }, - 'closure' => { - $values = ($values | where {|x| do $step $x}) - }, - $type => { - let step_span = (metadata $step).span - error make {msg: $'Incorrect path step type ($type)' - label: {text: 'Use a string or int as a step' - start: $step_span.start end: $step_span.end}} - } - } - - if ($values | is-empty) { - return [] - } - } - $values -} - -def xupdate-string-step [ step: string rest: list updater: closure ] { - let input = $in - - # Get a list of elements to be updated and their indices - let to_update = ($input.content | enumerate | filter {|it| - let item = $it.item - $step == '*' or $item.tag == $step - }) - - if ($to_update | is-empty) { - return $input - } - - let new_values = ($to_update.item | xupdate-internal $rest $updater) - - mut reenumerated_new_values = ($to_update.index | zip $new_values | each {|x| {index: $x.0 item: $x.1}}) - - mut new_content = [] - for it in ($input.content | enumerate) { - let item = $it.item - let idx = $it.index - - let next = (if (not ($reenumerated_new_values | is-empty)) and $idx == $reenumerated_new_values.0.index { - let tmp = $reenumerated_new_values.0 - $reenumerated_new_values = ($reenumerated_new_values | skip 1) - $tmp.item - } else { - $item - }) - - $new_content = ($new_content | append $next) - } - - {tag: $input.tag attributes: $input.attributes content: $new_content} -} - -def xupdate-int-step [ step: int rest: list updater: closure ] { - $in | enumerate | each {|it| - let item = $it.item - let idx = $it.index - - if $idx == $step { - [ $item ] | xupdate-internal $rest $updater | get 0 - } else { - $item - } - } -} - -def xupdate-closure-step [ step: closure rest: list updater: closure ] { - $in | each {|it| - if (do $step $it) { - [ $it ] | xupdate-internal $rest $updater | get 0 - } else { - $it - } - } -} - -def xupdate-internal [ path: list updater: closure ] { - let input = $in - - if ($path | is-empty) { - $input | each $updater - } else { - let step = $path.0 - let rest = ($path | skip 1) - - match ($step | describe) { - 'string' => { - $input | each {|x| $x | xupdate-string-step $step $rest $updater} - }, - 'int' => { - $input | xupdate-int-step $step $rest $updater - }, - 'closure' => { - $input | xupdate-closure-step $step $rest $updater - }, - $type => { - let step_span = (metadata $step).span - error make {msg: $'Incorrect path step type ($type)' - label: {text: 'Use a string or int as a step' - start: $step_span.start end: $step_span.end}} - } - } - } - -} - -# Update xml data entries matching simple xpath-inspired query -export def xupdate [ - path: list # List of steps. Each step can be a - # 1. String with tag name. Finds all children with specified name. Equivalent to `child::A` in xpath - # 2. `*` string. Get all children without any filter. Equivalent to `descendant` in xpath - # 3. Int. Select n-th among nodes selected by previous path. Equivalent to `(...)[1]` in xpath, but is indexed from 0. - # 4. Closure. Predicate accepting entry. Selects all entries among nodes selected by previous path for which predicate returns true. - updater: closure # A closure used to transform entries matching path. -] { - {tag:? attributes:? content: [$in]} | xupdate-internal $path $updater | get content.0 -} - -# Get type of an xml entry -# -# Possible types are 'tag', 'text', 'pi' and 'comment' -export def xtype [] { - let input = $in - if (($input | describe) == 'string' or - ($input.tag? == null and $input.attributes? == null and ($input.content? | describe) == 'string')) { - 'text' - } else if $input.tag? == '!' { - 'comment' - } else if $input.tag? != null and ($input.tag? | str starts-with '?') { - 'pi' - } else if $input.tag? != null { - 'tag' - } else { - error make {msg: 'Not an xml emtry. Check valid types of xml entries via `help to xml`'} - } -} - -# Insert new entry to elements matching simple xpath-inspired query -export def xinsert [ - path: list # List of steps. Each step can be a - # 1. String with tag name. Finds all children with specified name. Equivalent to `child::A` in xpath - # 2. `*` string. Get all children without any filter. Equivalent to `descendant` in xpath - # 3. Int. Select n-th among nodes selected by previous path. Equivalent to `(...)[1]` in xpath, but is indexed from 0. - # 4. Closure. Predicate accepting entry. Selects all entries among nodes selected by previous path for which predicate returns true. - new_entry: record # A new entry to insert into `content` field of record at specified position - position?: int # Position to insert `new_entry` into. If specified inserts entry at given position (or end if - # position is greater than number of elements) in content of all entries of input matched by - # path. If not specified inserts at the end. -] { - $in | xupdate $path {|entry| - match ($entry | xtype) { - 'tag' => { - let new_content = if $position == null { - $entry.content | append $new_entry - } else { - let position = if $position > ($entry.content | length) { - $entry.content | length - } else { - $position - } - $entry.content | insert $position $new_entry - } - - - {tag: $entry.tag attributes: $entry.attributes content: $new_content} - }, - _ => (error make {msg: 'Can insert entry only into content of a tag node'}) - } - } -} - -# print a command name as dimmed and italic -def pretty-command [] { - let command = $in - return $"(ansi default_dimmed)(ansi default_italic)($command)(ansi reset)" -} - -# give a hint error when the clip command is not available on the system -def check-clipboard [ - clipboard: string # the clipboard command name - --system: string # some information about the system running, for better error -] { - if (which $clipboard | is-empty) { - error make --unspanned { - msg: $"(ansi red)clipboard_not_found(ansi reset): - you are running ($system) - but - the ($clipboard | pretty-command) clipboard command was not found on your system." - } - } -} - -# put the end of a pipe into the system clipboard. -# -# Dependencies: -# - xclip on linux x11 -# - wl-copy on linux wayland -# - clip.exe on windows -# -# Examples: -# put a simple string to the clipboard, will be stripped to remove ANSI sequences -# >_ "my wonderful string" | clip -# my wonderful string -# saved to clipboard (stripped) -# -# put a whole table to the clipboard -# >_ ls *.toml | clip -# ╭───┬─────────────────────┬──────┬────────┬───────────────╮ -# │ # │ name │ type │ size │ modified │ -# ├───┼─────────────────────┼──────┼────────┼───────────────┤ -# │ 0 │ Cargo.toml │ file │ 5.0 KB │ 3 minutes ago │ -# │ 1 │ Cross.toml │ file │ 363 B │ 2 weeks ago │ -# │ 2 │ rust-toolchain.toml │ file │ 1.1 KB │ 2 weeks ago │ -# ╰───┴─────────────────────┴──────┴────────┴───────────────╯ -# -# saved to clipboard -# -# put huge structured data in the clipboard, but silently -# >_ open Cargo.toml --raw | from toml | clip --silent -# -# when the clipboard system command is not installed -# >_ "mm this is fishy..." | clip -# Error: -# × clipboard_not_found: -# │ you are using xorg on linux -# │ but -# │ the xclip clipboard command was not found on your system. -export def clip [ - --silent: bool # do not print the content of the clipboard to the standard output - --no-notify: bool # do not throw a notification (only on linux) -] { - let input = $in - let input = if ($input | describe) == "string" { - $input | ansi strip - } else { $input } - - match $nu.os-info.name { - "linux" => { - if ($env.WAYLAND_DISPLAY? | is-empty) { - check-clipboard xclip --system $"('xorg' | pretty-command) on linux" - $input | xclip -sel clip - } else { - check-clipboard wl-copy --system $"('wayland' | pretty-command) on linux" - $input | wl-copy - } - }, - "windows" => { - chcp 65001 # see https://discord.com/channels/601130461678272522/601130461678272524/1085535756237426778 - check-clipboard clip.exe --system $"('xorg' | pretty-command) on linux" - $input | clip.exe - }, - "macos" => { - check-clipboard pbcopy --system macOS - $input | pbcopy - }, - _ => { - error make --unspanned { - msg: $"(ansi red)unknown_operating_system(ansi reset): - '($nu.os-info.name)' is not supported by the ('clip' | pretty-command) command. - - please open a feature request in the [issue tracker](char lparen)https://github.com/nushell/nushell/issues/new/choose(char rparen) to add your operating system to the standard library." - } - }, - } - - if not $silent { - print $input - - print --no-newline $"(ansi white_italic)(ansi white_dimmed)saved to clipboard" - if ($input | describe) == "string" { - print " (stripped)" - } - print --no-newline $"(ansi reset)" - } - - if (not $no_notify) and ($nu.os-info.name == linux) { - notify-send "std clip" "saved to clipboard" - } -} - def error-fmt [] { $"(ansi red)($in)(ansi reset)" } @@ -1387,7 +674,7 @@ def pretty-cmd [] { # # search for string in command names, usage and search terms # > help --find char -export def help [ +export def main [ ...item: string # the name of the help item to get help on --find (-f): string # string to find in help items names and usage ] { diff --git a/crates/nu-std/lib/log.nu b/crates/nu-std/lib/log.nu new file mode 100644 index 000000000000..59c1204173ae --- /dev/null +++ b/crates/nu-std/lib/log.nu @@ -0,0 +1,63 @@ +def CRITICAL_LEVEL [] { 50 } +def ERROR_LEVEL [] { 40 } +def WARNING_LEVEL [] { 30 } +def INFO_LEVEL [] { 20 } +def DEBUG_LEVEL [] { 10 } + +def parse-string-level [level: string] { + ( + if $level == "CRITICAL" { (CRITICAL_LEVEL)} + else if $level == "CRIT" { (CRITICAL_LEVEL)} + else if $level == "ERROR" { (ERROR_LEVEL) } + else if $level == "WARNING" { (WARNING_LEVEL) } + else if $level == "WARN" { (WARNING_LEVEL) } + else if $level == "INFO" { (INFO_LEVEL) } + else if $level == "DEBUG" { (DEBUG_LEVEL) } + else { (INFO_LEVEL) } + ) +} + +def current-log-level [] { + let env_level = ($env | get -i NU_LOG_LEVEL | default (INFO_LEVEL)) + + try { + ($env_level | into int) + } catch { + parse-string-level $env_level + } +} + +def now [] { + date now | date format "%Y-%m-%dT%H:%M:%S%.3f" +} + +# Log critical message +export def "log critical" [message: string] { + if (current-log-level) > (CRITICAL_LEVEL) { return } + + print --stderr $"(ansi red_bold)CRT|(now)|($message)(ansi reset)" +} +# Log error message +export def "log error" [message: string] { + if (current-log-level) > (ERROR_LEVEL) { return } + + print --stderr $"(ansi red)ERR|(now)|($message)(ansi reset)" +} +# Log warning message +export def "log warning" [message: string] { + if (current-log-level) > (WARNING_LEVEL) { return } + + print --stderr $"(ansi yellow)WRN|(now)|($message)(ansi reset)" +} +# Log info message +export def "log info" [message: string] { + if (current-log-level) > (INFO_LEVEL) { return } + + print --stderr $"(ansi default)INF|(now)|($message)(ansi reset)" +} +# Log debug message +export def "log debug" [message: string] { + if (current-log-level) > (DEBUG_LEVEL) { return } + + print --stderr $"(ansi default_dimmed)DBG|(now)|($message)(ansi reset)" +} diff --git a/crates/nu-std/lib/mod.nu b/crates/nu-std/lib/mod.nu new file mode 100644 index 000000000000..a3721ee0c615 --- /dev/null +++ b/crates/nu-std/lib/mod.nu @@ -0,0 +1,157 @@ +# std.nu, `used` to load all standard library components + +export use assert * +export use dirs * +export-env { + use dirs * +} +export use help * +export use log * +export use xml * + +# Add the given paths to the PATH. +# +# # Example +# - adding some dummy paths to an empty PATH +# ```nushell +# >_ with-env [PATH []] { +# std path add "foo" +# std path add "bar" "baz" +# std path add "fooo" --append +# +# assert equal $env.PATH ["bar" "baz" "foo" "fooo"] +# +# print (std path add "returned" --ret) +# } +# ╭───┬──────────╮ +# │ 0 │ returned │ +# │ 1 │ bar │ +# │ 2 │ baz │ +# │ 3 │ foo │ +# │ 4 │ fooo │ +# ╰───┴──────────╯ +# ``` +export def-env "path add" [ + --ret (-r) # return $env.PATH, useful in pipelines to avoid scoping. + --append (-a) # append to $env.PATH instead of prepending to. + ...paths # the paths to add to $env.PATH. +] { + let-env PATH = ( + $env.PATH + | if $append { append $paths } + else { prepend $paths } + ) + + if $ret { + $env.PATH + } +} + +# print a command name as dimmed and italic +def pretty-command [] { + let command = $in + return $"(ansi default_dimmed)(ansi default_italic)($command)(ansi reset)" +} + +# give a hint error when the clip command is not available on the system +def check-clipboard [ + clipboard: string # the clipboard command name + --system: string # some information about the system running, for better error +] { + if (which $clipboard | is-empty) { + error make --unspanned { + msg: $"(ansi red)clipboard_not_found(ansi reset): + you are running ($system) + but + the ($clipboard | pretty-command) clipboard command was not found on your system." + } + } +} + +# put the end of a pipe into the system clipboard. +# +# Dependencies: +# - xclip on linux x11 +# - wl-copy on linux wayland +# - clip.exe on windows +# +# Examples: +# put a simple string to the clipboard, will be stripped to remove ANSI sequences +# >_ "my wonderful string" | clip +# my wonderful string +# saved to clipboard (stripped) +# +# put a whole table to the clipboard +# >_ ls *.toml | clip +# ╭───┬─────────────────────┬──────┬────────┬───────────────╮ +# │ # │ name │ type │ size │ modified │ +# ├───┼─────────────────────┼──────┼────────┼───────────────┤ +# │ 0 │ Cargo.toml │ file │ 5.0 KB │ 3 minutes ago │ +# │ 1 │ Cross.toml │ file │ 363 B │ 2 weeks ago │ +# │ 2 │ rust-toolchain.toml │ file │ 1.1 KB │ 2 weeks ago │ +# ╰───┴─────────────────────┴──────┴────────┴───────────────╯ +# +# saved to clipboard +# +# put huge structured data in the clipboard, but silently +# >_ open Cargo.toml --raw | from toml | clip --silent +# +# when the clipboard system command is not installed +# >_ "mm this is fishy..." | clip +# Error: +# × clipboard_not_found: +# │ you are using xorg on linux +# │ but +# │ the xclip clipboard command was not found on your system. +export def clip [ + --silent: bool # do not print the content of the clipboard to the standard output + --no-notify: bool # do not throw a notification (only on linux) +] { + let input = $in + let input = if ($input | describe) == "string" { + $input | ansi strip + } else { $input } + + match $nu.os-info.name { + "linux" => { + if ($env.WAYLAND_DISPLAY? | is-empty) { + check-clipboard xclip --system $"('xorg' | pretty-command) on linux" + $input | xclip -sel clip + } else { + check-clipboard wl-copy --system $"('wayland' | pretty-command) on linux" + $input | wl-copy + } + }, + "windows" => { + chcp 65001 # see https://discord.com/channels/601130461678272522/601130461678272524/1085535756237426778 + check-clipboard clip.exe --system $"('xorg' | pretty-command) on linux" + $input | clip.exe + }, + "macos" => { + check-clipboard pbcopy --system macOS + $input | pbcopy + }, + _ => { + error make --unspanned { + msg: $"(ansi red)unknown_operating_system(ansi reset): + '($nu.os-info.name)' is not supported by the ('clip' | pretty-command) command. + + please open a feature request in the [issue tracker](char lparen)https://github.com/nushell/nushell/issues/new/choose(char rparen) to add your operating system to the standard library." + } + }, + } + + if not $silent { + print $input + + print --no-newline $"(ansi white_italic)(ansi white_dimmed)saved to clipboard" + if ($input | describe) == "string" { + print " (stripped)" + } + print --no-newline $"(ansi reset)" + } + + if (not $no_notify) and ($nu.os-info.name == linux) { + notify-send "std clip" "saved to clipboard" + } +} diff --git a/crates/nu-std/lib/xml.nu b/crates/nu-std/lib/xml.nu new file mode 100644 index 000000000000..83ad08e2ddf3 --- /dev/null +++ b/crates/nu-std/lib/xml.nu @@ -0,0 +1,206 @@ +# Utility functions to read, change and create XML data in format supported +# by `to xml` and `from xml` commands + +# Get all xml entries matching simple xpath-inspired query +export def xaccess [ + path: list # List of steps. Each step can be a + # 1. String with tag name. Finds all children with specified name. Equivalent to `child::A` in xpath + # 2. `*` string. Get all children without any filter. Equivalent to `descendant` in xpath + # 3. Int. Select n-th among nodes selected by previous path. Equivalent to `(...)[1]` in xpath, but is indexed from 0. + # 4. Closure. Predicate accepting entry. Selects all entries among nodes selected by previous path for which predicate returns true. +] { + let input = $in + if ($path | is-empty) { + let path_span = (metadata $path).span + error make {msg: 'Empty path provided' + label: {text: 'Use a non-empty list of path steps' + start: $path_span.start end: $path_span.end}} + } + # In xpath first element in path is applied to root element + # this way it is possible to apply first step to root element + # of nu xml without unrolling one step of loop + mut values = () + $values = {content: [ { content: $input } ] } + for $step in ($path) { + match ($step | describe) { + 'string' => { + if $step == '*' { + $values = ($values.content | flatten) + } else { + $values = ($values.content | flatten | where tag == $step) + } + }, + 'int' => { + $values = [ ($values | get $step) ] + }, + 'closure' => { + $values = ($values | where {|x| do $step $x}) + }, + $type => { + let step_span = (metadata $step).span + error make {msg: $'Incorrect path step type ($type)' + label: {text: 'Use a string or int as a step' + start: $step_span.start end: $step_span.end}} + } + } + + if ($values | is-empty) { + return [] + } + } + $values +} + +def xupdate-string-step [ step: string rest: list updater: closure ] { + let input = $in + + # Get a list of elements to be updated and their indices + let to_update = ($input.content | enumerate | filter {|it| + let item = $it.item + $step == '*' or $item.tag == $step + }) + + if ($to_update | is-empty) { + return $input + } + + let new_values = ($to_update.item | xupdate-internal $rest $updater) + + mut reenumerated_new_values = ($to_update.index | zip $new_values | each {|x| {index: $x.0 item: $x.1}}) + + mut new_content = [] + for it in ($input.content | enumerate) { + let item = $it.item + let idx = $it.index + + let next = (if (not ($reenumerated_new_values | is-empty)) and $idx == $reenumerated_new_values.0.index { + let tmp = $reenumerated_new_values.0 + $reenumerated_new_values = ($reenumerated_new_values | skip 1) + $tmp.item + } else { + $item + }) + + $new_content = ($new_content | append $next) + } + + {tag: $input.tag attributes: $input.attributes content: $new_content} +} + +def xupdate-int-step [ step: int rest: list updater: closure ] { + $in | enumerate | each {|it| + let item = $it.item + let idx = $it.index + + if $idx == $step { + [ $item ] | xupdate-internal $rest $updater | get 0 + } else { + $item + } + } +} + +def xupdate-closure-step [ step: closure rest: list updater: closure ] { + $in | each {|it| + if (do $step $it) { + [ $it ] | xupdate-internal $rest $updater | get 0 + } else { + $it + } + } +} + +def xupdate-internal [ path: list updater: closure ] { + let input = $in + + if ($path | is-empty) { + $input | each $updater + } else { + let step = $path.0 + let rest = ($path | skip 1) + + match ($step | describe) { + 'string' => { + $input | each {|x| $x | xupdate-string-step $step $rest $updater} + }, + 'int' => { + $input | xupdate-int-step $step $rest $updater + }, + 'closure' => { + $input | xupdate-closure-step $step $rest $updater + }, + $type => { + let step_span = (metadata $step).span + error make {msg: $'Incorrect path step type ($type)' + label: {text: 'Use a string or int as a step' + start: $step_span.start end: $step_span.end}} + } + } + } + +} + +# Update xml data entries matching simple xpath-inspired query +export def xupdate [ + path: list # List of steps. Each step can be a + # 1. String with tag name. Finds all children with specified name. Equivalent to `child::A` in xpath + # 2. `*` string. Get all children without any filter. Equivalent to `descendant` in xpath + # 3. Int. Select n-th among nodes selected by previous path. Equivalent to `(...)[1]` in xpath, but is indexed from 0. + # 4. Closure. Predicate accepting entry. Selects all entries among nodes selected by previous path for which predicate returns true. + updater: closure # A closure used to transform entries matching path. +] { + {tag:? attributes:? content: [$in]} | xupdate-internal $path $updater | get content.0 +} + +# Get type of an xml entry +# +# Possible types are 'tag', 'text', 'pi' and 'comment' +export def xtype [] { + let input = $in + if (($input | describe) == 'string' or + ($input.tag? == null and $input.attributes? == null and ($input.content? | describe) == 'string')) { + 'text' + } else if $input.tag? == '!' { + 'comment' + } else if $input.tag? != null and ($input.tag? | str starts-with '?') { + 'pi' + } else if $input.tag? != null { + 'tag' + } else { + error make {msg: 'Not an xml emtry. Check valid types of xml entries via `help to xml`'} + } +} + +# Insert new entry to elements matching simple xpath-inspired query +export def xinsert [ + path: list # List of steps. Each step can be a + # 1. String with tag name. Finds all children with specified name. Equivalent to `child::A` in xpath + # 2. `*` string. Get all children without any filter. Equivalent to `descendant` in xpath + # 3. Int. Select n-th among nodes selected by previous path. Equivalent to `(...)[1]` in xpath, but is indexed from 0. + # 4. Closure. Predicate accepting entry. Selects all entries among nodes selected by previous path for which predicate returns true. + new_entry: record # A new entry to insert into `content` field of record at specified position + position?: int # Position to insert `new_entry` into. If specified inserts entry at given position (or end if + # position is greater than number of elements) in content of all entries of input matched by + # path. If not specified inserts at the end. +] { + $in | xupdate $path {|entry| + match ($entry | xtype) { + 'tag' => { + let new_content = if $position == null { + $entry.content | append $new_entry + } else { + let position = if $position > ($entry.content | length) { + $entry.content | length + } else { + $position + } + $entry.content | insert $position $new_entry + } + + + {tag: $entry.tag attributes: $entry.attributes content: $new_content} + }, + _ => (error make {msg: 'Can insert entry only into content of a tag node'}) + } + } +} diff --git a/crates/nu-std/src/lib.rs b/crates/nu-std/src/lib.rs index 627c42d72f40..674805b155b4 100644 --- a/crates/nu-std/src/lib.rs +++ b/crates/nu-std/src/lib.rs @@ -2,8 +2,29 @@ use nu_parser::{parse, parse_module_block}; use nu_protocol::report_error; use nu_protocol::{engine::StateWorkingSet, Module, ShellError, Span}; -fn get_standard_library() -> &'static str { - include_str!("../std.nu") +fn add_file( + working_set: &mut StateWorkingSet, + name: &String, + content: &[u8], +) -> (Module, Vec) { + let start = working_set.next_span_start(); + working_set.add_file(name.clone(), content); + let end = working_set.next_span_start(); + + let (_, module, comments) = + parse_module_block(working_set, Span::new(start, end), name.as_bytes()); + + if let Some(err) = working_set.parse_errors.first() { + report_error(working_set, err); + } + + parse(working_set, Some(name), content, true); + + if let Some(err) = working_set.parse_errors.first() { + report_error(working_set, err); + } + + (module, comments) } fn load_prelude(working_set: &mut StateWorkingSet, prelude: Vec<(&str, &str)>, module: &Module) { @@ -45,26 +66,15 @@ pub fn load_standard_library( ) -> Result<(), miette::ErrReport> { let delta = { let name = "std".to_string(); - let content = get_standard_library().as_bytes(); - - let mut working_set = StateWorkingSet::new(engine_state); - - let start = working_set.next_span_start(); - working_set.add_file(name.clone(), content); - let end = working_set.next_span_start(); - - let (_, module, comments) = - parse_module_block(&mut working_set, Span::new(start, end), name.as_bytes()); - - if let Some(err) = working_set.parse_errors.first() { - report_error(&working_set, err); - } - - parse(&mut working_set, Some(&name), content, true); - - if let Some(err) = working_set.parse_errors.first() { - report_error(&working_set, err); - } + let content = include_str!("../lib/mod.nu"); + + let submodules = vec![ + ("assert", include_str!("../lib/assert.nu")), + ("dirs", include_str!("../lib/dirs.nu")), + ("help", include_str!("../lib/help.nu")), + ("log", include_str!("../lib/log.nu")), + ("xml", include_str!("../lib/xml.nu")), + ]; let prelude = vec![ ("std help", "help"), @@ -75,8 +85,16 @@ pub fn load_standard_library( ("std help operators", "help operators"), ]; - load_prelude(&mut working_set, prelude, &module); + let mut working_set = StateWorkingSet::new(engine_state); + for (name, content) in submodules { + let (module, comments) = + add_file(&mut working_set, &name.to_string(), content.as_bytes()); + working_set.add_module(name, module, comments); + } + + let (module, comments) = add_file(&mut working_set, &name, content.as_bytes()); + load_prelude(&mut working_set, prelude, &module); working_set.add_module(&name, module, comments); working_set.render() diff --git a/crates/nu-std/tests.nu b/crates/nu-std/tests/run.nu similarity index 100% rename from crates/nu-std/tests.nu rename to crates/nu-std/tests/run.nu diff --git a/crates/nu-std/test_asserts.nu b/crates/nu-std/tests/test_asserts.nu similarity index 100% rename from crates/nu-std/test_asserts.nu rename to crates/nu-std/tests/test_asserts.nu diff --git a/crates/nu-std/test_dirs.nu b/crates/nu-std/tests/test_dirs.nu similarity index 100% rename from crates/nu-std/test_dirs.nu rename to crates/nu-std/tests/test_dirs.nu diff --git a/crates/nu-std/test_logger.nu b/crates/nu-std/tests/test_logger.nu similarity index 83% rename from crates/nu-std/test_logger.nu rename to crates/nu-std/tests/test_logger.nu index be935df81d2c..5272789231c9 100644 --- a/crates/nu-std/test_logger.nu +++ b/crates/nu-std/tests/test_logger.nu @@ -37,10 +37,3 @@ export def test_debug [] { assert no message INFO debug assert message DEBUG debug DBG } -export def example [] { - log debug "Debug message" - log info "Info message" - log warning "Warning message" - log error "Error message" - log critical "Critical message" -} diff --git a/crates/nu-std/test_std.nu b/crates/nu-std/tests/test_std.nu similarity index 100% rename from crates/nu-std/test_std.nu rename to crates/nu-std/tests/test_std.nu diff --git a/crates/nu-std/test_xml.nu b/crates/nu-std/tests/test_xml.nu similarity index 100% rename from crates/nu-std/test_xml.nu rename to crates/nu-std/tests/test_xml.nu diff --git a/toolkit.nu b/toolkit.nu index 8ba0f138faa9..aa2ebc186089 100644 --- a/toolkit.nu +++ b/toolkit.nu @@ -37,7 +37,7 @@ export def test [ # run the tests for the standard library export def "test stdlib" [] { - cargo run -- crates/nu-std/tests.nu + cargo run -- crates/nu-std/tests/run.nu } # print the pipe input inside backticks, dimmed and italic, as a pretty command