Add virtual path abstraction layer (#9245)

This commit is contained in:
Jakub Žádník
2023-05-23 23:48:50 +03:00
committed by GitHub
parent db4b26c1ac
commit 74724dee80
19 changed files with 500 additions and 323 deletions

View File

@ -11,3 +11,4 @@ version = "0.80.1"
miette = { version = "5.6.0", features = ["fancy-no-backtrace"] }
nu-parser = { version = "0.80.1", path = "../nu-parser" }
nu-protocol = { version = "0.80.1", path = "../nu-protocol" }
nu-engine = { version = "0.80.1", path = "../nu-engine" }

View File

@ -1,126 +1,136 @@
use nu_parser::{parse, parse_module_block};
use nu_protocol::report_error;
use nu_protocol::{engine::StateWorkingSet, Module, ShellError, Span};
use std::path::PathBuf;
fn add_file(
working_set: &mut StateWorkingSet,
name: &String,
content: &[u8],
) -> (Module, Vec<Span>) {
let file_id = working_set.add_file(name.clone(), content);
let new_span = working_set.get_span_for_file(file_id);
use nu_engine::{env::current_dir, eval_block};
use nu_parser::parse;
use nu_protocol::engine::{Stack, StateWorkingSet, VirtualPath};
use nu_protocol::{report_error, PipelineData};
let (_, module, comments) = parse_module_block(working_set, new_span, 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) {
let mut decls = Vec::new();
let mut errs = Vec::new();
for (name, search_name) in prelude {
if let Some(id) = module.decls.get(&search_name.as_bytes().to_vec()) {
let decl = (name.as_bytes().to_vec(), id.to_owned());
decls.push(decl);
} else {
errs.push(ShellError::GenericError(
format!("could not load `{}` from `std`.", search_name),
String::new(),
None,
None,
Vec::new(),
));
}
}
if !errs.is_empty() {
report_error(
working_set,
&ShellError::GenericError(
"Unable to load the prelude of the standard library.".into(),
String::new(),
None,
Some("this is a bug: please file an issue in the [issue tracker](https://github.com/nushell/nushell/issues/new/choose)".to_string()),
errs,
),
);
}
working_set.use_decls(decls);
}
// Virtual std directory unlikely to appear in user's file system
const NU_STDLIB_VIRTUAL_DIR: &str = "NU_STDLIB_VIRTUAL_DIR";
pub fn load_standard_library(
engine_state: &mut nu_protocol::engine::EngineState,
) -> Result<(), miette::ErrReport> {
let delta = {
let name = "std".to_string();
let content = include_str!("../lib/mod.nu");
// these modules are loaded in the order they appear in this list
#[rustfmt::skip]
let submodules = vec![
// helper modules that could be used in other parts of the library
("log", include_str!("../lib/log.nu")),
// the rest of the library
("dirs", include_str!("../lib/dirs.nu")),
("iter", include_str!("../lib/iter.nu")),
("help", include_str!("../lib/help.nu")),
("testing", include_str!("../lib/testing.nu")),
("xml", include_str!("../lib/xml.nu")),
("dt", include_str!("../lib/dt.nu")),
];
// Define commands to be preloaded into the default (top level, unprefixed) namespace.
// User can invoke these without having to `use std` beforehand.
// Entries are: (name to add to default namespace, path under std to find implementation)
//
// Conventionally, for a command implemented as `std foo`, the name added
// is either `std foo` or bare `foo`, not some arbitrary rename.
#[rustfmt::skip]
let prelude = vec![
("std help", "help"),
("std help commands", "help commands"),
("std help aliases", "help aliases"),
("std help modules", "help modules"),
("std help externs", "help externs"),
("std help operators", "help operators"),
("enter", "dirs enter"),
("shells", "dirs shells"),
("g", "dirs g"),
("n", "dirs n"),
("p", "dirs p"),
("dexit", "dirs dexit"),
let (block, delta) = {
let mut std_files = vec![
(
PathBuf::from(NU_STDLIB_VIRTUAL_DIR)
.join("std")
.join("mod.nu"),
include_str!("../std/mod.nu"),
),
(
PathBuf::from(NU_STDLIB_VIRTUAL_DIR)
.join("std")
.join("dirs.nu"),
include_str!("../std/dirs.nu"),
),
(
PathBuf::from(NU_STDLIB_VIRTUAL_DIR)
.join("std")
.join("dt.nu"),
include_str!("../std/dt.nu"),
),
(
PathBuf::from(NU_STDLIB_VIRTUAL_DIR)
.join("std")
.join("help.nu"),
include_str!("../std/help.nu"),
),
(
PathBuf::from(NU_STDLIB_VIRTUAL_DIR)
.join("std")
.join("iter.nu"),
include_str!("../std/iter.nu"),
),
(
PathBuf::from(NU_STDLIB_VIRTUAL_DIR)
.join("std")
.join("log.nu"),
include_str!("../std/log.nu"),
),
(
PathBuf::from(NU_STDLIB_VIRTUAL_DIR)
.join("std")
.join("testing.nu"),
include_str!("../std/testing.nu"),
),
(
PathBuf::from(NU_STDLIB_VIRTUAL_DIR)
.join("std")
.join("xml.nu"),
include_str!("../std/xml.nu"),
),
];
let mut working_set = StateWorkingSet::new(engine_state);
let mut std_virt_paths = vec![];
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);
for (name, content) in std_files.drain(..) {
let file_id =
working_set.add_file(name.to_string_lossy().to_string(), content.as_bytes());
let virtual_file_id = working_set.add_virtual_path(
name.to_string_lossy().to_string(),
VirtualPath::File(file_id),
);
std_virt_paths.push(virtual_file_id);
}
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);
// Using full virtual path to avoid potential conflicts with user having 'std' directory
// in their working directory.
let std_dir = PathBuf::from(NU_STDLIB_VIRTUAL_DIR)
.join("std")
.to_string_lossy()
.to_string();
let source = format!(
r#"
# Define the `std` module
module {std_dir}
working_set.render()
# Prelude
use std dirs [ enter, shells, g, n, p, dexit ]
"#
);
let _ = working_set.add_virtual_path(std_dir, VirtualPath::Dir(std_virt_paths));
// Change the currently parsed directory
let prev_currently_parsed_cwd = working_set.currently_parsed_cwd.clone();
working_set.currently_parsed_cwd = Some(PathBuf::from(NU_STDLIB_VIRTUAL_DIR));
let block = parse(
&mut working_set,
Some("loading stdlib"),
source.as_bytes(),
false,
);
if let Some(err) = working_set.parse_errors.first() {
report_error(&working_set, err);
}
// Restore the currently parsed directory back
working_set.currently_parsed_cwd = prev_currently_parsed_cwd;
(block, working_set.render())
};
engine_state.merge_delta(delta)?;
// We need to evaluate the module in order to run the `export-env` blocks.
let mut stack = Stack::new();
let pipeline_data = PipelineData::Empty;
eval_block(
engine_state,
&mut stack,
&block,
pipeline_data,
false,
false,
)?;
let cwd = current_dir(engine_state, &stack)?;
engine_state.merge_env(&mut stack, cwd)?;
Ok(())
}

View File

@ -1,15 +1,12 @@
# std.nu, `used` to load all standard library components
export use dirs
export-env {
use dirs *
use dirs.nu []
}
export use help
export use iter
export use log
export use testing *
export use xml
use dt [datetime-diff, pretty-print-duration]
export use testing.nu *
use dt.nu [datetime-diff, pretty-print-duration]
# Add the given paths to the PATH.
#
@ -46,7 +43,7 @@ export def-env "path add" [
| if $append { append $paths }
else { prepend $paths }
)
if $ret {
$env | get $path_name
}

View File

@ -5,7 +5,7 @@
# Assert commands and test runner.
#
##################################################################################
use log
export use log.nu
# Universal assert command
#
@ -17,7 +17,7 @@ use log
# >_ assert (3 == 3)
# >_ assert (42 == 3)
# Error:
# × Assertion failed:
# × Assertion failed:
# ╭─[myscript.nu:11:1]
# 11 │ assert (3 == 3)
# 12 │ assert (42 == 3)
@ -38,7 +38,7 @@ use log
# }
# ```
export def assert [
condition: bool, # Condition, which should be true
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
] {
@ -64,7 +64,7 @@ export def assert [
# >_ assert (42 == 3)
# >_ assert (3 == 3)
# Error:
# × Assertion failed:
# × Assertion failed:
# ╭─[myscript.nu:11:1]
# 11 │ assert (42 == 3)
# 12 │ assert (3 == 3)
@ -73,7 +73,7 @@ export def assert [
# 13 │
# ╰────
#
#
#
# The --error-label flag can be used if you want to create a custom assert command:
# ```
# def "assert not even" [number: int] {
@ -86,7 +86,7 @@ export def assert [
# ```
#
export def "assert not" [
condition: bool, # Condition, which should be false
condition: bool, # Condition, which should be false
message?: string, # Optional error message
--error-label: record # Label for `error make` if you want to create a custom assert
] {
@ -106,7 +106,7 @@ export def "assert not" [
# Assert that executing the code generates an error
#
# For more documentation see the assert command
#
#
# # Examples
#
# > assert error {|| missing_command} # passes
@ -138,7 +138,7 @@ export def "assert skip" [] {
# 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

View File

@ -3,8 +3,7 @@ use std "assert length"
use std "assert equal"
use std "assert not equal"
use std "assert error"
use std "log info"
use std "log debug"
use std log
# A couple of nuances to understand when testing module that exports environment:
# Each 'use' for that module in the test script will execute the export def-env block.
@ -41,20 +40,15 @@ def cur_ring_check [expect_dir:string, expect_position: int scenario:string] {
export def test_dirs_command [] {
# careful with order of these statements!
# must capture value of $in before executing `use`s
let $c = $in
let $c = $in
# must set PWD *before* doing `use` that will run the export def-env block in dirs module.
cd $c.base_path
# must execute these uses for the UOT commands *after* the test and *not* just put them at top of test module.
# the export def-env gets messed up
use std "dirs next"
use std "dirs prev"
use std "dirs add"
use std "dirs drop"
use std "dirs show"
use std "dirs goto"
use std dirs
assert equal [$c.base_path] $env.DIRS_LIST "list is just pwd after initialization"
dirs next
@ -85,13 +79,12 @@ export def test_dirs_command [] {
export def test_dirs_next [] {
# must capture value of $in before executing `use`s
let $c = $in
let $c = $in
# must set PWD *before* doing `use` that will run the export def-env block in dirs module.
cd $c.base_path
assert equal $env.PWD $c.base_path "test setup"
use std "dirs next"
use std "dirs add"
use std dirs
cur_dir_check $c.base_path "use module test setup"
dirs add $c.path_a $c.path_b
@ -107,15 +100,11 @@ export def test_dirs_next [] {
export def test_dirs_cd [] {
# must capture value of $in before executing `use`s
let $c = $in
let $c = $in
# must set PWD *before* doing `use` that will run the export def-env block in dirs module.
cd $c.base_path
use std # necessary to define $env.config??
use std "dirs next"
use std "dirs add"
use std "dirs drop"
use std dirs
cur_dir_check $c.base_path "use module test setup"