Files
nushell/crates/nu-std/src/lib.rs
Bahex e7d2717424 feat(std-rfc): add iter module and recurse command (#15840)
# Description
`recurse` command is similar to `jq`'s `recurse`/`..` command. Along
with values, it also returns their cell-paths relative to the "root"
(initial input)

By default it uses breadth-first traversal, collecting child items of
all available sibling items before starting to process those child
items. This means output is ordered in increasing depth.
With the `--depth-first` flag it uses a stack based recursive descend,
which results in output order identical to `jq`'s `recurse`.

It can be used in the following ways:
- `... | recurse`: Recursively traverses the input value, returns each
value it finds as a stream.
- `... | recurse foo.bar`: Only descend through the given cell-path.
- `... | recurse {|parent| ... }`: Produce child values with a closure.

```nushell
{
    "foo": {
        "egg": "X"
        "spam": "Y"
    }
    "bar": {
        "quox": ["A" "B"]
    }
}
| recurse
| update item { to nuon }

# => ╭───┬──────────────┬───────────────────────────────────────────────╮
# => │ # │     path     │                     item                      │
# => ├───┼──────────────┼───────────────────────────────────────────────┤
# => │ 0 │ $.           │ {foo: {egg: X, spam: Y}, bar: {quox: [A, B]}} │
# => │ 1 │ $.foo        │ {egg: X, spam: Y}                             │
# => │ 2 │ $.bar        │ {quox: [A, B]}                                │
# => │ 3 │ $.foo.egg    │ "X"                                           │
# => │ 4 │ $.foo.spam   │ "Y"                                           │
# => │ 5 │ $.bar.quox   │ [A, B]                                        │
# => │ 6 │ $.bar.quox.0 │ "A"                                           │
# => │ 7 │ $.bar.quox.1 │ "B"                                           │
# => ╰───┴──────────────┴───────────────────────────────────────────────╯


{"name": "/", "children": [
    {"name": "/bin", "children": [
        {"name": "/bin/ls", "children": []},
        {"name": "/bin/sh", "children": []}]},
    {"name": "/home", "children": [
        {"name": "/home/stephen", "children": [
            {"name": "/home/stephen/jq", "children": []}]}]}]}
| recurse children
| get item.name

# => ╭───┬──────────────────╮
# => │ 0 │ /                │
# => │ 1 │ /bin             │
# => │ 2 │ /home            │
# => │ 3 │ /bin/ls          │
# => │ 4 │ /bin/sh          │
# => │ 5 │ /home/stephen    │
# => │ 6 │ /home/stephen/jq │
# => ╰───┴──────────────────╯


{"name": "/", "children": [
    {"name": "/bin", "children": [
        {"name": "/bin/ls", "children": []},
        {"name": "/bin/sh", "children": []}]},
    {"name": "/home", "children": [
        {"name": "/home/stephen", "children": [
            {"name": "/home/stephen/jq", "children": []}]}]}]}
| recurse children --depth-first
| get item.name

# => ╭───┬──────────────────╮
# => │ 0 │ /                │
# => │ 1 │ /bin             │
# => │ 2 │ /bin/ls          │
# => │ 3 │ /bin/sh          │
# => │ 4 │ /home            │
# => │ 5 │ /home/stephen    │
# => │ 6 │ /home/stephen/jq │
# => ╰───┴──────────────────╯


2
| recurse { ({path: square item: ($in * $in)}) }
| take while { $in.item < 100 }

# => ╭───┬─────────────────┬──────╮
# => │ # │      path       │ item │
# => ├───┼─────────────────┼──────┤
# => │ 0 │ $.              │    2 │
# => │ 1 │ $.square        │    4 │
# => │ 2 │ $.square.square │   16 │
# => ╰───┴─────────────────┴──────╯
``` 

# User-Facing Changes
No changes other than the new command.

# Tests + Formatting
Added tests for examples. (As we can't run them directly as tests yet.)
- 🟢 `toolkit test stdlib`

# After Submitting
- Update relevant parts of
https://www.nushell.sh/cookbook/jq_v_nushell.html
- `$env.config | recurse | where ($it.item | describe -d).type not-in
[list, record, table]` can partially cover the use case of `config
flatten`, should we do something?

---------

Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com>
2025-06-03 11:21:12 -04:00

189 lines
6.2 KiB
Rust

#![doc = include_str!("../README.md")]
use log::trace;
use nu_parser::parse;
use nu_protocol::{
VirtualPathId,
engine::{FileStack, StateWorkingSet, VirtualPath},
report_parse_error,
};
use std::path::PathBuf;
fn create_virt_file(working_set: &mut StateWorkingSet, name: &str, content: &str) -> VirtualPathId {
let sanitized_name = PathBuf::from(name).to_string_lossy().to_string();
let file_id = working_set.add_file(sanitized_name.clone(), content.as_bytes());
working_set.add_virtual_path(sanitized_name, VirtualPath::File(file_id))
}
pub fn load_standard_library(
engine_state: &mut nu_protocol::engine::EngineState,
) -> Result<(), miette::ErrReport> {
trace!("load_standard_library");
let mut working_set = StateWorkingSet::new(engine_state);
// Contents of the std virtual directory
let mut std_virt_paths = vec![];
// std/mod.nu
let std_mod_virt_file_id = create_virt_file(
&mut working_set,
"std/mod.nu",
include_str!("../std/mod.nu"),
);
std_virt_paths.push(std_mod_virt_file_id);
// Submodules/subdirectories ... std/<module>/mod.nu
let mut std_submodules = vec![
// Loaded at startup - Not technically part of std
(
"mod.nu",
"std/prelude",
include_str!("../std/prelude/mod.nu"),
),
// std submodules
("mod.nu", "std/assert", include_str!("../std/assert/mod.nu")),
("mod.nu", "std/bench", include_str!("../std/bench/mod.nu")),
("mod.nu", "std/dirs", include_str!("../std/dirs/mod.nu")),
("mod.nu", "std/dt", include_str!("../std/dt/mod.nu")),
(
"mod.nu",
"std/formats",
include_str!("../std/formats/mod.nu"),
),
("mod.nu", "std/help", include_str!("../std/help/mod.nu")),
("mod.nu", "std/input", include_str!("../std/input/mod.nu")),
("mod.nu", "std/iter", include_str!("../std/iter/mod.nu")),
("mod.nu", "std/log", include_str!("../std/log/mod.nu")),
("mod.nu", "std/math", include_str!("../std/math/mod.nu")),
("mod.nu", "std/util", include_str!("../std/util/mod.nu")),
("mod.nu", "std/xml", include_str!("../std/xml/mod.nu")),
("mod.nu", "std/config", include_str!("../std/config/mod.nu")),
(
"mod.nu",
"std/testing",
include_str!("../std/testing/mod.nu"),
),
];
for (filename, std_subdir_name, content) in std_submodules.drain(..) {
let mod_dir = PathBuf::from(std_subdir_name);
let name = mod_dir.join(filename);
let virt_file_id = create_virt_file(&mut working_set, &name.to_string_lossy(), content);
// Place file in virtual subdir
let mod_dir_filelist = vec![virt_file_id];
let virt_dir_id = working_set.add_virtual_path(
mod_dir.to_string_lossy().to_string(),
VirtualPath::Dir(mod_dir_filelist),
);
// Add the subdir to the list of paths in std
std_virt_paths.push(virt_dir_id);
}
// Create std virtual dir with all subdirs and files
let std_dir = PathBuf::from("std").to_string_lossy().to_string();
let _ = working_set.add_virtual_path(std_dir, VirtualPath::Dir(std_virt_paths));
// Add std-rfc files
let mut std_rfc_virt_paths = vec![];
// std-rfc/mod.nu
let std_rfc_mod_virt_file_id = create_virt_file(
&mut working_set,
"std-rfc/mod.nu",
include_str!("../std-rfc/mod.nu"),
);
std_rfc_virt_paths.push(std_rfc_mod_virt_file_id);
// Submodules/subdirectories ... std-rfc/<module>/mod.nu
let mut std_rfc_submodules = vec![
(
"mod.nu",
"std-rfc/clip",
include_str!("../std-rfc/clip/mod.nu"),
),
(
"mod.nu",
"std-rfc/conversions",
include_str!("../std-rfc/conversions/mod.nu"),
),
#[cfg(feature = "sqlite")]
("mod.nu", "std-rfc/kv", include_str!("../std-rfc/kv/mod.nu")),
(
"mod.nu",
"std-rfc/path",
include_str!("../std-rfc/path/mod.nu"),
),
(
"mod.nu",
"std-rfc/str",
include_str!("../std-rfc/str/mod.nu"),
),
(
"mod.nu",
"std-rfc/tables",
include_str!("../std-rfc/tables/mod.nu"),
),
(
"mod.nu",
"std-rfc/iter",
include_str!("../std-rfc/iter/mod.nu"),
),
];
for (filename, std_rfc_subdir_name, content) in std_rfc_submodules.drain(..) {
let mod_dir = PathBuf::from(std_rfc_subdir_name);
let name = mod_dir.join(filename);
let virt_file_id = create_virt_file(&mut working_set, &name.to_string_lossy(), content);
// Place file in virtual subdir
let mod_dir_filelist = vec![virt_file_id];
let virt_dir_id = working_set.add_virtual_path(
mod_dir.to_string_lossy().to_string(),
VirtualPath::Dir(mod_dir_filelist),
);
// Add the subdir to the list of paths in std
std_rfc_virt_paths.push(virt_dir_id);
}
// Create std virtual dir with all subdirs and files
let std_rfc_dir = PathBuf::from("std-rfc").to_string_lossy().to_string();
let _ = working_set.add_virtual_path(std_rfc_dir, VirtualPath::Dir(std_rfc_virt_paths));
// Load prelude
let (_, delta) = {
let source = r#"
# Prelude
use std/prelude *
"#;
// Add a placeholder file to the stack of files being evaluated.
// The name of this file doesn't matter; it's only there to set the current working directory to NU_STDLIB_VIRTUAL_DIR.
let placeholder = PathBuf::from("load std/prelude");
working_set.files = FileStack::with_file(placeholder);
let block = parse(
&mut working_set,
Some("loading stdlib prelude"),
source.as_bytes(),
false,
);
// Remove the placeholder file from the stack of files being evaluated.
working_set.files.pop();
if let Some(err) = working_set.parse_errors.first() {
report_parse_error(&working_set, err);
}
(block, working_set.render())
};
engine_state.merge_delta(delta)?;
Ok(())
}