From fd222117375167e1741e51d93b2e4259f7816e5c Mon Sep 17 00:00:00 2001 From: JT <547158+jntrnr@users.noreply.github.com> Date: Sun, 20 Feb 2022 16:26:41 -0500 Subject: [PATCH] Add nuon format for fun (#4401) * Add nuon format for fun * more fun * More nuon fixes, allow comments, improve errors --- Cargo.toml | 2 +- crates/nu-command/src/default_context.rs | 2 + crates/nu-command/src/filesystem/open.rs | 2 +- crates/nu-command/src/formats/from/json.rs | 4 +- crates/nu-command/src/formats/from/mod.rs | 2 + crates/nu-command/src/formats/from/nuon.rs | 187 +++++++++++++++++++++ crates/nu-command/src/formats/to/mod.rs | 2 + crates/nu-command/src/formats/to/nuon.rs | 98 +++++++++++ crates/nu-protocol/src/shell_error.rs | 4 +- src/default_config.nu | 2 +- 10 files changed, 298 insertions(+), 7 deletions(-) create mode 100644 crates/nu-command/src/formats/from/nuon.rs create mode 100644 crates/nu-command/src/formats/to/nuon.rs diff --git a/Cargo.toml b/Cargo.toml index c7826ece59..f755a68f6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,7 @@ itertools = "0.10.3" plugin = ["nu-plugin", "nu-parser/plugin", "nu-command/plugin", "nu-protocol/plugin", "nu-engine/plugin"] default = ["plugin", "inc", "example", "which"] stable = ["default"] -extra = [ "default", "dataframe", "gstat", "zip-support", "query", ] +extra = ["default", "dataframe", "gstat", "zip-support", "query", "trash-support"] wasi = ["inc"] trash-support = ["nu-command/trash-support"] diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 1a307bc204..50c410a220 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -235,6 +235,7 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { FromIcs, FromIni, FromJson, + FromNuon, FromOds, FromSsv, FromToml, @@ -250,6 +251,7 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { ToHtml, ToJson, ToMd, + ToNuon, ToToml, ToTsv, ToCsv, diff --git a/crates/nu-command/src/filesystem/open.rs b/crates/nu-command/src/filesystem/open.rs index d3afce0d34..8521ddcecd 100644 --- a/crates/nu-command/src/filesystem/open.rs +++ b/crates/nu-command/src/filesystem/open.rs @@ -142,7 +142,7 @@ impl Command for Open { Some(converter_id) => engine_state.get_decl(converter_id).run( engine_state, stack, - &Call::new(call_span), + &Call::new(arg_span), output, ), None => Ok(output), diff --git a/crates/nu-command/src/formats/from/json.rs b/crates/nu-command/src/formats/from/json.rs index 502b8e1188..df3a1b9a06 100644 --- a/crates/nu-command/src/formats/from/json.rs +++ b/crates/nu-command/src/formats/from/json.rs @@ -26,7 +26,7 @@ impl Command for FromJson { fn examples(&self) -> Vec { vec![ Example { - example: "'{ a:1 }' | from json", + example: r#"'{ "a": 1 }' | from json"#, description: "Converts json formatted string to table", result: Some(Value::Record { cols: vec!["a".to_string()], @@ -38,7 +38,7 @@ impl Command for FromJson { }), }, Example { - example: "'{ a:1, b: [1, 2] }' | from json", + example: r#"'{ "a": 1, "b": [1, 2] }' | from json"#, description: "Converts json formatted string to table", result: Some(Value::Record { cols: vec!["a".to_string(), "b".to_string()], diff --git a/crates/nu-command/src/formats/from/mod.rs b/crates/nu-command/src/formats/from/mod.rs index 7f650d1641..defb180daf 100644 --- a/crates/nu-command/src/formats/from/mod.rs +++ b/crates/nu-command/src/formats/from/mod.rs @@ -5,6 +5,7 @@ mod eml; mod ics; mod ini; mod json; +mod nuon; mod ods; mod ssv; mod toml; @@ -23,6 +24,7 @@ pub use eml::FromEml; pub use ics::FromIcs; pub use ini::FromIni; pub use json::FromJson; +pub use nuon::FromNuon; pub use ods::FromOds; pub use ssv::FromSsv; pub use tsv::FromTsv; diff --git a/crates/nu-command/src/formats/from/nuon.rs b/crates/nu-command/src/formats/from/nuon.rs new file mode 100644 index 0000000000..2ca2bdb37e --- /dev/null +++ b/crates/nu-command/src/formats/from/nuon.rs @@ -0,0 +1,187 @@ +use std::collections::HashMap; + +use nu_engine::{current_dir, eval_expression}; +use nu_protocol::ast::{Call, Expr, Expression}; +use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value, + CONFIG_VARIABLE_ID, +}; +#[derive(Clone)] +pub struct FromNuon; + +impl Command for FromNuon { + fn name(&self) -> &str { + "from nuon" + } + + fn usage(&self) -> &str { + "Convert from nuon to structured data" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("from nuon").category(Category::Experimental) + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "'{ a:1 }' | from nuon", + description: "Converts nuon formatted string to table", + result: Some(Value::Record { + cols: vec!["a".to_string()], + vals: vec![Value::Int { + val: 1, + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + example: "'{ a:1, b: [1, 2] }' | from nuon", + description: "Converts nuon formatted string to table", + result: Some(Value::Record { + cols: vec!["a".to_string(), "b".to_string()], + vals: vec![ + Value::Int { + val: 1, + span: Span::test_data(), + }, + Value::List { + vals: vec![ + Value::Int { + val: 1, + span: Span::test_data(), + }, + Value::Int { + val: 2, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let config = stack.get_config().unwrap_or_default(); + let string_input = input.collect_string("", &config)?; + let cwd = current_dir(engine_state, stack)?; + + { + let mut engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + let mut stack = stack.captures_to_stack(&HashMap::new()); + + let _ = working_set.add_file("nuon file".to_string(), string_input.as_bytes()); + + let mut error = None; + + let (lexed, err) = + nu_parser::lex(string_input.as_bytes(), 0, &[b'\n', b'\r'], &[], true); + error = error.or(err); + + let (lite_block, err) = nu_parser::lite_parse(&lexed); + error = error.or(err); + + let (block, err) = nu_parser::parse_block(&mut working_set, &lite_block, true); + error = error.or(err); + + if block.pipelines.get(1).is_some() { + return Err(ShellError::SpannedLabeledError( + "error when loading".into(), + "excess values when loading".into(), + head, + )); + } + + let expr = if let Some(pipeline) = block.pipelines.get(0) { + if pipeline.expressions.get(1).is_some() { + return Err(ShellError::SpannedLabeledError( + "error when loading".into(), + "detected a pipeline in nuon file".into(), + head, + )); + } + + if let Some(expr) = pipeline.expressions.get(0) { + expr.clone() + } else { + Expression { + expr: Expr::Nothing, + span: head, + custom_completion: None, + ty: Type::Nothing, + } + } + } else { + Expression { + expr: Expr::Nothing, + span: head, + custom_completion: None, + ty: Type::Nothing, + } + }; + + if let Some(err) = error { + return Err(ShellError::SpannedLabeledError( + "error when loading".into(), + err.to_string(), + head, + )); + } + + let delta = working_set.render(); + + engine_state.merge_delta(delta, Some(&mut stack), &cwd)?; + + stack.add_var( + CONFIG_VARIABLE_ID, + Value::Record { + cols: vec![], + vals: vec![], + span: head, + }, + ); + + let result = eval_expression(&engine_state, &mut stack, &expr); + + match result { + Ok(result) => Ok(result.into_pipeline_data()), + Err(ShellError::ExternalNotSupported(..)) => Err(ShellError::SpannedLabeledError( + "error when loading".into(), + "running commands not supported in nuon".into(), + head, + )), + Err(err) => Err(ShellError::SpannedLabeledError( + "error when loading".into(), + err.to_string(), + head, + )), + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromNuon {}) + } +} diff --git a/crates/nu-command/src/formats/to/mod.rs b/crates/nu-command/src/formats/to/mod.rs index 6dab222368..22c86f8585 100644 --- a/crates/nu-command/src/formats/to/mod.rs +++ b/crates/nu-command/src/formats/to/mod.rs @@ -4,6 +4,7 @@ mod delimited; mod html; mod json; mod md; +mod nuon; mod toml; mod tsv; mod url; @@ -17,6 +18,7 @@ pub use command::To; pub use html::ToHtml; pub use json::ToJson; pub use md::ToMd; +pub use nuon::ToNuon; pub use tsv::ToTsv; pub use xml::ToXml; pub use yaml::ToYaml; diff --git a/crates/nu-command/src/formats/to/nuon.rs b/crates/nu-command/src/formats/to/nuon.rs new file mode 100644 index 0000000000..62315ddcd5 --- /dev/null +++ b/crates/nu-command/src/formats/to/nuon.rs @@ -0,0 +1,98 @@ +use nu_protocol::ast::{Call, RangeInclusion}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct ToNuon; + +impl Command for ToNuon { + fn name(&self) -> &str { + "to nuon" + } + + fn signature(&self) -> Signature { + Signature::build("to nuon").category(Category::Experimental) + } + + fn usage(&self) -> &str { + "Converts table data into Nuon (Nushell Object Notation) text." + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + Ok(Value::String { + val: to_nuon(call, input)?, + span: call.head, + } + .into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Outputs a nuon string representing the contents of this table", + example: "[1 2 3] | to nuon", + result: Some(Value::test_string("[1, 2, 3]")), + }] + } +} + +fn value_to_string(v: &Value, span: Span) -> Result { + match v { + Value::Binary { .. } => Err(ShellError::UnsupportedInput("binary".into(), span)), + Value::Block { .. } => Err(ShellError::UnsupportedInput("block".into(), span)), + Value::Bool { val, .. } => { + if *val { + Ok("$true".to_string()) + } else { + Ok("$false".to_string()) + } + } + Value::CellPath { .. } => Err(ShellError::UnsupportedInput("cellpath".to_string(), span)), + Value::CustomValue { .. } => Err(ShellError::UnsupportedInput("custom".to_string(), span)), + Value::Date { .. } => Err(ShellError::UnsupportedInput("date".to_string(), span)), + Value::Duration { val, .. } => Ok(format!("{}ns", *val)), + Value::Error { .. } => Err(ShellError::UnsupportedInput("error".to_string(), span)), + Value::Filesize { val, .. } => Ok(format!("{}b", *val)), + Value::Float { val, .. } => Ok(format!("{}", *val)), + Value::Int { val, .. } => Ok(format!("{}", *val)), + Value::List { vals, .. } => { + let mut collection = vec![]; + for val in vals { + collection.push(value_to_string(val, span)?); + } + Ok(format!("[{}]", collection.join(", "))) + } + Value::Nothing { .. } => Ok("$nothing".to_string()), + Value::Range { val, .. } => Ok(format!( + "{}..{}{}", + value_to_string(&val.from, span)?, + if val.inclusion == RangeInclusion::RightExclusive { + "<" + } else { + "" + }, + value_to_string(&val.to, span)? + )), + Value::Record { cols, vals, .. } => { + let mut collection = vec![]; + for (col, val) in cols.iter().zip(vals) { + collection.push(format!("\"{}\": {}", col, value_to_string(val, span)?)); + } + Ok(format!("{{{}}}", collection.join(", "))) + } + Value::String { val, .. } => Ok(format!("\"{}\"", val)), + } +} + +fn to_nuon(call: &Call, input: PipelineData) -> Result { + let v = input.into_value(call.head); + + value_to_string(&v, call.head) +} diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 513a230ac6..fc1c7a48a6 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -74,7 +74,7 @@ pub enum ShellError { #[diagnostic(code(nu::shell::feature_not_enabled), url(docsrs))] FeatureNotEnabled(#[label = "feature not enabled"] Span), - #[error("External commands not yet supported")] + #[error("Running external commands not supported")] #[diagnostic(code(nu::shell::external_commands), url(docsrs))] ExternalNotSupported(#[label = "external not supported"] Span), @@ -152,7 +152,7 @@ pub enum ShellError { #[error("Unsupported input")] #[diagnostic(code(nu::shell::unsupported_input), url(docsrs))] - UnsupportedInput(String, #[label("{0}")] Span), + UnsupportedInput(String, #[label("{0} not supported")] Span), #[error("Network failure")] #[diagnostic(code(nu::shell::network_failure), url(docsrs))] diff --git a/src/default_config.nu b/src/default_config.nu index 70281f3f2d..308f2e79dd 100644 --- a/src/default_config.nu +++ b/src/default_config.nu @@ -137,7 +137,7 @@ let $config = { animate_prompt: $false # redraw the prompt every second float_precision: 2 use_ansi_coloring: $true - filesize_format: "b" # b, kb, kib, mb, mib, gb, gib, tb, tib, pb, pib, eb, eib, zb, zib, auto + filesize_format: "auto" # b, kb, kib, mb, mib, gb, gib, tb, tib, pb, pib, eb, eib, zb, zib, auto edit_mode: emacs # emacs, vi max_history_size: 10000 menu_config: {