Merge branch 'main' into typed-tagged-path-forms

This commit is contained in:
Ian Manske 2024-06-11 21:56:22 -07:00
commit feee1c5ec5
34 changed files with 458 additions and 410 deletions

View File

@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4.1.6
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
- name: cargo fmt
run: cargo fmt --all -- --check
@ -69,7 +69,7 @@ jobs:
- uses: actions/checkout@v4.1.6
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
- name: Tests
run: cargo test --workspace --profile ci --exclude nu_plugin_* ${{ matrix.default-flags }}
@ -98,7 +98,7 @@ jobs:
- uses: actions/checkout@v4.1.6
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
- name: Install Nushell
run: cargo install --path . --locked --no-default-features
@ -149,7 +149,7 @@ jobs:
- uses: actions/checkout@v4.1.6
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
- name: Clippy
run: cargo clippy --package nu_plugin_* -- $CLIPPY_OPTIONS

View File

@ -122,7 +122,7 @@ jobs:
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
with:
rustflags: ''

View File

@ -69,7 +69,7 @@ jobs:
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
with:
cache: false

View File

@ -10,4 +10,4 @@ jobs:
uses: actions/checkout@v4.1.6
- name: Check spelling
uses: crate-ci/typos@v1.22.1
uses: crate-ci/typos@v1.22.4

54
Cargo.lock generated
View File

@ -3011,7 +3011,7 @@ dependencies = [
"uu_mv",
"uu_uname",
"uu_whoami",
"uucore 0.0.25",
"uucore",
"uuid",
"v_htmlescape",
"wax",
@ -6352,93 +6352,77 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uu_cp"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcbe045dc92209114afdfd366bd18f7b95dbf999f3eaa85ad6dca910b0be3d56"
checksum = "c31fc5c95f7668999e129464a29e9080f69ba01ccf7a0ae43ff2cfdb15baa340"
dependencies = [
"clap",
"filetime",
"indicatif",
"libc",
"quick-error 2.0.1",
"uucore 0.0.26",
"uucore",
"walkdir",
"xattr",
]
[[package]]
name = "uu_mkdir"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "040aa4584036b2f65e05387b0ea9ac468afce1db325743ce5f350689fd9ce4ae"
checksum = "496d95e0e3121e4d424ba62019eb84a6f1102213ca8ca16c0a2f8c652c7236c3"
dependencies = [
"clap",
"uucore 0.0.26",
"uucore",
]
[[package]]
name = "uu_mktemp"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f240a99c36d768153874d198c43605a45c86996b576262689a0f18248cc3bc57"
checksum = "a28a0d9744bdc28ceaf13f70b959bacded91aedfd008402d72fa1e3224158653"
dependencies = [
"clap",
"rand",
"tempfile",
"uucore 0.0.26",
"uucore",
]
[[package]]
name = "uu_mv"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c99fd7c75e6e85553c92537314be3d9a64b4927051aa1608513feea2f933022"
checksum = "53680908b01c5ac3cc0ee8a376de3e51a36dde2c5a5227a115a3d0977cc4539b"
dependencies = [
"clap",
"fs_extra",
"indicatif",
"uucore 0.0.26",
"uucore",
]
[[package]]
name = "uu_uname"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5951832d73199636bde6c0d61cf960932b3c4450142c290375bc10c7abed6db5"
checksum = "a7f4125fb4f286313bca8f222abaefe39db54d65179ea788c91ebd3162345f4e"
dependencies = [
"clap",
"platform-info",
"uucore 0.0.26",
"uucore",
]
[[package]]
name = "uu_whoami"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b44166eb6335aeac42744ea368cc4c32d3f2287a4ff765a5ce44d927ab8bb4"
checksum = "7f7b313901a15cfde2d88f434fcd077903d690f73cc36d1cec20f47906960aec"
dependencies = [
"clap",
"libc",
"uucore 0.0.26",
"uucore",
"windows-sys 0.48.0",
]
[[package]]
name = "uucore"
version = "0.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23994a722acb43dbc56877e271c9723f167ae42c4c089f909b2d7dd106c3a9b4"
dependencies = [
"clap",
"glob",
"libc",
"nix",
"once_cell",
"os_display",
"uucore_procs",
"wild",
]
[[package]]
name = "uucore"
version = "0.0.26"

View File

@ -159,13 +159,13 @@ unicode-segmentation = "1.11"
unicode-width = "0.1"
ureq = { version = "2.9", default-features = false }
url = "2.2"
uu_cp = "0.0.25"
uu_mkdir = "0.0.25"
uu_mktemp = "0.0.25"
uu_mv = "0.0.25"
uu_whoami = "0.0.25"
uu_uname = "0.0.25"
uucore = "0.0.25"
uu_cp = "0.0.26"
uu_mkdir = "0.0.26"
uu_mktemp = "0.0.26"
uu_mv = "0.0.26"
uu_whoami = "0.0.26"
uu_uname = "0.0.26"
uucore = "0.0.26"
uuid = "1.8.0"
v_htmlescape = "0.15.0"
wax = "0.6"
@ -195,6 +195,7 @@ reedline = { workspace = true, features = ["bashisms", "sqlite"] }
crossterm = { workspace = true }
ctrlc = { workspace = true }
dirs-next = { workspace = true }
log = { workspace = true }
miette = { workspace = true, features = ["fancy-no-backtrace", "fancy"] }
mimalloc = { version = "0.1.42", default-features = false, optional = true }

View File

@ -52,7 +52,7 @@ To use `Nu` in GitHub Action, check [setup-nu](https://github.com/marketplace/ac
Detailed installation instructions can be found in the [installation chapter of the book](https://www.nushell.sh/book/installation.html). Nu is available via many package managers:
[![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg)](https://repology.org/project/nushell/versions)
[![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg?columns=3)](https://repology.org/project/nushell/versions)
For details about which platforms the Nushell team actively supports, see [our platform support policy](devdocs/PLATFORM_SUPPORT.md).

View File

@ -75,7 +75,7 @@ const DEFAULT_HELP_MENU: &str = r#"
// Adds all menus to line editor
pub(crate) fn add_menus(
mut line_editor: Reedline,
engine_state: Arc<EngineState>,
engine_state_ref: Arc<EngineState>,
stack: &Stack,
config: &Config,
) -> Result<Reedline, ShellError> {
@ -83,7 +83,7 @@ pub(crate) fn add_menus(
line_editor = line_editor.clear_menus();
for menu in &config.menus {
line_editor = add_menu(line_editor, menu, engine_state.clone(), stack, config)?
line_editor = add_menu(line_editor, menu, engine_state_ref.clone(), stack, config)?
}
// Checking if the default menus have been added from the config file
@ -93,13 +93,16 @@ pub(crate) fn add_menus(
("help_menu", DEFAULT_HELP_MENU),
];
let mut engine_state = (*engine_state_ref).clone();
let mut menu_eval_results = vec![];
for (name, definition) in default_menus {
if !config
.menus
.iter()
.any(|menu| menu.name.to_expanded_string("", config) == name)
{
let (block, _) = {
let (block, delta) = {
let mut working_set = StateWorkingSet::new(&engine_state);
let output = parse(
&mut working_set,
@ -111,15 +114,31 @@ pub(crate) fn add_menus(
(output, working_set.render())
};
engine_state.merge_delta(delta)?;
let mut temp_stack = Stack::new().capture();
let input = PipelineData::Empty;
let res = eval_block::<WithoutDebug>(&engine_state, &mut temp_stack, &block, input)?;
menu_eval_results.push(eval_block::<WithoutDebug>(
&engine_state,
&mut temp_stack,
&block,
input,
)?);
}
}
if let PipelineData::Value(value, None) = res {
for menu in create_menus(&value)? {
line_editor =
add_menu(line_editor, &menu, engine_state.clone(), stack, config)?;
}
let new_engine_state_ref = Arc::new(engine_state);
for res in menu_eval_results.into_iter() {
if let PipelineData::Value(value, None) = res {
for menu in create_menus(&value)? {
line_editor = add_menu(
line_editor,
&menu,
new_engine_state_ref.clone(),
stack,
config,
)?;
}
}
}

View File

@ -763,11 +763,13 @@ fn variables_completions() {
// Test completions for $nu
let suggestions = completer.complete("$nu.", 4);
assert_eq!(15, suggestions.len());
assert_eq!(17, suggestions.len());
let expected: Vec<String> = vec![
"cache-dir".into(),
"config-path".into(),
"current-exe".into(),
"data-dir".into(),
"default-config-dir".into(),
"env-path".into(),
"history-enabled".into(),

View File

@ -67,8 +67,8 @@ impl Command for Def {
},
Example {
description: "Define a custom wrapper for an external command",
example: r#"def --wrapped my-echo [...rest] { echo $rest }; my-echo spam"#,
result: Some(Value::test_list(vec![Value::test_string("spam")])),
example: r#"def --wrapped my-echo [...rest] { ^echo ...$rest }; my-echo -e 'spam\tspam'"#,
result: Some(Value::test_string("spam\tspam")),
},
]
}

View File

@ -1,5 +1,6 @@
use nu_engine::{command_prelude::*, get_eval_block, get_eval_expression};
use nu_protocol::engine::CommandType;
use nu_protocol::ParseWarning;
#[derive(Clone)]
pub struct For;
@ -30,7 +31,7 @@ impl Command for For {
.required("block", SyntaxShape::Block, "The block to run.")
.switch(
"numbered",
"return a numbered item ($it.index and $it.item)",
"DEPRECATED: return a numbered item ($it.index and $it.item)",
Some('n'),
)
.creates_scope()
@ -78,6 +79,20 @@ impl Command for For {
let value = eval_expression(engine_state, stack, keyword_expr)?;
let numbered = call.has_flag(engine_state, stack, "numbered")?;
if numbered {
nu_protocol::report_error_new(
engine_state,
&ParseWarning::DeprecatedWarning {
old_command: "--numbered/-n".into(),
new_suggestion: "use `enumerate`".into(),
span: call
.get_named_arg("numbered")
.expect("`get_named_arg` found `--numbered` but still failed")
.span,
url: "See `help for` examples".into(),
},
);
}
let ctrlc = engine_state.ctrlc.clone();
let engine_state = engine_state.clone();
@ -198,8 +213,7 @@ impl Command for For {
},
Example {
description: "Number each item and print a message",
example:
"for $it in ['bob' 'fred'] --numbered { print $\"($it.index) is ($it.item)\" }",
example: r#"for $it in (['bob' 'fred'] | enumerate) { print $"($it.index) is ($it.item)" }"#,
result: None,
},
]

View File

@ -122,7 +122,7 @@ pub fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value {
Value::Filesize { .. } => input.clone(),
Value::Int { val, .. } => Value::filesize(*val, value_span),
Value::Float { val, .. } => Value::filesize(*val as i64, value_span),
Value::String { val, .. } => match int_from_string(val, value_span) {
Value::String { val, .. } => match i64_from_string(val, value_span) {
Ok(val) => Value::filesize(val, value_span),
Err(error) => Value::error(error, value_span),
},
@ -139,7 +139,7 @@ pub fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value {
}
}
fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
fn i64_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
// Get the Locale so we know what the thousands separator is
let locale = get_system_locale();
@ -151,24 +151,35 @@ fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
// Hadle negative file size
if let Some(stripped_negative_string) = clean_string.strip_prefix('-') {
match stripped_negative_string.parse::<bytesize::ByteSize>() {
Ok(n) => Ok(-(n.as_u64() as i64)),
Ok(n) => i64_from_byte_size(n, true, span),
Err(_) => Err(string_convert_error(span)),
}
} else if let Some(stripped_positive_string) = clean_string.strip_prefix('+') {
match stripped_positive_string.parse::<bytesize::ByteSize>() {
Ok(n) if stripped_positive_string.starts_with(|c: char| c.is_ascii_digit()) => {
Ok(n.as_u64() as i64)
i64_from_byte_size(n, false, span)
}
_ => Err(string_convert_error(span)),
}
} else {
match clean_string.parse::<bytesize::ByteSize>() {
Ok(n) => Ok(n.as_u64() as i64),
Ok(n) => i64_from_byte_size(n, false, span),
Err(_) => Err(string_convert_error(span)),
}
}
}
fn i64_from_byte_size(
byte_size: bytesize::ByteSize,
is_negative: bool,
span: Span,
) -> Result<i64, ShellError> {
match i64::try_from(byte_size.as_u64()) {
Ok(n) => Ok(if is_negative { -n } else { n }),
Err(_) => Err(string_convert_error(span)),
}
}
fn string_convert_error(span: Span) -> ShellError {
ShellError::CantConvert {
to_type: "filesize".into(),

View File

@ -1,7 +1,10 @@
use super::PathSubcommandArguments;
use nu_engine::command_prelude::*;
use nu_protocol::engine::StateWorkingSet;
use std::path::{Path, PathBuf};
use std::{
io,
path::{Path, PathBuf},
};
struct Arguments {
pwd: PathBuf,
@ -36,7 +39,7 @@ impl Command for SubCommand {
fn extra_usage(&self) -> &str {
r#"This checks the file system to confirm the path's object type.
If nothing is found, an empty string will be returned."#
If the path does not exist, null will be returned."#
}
fn is_const(&self) -> bool {
@ -104,9 +107,11 @@ If nothing is found, an empty string will be returned."#
fn path_type(path: &Path, span: Span, args: &Arguments) -> Value {
let path = nu_path::expand_path_with(path, &args.pwd, true);
let meta = path.symlink_metadata();
let ty = meta.as_ref().map(get_file_type).unwrap_or("");
Value::string(ty, span)
match path.symlink_metadata() {
Ok(metadata) => Value::string(get_file_type(&metadata), span),
Err(err) if err.kind() == io::ErrorKind::NotFound => Value::nothing(span),
Err(err) => Value::error(err.into_spanned(span).into(), span),
}
}
fn get_file_type(md: &std::fs::Metadata) -> &str {

View File

@ -10,7 +10,6 @@ struct Arguments {
substring: String,
cell_paths: Option<Vec<CellPath>>,
case_insensitive: bool,
not_contain: bool,
}
impl CmdArgument for Arguments {
@ -40,7 +39,6 @@ impl Command for SubCommand {
"For a data structure input, check strings at the given cell paths, and replace with result.",
)
.switch("ignore-case", "search is case insensitive", Some('i'))
.switch("not", "DEPRECATED OPTION: does not contain", Some('n'))
.category(Category::Strings)
}
@ -63,27 +61,12 @@ impl Command for SubCommand {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
if call.has_flag(engine_state, stack, "not")? {
nu_protocol::report_error_new(
engine_state,
&ShellError::GenericError {
error: "Deprecated option".into(),
msg: "`str contains --not {string}` is deprecated and will be removed in 0.95."
.into(),
span: Some(call.head),
help: Some("Please use the `not` operator instead.".into()),
inner: vec![],
},
);
}
let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 1)?;
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
let args = Arguments {
substring: call.req::<String>(engine_state, stack, 0)?,
cell_paths,
case_insensitive: call.has_flag(engine_state, stack, "ignore-case")?,
not_contain: call.has_flag(engine_state, stack, "not")?,
};
operate(action, args, input, call.head, engine_state.ctrlc.clone())
}
@ -114,7 +97,6 @@ impl Command for SubCommand {
substring: call.req_const::<String>(working_set, 0)?,
cell_paths,
case_insensitive: call.has_flag_const(working_set, "ignore-case")?,
not_contain: call.has_flag_const(working_set, "not")?,
};
operate(
action,
@ -183,7 +165,6 @@ fn action(
input: &Value,
Arguments {
case_insensitive,
not_contain,
substring,
..
}: &Arguments,
@ -191,23 +172,11 @@ fn action(
) -> Value {
match input {
Value::String { val, .. } => Value::bool(
match case_insensitive {
true => {
if *not_contain {
!val.to_folded_case()
.contains(substring.to_folded_case().as_str())
} else {
val.to_folded_case()
.contains(substring.to_folded_case().as_str())
}
}
false => {
if *not_contain {
!val.contains(substring)
} else {
val.contains(substring)
}
}
if *case_insensitive {
val.to_folded_case()
.contains(substring.to_folded_case().as_str())
} else {
val.contains(substring)
},
head,
),

View File

@ -1,4 +1,6 @@
use super::trim_cstyle_null;
use nu_engine::command_prelude::*;
use sysinfo::{CpuRefreshKind, System, MINIMUM_CPU_UPDATE_INTERVAL};
#[derive(Clone)]
pub struct SysCpu;
@ -26,7 +28,7 @@ impl Command for SysCpu {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::cpu(call.head).into_pipeline_data())
Ok(cpu(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -37,3 +39,42 @@ impl Command for SysCpu {
}]
}
}
fn cpu(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_cpu_specifics(CpuRefreshKind::everything());
// We must refresh the CPU twice a while apart to get valid usage data.
// In theory we could just sleep MINIMUM_CPU_UPDATE_INTERVAL, but I've noticed that
// that gives poor results (error of ~5%). Decided to wait 2x that long, somewhat arbitrarily
std::thread::sleep(MINIMUM_CPU_UPDATE_INTERVAL * 2);
sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage());
let cpus = sys
.cpus()
.iter()
.map(|cpu| {
// sysinfo CPU usage numbers are not very precise unless you wait a long time between refreshes.
// Round to 1DP (chosen somewhat arbitrarily) so people aren't misled by high-precision floats.
let rounded_usage = (cpu.cpu_usage() * 10.0).round() / 10.0;
let load_avg = System::load_average();
let load_avg = format!(
"{:.2}, {:.2}, {:.2}",
load_avg.one, load_avg.five, load_avg.fifteen
);
let record = record! {
"name" => Value::string(trim_cstyle_null(cpu.name()), span),
"brand" => Value::string(trim_cstyle_null(cpu.brand()), span),
"freq" => Value::int(cpu.frequency() as i64, span),
"cpu_usage" => Value::float(rounded_usage.into(), span),
"load_average" => Value::string(load_avg, span),
"vendor_id" => Value::string(trim_cstyle_null(cpu.vendor_id()), span),
};
Value::record(record, span)
})
.collect();
Value::list(cpus, span)
}

View File

@ -1,4 +1,6 @@
use super::trim_cstyle_null;
use nu_engine::command_prelude::*;
use sysinfo::Disks;
#[derive(Clone)]
pub struct SysDisks;
@ -26,7 +28,7 @@ impl Command for SysDisks {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::disks(call.head).into_pipeline_data())
Ok(disks(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -37,3 +39,27 @@ impl Command for SysDisks {
}]
}
}
fn disks(span: Span) -> Value {
let disks = Disks::new_with_refreshed_list()
.iter()
.map(|disk| {
let device = trim_cstyle_null(disk.name().to_string_lossy());
let typ = trim_cstyle_null(disk.file_system().to_string_lossy());
let record = record! {
"device" => Value::string(device, span),
"type" => Value::string(typ, span),
"mount" => Value::string(disk.mount_point().to_string_lossy(), span),
"total" => Value::filesize(disk.total_space() as i64, span),
"free" => Value::filesize(disk.available_space() as i64, span),
"removable" => Value::bool(disk.is_removable(), span),
"kind" => Value::string(disk.kind().to_string(), span),
};
Value::record(record, span)
})
.collect();
Value::list(disks, span)
}

View File

@ -1,4 +1,7 @@
use super::trim_cstyle_null;
use chrono::{DateTime, FixedOffset, Local};
use nu_engine::command_prelude::*;
use sysinfo::System;
#[derive(Clone)]
pub struct SysHost;
@ -26,8 +29,7 @@ impl Command for SysHost {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let host = super::host(call.head);
Ok(Value::record(host, call.head).into_pipeline_data())
Ok(host(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -38,3 +40,53 @@ impl Command for SysHost {
}]
}
}
fn host(span: Span) -> Value {
let mut record = Record::new();
if let Some(name) = System::name() {
record.push("name", Value::string(trim_cstyle_null(name), span));
}
if let Some(version) = System::os_version() {
record.push("os_version", Value::string(trim_cstyle_null(version), span));
}
if let Some(long_version) = System::long_os_version() {
record.push(
"long_os_version",
Value::string(trim_cstyle_null(long_version), span),
);
}
if let Some(version) = System::kernel_version() {
record.push(
"kernel_version",
Value::string(trim_cstyle_null(version), span),
);
}
if let Some(hostname) = System::host_name() {
record.push("hostname", Value::string(trim_cstyle_null(hostname), span));
}
let uptime = System::uptime()
.saturating_mul(1_000_000_000)
.try_into()
.unwrap_or(i64::MAX);
record.push("uptime", Value::duration(uptime, span));
let boot_time = boot_time()
.map(|time| Value::date(time, span))
.unwrap_or(Value::nothing(span));
record.push("boot_time", boot_time);
Value::record(record, span)
}
fn boot_time() -> Option<DateTime<FixedOffset>> {
// Broken systems can apparently return really high values.
// See: https://github.com/nushell/nushell/issues/10155
// First, try to convert u64 to i64, and then try to create a `DateTime`.
let secs = System::boot_time().try_into().ok()?;
let time = DateTime::from_timestamp(secs, 0)?;
Some(time.with_timezone(&Local).fixed_offset())
}

View File

@ -1,4 +1,5 @@
use nu_engine::command_prelude::*;
use sysinfo::System;
#[derive(Clone)]
pub struct SysMem;
@ -26,7 +27,7 @@ impl Command for SysMem {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::mem(call.head).into_pipeline_data())
Ok(mem(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -37,3 +38,20 @@ impl Command for SysMem {
}]
}
}
fn mem(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_memory();
let record = record! {
"total" => Value::filesize(sys.total_memory() as i64, span),
"free" => Value::filesize(sys.free_memory() as i64, span),
"used" => Value::filesize(sys.used_memory() as i64, span),
"available" => Value::filesize(sys.available_memory() as i64, span),
"swap total" => Value::filesize(sys.total_swap() as i64, span),
"swap free" => Value::filesize(sys.free_swap() as i64, span),
"swap used" => Value::filesize(sys.used_swap() as i64, span),
};
Value::record(record, span)
}

View File

@ -16,202 +16,6 @@ pub use sys_::Sys;
pub use temp::SysTemp;
pub use users::SysUsers;
use chrono::{DateTime, FixedOffset, Local};
use nu_protocol::{record, Record, Span, Value};
use sysinfo::{
Components, CpuRefreshKind, Disks, Networks, System, Users, MINIMUM_CPU_UPDATE_INTERVAL,
};
pub fn trim_cstyle_null(s: impl AsRef<str>) -> String {
fn trim_cstyle_null(s: impl AsRef<str>) -> String {
s.as_ref().trim_matches('\0').into()
}
pub fn disks(span: Span) -> Value {
let disks = Disks::new_with_refreshed_list()
.iter()
.map(|disk| {
let device = trim_cstyle_null(disk.name().to_string_lossy());
let typ = trim_cstyle_null(disk.file_system().to_string_lossy());
let record = record! {
"device" => Value::string(device, span),
"type" => Value::string(typ, span),
"mount" => Value::string(disk.mount_point().to_string_lossy(), span),
"total" => Value::filesize(disk.total_space() as i64, span),
"free" => Value::filesize(disk.available_space() as i64, span),
"removable" => Value::bool(disk.is_removable(), span),
"kind" => Value::string(disk.kind().to_string(), span),
};
Value::record(record, span)
})
.collect();
Value::list(disks, span)
}
pub fn net(span: Span) -> Value {
let networks = Networks::new_with_refreshed_list()
.iter()
.map(|(iface, data)| {
let record = record! {
"name" => Value::string(trim_cstyle_null(iface), span),
"sent" => Value::filesize(data.total_transmitted() as i64, span),
"recv" => Value::filesize(data.total_received() as i64, span),
};
Value::record(record, span)
})
.collect();
Value::list(networks, span)
}
pub fn cpu(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_cpu_specifics(CpuRefreshKind::everything());
// We must refresh the CPU twice a while apart to get valid usage data.
// In theory we could just sleep MINIMUM_CPU_UPDATE_INTERVAL, but I've noticed that
// that gives poor results (error of ~5%). Decided to wait 2x that long, somewhat arbitrarily
std::thread::sleep(MINIMUM_CPU_UPDATE_INTERVAL * 2);
sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage());
let cpus = sys
.cpus()
.iter()
.map(|cpu| {
// sysinfo CPU usage numbers are not very precise unless you wait a long time between refreshes.
// Round to 1DP (chosen somewhat arbitrarily) so people aren't misled by high-precision floats.
let rounded_usage = (cpu.cpu_usage() * 10.0).round() / 10.0;
let load_avg = System::load_average();
let load_avg = format!(
"{:.2}, {:.2}, {:.2}",
load_avg.one, load_avg.five, load_avg.fifteen
);
let record = record! {
"name" => Value::string(trim_cstyle_null(cpu.name()), span),
"brand" => Value::string(trim_cstyle_null(cpu.brand()), span),
"freq" => Value::int(cpu.frequency() as i64, span),
"cpu_usage" => Value::float(rounded_usage.into(), span),
"load_average" => Value::string(load_avg, span),
"vendor_id" => Value::string(trim_cstyle_null(cpu.vendor_id()), span),
};
Value::record(record, span)
})
.collect();
Value::list(cpus, span)
}
pub fn mem(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_memory();
let record = record! {
"total" => Value::filesize(sys.total_memory() as i64, span),
"free" => Value::filesize(sys.free_memory() as i64, span),
"used" => Value::filesize(sys.used_memory() as i64, span),
"available" => Value::filesize(sys.available_memory() as i64, span),
"swap total" => Value::filesize(sys.total_swap() as i64, span),
"swap free" => Value::filesize(sys.free_swap() as i64, span),
"swap used" => Value::filesize(sys.used_swap() as i64, span),
};
Value::record(record, span)
}
pub fn users(span: Span) -> Value {
let users = Users::new_with_refreshed_list()
.iter()
.map(|user| {
let groups = user
.groups()
.iter()
.map(|group| Value::string(trim_cstyle_null(group.name()), span))
.collect();
let record = record! {
"name" => Value::string(trim_cstyle_null(user.name()), span),
"groups" => Value::list(groups, span),
};
Value::record(record, span)
})
.collect();
Value::list(users, span)
}
pub fn host(span: Span) -> Record {
let mut record = Record::new();
if let Some(name) = System::name() {
record.push("name", Value::string(trim_cstyle_null(name), span));
}
if let Some(version) = System::os_version() {
record.push("os_version", Value::string(trim_cstyle_null(version), span));
}
if let Some(long_version) = System::long_os_version() {
record.push(
"long_os_version",
Value::string(trim_cstyle_null(long_version), span),
);
}
if let Some(version) = System::kernel_version() {
record.push(
"kernel_version",
Value::string(trim_cstyle_null(version), span),
);
}
if let Some(hostname) = System::host_name() {
record.push("hostname", Value::string(trim_cstyle_null(hostname), span));
}
let uptime = System::uptime()
.saturating_mul(1_000_000_000)
.try_into()
.unwrap_or(i64::MAX);
record.push("uptime", Value::duration(uptime, span));
let boot_time = boot_time()
.map(|time| Value::date(time, span))
.unwrap_or(Value::nothing(span));
record.push("boot_time", boot_time);
record
}
fn boot_time() -> Option<DateTime<FixedOffset>> {
// Broken systems can apparently return really high values.
// See: https://github.com/nushell/nushell/issues/10155
// First, try to convert u64 to i64, and then try to create a `DateTime`.
let secs = System::boot_time().try_into().ok()?;
let time = DateTime::from_timestamp(secs, 0)?;
Some(time.with_timezone(&Local).fixed_offset())
}
pub fn temp(span: Span) -> Value {
let components = Components::new_with_refreshed_list()
.iter()
.map(|component| {
let mut record = record! {
"unit" => Value::string(component.label(), span),
"temp" => Value::float(component.temperature().into(), span),
"high" => Value::float(component.max().into(), span),
};
if let Some(critical) = component.critical() {
record.push("critical", Value::float(critical.into(), span));
}
Value::record(record, span)
})
.collect();
Value::list(components, span)
}

View File

@ -1,4 +1,6 @@
use super::trim_cstyle_null;
use nu_engine::command_prelude::*;
use sysinfo::Networks;
#[derive(Clone)]
pub struct SysNet;
@ -26,7 +28,7 @@ impl Command for SysNet {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::net(call.head).into_pipeline_data())
Ok(net(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -37,3 +39,20 @@ impl Command for SysNet {
}]
}
}
fn net(span: Span) -> Value {
let networks = Networks::new_with_refreshed_list()
.iter()
.map(|(iface, data)| {
let record = record! {
"name" => Value::string(trim_cstyle_null(iface), span),
"sent" => Value::filesize(data.total_transmitted() as i64, span),
"recv" => Value::filesize(data.total_received() as i64, span),
};
Value::record(record, span)
})
.collect();
Value::list(networks, span)
}

View File

@ -1,4 +1,4 @@
use nu_engine::command_prelude::*;
use nu_engine::{command_prelude::*, get_full_help};
#[derive(Clone)]
pub struct Sys;
@ -20,41 +20,17 @@ impl Command for Sys {
}
fn extra_usage(&self) -> &str {
"Note that this command may take a noticeable amount of time to run. To reduce the time taken, you can use the various `sys` sub commands to get the subset of information you are interested in."
"You must use one of the following subcommands. Using this command as-is will only produce this help message."
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
nu_protocol::report_error_new(
engine_state,
&ShellError::GenericError {
error: "Deprecated command".into(),
msg: "the `sys` command is deprecated, please use the new subcommands (`sys host`, `sys mem`, etc.)."
.into(),
span: Some(call.head),
help: None,
inner: vec![],
},
);
let head = call.head;
let mut host = super::host(head);
host.push("sessions", super::users(head));
let record = record! {
"host" => Value::record(host, head),
"cpu" => super::cpu(head),
"disks" => super::disks(head),
"mem" => super::mem(head),
"temp" => super::temp(head),
"net" => super::net(head),
};
Ok(Value::record(record, head).into_pipeline_data())
Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {

View File

@ -1,4 +1,5 @@
use nu_engine::command_prelude::*;
use sysinfo::Components;
#[derive(Clone)]
pub struct SysTemp;
@ -30,7 +31,7 @@ impl Command for SysTemp {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::temp(call.head).into_pipeline_data())
Ok(temp(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -41,3 +42,24 @@ impl Command for SysTemp {
}]
}
}
fn temp(span: Span) -> Value {
let components = Components::new_with_refreshed_list()
.iter()
.map(|component| {
let mut record = record! {
"unit" => Value::string(component.label(), span),
"temp" => Value::float(component.temperature().into(), span),
"high" => Value::float(component.max().into(), span),
};
if let Some(critical) = component.critical() {
record.push("critical", Value::float(critical.into(), span));
}
Value::record(record, span)
})
.collect();
Value::list(components, span)
}

View File

@ -1,4 +1,6 @@
use super::trim_cstyle_null;
use nu_engine::command_prelude::*;
use sysinfo::Users;
#[derive(Clone)]
pub struct SysUsers;
@ -25,7 +27,7 @@ impl Command for SysUsers {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::users(call.head).into_pipeline_data())
Ok(users(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -36,3 +38,25 @@ impl Command for SysUsers {
}]
}
}
fn users(span: Span) -> Value {
let users = Users::new_with_refreshed_list()
.iter()
.map(|user| {
let groups = user
.groups()
.iter()
.map(|group| Value::string(trim_cstyle_null(group.name()), span))
.collect();
let record = record! {
"name" => Value::string(trim_cstyle_null(user.name()), span),
"groups" => Value::list(groups, span),
};
Value::record(record, span)
})
.collect();
Value::list(users, span)
}

View File

@ -76,6 +76,13 @@ fn into_filesize_wrong_negative_str_filesize() {
assert!(actual.err.contains("can't convert string to filesize"));
}
#[test]
fn into_filesize_large_negative_str_filesize() {
let actual = nu!("'-10000PiB' | into filesize");
assert!(actual.err.contains("can't convert string to filesize"));
}
#[test]
fn into_filesize_negative_str() {
let actual = nu!("'-1' | into filesize");
@ -104,6 +111,13 @@ fn into_filesize_wrong_positive_str_filesize() {
assert!(actual.err.contains("can't convert string to filesize"));
}
#[test]
fn into_filesize_large_positive_str_filesize() {
let actual = nu!("'+10000PiB' | into filesize");
assert!(actual.err.contains("can't convert string to filesize"));
}
#[test]
fn into_filesize_positive_str() {
let actual = nu!("'+1' | into filesize");

View File

@ -906,7 +906,7 @@ fn test_cp_debug_default() {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if !actual
.out
.contains("copy offload: unknown, reflink: unsupported, sparse detection: no")
.contains("copy offload: yes, reflink: unsupported, sparse detection: no")
{
panic!("{}", format!("Failure: stdout was \n{}", actual.out));
}

View File

@ -6,18 +6,36 @@ pub fn home_dir() -> Option<PathBuf> {
dirs_next::home_dir()
}
/// Return the data directory for the current platform or XDG_DATA_HOME if specified.
pub fn data_dir() -> Option<PathBuf> {
match std::env::var("XDG_DATA_HOME").map(PathBuf::from) {
Ok(xdg_data) if xdg_data.is_absolute() => Some(canonicalize(&xdg_data).unwrap_or(xdg_data)),
_ => get_canonicalized_path(dirs_next::data_dir()),
}
}
/// Return the cache directory for the current platform or XDG_CACHE_HOME if specified.
pub fn cache_dir() -> Option<PathBuf> {
match std::env::var("XDG_CACHE_HOME").map(PathBuf::from) {
Ok(xdg_cache) if xdg_cache.is_absolute() => {
Some(canonicalize(&xdg_cache).unwrap_or(xdg_cache))
}
_ => get_canonicalized_path(dirs_next::cache_dir()),
}
}
/// Return the config directory for the current platform or XDG_CONFIG_HOME if specified.
pub fn config_dir() -> Option<PathBuf> {
match std::env::var("XDG_CONFIG_HOME").map(PathBuf::from) {
Ok(xdg_config) if xdg_config.is_absolute() => {
Some(canonicalize(&xdg_config).unwrap_or(xdg_config))
}
_ => config_dir_old(),
_ => get_canonicalized_path(dirs_next::config_dir()),
}
}
/// Get the old default config directory. Outside of Linux, this will ignore `XDG_CONFIG_HOME`
pub fn config_dir_old() -> Option<PathBuf> {
let path = dirs_next::config_dir()?;
pub fn get_canonicalized_path(path: Option<PathBuf>) -> Option<PathBuf> {
let path = path?;
Some(canonicalize(&path).unwrap_or(path))
}

View File

@ -10,7 +10,7 @@ mod trailing_slash;
pub use components::components;
pub use expansions::{canonicalize_with, expand_path_with, expand_to_real_path, locate_in_dirs};
pub use helpers::{config_dir, config_dir_old, home_dir};
pub use helpers::{cache_dir, config_dir, data_dir, get_canonicalized_path, home_dir};
pub use path::*;
pub use tilde::expand_tilde;
pub use trailing_slash::{has_trailing_slash, strip_trailing_slash};

View File

@ -10,7 +10,7 @@ pub enum ParseWarning {
DeprecatedWarning {
old_command: String,
new_suggestion: String,
#[label("`{old_command}` is deprecated and will be removed in 0.94. Please {new_suggestion} instead")]
#[label("`{old_command}` is deprecated and will be removed in a future release. Please {new_suggestion} instead")]
span: Span,
url: String,
},

View File

@ -149,6 +149,38 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu
},
);
record.push(
"data-dir",
if let Some(path) = nu_path::data_dir() {
let mut canon_data_path = canonicalize_path(engine_state, &path);
canon_data_path.push("nushell");
Value::string(canon_data_path.to_string_lossy(), span)
} else {
Value::error(
ShellError::IOError {
msg: "Could not get data path".into(),
},
span,
)
},
);
record.push(
"cache-dir",
if let Some(path) = nu_path::cache_dir() {
let mut canon_cache_path = canonicalize_path(engine_state, &path);
canon_cache_path.push("nushell");
Value::string(canon_cache_path.to_string_lossy(), span)
} else {
Value::error(
ShellError::IOError {
msg: "Could not get cache path".into(),
},
span,
)
},
);
record.push("temp-path", {
let canon_temp_path = canonicalize_path(engine_state, &std::env::temp_dir());
Value::string(canon_temp_path.to_string_lossy(), span)

View File

@ -77,6 +77,7 @@ $env.ENV_CONVERSIONS = {
# The default for this is $nu.default-config-dir/scripts
$env.NU_LIB_DIRS = [
($nu.default-config-dir | path join 'scripts') # add <nushell-config-dir>/scripts
($nu.data-dir | path join 'completions') # default home for nushell completions
]
# Directories to search for plugin binaries when calling register

View File

@ -35,6 +35,7 @@ impl PluginCommand for ToNu {
Some('n'),
)
.switch("tail", "shows tail rows", Some('t'))
.switch("index", "add an index column", Some('i'))
.input_output_types(vec![
(Type::Custom("expression".into()), Type::Any),
(Type::Custom("dataframe".into()), Type::table()),
@ -62,18 +63,18 @@ impl PluginCommand for ToNu {
vec![
Example {
description: "Shows head rows from dataframe",
example: "[[a b]; [1 2] [3 4]] | polars into-df | polars into-nu",
example: "[[a b]; [1 2] [3 4]] | polars into-df | polars into-nu --index",
result: Some(Value::list(vec![rec_1, rec_2], Span::test_data())),
},
Example {
description: "Shows tail rows from dataframe",
example:
"[[a b]; [1 2] [5 6] [3 4]] | polars into-df | polars into-nu --tail --rows 1",
"[[a b]; [1 2] [5 6] [3 4]] | polars into-df | polars into-nu --tail --rows 1 --index",
result: Some(Value::list(vec![rec_3], Span::test_data())),
},
Example {
description: "Convert a col expression into a nushell value",
example: "polars col a | polars into-nu",
example: "polars col a | polars into-nu --index",
result: Some(Value::test_record(record! {
"expr" => Value::test_string("column"),
"value" => Value::test_string("a"),
@ -106,17 +107,18 @@ fn dataframe_command(
) -> Result<PipelineData, ShellError> {
let rows: Option<usize> = call.get_flag("rows")?;
let tail: bool = call.has_flag("tail")?;
let index: bool = call.has_flag("index")?;
let df = NuDataFrame::try_from_value_coerce(plugin, &input, call.head)?;
let values = if tail {
df.tail(rows, call.head)?
df.tail(rows, index, call.head)?
} else {
// if rows is specified, return those rows, otherwise return everything
if rows.is_some() {
df.head(rows, call.head)?
df.head(rows, index, call.head)?
} else {
df.head(Some(df.height()), call.head)?
df.head(Some(df.height()), index, call.head)?
}
};

View File

@ -326,22 +326,22 @@ impl NuDataFrame {
}
// Print is made out a head and if the dataframe is too large, then a tail
pub fn print(&self, span: Span) -> Result<Vec<Value>, ShellError> {
pub fn print(&self, include_index: bool, span: Span) -> Result<Vec<Value>, ShellError> {
let df = &self.df;
let size: usize = 20;
if df.height() > size {
let sample_size = size / 2;
let mut values = self.head(Some(sample_size), span)?;
let mut values = self.head(Some(sample_size), include_index, span)?;
conversion::add_separator(&mut values, df, self.has_index(), span);
let remaining = df.height() - sample_size;
let tail_size = remaining.min(sample_size);
let mut tail_values = self.tail(Some(tail_size), span)?;
let mut tail_values = self.tail(Some(tail_size), include_index, span)?;
values.append(&mut tail_values);
Ok(values)
} else {
Ok(self.head(Some(size), span)?)
Ok(self.head(Some(size), include_index, span)?)
}
}
@ -349,26 +349,38 @@ impl NuDataFrame {
self.df.height()
}
pub fn head(&self, rows: Option<usize>, span: Span) -> Result<Vec<Value>, ShellError> {
pub fn head(
&self,
rows: Option<usize>,
include_index: bool,
span: Span,
) -> Result<Vec<Value>, ShellError> {
let to_row = rows.unwrap_or(5);
let values = self.to_rows(0, to_row, span)?;
let values = self.to_rows(0, to_row, include_index, span)?;
Ok(values)
}
pub fn tail(&self, rows: Option<usize>, span: Span) -> Result<Vec<Value>, ShellError> {
pub fn tail(
&self,
rows: Option<usize>,
include_index: bool,
span: Span,
) -> Result<Vec<Value>, ShellError> {
let df = &self.df;
let to_row = df.height();
let size = rows.unwrap_or(DEFAULT_ROWS);
let from_row = to_row.saturating_sub(size);
let values = self.to_rows(from_row, to_row, span)?;
let values = self.to_rows(from_row, to_row, include_index, span)?;
Ok(values)
}
/// Converts the dataframe to a nushell list of values
pub fn to_rows(
&self,
from_row: usize,
to_row: usize,
include_index: bool,
span: Span,
) -> Result<Vec<Value>, ShellError> {
let df = &self.df;
@ -400,7 +412,7 @@ impl NuDataFrame {
.map(|i| {
let mut record = Record::new();
if !has_index {
if !has_index && include_index {
record.push("index", Value::int((i + from_row) as i64, span));
}
@ -602,7 +614,7 @@ impl CustomValueSupport for NuDataFrame {
}
fn base_value(self, span: Span) -> Result<Value, ShellError> {
let vals = self.print(span)?;
let vals = self.print(true, span)?;
Ok(Value::list(vals, span))
}

View File

@ -63,45 +63,16 @@ fn compute_with_value(
op: Span,
right: &Value,
) -> Result<Value, ShellError> {
let rhs_span = right.span();
match right {
Value::Custom { val: rhs, .. } => {
let rhs = rhs.as_any().downcast_ref::<NuExpression>().ok_or_else(|| {
ShellError::TypeMismatch {
err_message: "Right hand side not a dataframe expression".into(),
span: rhs_span,
}
})?;
match rhs.as_ref() {
polars::prelude::Expr::Literal(..) => with_operator(
(plugin, engine),
operator,
left,
rhs,
lhs_span,
right.span(),
op,
),
_ => Err(ShellError::TypeMismatch {
err_message: "Only literal expressions or number".into(),
span: right.span(),
}),
}
}
_ => {
let rhs = NuExpression::try_from_value(plugin, right)?;
with_operator(
(plugin, engine),
operator,
left,
&rhs,
lhs_span,
right.span(),
op,
)
}
}
let rhs = NuExpression::try_from_value(plugin, right)?;
with_operator(
(plugin, engine),
operator,
left,
&rhs,
lhs_span,
right.span(),
op,
)
}
fn with_operator(

View File

@ -104,7 +104,9 @@ fn main() -> Result<()> {
default: nushell_config_path.display().to_string(),
},
);
} else if let Some(old_config) = nu_path::config_dir_old().map(|p| p.join("nushell")) {
} else if let Some(old_config) =
nu_path::get_canonicalized_path(dirs_next::config_dir()).map(|p| p.join("nushell"))
{
let xdg_config_empty = nushell_config_path
.read_dir()
.map_or(true, |mut dir| dir.next().is_none());
@ -125,13 +127,22 @@ fn main() -> Result<()> {
}
}
let default_nushell_completions_path = if let Some(mut path) = nu_path::data_dir() {
path.push("nushell");
path.push("completions");
path
} else {
std::path::PathBuf::new()
};
let mut default_nu_lib_dirs_path = nushell_config_path.clone();
default_nu_lib_dirs_path.push("scripts");
engine_state.add_env_var(
"NU_LIB_DIRS".to_string(),
Value::test_list(vec![Value::test_string(
default_nu_lib_dirs_path.to_string_lossy(),
)]),
Value::test_list(vec![
Value::test_string(default_nu_lib_dirs_path.to_string_lossy()),
Value::test_string(default_nushell_completions_path.to_string_lossy()),
]),
);
let mut default_nu_plugin_dirs_path = nushell_config_path;