mirror of
https://github.com/nushell/nushell.git
synced 2025-06-30 22:50:14 +02:00
Create Record
type (#10103)
# Description This PR creates a new `Record` type to reduce duplicate code and possibly bugs as well. (This is an edited version of #9648.) - `Record` implements `FromIterator` and `IntoIterator` and so can be iterated over or collected into. For example, this helps with conversions to and from (hash)maps. (Also, no more `cols.iter().zip(vals)`!) - `Record` has a `push(col, val)` function to help insure that the number of columns is equal to the number of values. I caught a few potential bugs thanks to this (e.g. in the `ls` command). - Finally, this PR also adds a `record!` macro that helps simplify record creation. It is used like so: ```rust record! { "key1" => some_value, "key2" => Value::string("text", span), "key3" => Value::int(optional_int.unwrap_or(0), span), "key4" => Value::bool(config.setting, span), } ``` Since macros hinder formatting, etc., the right hand side values should be relatively short and sweet like the examples above. Where possible, prefer `record!` or `.collect()` on an iterator instead of multiple `Record::push`s, since the first two automatically set the record capacity and do less work overall. # User-Facing Changes Besides the changes in `nu-protocol` the only other breaking changes are to `nu-table::{ExpandedTable::build_map, JustTable::kv_table}`.
This commit is contained in:
@ -3,7 +3,7 @@ use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::{Call, CellPath},
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value,
|
||||
Category, Example, PipelineData, Record, ShellError, Signature, Span, SyntaxShape, Type, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -60,31 +60,26 @@ impl Command for SubCommand {
|
||||
example: "[[value]; ['false'] ['1'] [0] [1.0] [true]] | into bool value",
|
||||
result: Some(Value::List {
|
||||
vals: vec![
|
||||
Value::Record {
|
||||
Value::test_record(Record {
|
||||
cols: vec!["value".to_string()],
|
||||
vals: vec![Value::bool(false, span)],
|
||||
span,
|
||||
},
|
||||
Value::Record {
|
||||
}),
|
||||
Value::test_record(Record {
|
||||
cols: vec!["value".to_string()],
|
||||
vals: vec![Value::bool(true, span)],
|
||||
span,
|
||||
},
|
||||
Value::Record {
|
||||
}),
|
||||
Value::test_record(Record {
|
||||
cols: vec!["value".to_string()],
|
||||
vals: vec![Value::bool(false, span)],
|
||||
span,
|
||||
},
|
||||
Value::Record {
|
||||
}),
|
||||
Value::test_record(Record {
|
||||
cols: vec!["value".to_string()],
|
||||
vals: vec![Value::bool(true, span)],
|
||||
span,
|
||||
},
|
||||
Value::Record {
|
||||
}),
|
||||
Value::test_record(Record {
|
||||
cols: vec!["value".to_string()],
|
||||
vals: vec![Value::bool(true, span)],
|
||||
span,
|
||||
},
|
||||
}),
|
||||
],
|
||||
span,
|
||||
}),
|
||||
|
@ -3,7 +3,7 @@ use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::{Call, CellPath},
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value,
|
||||
Category, Example, PipelineData, Record, ShellError, Signature, Span, SyntaxShape, Type, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -63,11 +63,10 @@ impl Command for SubCommand {
|
||||
description: "Convert string to decimal in table",
|
||||
example: "[[num]; ['5.01']] | into decimal num",
|
||||
result: Some(Value::List {
|
||||
vals: vec![Value::Record {
|
||||
vals: vec![Value::test_record(Record {
|
||||
cols: vec!["num".to_string()],
|
||||
vals: vec![Value::test_float(5.01)],
|
||||
span: Span::test_data(),
|
||||
}],
|
||||
})],
|
||||
span: Span::test_data(),
|
||||
}),
|
||||
},
|
||||
|
@ -3,7 +3,8 @@ use nu_parser::{parse_unit_value, DURATION_UNIT_GROUPS};
|
||||
use nu_protocol::{
|
||||
ast::{Call, CellPath, Expr},
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Unit, Value,
|
||||
Category, Example, PipelineData, Record, ShellError, Signature, Span, SyntaxShape, Type, Unit,
|
||||
Value,
|
||||
};
|
||||
|
||||
const NS_PER_SEC: i64 = 1_000_000_000;
|
||||
@ -80,46 +81,41 @@ impl Command for SubCommand {
|
||||
"[[value]; ['1sec'] ['2min'] ['3hr'] ['4day'] ['5wk']] | into duration value",
|
||||
result: Some(Value::List {
|
||||
vals: vec![
|
||||
Value::Record {
|
||||
Value::test_record(Record {
|
||||
cols: vec!["value".to_string()],
|
||||
vals: vec![Value::Duration {
|
||||
val: NS_PER_SEC,
|
||||
span,
|
||||
}],
|
||||
span,
|
||||
},
|
||||
Value::Record {
|
||||
}),
|
||||
Value::test_record(Record {
|
||||
cols: vec!["value".to_string()],
|
||||
vals: vec![Value::Duration {
|
||||
val: 2 * 60 * NS_PER_SEC,
|
||||
span,
|
||||
}],
|
||||
span,
|
||||
},
|
||||
Value::Record {
|
||||
}),
|
||||
Value::test_record(Record {
|
||||
cols: vec!["value".to_string()],
|
||||
vals: vec![Value::Duration {
|
||||
val: 3 * 60 * 60 * NS_PER_SEC,
|
||||
span,
|
||||
}],
|
||||
span,
|
||||
},
|
||||
Value::Record {
|
||||
}),
|
||||
Value::test_record(Record {
|
||||
cols: vec!["value".to_string()],
|
||||
vals: vec![Value::Duration {
|
||||
val: 4 * 24 * 60 * 60 * NS_PER_SEC,
|
||||
span,
|
||||
}],
|
||||
span,
|
||||
},
|
||||
Value::Record {
|
||||
}),
|
||||
Value::test_record(Record {
|
||||
cols: vec!["value".to_string()],
|
||||
vals: vec![Value::Duration {
|
||||
val: 5 * 7 * 24 * 60 * 60 * NS_PER_SEC,
|
||||
span,
|
||||
}],
|
||||
span,
|
||||
},
|
||||
}),
|
||||
],
|
||||
span,
|
||||
}),
|
||||
|
@ -3,7 +3,7 @@ use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::{Call, CellPath},
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value,
|
||||
Category, Example, PipelineData, Record, ShellError, Signature, Span, SyntaxShape, Type, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -81,7 +81,7 @@ impl Command for SubCommand {
|
||||
example: r#"[[device size]; ["/dev/sda1" "200"] ["/dev/loop0" "50"]] | into filesize size"#,
|
||||
result: Some(Value::List {
|
||||
vals: vec![
|
||||
Value::Record {
|
||||
Value::test_record(Record {
|
||||
cols: vec!["device".to_string(), "size".to_string()],
|
||||
vals: vec![
|
||||
Value::String {
|
||||
@ -93,9 +93,8 @@ impl Command for SubCommand {
|
||||
span: Span::test_data(),
|
||||
},
|
||||
],
|
||||
span: Span::test_data(),
|
||||
},
|
||||
Value::Record {
|
||||
}),
|
||||
Value::test_record(Record {
|
||||
cols: vec!["device".to_string(), "size".to_string()],
|
||||
vals: vec![
|
||||
Value::String {
|
||||
@ -107,8 +106,7 @@ impl Command for SubCommand {
|
||||
span: Span::test_data(),
|
||||
},
|
||||
],
|
||||
span: Span::test_data(),
|
||||
},
|
||||
}),
|
||||
],
|
||||
span: Span::test_data(),
|
||||
}),
|
||||
|
@ -1,10 +1,11 @@
|
||||
use chrono::{DateTime, Datelike, FixedOffset, Timelike};
|
||||
use nu_protocol::format_duration_as_timeperiod;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value,
|
||||
format_duration_as_timeperiod, record, Category, Example, IntoPipelineData, PipelineData,
|
||||
Record, ShellError, Signature, Span, Type, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SubCommand;
|
||||
|
||||
@ -50,42 +51,39 @@ impl Command for SubCommand {
|
||||
Example {
|
||||
description: "Convert from one row table to record",
|
||||
example: "[[value]; [false]] | into record",
|
||||
result: Some(Value::Record {
|
||||
result: Some(Value::test_record(Record {
|
||||
cols: vec!["value".to_string()],
|
||||
vals: vec![Value::bool(false, span)],
|
||||
span,
|
||||
}),
|
||||
})),
|
||||
},
|
||||
Example {
|
||||
description: "Convert from list to record",
|
||||
example: "[1 2 3] | into record",
|
||||
result: Some(Value::Record {
|
||||
result: Some(Value::test_record(Record {
|
||||
cols: vec!["0".to_string(), "1".to_string(), "2".to_string()],
|
||||
vals: vec![
|
||||
Value::Int { val: 1, span },
|
||||
Value::Int { val: 2, span },
|
||||
Value::Int { val: 3, span },
|
||||
],
|
||||
span,
|
||||
}),
|
||||
})),
|
||||
},
|
||||
Example {
|
||||
description: "Convert from range to record",
|
||||
example: "0..2 | into record",
|
||||
result: Some(Value::Record {
|
||||
result: Some(Value::test_record(Record {
|
||||
cols: vec!["0".to_string(), "1".to_string(), "2".to_string()],
|
||||
vals: vec![
|
||||
Value::Int { val: 0, span },
|
||||
Value::Int { val: 1, span },
|
||||
Value::Int { val: 2, span },
|
||||
],
|
||||
span,
|
||||
}),
|
||||
})),
|
||||
},
|
||||
Example {
|
||||
description: "convert duration to record (weeks max)",
|
||||
example: "(-500day - 4hr - 5sec) | into record",
|
||||
result: Some(Value::Record {
|
||||
result: Some(Value::test_record(Record {
|
||||
cols: vec![
|
||||
"week".into(),
|
||||
"day".into(),
|
||||
@ -103,22 +101,20 @@ impl Command for SubCommand {
|
||||
span,
|
||||
},
|
||||
],
|
||||
span,
|
||||
}),
|
||||
})),
|
||||
},
|
||||
Example {
|
||||
description: "convert record to record",
|
||||
example: "{a: 1, b: 2} | into record",
|
||||
result: Some(Value::Record {
|
||||
result: Some(Value::test_record(Record {
|
||||
cols: vec!["a".to_string(), "b".to_string()],
|
||||
vals: vec![Value::Int { val: 1, span }, Value::Int { val: 2, span }],
|
||||
span,
|
||||
}),
|
||||
})),
|
||||
},
|
||||
Example {
|
||||
description: "convert date to record",
|
||||
example: "2020-04-12T22:10:57+02:00 | into record",
|
||||
result: Some(Value::Record {
|
||||
result: Some(Value::test_record(Record {
|
||||
cols: vec![
|
||||
"year".into(),
|
||||
"month".into(),
|
||||
@ -140,8 +136,7 @@ impl Command for SubCommand {
|
||||
span,
|
||||
},
|
||||
],
|
||||
span,
|
||||
}),
|
||||
})),
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -155,34 +150,26 @@ fn into_record(
|
||||
let input = input.into_value(call.head);
|
||||
let input_type = input.get_type();
|
||||
let res = match input {
|
||||
Value::Date { val, span } => parse_date_into_record(Ok(val), span),
|
||||
Value::Date { val, span } => parse_date_into_record(val, span),
|
||||
Value::Duration { val, span } => parse_duration_into_record(val, span),
|
||||
Value::List { mut vals, span } => match input_type {
|
||||
Type::Table(..) if vals.len() == 1 => vals.pop().expect("already checked 1 item"),
|
||||
_ => {
|
||||
let mut cols = vec![];
|
||||
let mut values = vec![];
|
||||
for (idx, val) in vals.into_iter().enumerate() {
|
||||
cols.push(format!("{idx}"));
|
||||
values.push(val);
|
||||
}
|
||||
Value::Record {
|
||||
cols,
|
||||
vals: values,
|
||||
span,
|
||||
}
|
||||
}
|
||||
_ => Value::record(
|
||||
vals.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, val)| (format!("{idx}"), val))
|
||||
.collect(),
|
||||
span,
|
||||
),
|
||||
},
|
||||
Value::Range { val, span } => {
|
||||
let mut cols = vec![];
|
||||
let mut vals = vec![];
|
||||
for (idx, val) in val.into_range_iter(engine_state.ctrlc.clone())?.enumerate() {
|
||||
cols.push(format!("{idx}"));
|
||||
vals.push(val);
|
||||
}
|
||||
Value::Record { cols, vals, span }
|
||||
}
|
||||
Value::Record { cols, vals, span } => Value::Record { cols, vals, span },
|
||||
Value::Range { val, span } => Value::record(
|
||||
val.into_range_iter(engine_state.ctrlc.clone())?
|
||||
.enumerate()
|
||||
.map(|(idx, val)| (format!("{idx}"), val))
|
||||
.collect(),
|
||||
span,
|
||||
),
|
||||
Value::Record { val, span } => Value::Record { val, span },
|
||||
Value::Error { .. } => input,
|
||||
other => Value::Error {
|
||||
error: Box::new(ShellError::OnlySupportsThisInputType {
|
||||
@ -196,87 +183,50 @@ fn into_record(
|
||||
Ok(res.into_pipeline_data())
|
||||
}
|
||||
|
||||
fn parse_date_into_record(date: Result<DateTime<FixedOffset>, Value>, span: Span) -> Value {
|
||||
let cols = vec![
|
||||
"year".into(),
|
||||
"month".into(),
|
||||
"day".into(),
|
||||
"hour".into(),
|
||||
"minute".into(),
|
||||
"second".into(),
|
||||
"timezone".into(),
|
||||
];
|
||||
match date {
|
||||
Ok(x) => {
|
||||
let vals = vec![
|
||||
Value::Int {
|
||||
val: x.year() as i64,
|
||||
span,
|
||||
},
|
||||
Value::Int {
|
||||
val: x.month() as i64,
|
||||
span,
|
||||
},
|
||||
Value::Int {
|
||||
val: x.day() as i64,
|
||||
span,
|
||||
},
|
||||
Value::Int {
|
||||
val: x.hour() as i64,
|
||||
span,
|
||||
},
|
||||
Value::Int {
|
||||
val: x.minute() as i64,
|
||||
span,
|
||||
},
|
||||
Value::Int {
|
||||
val: x.second() as i64,
|
||||
span,
|
||||
},
|
||||
Value::String {
|
||||
val: x.offset().to_string(),
|
||||
span,
|
||||
},
|
||||
];
|
||||
Value::Record { cols, vals, span }
|
||||
}
|
||||
Err(e) => e,
|
||||
}
|
||||
fn parse_date_into_record(date: DateTime<FixedOffset>, span: Span) -> Value {
|
||||
Value::record(
|
||||
record! {
|
||||
"year" => Value::int(date.year() as i64, span),
|
||||
"month" => Value::int(date.month() as i64, span),
|
||||
"day" => Value::int(date.day() as i64, span),
|
||||
"hour" => Value::int(date.hour() as i64, span),
|
||||
"minute" => Value::int(date.minute() as i64, span),
|
||||
"second" => Value::int(date.second() as i64, span),
|
||||
"timezone" => Value::string(date.offset().to_string(), span),
|
||||
},
|
||||
span,
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_duration_into_record(duration: i64, span: Span) -> Value {
|
||||
let (sign, periods) = format_duration_as_timeperiod(duration);
|
||||
|
||||
let mut cols = vec![];
|
||||
let mut vals = vec![];
|
||||
let mut record = Record::new();
|
||||
for p in periods {
|
||||
let num_with_unit = p.to_text().to_string();
|
||||
let split = num_with_unit.split(' ').collect::<Vec<&str>>();
|
||||
cols.push(match split[1] {
|
||||
"ns" => "nanosecond".into(),
|
||||
"µs" => "microsecond".into(),
|
||||
"ms" => "millisecond".into(),
|
||||
"sec" => "second".into(),
|
||||
"min" => "minute".into(),
|
||||
"hr" => "hour".into(),
|
||||
"day" => "day".into(),
|
||||
"wk" => "week".into(),
|
||||
_ => "unknown".into(),
|
||||
});
|
||||
|
||||
vals.push(Value::Int {
|
||||
val: split[0].parse::<i64>().unwrap_or(0),
|
||||
span,
|
||||
});
|
||||
record.push(
|
||||
match split[1] {
|
||||
"ns" => "nanosecond",
|
||||
"µs" => "microsecond",
|
||||
"ms" => "millisecond",
|
||||
"sec" => "second",
|
||||
"min" => "minute",
|
||||
"hr" => "hour",
|
||||
"day" => "day",
|
||||
"wk" => "week",
|
||||
_ => "unknown",
|
||||
},
|
||||
Value::int(split[0].parse().unwrap_or(0), span),
|
||||
);
|
||||
}
|
||||
|
||||
cols.push("sign".into());
|
||||
vals.push(Value::String {
|
||||
val: if sign == -1 { "-".into() } else { "+".into() },
|
||||
span,
|
||||
});
|
||||
record.push(
|
||||
"sign",
|
||||
Value::string(if sign == -1 { "-" } else { "+" }, span),
|
||||
);
|
||||
|
||||
Value::Record { cols, vals, span }
|
||||
Value::record(record, span)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -257,11 +257,7 @@ fn action(input: &Value, args: &Arguments, span: Span) -> Value {
|
||||
val: "".to_string(),
|
||||
span,
|
||||
},
|
||||
Value::Record {
|
||||
cols: _,
|
||||
vals: _,
|
||||
span: _,
|
||||
} => Value::Error {
|
||||
Value::Record { .. } => Value::Error {
|
||||
// Watch out for CantConvert's argument order
|
||||
error: Box::new(ShellError::CantConvert {
|
||||
to_type: "string".into(),
|
||||
|
Reference in New Issue
Block a user