Cratification: Break out nu_cmd_lang into a separate crate (#8181)

# Description

This breaks out the core_commands into a separate crate called
nu_cmd_lang

_(Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.)_

_(Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.)_

# User-Facing Changes

_(List of all changes that impact the user experience here. This helps
us keep track of breaking changes.)_

# Tests + Formatting

Don't forget to add tests that cover your changes.

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

# After Submitting

If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
This commit is contained in:
Michael Angerman 2023-02-24 07:54:42 -08:00 committed by GitHub
parent d0aefa99eb
commit 585e104608
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 531 additions and 60 deletions

40
Cargo.lock generated
View file

@ -2561,6 +2561,7 @@ dependencies = [
"nix",
"nu-ansi-term",
"nu-cli",
"nu-cmd-lang",
"nu-color-config",
"nu-command",
"nu-engine",
@ -2627,6 +2628,43 @@ dependencies = [
"thiserror",
]
[[package]]
name = "nu-cmd-lang"
version = "0.76.1"
dependencies = [
"atty",
"chrono",
"crossterm 0.24.0",
"fancy-regex",
"itertools",
"libc",
"log",
"lscolors",
"nu-ansi-term",
"nu-color-config",
"nu-engine",
"nu-explore",
"nu-json",
"nu-parser",
"nu-path",
"nu-pretty-hex",
"nu-protocol",
"nu-system",
"nu-table",
"nu-term-grid",
"nu-test-support",
"nu-utils",
"once_cell",
"pathdiff",
"rayon",
"serde",
"shadow-rs",
"sysinfo 0.27.7",
"tabled",
"terminal_size 0.2.1",
"url",
]
[[package]]
name = "nu-color-config"
version = "0.76.1"
@ -2680,6 +2718,7 @@ dependencies = [
"mime_guess",
"notify",
"nu-ansi-term",
"nu-cmd-lang",
"nu-color-config",
"nu-engine",
"nu-explore",
@ -5008,6 +5047,7 @@ dependencies = [
"libc",
"ntapi",
"once_cell",
"rayon",
"winapi",
]

View file

@ -27,6 +27,7 @@ members = [
"crates/nu-engine",
"crates/nu-parser",
"crates/nu-system",
"crates/nu-cmd-lang",
"crates/nu-command",
"crates/nu-protocol",
"crates/nu-plugin",
@ -48,6 +49,7 @@ miette = { version = "5.5.0", features = ["fancy-no-backtrace"] }
nu-ansi-term = "0.46.0"
nu-cli = { path = "./crates/nu-cli", version = "0.76.1" }
nu-color-config = { path = "./crates/nu-color-config", version = "0.76.1" }
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.76.1" }
nu-command = { path = "./crates/nu-command", version = "0.76.1" }
nu-engine = { path = "./crates/nu-engine", version = "0.76.1" }
nu-json = { path = "./crates/nu-json", version = "0.76.1" }

View file

@ -0,0 +1,55 @@
[package]
authors = ["The Nushell Project Developers"]
build = "build.rs"
description = "Nushell's core language commands"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-lang"
edition = "2021"
license = "MIT"
name = "nu-cmd-lang"
version = "0.76.1"
[lib]
bench = false
[dependencies]
serde = { version="1.0.123", features=["derive"] }
# used only for text_style Alignments
tabled = { version = "0.10.0", features = ["color"], default-features = false }
nu-ansi-term = "0.46.0"
nu-color-config = { path = "../nu-color-config", version = "0.76.1" }
nu-engine = { path = "../nu-engine", version = "0.76.1" }
nu-explore = { path = "../nu-explore", version = "0.76.1" }
nu-json = { path="../nu-json", version = "0.76.1" }
nu-parser = { path = "../nu-parser", version = "0.76.1" }
nu-path = { path = "../nu-path", version = "0.76.1" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.76.1" }
nu-protocol = { path = "../nu-protocol", version = "0.76.1" }
nu-system = { path = "../nu-system", version = "0.76.1" }
nu-table = { path = "../nu-table", version = "0.76.1" }
nu-term-grid = { path = "../nu-term-grid", version = "0.76.1" }
nu-utils = { path = "../nu-utils", version = "0.76.1" }
atty = "0.2.14"
chrono = { version = "0.4.23", features = ["std", "unstable-locales"], default-features = false }
crossterm = "0.24.0"
fancy-regex = "0.11.0"
itertools = "0.10.0"
log = "0.4.14"
lscolors = { version = "0.12.0", features = ["crossterm"], default-features = false }
once_cell = "1.17"
pathdiff = "0.2.1"
rayon = "1.6.1"
shadow-rs = { version = "0.20.0", default-features = false }
sysinfo = "0.27.7"
terminal_size = "0.2.1"
url = "2.2.1"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[build-dependencies]
shadow-rs = { version = "0.20.0", default-features = false }
[dev-dependencies]
nu-test-support = { path="../nu-test-support", version = "0.76.1" }

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 - 2022 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,20 @@
use std::process::Command;
fn main() -> shadow_rs::SdResult<()> {
// Look up the current Git commit ourselves instead of relying on shadow_rs,
// because shadow_rs does it in a really slow-to-compile way (it builds libgit2)
let hash = get_git_hash().unwrap_or_default();
println!("cargo:rustc-env=NU_COMMIT_HASH={hash}");
shadow_rs::new()
}
fn get_git_hash() -> Option<String> {
Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|hash| hash.trim().to_string())
}

View file

@ -76,6 +76,7 @@ impl Command for Describe {
example: "'hello' | describe",
result: Some(Value::test_string("string")),
},
/*
Example {
description: "Describe a stream of data, collecting it first",
example: "[1 2 3] | each {|i| $i} | describe",
@ -86,6 +87,7 @@ impl Command for Describe {
example: "[1 2 3] | each {|i| $i} | describe --no-collect",
result: Some(Value::test_string("stream")),
},
*/
]
}

View file

@ -75,8 +75,8 @@ pub use try_::Try;
pub use use_::Use;
pub use version::Version;
pub use while_::While;
#[cfg(feature = "plugin")]
//#[cfg(feature = "plugin")]
mod register;
#[cfg(feature = "plugin")]
//#[cfg(feature = "plugin")]
pub use register::Register;

View file

@ -0,0 +1,74 @@
use nu_protocol::engine::{EngineState, StateWorkingSet};
use crate::*;
pub fn create_default_context() -> EngineState {
let mut engine_state = EngineState::new();
let delta = {
let mut working_set = StateWorkingSet::new(&engine_state);
macro_rules! bind_command {
( $( $command:expr ),* $(,)? ) => {
$( working_set.add_decl(Box::new($command)); )*
};
}
// Core
bind_command! {
Alias,
Break,
Commandline,
Const,
Continue,
Def,
DefEnv,
Describe,
Do,
Echo,
ErrorMake,
ExportAlias,
ExportCommand,
ExportDef,
ExportDefEnv,
ExportExtern,
ExportUse,
Extern,
For,
Help,
HelpAliases,
HelpCommands,
HelpModules,
HelpOperators,
Hide,
HideEnv,
If,
Ignore,
Overlay,
OverlayUse,
OverlayList,
OverlayNew,
OverlayHide,
Let,
Loop,
Module,
Mut,
Return,
Try,
Use,
Version,
While,
};
//#[cfg(feature = "plugin")]
bind_command!(Register);
working_set.render()
};
if let Err(err) = engine_state.merge_delta(delta) {
eprintln!("Error creating default context: {err:?}");
}
engine_state
}

View file

@ -0,0 +1,298 @@
#[cfg(test)]
use nu_protocol::engine::Command;
#[cfg(test)]
pub fn test_examples(cmd: impl Command + 'static) {
test_examples::test_examples(cmd);
}
#[cfg(test)]
mod test_examples {
use crate::{Break, Describe, Mut};
use crate::{Echo, If, Let};
use itertools::Itertools;
use nu_protocol::{
ast::Block,
engine::{Command, EngineState, Stack, StateDelta, StateWorkingSet},
Example, PipelineData, Signature, Span, Type, Value,
};
use std::collections::HashSet;
pub fn test_examples(cmd: impl Command + 'static) {
let examples = cmd.examples();
let signature = cmd.signature();
let mut engine_state = make_engine_state(cmd.clone_box());
let cwd = std::env::current_dir().expect("Could not get current working directory.");
let mut witnessed_type_transformations = HashSet::<(Type, Type)>::new();
for example in examples {
if example.result.is_none() {
continue;
}
witnessed_type_transformations.extend(
check_example_input_and_output_types_match_command_signature(
&example,
&cwd,
&mut make_engine_state(cmd.clone_box()),
&signature.input_output_types,
signature.operates_on_cell_paths(),
signature.vectorizes_over_list,
),
);
check_example_evaluates_to_expected_output(&example, cwd.as_path(), &mut engine_state);
}
check_all_signature_input_output_types_entries_have_examples(
signature,
witnessed_type_transformations,
);
}
fn make_engine_state(cmd: Box<dyn Command>) -> Box<EngineState> {
let mut engine_state = Box::new(EngineState::new());
let delta = {
// Base functions that are needed for testing
// Try to keep this working set small to keep tests running as fast as possible
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(Break));
working_set.add_decl(Box::new(Describe));
working_set.add_decl(Box::new(Echo));
working_set.add_decl(Box::new(If));
working_set.add_decl(Box::new(Let));
working_set.add_decl(Box::new(Mut));
// Adding the command that is being tested to the working set
working_set.add_decl(cmd);
working_set.render()
};
engine_state
.merge_delta(delta)
.expect("Error merging delta");
engine_state
}
fn check_example_input_and_output_types_match_command_signature(
example: &Example,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
signature_input_output_types: &Vec<(Type, Type)>,
signature_operates_on_cell_paths: bool,
signature_vectorizes_over_list: bool,
) -> HashSet<(Type, Type)> {
let mut witnessed_type_transformations = HashSet::<(Type, Type)>::new();
// Skip tests that don't have results to compare to
if let Some(example_output) = example.result.as_ref() {
if let Some(example_input_type) =
eval_pipeline_without_terminal_expression(example.example, cwd, engine_state)
{
let example_input_type = example_input_type.get_type();
let example_output_type = example_output.get_type();
let example_matches_signature =
signature_input_output_types
.iter()
.any(|(sig_in_type, sig_out_type)| {
example_input_type.is_subtype(sig_in_type)
&& example_output_type.is_subtype(sig_out_type)
&& {
witnessed_type_transformations
.insert((sig_in_type.clone(), sig_out_type.clone()));
true
}
});
// The example type checks as vectorization over an input list if both:
// 1. The command is declared to vectorize over list input.
// 2. There exists an entry t -> u in the type map such that the
// example_input_type is a subtype of list<t> and the
// example_output_type is a subtype of list<u>.
let example_matches_signature_via_vectorization_over_list =
signature_vectorizes_over_list
&& match &example_input_type {
Type::List(ex_in_type) => {
match signature_input_output_types.iter().find_map(
|(sig_in_type, sig_out_type)| {
if ex_in_type.is_subtype(sig_in_type) {
Some((sig_in_type, sig_out_type))
} else {
None
}
},
) {
Some((sig_in_type, sig_out_type)) => match &example_output_type
{
Type::List(ex_out_type)
if ex_out_type.is_subtype(sig_out_type) =>
{
witnessed_type_transformations.insert((
sig_in_type.clone(),
sig_out_type.clone(),
));
true
}
_ => false,
},
None => false,
}
}
_ => false,
};
// The example type checks as a cell path operation if both:
// 1. The command is declared to operate on cell paths.
// 2. The example_input_type is list or record or table, and the example
// output shape is the same as the input shape.
let example_matches_signature_via_cell_path_operation =
signature_operates_on_cell_paths
&& example_input_type.accepts_cell_paths()
// TODO: This is too permissive; it should make use of the signature.input_output_types at least.
&& example_output_type.to_shape() == example_input_type.to_shape();
if !(example_matches_signature
|| example_matches_signature_via_vectorization_over_list
|| example_matches_signature_via_cell_path_operation)
{
panic!(
"The example `{}` demonstrates a transformation of type {:?} -> {:?}. \
However, this does not match the declared signature: {:?}.{} \
For this command, `vectorizes_over_list` is {} and `operates_on_cell_paths()` is {}.",
example.example,
example_input_type,
example_output_type,
signature_input_output_types,
if signature_input_output_types.is_empty() { " (Did you forget to declare the input and output types for the command?)" } else { "" },
signature_vectorizes_over_list,
signature_operates_on_cell_paths
);
};
};
}
witnessed_type_transformations
}
fn check_example_evaluates_to_expected_output(
example: &Example,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
) {
let mut stack = Stack::new();
// Set up PWD
stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy()));
engine_state
.merge_env(&mut stack, cwd)
.expect("Error merging environment");
let empty_input = PipelineData::empty();
let result = eval(example.example, empty_input, cwd, engine_state);
// Note. Value implements PartialEq for Bool, Int, Float, String and Block
// If the command you are testing requires to compare another case, then
// you need to define its equality in the Value struct
if let Some(expected) = example.result.as_ref() {
assert_eq!(
&result, expected,
"The example result differs from the expected value",
)
}
}
fn check_all_signature_input_output_types_entries_have_examples(
signature: Signature,
witnessed_type_transformations: HashSet<(Type, Type)>,
) {
let declared_type_transformations =
HashSet::from_iter(signature.input_output_types.into_iter());
assert!(
witnessed_type_transformations.is_subset(&declared_type_transformations),
"This should not be possible (bug in test): the type transformations \
collected in the course of matching examples to the signature type map \
contain type transformations not present in the signature type map."
);
if !signature.allow_variants_without_examples {
assert_eq!(
witnessed_type_transformations,
declared_type_transformations,
"There are entries in the signature type map which do not correspond to any example: \
{:?}",
declared_type_transformations
.difference(&witnessed_type_transformations)
.map(|(s1, s2)| format!("{s1} -> {s2}"))
.join(", ")
);
}
}
fn eval(
contents: &str,
input: PipelineData,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
) -> Value {
let (block, delta) = parse(contents, engine_state);
eval_block(block, input, cwd, engine_state, delta)
}
fn parse(contents: &str, engine_state: &EngineState) -> (Block, StateDelta) {
let mut working_set = StateWorkingSet::new(engine_state);
let (output, err) =
nu_parser::parse(&mut working_set, None, contents.as_bytes(), false, &[]);
if let Some(err) = err {
panic!("test parse error in `{contents}`: {err:?}")
}
(output, working_set.render())
}
fn eval_block(
block: Block,
input: PipelineData,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
delta: StateDelta,
) -> Value {
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let mut stack = Stack::new();
stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy()));
match nu_engine::eval_block(engine_state, &mut stack, &block, input, true, true) {
Err(err) => panic!("test eval error in `{}`: {:?}", "TODO", err),
Ok(result) => result.into_value(Span::test_data()),
}
}
fn eval_pipeline_without_terminal_expression(
src: &str,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
) -> Option<Value> {
let (mut block, delta) = parse(src, engine_state);
if block.pipelines.len() == 1 {
let n_expressions = block.pipelines[0].elements.len();
block.pipelines[0].elements.truncate(&n_expressions - 1);
if !block.pipelines[0].elements.is_empty() {
let empty_input = PipelineData::empty();
Some(eval_block(block, empty_input, cwd, engine_state, delta))
} else {
Some(Value::nothing(Span::test_data()))
}
} else {
// E.g. multiple semicolon-separated statements
None
}
}
}

View file

@ -0,0 +1,8 @@
mod core_commands;
mod default_context;
mod example_test;
pub use core_commands::*;
pub use default_context::*;
#[cfg(test)]
pub use example_test::test_examples;

View file

@ -15,6 +15,7 @@ bench = false
[dependencies]
nu-ansi-term = "0.46.0"
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.76.1" }
nu-color-config = { path = "../nu-color-config", version = "0.76.1" }
nu-engine = { path = "../nu-engine", version = "0.76.1" }
nu-explore = { path = "../nu-explore", version = "0.76.1" }

View file

@ -8,7 +8,7 @@ use nu_protocol::{
use super::eager::ToDataFrame;
use super::expressions::ExprCol;
use super::lazy::{LazyCollect, ToLazyFrame};
use crate::Let;
use nu_cmd_lang::Let;
pub fn test_dataframe(cmds: Vec<Box<dyn Command + 'static>>) {
if cmds.is_empty() {

View file

@ -3,7 +3,7 @@ use nu_protocol::engine::{EngineState, StateWorkingSet};
use crate::*;
pub fn create_default_context() -> EngineState {
let mut engine_state = EngineState::new();
let mut engine_state = nu_cmd_lang::create_default_context();
let delta = {
let mut working_set = StateWorkingSet::new(&engine_state);
@ -26,52 +26,6 @@ pub fn create_default_context() -> EngineState {
#[cfg(feature = "sqlite")]
add_database_decls(&mut working_set);
// Core
bind_command! {
Alias,
Break,
Commandline,
Const,
Continue,
Def,
DefEnv,
Describe,
Do,
Echo,
ErrorMake,
ExportAlias,
ExportCommand,
ExportDef,
ExportDefEnv,
ExportExtern,
ExportUse,
Extern,
For,
Help,
HelpAliases,
HelpCommands,
HelpModules,
HelpOperators,
Hide,
HideEnv,
If,
Ignore,
Overlay,
OverlayUse,
OverlayList,
OverlayNew,
OverlayHide,
Let,
Loop,
Module,
Mut,
Return,
Try,
Use,
Version,
While,
};
// Charts
bind_command! {
Histogram
@ -498,9 +452,6 @@ pub fn create_default_context() -> EngineState {
MathEvalDeprecated,
};
#[cfg(feature = "plugin")]
bind_command!(Register);
working_set.render()
};

View file

@ -9,12 +9,13 @@ pub fn test_examples(cmd: impl Command + 'static) {
#[cfg(test)]
mod test_examples {
use super::super::{
Ansi, Date, Echo, Enumerate, Flatten, From, Get, If, Into, IntoString, Let, LetEnv, Math,
MathEuler, MathPi, MathRound, ParEach, Path, Random, Sort, SortBy, Split, SplitColumn,
SplitRow, Str, StrJoin, StrLength, StrReplace, Update, Url, Values, Wrap,
Ansi, Date, Enumerate, Flatten, From, Get, Into, IntoString, LetEnv, Math, MathEuler,
MathPi, MathRound, ParEach, Path, Random, Sort, SortBy, Split, SplitColumn, SplitRow, Str,
StrJoin, StrLength, StrReplace, Update, Url, Values, Wrap,
};
use crate::{Break, Each, Mut, To};
use crate::{Each, To};
use itertools::Itertools;
use nu_cmd_lang::{Break, Echo, If, Let, Mut};
use nu_protocol::{
ast::Block,
engine::{Command, EngineState, Stack, StateDelta, StateWorkingSet},

View file

@ -1,4 +1,4 @@
use crate::help::highlight_search_string;
use nu_cmd_lang::help::highlight_search_string;
use fancy_regex::Regex;
use lscolors::{Color as LsColors_Color, LsColors, Style as LsColors_Style};

View file

@ -2,7 +2,6 @@ mod bits;
mod bytes;
mod charting;
mod conversions;
mod core_commands;
mod date;
mod debug;
mod default_context;
@ -33,7 +32,6 @@ pub use bits::*;
pub use bytes::*;
pub use charting::*;
pub use conversions::*;
pub use core_commands::*;
pub use date::*;
pub use debug::*;
pub use default_context::*;