From e402adbba0f5932f06c4c1306c325896d40f2c6d Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Tue, 5 Oct 2021 08:43:20 -0500 Subject: [PATCH 01/24] WIP: output `ls` as a grid vs table --- .gitignore | 3 +- Cargo.lock | 12 + crates/nu-command/Cargo.toml | 4 +- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/viewers/grid.rs | 766 +++++++++++++++++++++++ crates/nu-command/src/viewers/griddle.rs | 311 +++++++++ crates/nu-command/src/viewers/mod.rs | 4 + 7 files changed, 1099 insertions(+), 2 deletions(-) create mode 100644 crates/nu-command/src/viewers/grid.rs create mode 100644 crates/nu-command/src/viewers/griddle.rs diff --git a/.gitignore b/.gitignore index 92defc3f3f..6373c46a33 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ history.txt -/target \ No newline at end of file +/target +/.vscode \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 5aa2a2c69f..324510e4a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -524,7 +524,9 @@ dependencies = [ "nu-protocol", "nu-table", "sysinfo", + "terminal_size", "thiserror", + "unicode-width", ] [[package]] @@ -1047,6 +1049,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "textwrap" version = "0.14.2" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index f58ebd5795..ba09d1e3be 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -16,4 +16,6 @@ nu-table = { path = "../nu-table" } glob = "0.3.0" thiserror = "1.0.29" sysinfo = "0.20.4" -chrono = { version="0.4.19", features=["serde"] } \ No newline at end of file +chrono = { version="0.4.19", features=["serde"] } +unicode-width = "0.1.9" +terminal_size = "0.1.17" diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 8418b2a384..30065069dd 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -26,6 +26,7 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(From)); working_set.add_decl(Box::new(FromJson)); working_set.add_decl(Box::new(Get)); + working_set.add_decl(Box::new(Griddle)); working_set.add_decl(Box::new(Help)); working_set.add_decl(Box::new(Hide)); working_set.add_decl(Box::new(If)); diff --git a/crates/nu-command/src/viewers/grid.rs b/crates/nu-command/src/viewers/grid.rs new file mode 100644 index 0000000000..dacb476403 --- /dev/null +++ b/crates/nu-command/src/viewers/grid.rs @@ -0,0 +1,766 @@ +// #![warn(future_incompatible)] +// #![warn(missing_copy_implementations)] +// #![warn(missing_docs)] +// #![warn(nonstandard_style)] +// #![warn(rust_2018_compatibility)] +// #![warn(rust_2018_idioms)] +// #![warn(trivial_casts, trivial_numeric_casts)] +// #![warn(unused)] +// #![deny(unsafe_code)] + +//! This library arranges textual data in a grid format suitable for +//! fixed-width fonts, using an algorithm to minimise the amount of space +//! needed. For example: +//! +//! ```rust +//! use nu_grid::{Grid, GridOptions, Direction, Filling, Cell}; +//! +//! let mut grid = Grid::new(GridOptions { +//! filling: Filling::Spaces(1), +//! direction: Direction::LeftToRight, +//! }); +//! +//! for s in &["one", "two", "three", "four", "five", "six", "seven", +//! "eight", "nine", "ten", "eleven", "twelve"] +//! { +//! grid.add(Cell::from(*s)); +//! } +//! +//! println!("{}", grid.fit_into_width(24).unwrap()); +//! ``` +//! +//! Produces the following tabular result: +//! +//! ```text +//! one two three four +//! five six seven eight +//! nine ten eleven twelve +//! ``` +//! +//! +//! ## Creating a grid +//! +//! To add data to a grid, first create a new [`Grid`] value, and then add +//! cells to them with the `add` function. +//! +//! There are two options that must be specified in the [`GridOptions`] value +//! that dictate how the grid is formatted: +//! +//! - `filling`: what to put in between two columns — either a number of +//! spaces, or a text string; +//! - `direction`, which specifies whether the cells should go along +//! rows, or columns: +//! - `Direction::LeftToRight` starts them in the top left and +//! moves *rightwards*, going to the start of a new row after reaching the +//! final column; +//! - `Direction::TopToBottom` starts them in the top left and moves +//! *downwards*, going to the top of a new column after reaching the final +//! row. +//! +//! +//! ## Displaying a grid +//! +//! When display a grid, you can either specify the number of columns in advance, +//! or try to find the maximum number of columns that can fit in an area of a +//! given width. +//! +//! Splitting a series of cells into columns — or, in other words, starting a new +//! row every n cells — is achieved with the [`fit_into_columns`] function +//! on a `Grid` value. It takes as its argument the number of columns. +//! +//! Trying to fit as much data onto one screen as possible is the main use case +//! for specifying a maximum width instead. This is achieved with the +//! [`fit_into_width`] function. It takes the maximum allowed width, including +//! separators, as its argument. However, it returns an *optional* [`Display`] +//! value, depending on whether any of the cells actually had a width greater than +//! the maximum width! If this is the case, your best bet is to just output the +//! cells with one per line. +//! +//! +//! ## Cells and data +//! +//! Grids to not take `String`s or `&str`s — they take [`Cell`] values. +//! +//! A **Cell** is a struct containing an individual cell’s contents, as a string, +//! and its pre-computed length, which gets used when calculating a grid’s final +//! dimensions. Usually, you want the *Unicode width* of the string to be used for +//! this, so you can turn a `String` into a `Cell` with the `.into()` function. +//! +//! However, you may also want to supply your own width: when you already know the +//! width in advance, or when you want to change the measurement, such as skipping +//! over terminal control characters. For cases like these, the fields on the +//! `Cell` values are public, meaning you can construct your own instances as +//! necessary. +//! +//! [`Cell`]: ./struct.Cell.html +//! [`Display`]: ./struct.Display.html +//! [`Grid`]: ./struct.Grid.html +//! [`fit_into_columns`]: ./struct.Grid.html#method.fit_into_columns +//! [`fit_into_width`]: ./struct.Grid.html#method.fit_into_width +//! [`GridOptions`]: ./struct.GridOptions.html + +use std::cmp::max; +use std::fmt; +use std::iter::repeat; + +// extern crate unicode_width; +use unicode_width::UnicodeWidthStr; + +/// Alignment indicate on which side the content should stick if some filling +/// is required. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Alignment { + /// The content will stick to the left. + Left, + + /// The content will stick to the right. + Right, +} + +/// A **Cell** is the combination of a string and its pre-computed length. +/// +/// The easiest way to create a Cell is just by using `string.into()`, which +/// uses the **unicode width** of the string (see the `unicode_width` crate). +/// However, the fields are public, if you wish to provide your own length. +#[derive(PartialEq, Debug, Clone)] +pub struct Cell { + /// The string to display when this cell gets rendered. + pub contents: String, + + /// The pre-computed length of the string. + pub width: Width, + + /// The side (left/right) to align the content if some filling is required. + pub alignment: Alignment, +} + +impl From for Cell { + fn from(string: String) -> Self { + Self { + width: UnicodeWidthStr::width(&*string), + contents: string, + alignment: Alignment::Left, + } + } +} + +impl<'a> From<&'a str> for Cell { + fn from(string: &'a str) -> Self { + Self { + width: UnicodeWidthStr::width(&*string), + contents: string.into(), + alignment: Alignment::Left, + } + } +} + +/// Direction cells should be written in — either across, or downwards. +#[derive(PartialEq, Debug, Copy, Clone)] +pub enum Direction { + /// Starts at the top left and moves rightwards, going back to the first + /// column for a new row, like a typewriter. + LeftToRight, + + /// Starts at the top left and moves downwards, going back to the first + /// row for a new column, like how `ls` lists files by default. + TopToBottom, +} + +/// The width of a cell, in columns. +pub type Width = usize; + +/// The text to put in between each pair of columns. +/// This does not include any spaces used when aligning cells. +#[derive(PartialEq, Debug)] +pub enum Filling { + /// A certain number of spaces should be used as the separator. + Spaces(Width), + + /// An arbitrary string. + /// `"|"` is a common choice. + Text(String), +} + +impl Filling { + fn width(&self) -> Width { + match *self { + Filling::Spaces(w) => w, + Filling::Text(ref t) => UnicodeWidthStr::width(&t[..]), + } + } +} + +/// The user-assignable options for a grid view that should be passed to +/// [`Grid::new()`](struct.Grid.html#method.new). +#[derive(PartialEq, Debug)] +pub struct GridOptions { + /// The direction that the cells should be written in — either + /// across, or downwards. + pub direction: Direction, + + /// The number of spaces to put in between each column of cells. + pub filling: Filling, +} + +#[derive(PartialEq, Debug)] +struct Dimensions { + /// The number of lines in the grid. + num_lines: Width, + + /// The width of each column in the grid. The length of this vector serves + /// as the number of columns. + widths: Vec, +} + +impl Dimensions { + fn total_width(&self, separator_width: Width) -> Width { + if self.widths.is_empty() { + 0 + } else { + let values = self.widths.iter().sum::(); + let separators = separator_width * (self.widths.len() - 1); + values + separators + } + } +} + +/// Everything needed to format the cells with the grid options. +/// +/// For more information, see the [`nu_grid` crate documentation](index.html). +#[derive(PartialEq, Debug)] +pub struct Grid { + options: GridOptions, + cells: Vec, + widest_cell_length: Width, + width_sum: Width, + cell_count: usize, +} + +impl Grid { + /// Creates a new grid view with the given options. + pub fn new(options: GridOptions) -> Self { + let cells = Vec::new(); + Self { + options, + cells, + widest_cell_length: 0, + width_sum: 0, + cell_count: 0, + } + } + + /// Reserves space in the vector for the given number of additional cells + /// to be added. (See the `Vec::reserve` function.) + pub fn reserve(&mut self, additional: usize) { + self.cells.reserve(additional) + } + + /// Adds another cell onto the vector. + pub fn add(&mut self, cell: Cell) { + if cell.width > self.widest_cell_length { + self.widest_cell_length = cell.width; + } + self.width_sum += cell.width; + self.cell_count += 1; + self.cells.push(cell) + } + + /// Returns a displayable grid that’s been packed to fit into the given + /// width in the fewest number of rows. + /// + /// Returns `None` if any of the cells has a width greater than the + /// maximum width. + pub fn fit_into_width(&self, maximum_width: Width) -> Option> { + self.width_dimensions(maximum_width).map(|dims| Display { + grid: self, + dimensions: dims, + }) + } + + /// Returns a displayable grid with the given number of columns, and no + /// maximum width. + pub fn fit_into_columns(&self, num_columns: usize) -> Display<'_> { + Display { + grid: self, + dimensions: self.columns_dimensions(num_columns), + } + } + + fn columns_dimensions(&self, num_columns: usize) -> Dimensions { + let mut num_lines = self.cells.len() / num_columns; + if self.cells.len() % num_columns != 0 { + num_lines += 1; + } + + self.column_widths(num_lines, num_columns) + } + + fn column_widths(&self, num_lines: usize, num_columns: usize) -> Dimensions { + let mut widths: Vec = repeat(0).take(num_columns).collect(); + for (index, cell) in self.cells.iter().enumerate() { + let index = match self.options.direction { + Direction::LeftToRight => index % num_columns, + Direction::TopToBottom => index / num_lines, + }; + widths[index] = max(widths[index], cell.width); + } + + Dimensions { num_lines, widths } + } + + fn theoretical_max_num_lines(&self, maximum_width: usize) -> usize { + // TODO: Make code readable / efficient. + let mut theoretical_min_num_cols = 0; + let mut col_total_width_so_far = 0; + + let mut cells = self.cells.clone(); + cells.sort_unstable_by(|a, b| b.width.cmp(&a.width)); // Sort in reverse order + + for cell in &cells { + if cell.width + col_total_width_so_far <= maximum_width { + theoretical_min_num_cols += 1; + col_total_width_so_far += cell.width; + } else { + let mut theoretical_max_num_lines = self.cell_count / theoretical_min_num_cols; + if self.cell_count % theoretical_min_num_cols != 0 { + theoretical_max_num_lines += 1; + } + return theoretical_max_num_lines; + } + col_total_width_so_far += self.options.filling.width() + } + + // If we make it to this point, we have exhausted all cells before + // reaching the maximum width; the theoretical max number of lines + // needed to display all cells is 1. + 1 + } + + fn width_dimensions(&self, maximum_width: Width) -> Option { + if self.widest_cell_length > maximum_width { + // Largest cell is wider than maximum width; it is impossible to fit. + return None; + } + + if self.cell_count == 0 { + return Some(Dimensions { + num_lines: 0, + widths: Vec::new(), + }); + } + + if self.cell_count == 1 { + let the_cell = &self.cells[0]; + return Some(Dimensions { + num_lines: 1, + widths: vec![the_cell.width], + }); + } + + let theoretical_max_num_lines = self.theoretical_max_num_lines(maximum_width); + if theoretical_max_num_lines == 1 { + // This if—statement is neccesary for the function to work correctly + // for small inputs. + return Some(Dimensions { + num_lines: 1, + // I clone self.cells twice. Once here, and once in + // self.theoretical_max_num_lines. Perhaps not the best for + // performance? + widths: self + .cells + .clone() + .into_iter() + .map(|cell| cell.width) + .collect(), + }); + } + // Instead of numbers of columns, try to find the fewest number of *lines* + // that the output will fit in. + let mut smallest_dimensions_yet = None; + for num_lines in (1..=theoretical_max_num_lines).rev() { + // The number of columns is the number of cells divided by the number + // of lines, *rounded up*. + let mut num_columns = self.cell_count / num_lines; + if self.cell_count % num_lines != 0 { + num_columns += 1; + } + // Early abort: if there are so many columns that the width of the + // *column separators* is bigger than the width of the screen, then + // don’t even try to tabulate it. + // This is actually a necessary check, because the width is stored as + // a usize, and making it go negative makes it huge instead, but it + // also serves as a speed-up. + let total_separator_width = (num_columns - 1) * self.options.filling.width(); + if maximum_width < total_separator_width { + continue; + } + + // Remove the separator width from the available space. + let adjusted_width = maximum_width - total_separator_width; + let potential_dimensions = self.column_widths(num_lines, num_columns); + if potential_dimensions.widths.iter().sum::() < adjusted_width { + smallest_dimensions_yet = Some(potential_dimensions); + } else { + return smallest_dimensions_yet; + } + } + + None + } +} + +/// A displayable representation of a [`Grid`](struct.Grid.html). +/// +/// This type implements `Display`, so you can get the textual version +/// of the grid by calling `.to_string()`. +#[derive(PartialEq, Debug)] +pub struct Display<'grid> { + /// The grid to display. + grid: &'grid Grid, + + /// The pre-computed column widths for this grid. + dimensions: Dimensions, +} + +impl Display<'_> { + /// Returns how many columns this display takes up, based on the separator + /// width and the number and width of the columns. + pub fn width(&self) -> Width { + self.dimensions + .total_width(self.grid.options.filling.width()) + } + + /// Returns how many rows this display takes up. + pub fn row_count(&self) -> usize { + self.dimensions.num_lines + } + + /// Returns whether this display takes up as many columns as were allotted + /// to it. + /// + /// It’s possible to construct tables that don’t actually use up all the + /// columns that they could, such as when there are more columns than + /// cells! In this case, a column would have a width of zero. This just + /// checks for that. + pub fn is_complete(&self) -> bool { + self.dimensions.widths.iter().all(|&x| x > 0) + } +} + +impl fmt::Display for Display<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + for y in 0..self.dimensions.num_lines { + for x in 0..self.dimensions.widths.len() { + let num = match self.grid.options.direction { + Direction::LeftToRight => y * self.dimensions.widths.len() + x, + Direction::TopToBottom => y + self.dimensions.num_lines * x, + }; + + // Abandon a line mid-way through if that’s where the cells end + if num >= self.grid.cells.len() { + continue; + } + + let cell = &self.grid.cells[num]; + if x == self.dimensions.widths.len() - 1 { + match cell.alignment { + Alignment::Left => { + // The final column doesn’t need to have trailing spaces, + // as long as it’s left-aligned. + write!(f, "{}", cell.contents)?; + } + Alignment::Right => { + let extra_spaces = self.dimensions.widths[x] - cell.width; + write!( + f, + "{}", + pad_string(&cell.contents, extra_spaces, Alignment::Right) + )?; + } + } + } else { + assert!(self.dimensions.widths[x] >= cell.width); + match (&self.grid.options.filling, cell.alignment) { + (Filling::Spaces(n), Alignment::Left) => { + let extra_spaces = self.dimensions.widths[x] - cell.width + n; + write!( + f, + "{}", + pad_string(&cell.contents, extra_spaces, cell.alignment) + )?; + } + (Filling::Spaces(n), Alignment::Right) => { + let s = spaces(*n); + let extra_spaces = self.dimensions.widths[x] - cell.width; + write!( + f, + "{}{}", + pad_string(&cell.contents, extra_spaces, cell.alignment), + s + )?; + } + (Filling::Text(ref t), _) => { + let extra_spaces = self.dimensions.widths[x] - cell.width; + write!( + f, + "{}{}", + pad_string(&cell.contents, extra_spaces, cell.alignment), + t + )?; + } + } + } + } + + writeln!(f)?; + } + + Ok(()) + } +} + +/// Pad a string with the given number of spaces. +fn spaces(length: usize) -> String { + repeat(" ").take(length).collect() +} + +/// Pad a string with the given alignment and number of spaces. +/// +/// This doesn’t take the width the string *should* be, rather the number +/// of spaces to add. +fn pad_string(string: &str, padding: usize, alignment: Alignment) -> String { + if alignment == Alignment::Left { + format!("{}{}", string, spaces(padding)) + } else { + format!("{}{}", spaces(padding), string) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn no_items() { + let grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + let display = grid.fit_into_width(40).unwrap(); + + assert_eq!(display.dimensions.num_lines, 0); + assert!(display.dimensions.widths.is_empty()); + + assert_eq!(display.width(), 0); + } + + #[test] + fn one_item() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + grid.add(Cell::from("1")); + + let display = grid.fit_into_width(40).unwrap(); + + assert_eq!(display.dimensions.num_lines, 1); + assert_eq!(display.dimensions.widths, vec![1]); + + assert_eq!(display.width(), 1); + } + + #[test] + fn one_item_exact_width() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + grid.add(Cell::from("1234567890")); + + let display = grid.fit_into_width(10).unwrap(); + + assert_eq!(display.dimensions.num_lines, 1); + assert_eq!(display.dimensions.widths, vec![10]); + + assert_eq!(display.width(), 10); + } + + #[test] + fn one_item_just_over() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + grid.add(Cell::from("1234567890!")); + + assert_eq!(grid.fit_into_width(10), None); + } + + #[test] + fn two_small_items() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + grid.add(Cell::from("1")); + grid.add(Cell::from("2")); + + let display = grid.fit_into_width(40).unwrap(); + + assert_eq!(display.dimensions.num_lines, 1); + assert_eq!(display.dimensions.widths, vec![1, 1]); + + assert_eq!(display.width(), 1 + 2 + 1); + } + + #[test] + fn two_medium_size_items() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + grid.add(Cell::from("hello there")); + grid.add(Cell::from("how are you today?")); + + let display = grid.fit_into_width(40).unwrap(); + + assert_eq!(display.dimensions.num_lines, 1); + assert_eq!(display.dimensions.widths, vec![11, 18]); + + assert_eq!(display.width(), 11 + 2 + 18); + } + + #[test] + fn two_big_items() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + grid.add(Cell::from( + "nuihuneihsoenhisenouiuteinhdauisdonhuisudoiosadiuohnteihaosdinhteuieudi", + )); + grid.add(Cell::from( + "oudisnuthasuouneohbueobaugceoduhbsauglcobeuhnaeouosbubaoecgueoubeohubeo", + )); + + assert_eq!(grid.fit_into_width(40), None); + } + + #[test] + fn that_example_from_earlier() { + let mut grid = Grid::new(GridOptions { + filling: Filling::Spaces(1), + direction: Direction::LeftToRight, + }); + + for s in &[ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", + ] { + grid.add(Cell::from(*s)); + } + + let bits = "one two three four\nfive six seven eight\nnine ten eleven twelve\n"; + assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits); + assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3); + } + + #[test] + fn number_grid_with_pipe() { + let mut grid = Grid::new(GridOptions { + filling: Filling::Text("|".into()), + direction: Direction::LeftToRight, + }); + + for s in &[ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", + ] { + grid.add(Cell::from(*s)); + } + + let bits = "one |two|three |four\nfive|six|seven |eight\nnine|ten|eleven|twelve\n"; + assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits); + assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3); + } + + #[test] + fn numbers_right() { + let mut grid = Grid::new(GridOptions { + filling: Filling::Spaces(1), + direction: Direction::LeftToRight, + }); + + for s in &[ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", + ] { + let mut cell = Cell::from(*s); + cell.alignment = Alignment::Right; + grid.add(cell); + } + + let bits = " one two three four\nfive six seven eight\nnine ten eleven twelve\n"; + assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits); + assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3); + } + + #[test] + fn numbers_right_pipe() { + let mut grid = Grid::new(GridOptions { + filling: Filling::Text("|".into()), + direction: Direction::LeftToRight, + }); + + for s in &[ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", + ] { + let mut cell = Cell::from(*s); + cell.alignment = Alignment::Right; + grid.add(cell); + } + + let bits = " one|two| three| four\nfive|six| seven| eight\nnine|ten|eleven|twelve\n"; + assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits); + assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3); + } + + #[test] + fn huge_separator() { + let mut grid = Grid::new(GridOptions { + filling: Filling::Spaces(100), + direction: Direction::LeftToRight, + }); + + grid.add("a".into()); + grid.add("b".into()); + + assert_eq!(grid.fit_into_width(99), None); + } + + #[test] + fn huge_yet_unused_separator() { + let mut grid = Grid::new(GridOptions { + filling: Filling::Spaces(100), + direction: Direction::LeftToRight, + }); + + grid.add("abcd".into()); + + let display = grid.fit_into_width(99).unwrap(); + + assert_eq!(display.dimensions.num_lines, 1); + assert_eq!(display.dimensions.widths, vec![4]); + + assert_eq!(display.width(), 4); + } +} diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs new file mode 100644 index 0000000000..e7c0cc0d19 --- /dev/null +++ b/crates/nu-command/src/viewers/griddle.rs @@ -0,0 +1,311 @@ +// use nu_protocol::ast::{Call, PathMember}; +// use nu_protocol::engine::{Command, EvaluationContext}; +// use nu_protocol::{Signature, Span, Spanned, SyntaxShape, Value}; +// use nu_table::StyledString; +// use std::collections::HashMap; +use super::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions}; +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, PathMember}, + engine::{Command, EvaluationContext}, + Example, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; +use terminal_size::{Height, Width}; + +pub struct Griddle; + +//NOTE: this is not a real implementation :D. It's just a simple one to test with until we port the real one. +impl Command for Griddle { + fn name(&self) -> &str { + "grid" + } + + fn usage(&self) -> &str { + "Render the grid." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("grid").named( + "columns", + SyntaxShape::Int, + "number of columns wide", + Some('c'), + ) + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + input: Value, + ) -> Result { + let columns: Option = call.get_flag(context, "columns")?; + + match input { + Value::List { vals, .. } => { + // let table = convert_to_table(vals); + + // if let Some(table) = table { + // let result = nu_table::draw_table(&table, 80, &HashMap::new()); + + // Ok(Value::String { + // val: result, + // span: call.head, + // }) + // } else { + // Ok(Value::Nothing { span: call.head }) + // } + dbg!("value::list"); + dbg!("{:#?}", vals); + Ok(Value::Nothing { span: call.head }) + } + Value::Stream { stream, .. } => { + // dbg!("value::stream"); + // let table = convert_to_table(stream); + + // if let Some(table) = table { + // let result = nu_table::draw_table(&table, 80, &HashMap::new()); + + // Ok(Value::String { + // val: result, + // span: call.head, + // }) + // } else { + // Ok(Value::Nothing { span: call.head }) + // } + let data = convert_to_list(stream); + if let Some(data) = data { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Text(" | ".into()), + }); + + for h in data { + let a_string = format!("{}", &h[1]); // bytes ->, &h[3]); + let mut cell = Cell::from(a_string); + cell.alignment = Alignment::Right; + grid.add(cell); + } + + let cols = if let Some(col) = columns { + col.parse::().unwrap() + } else { + // 80usize + if let Some((Width(w), Height(h))) = terminal_size::terminal_size() { + w + } else { + 80u16 + } + }; + + // eprintln!("columns size = {}", cols); + if let Some(grid_display) = grid.fit_into_width(cols as usize) { + // println!("{}", grid_display); + Ok(Value::String { + val: grid_display.to_string(), + span: call.head, + }) + } else { + // println!("Couldn't fit grid into 80 columns!"); + Ok(Value::String { + val: "Couldn't fit grid into 80 columns!".to_string(), + span: call.head, + }) + } + + // Ok(Value::String { + // val: "".to_string(), + // span: call.head, + // }) + } else { + // dbg!(data); + Ok(Value::Nothing { span: call.head }) + } + } + Value::Record { cols, vals, .. } => { + // let mut output = vec![]; + + // for (c, v) in cols.into_iter().zip(vals.into_iter()) { + // output.push(vec![ + // StyledString { + // contents: c, + // style: nu_table::TextStyle::default_field(), + // }, + // StyledString { + // contents: v.into_string(), + // style: nu_table::TextStyle::default(), + // }, + // ]) + // } + + // let table = nu_table::Table { + // headers: vec![], + // data: output, + // theme: nu_table::Theme::rounded(), + // }; + + // let result = nu_table::draw_table(&table, 80, &HashMap::new()); + + // Ok(Value::String { + // val: result, + // span: call.head, + // }) + dbg!("value::record"); + Ok(Value::Nothing { span: call.head }) + } + x => Ok(x), + } + } +} + +fn convert_to_list(iter: impl IntoIterator) -> Option>> { + let mut iter = iter.into_iter().peekable(); + let mut data = vec![]; + + if let Some(first) = iter.peek() { + let mut headers = first.columns(); + + if !headers.is_empty() { + headers.insert(0, "#".into()); + } + + for (row_num, item) in iter.enumerate() { + let mut row = vec![row_num.to_string()]; + + if headers.is_empty() { + row.push(item.into_string()) + } else { + for header in headers.iter().skip(1) { + let result = match item { + Value::Record { .. } => { + item.clone().follow_cell_path(&[PathMember::String { + val: header.into(), + span: Span::unknown(), + }]) + } + _ => Ok(item.clone()), + }; + + match result { + Ok(value) => row.push(value.into_string()), + Err(_) => row.push(String::new()), + } + } + } + + data.push(row); + } + + Some(data) + } else { + None + } + // Some(nu_table::Table { + // headers: headers + // .into_iter() + // .map(|x| StyledString { + // contents: x, + // style: nu_table::TextStyle::default_header(), + // }) + // .collect(), + // data: data + // .into_iter() + // .map(|x| { + // x.into_iter() + // .enumerate() + // .map(|(col, y)| { + // if col == 0 { + // StyledString { + // contents: y, + // style: nu_table::TextStyle::default_header(), + // } + // } else { + // StyledString { + // contents: y, + // style: nu_table::TextStyle::basic_left(), + // } + // } + // }) + // .collect::>() + // }) + // .collect(), + // theme: nu_table::Theme::rounded(), + // }) + // } else { + // None + // } +} +// fn convert_to_table(iter: impl IntoIterator) -> Option { +// let mut iter = iter.into_iter().peekable(); + +// if let Some(first) = iter.peek() { +// let mut headers = first.columns(); + +// if !headers.is_empty() { +// headers.insert(0, "#".into()); +// } + +// let mut data = vec![]; + +// for (row_num, item) in iter.enumerate() { +// let mut row = vec![row_num.to_string()]; + +// if headers.is_empty() { +// row.push(item.into_string()) +// } else { +// for header in headers.iter().skip(1) { +// let result = match item { +// Value::Record { .. } => { +// item.clone().follow_cell_path(&[PathMember::String { +// val: header.into(), +// span: Span::unknown(), +// }]) +// } +// _ => Ok(item.clone()), +// }; + +// match result { +// Ok(value) => row.push(value.into_string()), +// Err(_) => row.push(String::new()), +// } +// } +// } + +// data.push(row); +// } + +// Some(nu_table::Table { +// headers: headers +// .into_iter() +// .map(|x| StyledString { +// contents: x, +// style: nu_table::TextStyle::default_header(), +// }) +// .collect(), +// data: data +// .into_iter() +// .map(|x| { +// x.into_iter() +// .enumerate() +// .map(|(col, y)| { +// if col == 0 { +// StyledString { +// contents: y, +// style: nu_table::TextStyle::default_header(), +// } +// } else { +// StyledString { +// contents: y, +// style: nu_table::TextStyle::basic_left(), +// } +// } +// }) +// .collect::>() +// }) +// .collect(), +// theme: nu_table::Theme::rounded(), +// }) +// } else { +// None +// } +// } diff --git a/crates/nu-command/src/viewers/mod.rs b/crates/nu-command/src/viewers/mod.rs index 0ed6450087..9600f57b5e 100644 --- a/crates/nu-command/src/viewers/mod.rs +++ b/crates/nu-command/src/viewers/mod.rs @@ -1,3 +1,7 @@ +mod grid; +mod griddle; mod table; +pub use grid::Grid; +pub use griddle::Griddle; pub use table::Table; From 3c843f7f615bc82402c54e29c57bd502f3264402 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Tue, 5 Oct 2021 10:22:57 -0500 Subject: [PATCH 02/24] renamed nu_grid to grid --- crates/nu-command/src/viewers/grid.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/nu-command/src/viewers/grid.rs b/crates/nu-command/src/viewers/grid.rs index dacb476403..36889e1d9f 100644 --- a/crates/nu-command/src/viewers/grid.rs +++ b/crates/nu-command/src/viewers/grid.rs @@ -13,7 +13,7 @@ //! needed. For example: //! //! ```rust -//! use nu_grid::{Grid, GridOptions, Direction, Filling, Cell}; +//! use grid::{Grid, GridOptions, Direction, Filling, Cell}; //! //! let mut grid = Grid::new(GridOptions { //! filling: Filling::Spaces(1), @@ -226,7 +226,7 @@ impl Dimensions { /// Everything needed to format the cells with the grid options. /// -/// For more information, see the [`nu_grid` crate documentation](index.html). +/// For more information, see the [`grid` crate documentation](index.html). #[derive(PartialEq, Debug)] pub struct Grid { options: GridOptions, From 11b40a6c314b1d3f477cc8bbae94edb6e4fbe5f1 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Tue, 5 Oct 2021 10:30:49 -0500 Subject: [PATCH 03/24] clippy --- crates/nu-command/src/viewers/griddle.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index e7c0cc0d19..4d5c7fb1cd 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -8,7 +8,7 @@ use nu_engine::CallExt; use nu_protocol::{ ast::{Call, PathMember}, engine::{Command, EvaluationContext}, - Example, ShellError, Signature, Span, Spanned, SyntaxShape, Value, + Signature, Span, SyntaxShape, Value, }; use terminal_size::{Height, Width}; @@ -81,17 +81,17 @@ impl Command for Griddle { }); for h in data { - let a_string = format!("{}", &h[1]); // bytes ->, &h[3]); + let a_string = (&h[1]).to_string(); // bytes ->, &h[3]); let mut cell = Cell::from(a_string); cell.alignment = Alignment::Right; grid.add(cell); } let cols = if let Some(col) = columns { - col.parse::().unwrap() + col.parse::().unwrap_or(80) } else { // 80usize - if let Some((Width(w), Height(h))) = terminal_size::terminal_size() { + if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { w } else { 80u16 @@ -122,7 +122,9 @@ impl Command for Griddle { Ok(Value::Nothing { span: call.head }) } } - Value::Record { cols, vals, .. } => { + Value::Record { + cols: _, vals: _, .. + } => { // let mut output = vec![]; // for (c, v) in cols.into_iter().zip(vals.into_iter()) { From 51a43f561717e296f298c8f427e58f3e5ede4631 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Tue, 5 Oct 2021 11:14:31 -0500 Subject: [PATCH 04/24] mayve fix ci --- crates/nu-command/src/viewers/grid.rs | 2 +- crates/nu-command/src/viewers/griddle.rs | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/nu-command/src/viewers/grid.rs b/crates/nu-command/src/viewers/grid.rs index 36889e1d9f..56834e32b6 100644 --- a/crates/nu-command/src/viewers/grid.rs +++ b/crates/nu-command/src/viewers/grid.rs @@ -13,7 +13,7 @@ //! needed. For example: //! //! ```rust -//! use grid::{Grid, GridOptions, Direction, Filling, Cell}; +//! use super::grid::{Grid, GridOptions, Direction, Filling, Cell}; //! //! let mut grid = Grid::new(GridOptions { //! filling: Filling::Spaces(1), diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index 4d5c7fb1cd..3a9d7433cb 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -1,8 +1,3 @@ -// use nu_protocol::ast::{Call, PathMember}; -// use nu_protocol::engine::{Command, EvaluationContext}; -// use nu_protocol::{Signature, Span, Spanned, SyntaxShape, Value}; -// use nu_table::StyledString; -// use std::collections::HashMap; use super::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions}; use nu_engine::CallExt; use nu_protocol::{ @@ -10,7 +5,7 @@ use nu_protocol::{ engine::{Command, EvaluationContext}, Signature, Span, SyntaxShape, Value, }; -use terminal_size::{Height, Width}; +use terminal_size::{Height, Width}; //{Alignment, Cell, Direction, Filling, Grid, GridOptions}; pub struct Griddle; From 7697f7bdce64356be5d1a6fd73e371d33ad81aa6 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Tue, 5 Oct 2021 12:58:48 -0500 Subject: [PATCH 05/24] fix doc-test --- crates/nu-command/src/viewers/grid.rs | 4 ++-- crates/nu-command/src/viewers/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/nu-command/src/viewers/grid.rs b/crates/nu-command/src/viewers/grid.rs index 56834e32b6..8856bdb8af 100644 --- a/crates/nu-command/src/viewers/grid.rs +++ b/crates/nu-command/src/viewers/grid.rs @@ -13,7 +13,7 @@ //! needed. For example: //! //! ```rust -//! use super::grid::{Grid, GridOptions, Direction, Filling, Cell}; +//! use nu_command::grid::{Grid, GridOptions, Direction, Filling, Cell}; //! //! let mut grid = Grid::new(GridOptions { //! filling: Filling::Spaces(1), @@ -521,7 +521,7 @@ impl fmt::Display for Display<'_> { /// Pad a string with the given number of spaces. fn spaces(length: usize) -> String { - repeat(" ").take(length).collect() + " ".repeat(length) } /// Pad a string with the given alignment and number of spaces. diff --git a/crates/nu-command/src/viewers/mod.rs b/crates/nu-command/src/viewers/mod.rs index 9600f57b5e..a2f76b56ca 100644 --- a/crates/nu-command/src/viewers/mod.rs +++ b/crates/nu-command/src/viewers/mod.rs @@ -1,4 +1,4 @@ -mod grid; +pub mod grid; mod griddle; mod table; From 58d73d4c233beccc4ca84f9829bfdabfa0535937 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Thu, 7 Oct 2021 10:32:39 -0500 Subject: [PATCH 06/24] moved `grid` to it's own crate named `nu-term-grid` --- Cargo.lock | 10 ++++++- Cargo.toml | 1 + crates/nu-command/Cargo.toml | 2 +- crates/nu-command/src/viewers/griddle.rs | 2 +- crates/nu-command/src/viewers/mod.rs | 2 -- crates/nu-term-grid/.gitignore | 22 ++++++++++++++ crates/nu-term-grid/Cargo.toml | 15 ++++++++++ .../src/viewers => nu-term-grid/src}/grid.rs | 2 +- crates/nu-term-grid/src/lib.rs | 3 ++ crates/nu-term-grid/src/main.rs | 30 +++++++++++++++++++ 10 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 crates/nu-term-grid/.gitignore create mode 100644 crates/nu-term-grid/Cargo.toml rename crates/{nu-command/src/viewers => nu-term-grid/src}/grid.rs (99%) create mode 100644 crates/nu-term-grid/src/lib.rs create mode 100644 crates/nu-term-grid/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 324510e4a6..a17184eea1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,6 +294,7 @@ dependencies = [ "nu-path", "nu-protocol", "nu-table", + "nu-term-grid", "pretty_assertions", "reedline", "tempfile", @@ -523,10 +524,10 @@ dependencies = [ "nu-path", "nu-protocol", "nu-table", + "nu-term-grid", "sysinfo", "terminal_size", "thiserror", - "unicode-width", ] [[package]] @@ -589,6 +590,13 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "nu-term-grid" +version = "0.36.0" +dependencies = [ + "unicode-width", +] + [[package]] name = "num-integer" version = "0.1.44" diff --git a/Cargo.toml b/Cargo.toml index 826e3d542e..5f43cb1172 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ nu-parser = { path="./crates/nu-parser" } nu-path = { path="./crates/nu-path" } nu-protocol = { path = "./crates/nu-protocol" } nu-table = { path = "./crates/nu-table" } +nu-term-grid = { path = "./crates/nu-term-grid" } miette = "3.0.0" # mimalloc = { version = "*", default-features = false } diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index ba09d1e3be..4d0f13c78b 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -11,11 +11,11 @@ nu-json = { path = "../nu-json" } nu-path = { path = "../nu-path" } nu-protocol = { path = "../nu-protocol" } nu-table = { path = "../nu-table" } +nu-term-grid = { path = "../nu-term-grid" } # Potential dependencies for extras glob = "0.3.0" thiserror = "1.0.29" sysinfo = "0.20.4" chrono = { version="0.4.19", features=["serde"] } -unicode-width = "0.1.9" terminal_size = "0.1.17" diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index 3a9d7433cb..c94781efee 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -1,10 +1,10 @@ -use super::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions}; use nu_engine::CallExt; use nu_protocol::{ ast::{Call, PathMember}, engine::{Command, EvaluationContext}, Signature, Span, SyntaxShape, Value, }; +use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions}; use terminal_size::{Height, Width}; //{Alignment, Cell, Direction, Filling, Grid, GridOptions}; pub struct Griddle; diff --git a/crates/nu-command/src/viewers/mod.rs b/crates/nu-command/src/viewers/mod.rs index a2f76b56ca..3ff09eff39 100644 --- a/crates/nu-command/src/viewers/mod.rs +++ b/crates/nu-command/src/viewers/mod.rs @@ -1,7 +1,5 @@ -pub mod grid; mod griddle; mod table; -pub use grid::Grid; pub use griddle::Griddle; pub use table::Table; diff --git a/crates/nu-term-grid/.gitignore b/crates/nu-term-grid/.gitignore new file mode 100644 index 0000000000..4c234e523b --- /dev/null +++ b/crates/nu-term-grid/.gitignore @@ -0,0 +1,22 @@ +/target +/scratch +**/*.rs.bk +history.txt +tests/fixtures/nuplayground +crates/*/target + +# Debian/Ubuntu +debian/.debhelper/ +debian/debhelper-build-stamp +debian/files +debian/nu.substvars +debian/nu/ + +# macOS junk +.DS_Store + +# JetBrains' IDE items +.idea/* + +# VSCode's IDE items +.vscode/* diff --git a/crates/nu-term-grid/Cargo.toml b/crates/nu-term-grid/Cargo.toml new file mode 100644 index 0000000000..183de8ecc9 --- /dev/null +++ b/crates/nu-term-grid/Cargo.toml @@ -0,0 +1,15 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "Nushell grid printing" +edition = "2018" +license = "MIT" +name = "nu-term-grid" +version = "0.36.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "grid" +path = "src/main.rs" + +[dependencies] +unicode-width = "0.1.9" diff --git a/crates/nu-command/src/viewers/grid.rs b/crates/nu-term-grid/src/grid.rs similarity index 99% rename from crates/nu-command/src/viewers/grid.rs rename to crates/nu-term-grid/src/grid.rs index 8856bdb8af..f15b2fe376 100644 --- a/crates/nu-command/src/viewers/grid.rs +++ b/crates/nu-term-grid/src/grid.rs @@ -13,7 +13,7 @@ //! needed. For example: //! //! ```rust -//! use nu_command::grid::{Grid, GridOptions, Direction, Filling, Cell}; +//! use nu_term_grid::grid::{Grid, GridOptions, Direction, Filling, Cell}; //! //! let mut grid = Grid::new(GridOptions { //! filling: Filling::Spaces(1), diff --git a/crates/nu-term-grid/src/lib.rs b/crates/nu-term-grid/src/lib.rs new file mode 100644 index 0000000000..79b146593f --- /dev/null +++ b/crates/nu-term-grid/src/lib.rs @@ -0,0 +1,3 @@ +pub mod grid; + +pub use grid::Grid; diff --git a/crates/nu-term-grid/src/main.rs b/crates/nu-term-grid/src/main.rs new file mode 100644 index 0000000000..a9ef426790 --- /dev/null +++ b/crates/nu-term-grid/src/main.rs @@ -0,0 +1,30 @@ +use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions}; + +// This produces: +// +// 1 | 128 | 16384 | 2097152 | 268435456 | 34359738368 | 4398046511104 +// 2 | 256 | 32768 | 4194304 | 536870912 | 68719476736 | 8796093022208 +// 4 | 512 | 65536 | 8388608 | 1073741824 | 137438953472 | 17592186044416 +// 8 | 1024 | 131072 | 16777216 | 2147483648 | 274877906944 | 35184372088832 +// 16 | 2048 | 262144 | 33554432 | 4294967296 | 549755813888 | 70368744177664 +// 32 | 4096 | 524288 | 67108864 | 8589934592 | 1099511627776 | 140737488355328 +// 64 | 8192 | 1048576 | 134217728 | 17179869184 | 2199023255552 | + +fn main() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Text(" | ".into()), + }); + + for i in 0..48 { + let mut cell = Cell::from(format!("{}", 2_isize.pow(i))); + cell.alignment = Alignment::Right; + grid.add(cell) + } + + if let Some(grid_display) = grid.fit_into_width(80) { + println!("{}", grid_display); + } else { + println!("Couldn't fit grid into 80 columns!"); + } +} From ae8b315e7604188756726662941a5c38ba05beec Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Thu, 7 Oct 2021 11:00:49 -0500 Subject: [PATCH 07/24] added list output --- crates/nu-command/src/viewers/griddle.rs | 116 ++++++++++++++--------- 1 file changed, 70 insertions(+), 46 deletions(-) diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index c94781efee..1907cb4eb8 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -5,11 +5,10 @@ use nu_protocol::{ Signature, Span, SyntaxShape, Value, }; use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions}; -use terminal_size::{Height, Width}; //{Alignment, Cell, Direction, Filling, Grid, GridOptions}; +use terminal_size::{Height, Width}; pub struct Griddle; -//NOTE: this is not a real implementation :D. It's just a simple one to test with until we port the real one. impl Command for Griddle { fn name(&self) -> &str { "grid" @@ -34,55 +33,26 @@ impl Command for Griddle { call: &Call, input: Value, ) -> Result { - let columns: Option = call.get_flag(context, "columns")?; + let columns_param: Option = call.get_flag(context, "columns")?; match input { Value::List { vals, .. } => { - // let table = convert_to_table(vals); - - // if let Some(table) = table { - // let result = nu_table::draw_table(&table, 80, &HashMap::new()); - - // Ok(Value::String { - // val: result, - // span: call.head, - // }) - // } else { - // Ok(Value::Nothing { span: call.head }) - // } - dbg!("value::list"); - dbg!("{:#?}", vals); - Ok(Value::Nothing { span: call.head }) - } - Value::Stream { stream, .. } => { - // dbg!("value::stream"); - // let table = convert_to_table(stream); - - // if let Some(table) = table { - // let result = nu_table::draw_table(&table, 80, &HashMap::new()); - - // Ok(Value::String { - // val: result, - // span: call.head, - // }) - // } else { - // Ok(Value::Nothing { span: call.head }) - // } - let data = convert_to_list(stream); - if let Some(data) = data { + // dbg!("value::list"); + let data = convert_to_list(vals); + if let Some(items) = data { let mut grid = Grid::new(GridOptions { direction: Direction::TopToBottom, filling: Filling::Text(" | ".into()), }); - - for h in data { - let a_string = (&h[1]).to_string(); // bytes ->, &h[3]); + for list in items { + // looks like '&list = [ "0", "one",]' + let a_string = (&list[1]).to_string(); let mut cell = Cell::from(a_string); cell.alignment = Alignment::Right; grid.add(cell); } - let cols = if let Some(col) = columns { + let cols = if let Some(col) = columns_param { col.parse::().unwrap_or(80) } else { // 80usize @@ -103,15 +73,63 @@ impl Command for Griddle { } else { // println!("Couldn't fit grid into 80 columns!"); Ok(Value::String { - val: "Couldn't fit grid into 80 columns!".to_string(), + val: format!("Couldn't fit grid into {} columns!", cols), span: call.head, }) } + } else { + Ok(Value::Nothing { span: call.head }) + } + } + Value::Stream { stream, .. } => { + // dbg!("value::stream"); + let data = convert_to_list(stream); + if let Some(items) = data { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Text(" | ".into()), + }); - // Ok(Value::String { - // val: "".to_string(), - // span: call.head, - // }) + for list in items { + // dbg!(&list); + // from the output of ls, it looks like + // '&list = [ "0", ".git", "Dir", "4.1 KB", "23 minutes ago",]' + // so we take the 1th index for the file name + // but this [[col1 col2]; [one two] [three four]] | grid + // prints one | three + // TODO: what should we do about tables in the grid? should we + // allow one to specify a column or perhaps all columns? + let a_string = (&list[1]).to_string(); // bytes ->, &h[3]); + let mut cell = Cell::from(a_string); + cell.alignment = Alignment::Right; + grid.add(cell); + } + + let cols = if let Some(col) = columns_param { + col.parse::().unwrap_or(80) + } else { + // 80usize + if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { + w + } else { + 80u16 + } + }; + + // eprintln!("columns size = {}", cols); + if let Some(grid_display) = grid.fit_into_width(cols as usize) { + // println!("{}", grid_display); + Ok(Value::String { + val: grid_display.to_string(), + span: call.head, + }) + } else { + // println!("Couldn't fit grid into 80 columns!"); + Ok(Value::String { + val: format!("Couldn't fit grid into {} columns!", cols), + span: call.head, + }) + } } else { // dbg!(data); Ok(Value::Nothing { span: call.head }) @@ -120,6 +138,8 @@ impl Command for Griddle { Value::Record { cols: _, vals: _, .. } => { + dbg!("value::record"); + // let mut output = vec![]; // for (c, v) in cols.into_iter().zip(vals.into_iter()) { @@ -147,10 +167,13 @@ impl Command for Griddle { // val: result, // span: call.head, // }) - dbg!("value::record"); Ok(Value::Nothing { span: call.head }) } - x => Ok(x), + x => { + // dbg!("other value"); + // dbg!(x.get_type()); + Ok(x) + } } } } @@ -160,6 +183,7 @@ fn convert_to_list(iter: impl IntoIterator) -> Option Date: Thu, 7 Oct 2021 14:18:03 -0700 Subject: [PATCH 08/24] add touch command --- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/filesystem/mod.rs | 2 + crates/nu-command/src/filesystem/touch.rs | 56 +++++++++++++++++++++++ crates/nu-protocol/src/shell_error.rs | 4 ++ 4 files changed, 63 insertions(+) create mode 100644 crates/nu-command/src/filesystem/touch.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index fd2ea37555..06abdd8bbd 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -41,6 +41,7 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(Select)); working_set.add_decl(Box::new(Sys)); working_set.add_decl(Box::new(Table)); + working_set.add_decl(Box::new(Touch)); working_set.add_decl(Box::new(Use)); working_set.add_decl(Box::new(Where)); working_set.add_decl(Box::new(Wrap)); diff --git a/crates/nu-command/src/filesystem/mod.rs b/crates/nu-command/src/filesystem/mod.rs index 2d2212766a..926ffe2e54 100644 --- a/crates/nu-command/src/filesystem/mod.rs +++ b/crates/nu-command/src/filesystem/mod.rs @@ -2,9 +2,11 @@ mod cd; mod cp; mod ls; mod mv; +mod touch; mod util; pub use cd::Cd; pub use cp::Cp; pub use ls::Ls; pub use mv::Mv; +pub use touch::Touch; diff --git a/crates/nu-command/src/filesystem/touch.rs b/crates/nu-command/src/filesystem/touch.rs new file mode 100644 index 0000000000..c895538858 --- /dev/null +++ b/crates/nu-command/src/filesystem/touch.rs @@ -0,0 +1,56 @@ +use std::fs::OpenOptions; + +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{ShellError, Signature, SyntaxShape, Value}; + +pub struct Touch; + +impl Command for Touch { + fn name(&self) -> &str { + "touch" + } + + fn signature(&self) -> Signature { + Signature::build("touch") + .required( + "filename", + SyntaxShape::Filepath, + "the path of the file you want to create", + ) + .rest("rest", SyntaxShape::Filepath, "additional files to create") + } + + fn usage(&self) -> &str { + "Creates one or more files." + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result { + touch(context, call) + } +} + +fn touch(context: &EvaluationContext, call: &Call) -> Result { + let target: String = call.req(context, 0)?; + let rest: Vec = call.rest(context, 1)?; + + for (index, item) in vec![target].into_iter().chain(rest).enumerate() { + match OpenOptions::new().write(true).create(true).open(&item) { + Ok(_) => continue, + Err(err) => { + return Err(ShellError::CreateNotPossible( + format!("Failed to create file: {}", err), + call.positional[index].span, + )); + } + } + } + + Ok(Value::Nothing { span: call.head }) +} diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 6a7464508e..ab7da16eae 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -109,6 +109,10 @@ pub enum ShellError { #[error("Move not possible")] #[diagnostic(code(nu::shell::move_not_possible_single), url(docsrs))] MoveNotPossibleSingle(String, #[label("{0}")] Span), + + #[error("Create not possible")] + #[diagnostic(code(nu::shell::move_not_possible_single), url(docsrs))] + CreateNotPossible(String, #[label("{0}")] Span), } impl From for ShellError { From e8e1ead99d1892cd4f5c200189b6dd57649df2a8 Mon Sep 17 00:00:00 2001 From: xiuxiu62 Date: Thu, 7 Oct 2021 14:20:03 -0700 Subject: [PATCH 09/24] change diagnostic code on CreateNotPossible --- crates/nu-protocol/src/shell_error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index ab7da16eae..1b1c172bc6 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -111,7 +111,7 @@ pub enum ShellError { MoveNotPossibleSingle(String, #[label("{0}")] Span), #[error("Create not possible")] - #[diagnostic(code(nu::shell::move_not_possible_single), url(docsrs))] + #[diagnostic(code(nu::shell::create_not_possible), url(docsrs))] CreateNotPossible(String, #[label("{0}")] Span), } From 8550f50522c738d6d0477d7ed7830fc82a807835 Mon Sep 17 00:00:00 2001 From: xiuxiu62 Date: Thu, 7 Oct 2021 14:36:47 -0700 Subject: [PATCH 10/24] substitute idiomatic call flag check --- crates/nu-command/src/filesystem/cp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/nu-command/src/filesystem/cp.rs b/crates/nu-command/src/filesystem/cp.rs index 79751f2c93..b042d50f11 100644 --- a/crates/nu-command/src/filesystem/cp.rs +++ b/crates/nu-command/src/filesystem/cp.rs @@ -60,7 +60,7 @@ impl Command for Cp { } let any_source_is_dir = sources.iter().any(|f| matches!(f, Ok(f) if f.is_dir())); - let recursive = call.named.iter().any(|p| &p.0 == "recursive"); + let recursive: bool = call.has_flag("recursive"); if any_source_is_dir && !recursive { return Err(ShellError::MoveNotPossibleSingle( "Directories must be copied using \"--recursive\"".to_string(), From 54a41c535bb23b42e0fe99155ca56976226541a3 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Thu, 7 Oct 2021 16:50:27 -0500 Subject: [PATCH 11/24] only print items with `name` column --- crates/nu-command/src/viewers/griddle.rs | 330 ++++++++++------------- crates/nu-term-grid/src/grid.rs | 10 +- 2 files changed, 149 insertions(+), 191 deletions(-) diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index 1907cb4eb8..fd1b1e9f82 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -38,136 +38,42 @@ impl Command for Griddle { match input { Value::List { vals, .. } => { // dbg!("value::list"); - let data = convert_to_list(vals); + let data = convert_to_list2(vals); if let Some(items) = data { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Text(" | ".into()), - }); - for list in items { - // looks like '&list = [ "0", "one",]' - let a_string = (&list[1]).to_string(); - let mut cell = Cell::from(a_string); - cell.alignment = Alignment::Right; - grid.add(cell); - } - - let cols = if let Some(col) = columns_param { - col.parse::().unwrap_or(80) - } else { - // 80usize - if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { - w - } else { - 80u16 - } - }; - - // eprintln!("columns size = {}", cols); - if let Some(grid_display) = grid.fit_into_width(cols as usize) { - // println!("{}", grid_display); - Ok(Value::String { - val: grid_display.to_string(), - span: call.head, - }) - } else { - // println!("Couldn't fit grid into 80 columns!"); - Ok(Value::String { - val: format!("Couldn't fit grid into {} columns!", cols), - span: call.head, - }) - } + Ok(create_grid_output2(items, call, columns_param)) } else { Ok(Value::Nothing { span: call.head }) } } Value::Stream { stream, .. } => { // dbg!("value::stream"); - let data = convert_to_list(stream); + let data = convert_to_list2(stream); if let Some(items) = data { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Text(" | ".into()), - }); - - for list in items { - // dbg!(&list); - // from the output of ls, it looks like - // '&list = [ "0", ".git", "Dir", "4.1 KB", "23 minutes ago",]' - // so we take the 1th index for the file name - // but this [[col1 col2]; [one two] [three four]] | grid - // prints one | three - // TODO: what should we do about tables in the grid? should we - // allow one to specify a column or perhaps all columns? - let a_string = (&list[1]).to_string(); // bytes ->, &h[3]); - let mut cell = Cell::from(a_string); - cell.alignment = Alignment::Right; - grid.add(cell); - } - - let cols = if let Some(col) = columns_param { - col.parse::().unwrap_or(80) - } else { - // 80usize - if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { - w - } else { - 80u16 - } - }; - - // eprintln!("columns size = {}", cols); - if let Some(grid_display) = grid.fit_into_width(cols as usize) { - // println!("{}", grid_display); - Ok(Value::String { - val: grid_display.to_string(), - span: call.head, - }) - } else { - // println!("Couldn't fit grid into 80 columns!"); - Ok(Value::String { - val: format!("Couldn't fit grid into {} columns!", cols), - span: call.head, - }) - } + Ok(create_grid_output2(items, call, columns_param)) } else { // dbg!(data); Ok(Value::Nothing { span: call.head }) } } - Value::Record { - cols: _, vals: _, .. - } => { - dbg!("value::record"); + Value::Record { cols, vals, .. } => { + // dbg!("value::record"); - // let mut output = vec![]; + // let mut items = vec![]; // for (c, v) in cols.into_iter().zip(vals.into_iter()) { - // output.push(vec![ - // StyledString { - // contents: c, - // style: nu_table::TextStyle::default_field(), - // }, - // StyledString { - // contents: v.into_string(), - // style: nu_table::TextStyle::default(), - // }, - // ]) + // items.push(vec![c, v.into_string()]) // } + // dbg!(&items); - // let table = nu_table::Table { - // headers: vec![], - // data: output, - // theme: nu_table::Theme::rounded(), - // }; + // Ok(create_grid_output(items, call, columns_param)) + let mut items = vec![]; - // let result = nu_table::draw_table(&table, 80, &HashMap::new()); + for (i, (c, v)) in cols.into_iter().zip(vals.into_iter()).enumerate() { + items.push((i, c, v.into_string())) + } + // dbg!(&items); - // Ok(Value::String { - // val: result, - // span: call.head, - // }) - Ok(Value::Nothing { span: call.head }) + Ok(create_grid_output2(items, call, columns_param)) } x => { // dbg!("other value"); @@ -178,18 +84,106 @@ impl Command for Griddle { } } -fn convert_to_list(iter: impl IntoIterator) -> Option>> { +fn create_grid_output2( + items: Vec<(usize, String, String)>, + call: &Call, + columns_param: Option, +) -> Value { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Text(" | ".into()), + }); + + for (_row_index, header, value) in items { + // only output value if the header name is 'name' + if header == "name" { + let mut cell = Cell::from(value); + cell.alignment = Alignment::Right; + grid.add(cell); + } + } + + let cols = if let Some(col) = columns_param { + col.parse::().unwrap_or(80) + } else { + if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { + w + } else { + 80u16 + } + }; + + if let Some(grid_display) = grid.fit_into_width(cols as usize) { + Value::String { + val: grid_display.to_string(), + span: call.head, + } + } else { + Value::String { + val: format!("Couldn't fit grid into {} columns!", cols), + span: call.head, + } + } +} + +// fn create_grid_output( +// items: Vec>, +// call: &Call, +// columns_param: Option, +// ) -> Value { +// let mut grid = Grid::new(GridOptions { +// direction: Direction::TopToBottom, +// filling: Filling::Text(" | ".into()), +// }); + +// for list in items { +// dbg!(&list); +// // looks like '&list = [ "0", "one",]' +// let a_string = (&list[1]).to_string(); +// let mut cell = Cell::from(a_string); +// cell.alignment = Alignment::Right; +// grid.add(cell); +// } + +// let cols = if let Some(col) = columns_param { +// col.parse::().unwrap_or(80) +// } else { +// // 80usize +// if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { +// w +// } else { +// 80u16 +// } +// }; + +// // eprintln!("columns size = {}", cols); +// if let Some(grid_display) = grid.fit_into_width(cols as usize) { +// // println!("{}", grid_display); +// Value::String { +// val: grid_display.to_string(), +// span: call.head, +// } +// } else { +// // println!("Couldn't fit grid into 80 columns!"); +// Value::String { +// val: format!("Couldn't fit grid into {} columns!", cols), +// span: call.head, +// } +// } +// } + +fn convert_to_list2(iter: impl IntoIterator) -> Option> { let mut iter = iter.into_iter().peekable(); - let mut data = vec![]; if let Some(first) = iter.peek() { - // dbg!(&first); let mut headers = first.columns(); if !headers.is_empty() { headers.insert(0, "#".into()); } + let mut data = vec![]; + for (row_num, item) in iter.enumerate() { let mut row = vec![row_num.to_string()]; @@ -217,57 +211,59 @@ fn convert_to_list(iter: impl IntoIterator) -> Option = headers.into_iter().map(|x| x.trim().to_string()).collect(); + // let d: Vec> = data.into_iter().map(|x| x.into_iter().collect()).collect(); + + // dbg!(&headers); + // dbg!(&data); + let mut h: Vec = headers.into_iter().collect(); + let d: Vec> = data.into_iter().collect(); + + // This is just a list + if h.is_empty() { + // let's fake the header + h.push("#".to_string()); + h.push("name".to_string()); + } + // dbg!(&h); + // dbg!(&d); + + // this tuple is (row_index, header_name, value) + let mut interleaved = vec![]; + for (i, v) in d.into_iter().enumerate() { + for (n, s) in v.into_iter().enumerate() { + // dbg!(n); + // dbg!(&s); + // dbg!(&h.len()); + if h.len() == 1 { + // always get the first element since this is a simple list + // and we hacked the header above because it was empty + interleaved.push((i, h[1].clone(), s)) + } else { + interleaved.push((i, h[n].clone(), s)) + } + } + } + // dbg!(&interleaved); + Some(interleaved) } else { None } - // Some(nu_table::Table { - // headers: headers - // .into_iter() - // .map(|x| StyledString { - // contents: x, - // style: nu_table::TextStyle::default_header(), - // }) - // .collect(), - // data: data - // .into_iter() - // .map(|x| { - // x.into_iter() - // .enumerate() - // .map(|(col, y)| { - // if col == 0 { - // StyledString { - // contents: y, - // style: nu_table::TextStyle::default_header(), - // } - // } else { - // StyledString { - // contents: y, - // style: nu_table::TextStyle::basic_left(), - // } - // } - // }) - // .collect::>() - // }) - // .collect(), - // theme: nu_table::Theme::rounded(), - // }) - // } else { - // None - // } } -// fn convert_to_table(iter: impl IntoIterator) -> Option { + +// fn convert_to_list(iter: impl IntoIterator) -> Option>> { // let mut iter = iter.into_iter().peekable(); +// let mut data = vec![]; // if let Some(first) = iter.peek() { +// // dbg!(&first); // let mut headers = first.columns(); // if !headers.is_empty() { // headers.insert(0, "#".into()); // } -// let mut data = vec![]; - // for (row_num, item) in iter.enumerate() { // let mut row = vec![row_num.to_string()]; @@ -295,37 +291,7 @@ fn convert_to_list(iter: impl IntoIterator) -> Option>() -// }) -// .collect(), -// theme: nu_table::Theme::rounded(), -// }) +// Some(data) // } else { // None // } diff --git a/crates/nu-term-grid/src/grid.rs b/crates/nu-term-grid/src/grid.rs index f15b2fe376..212005354c 100644 --- a/crates/nu-term-grid/src/grid.rs +++ b/crates/nu-term-grid/src/grid.rs @@ -1,12 +1,4 @@ -// #![warn(future_incompatible)] -// #![warn(missing_copy_implementations)] -// #![warn(missing_docs)] -// #![warn(nonstandard_style)] -// #![warn(rust_2018_compatibility)] -// #![warn(rust_2018_idioms)] -// #![warn(trivial_casts, trivial_numeric_casts)] -// #![warn(unused)] -// #![deny(unsafe_code)] +// Thanks to https://github.com/ogham/rust-term-grid for making this available //! This library arranges textual data in a grid format suitable for //! fixed-width fonts, using an algorithm to minimise the amount of space From c4977ae1439cbc61d85bc87058aed7d8e4dfb001 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Thu, 7 Oct 2021 16:59:01 -0500 Subject: [PATCH 12/24] clippy --- crates/nu-command/src/viewers/griddle.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index fd1b1e9f82..d7747ca288 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -105,12 +105,10 @@ fn create_grid_output2( let cols = if let Some(col) = columns_param { col.parse::().unwrap_or(80) + } else if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { + w } else { - if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { - w - } else { - 80u16 - } + 80u16 }; if let Some(grid_display) = grid.fit_into_width(cols as usize) { @@ -218,7 +216,7 @@ fn convert_to_list2(iter: impl IntoIterator) -> Option = headers.into_iter().collect(); - let d: Vec> = data.into_iter().collect(); + // let d: Vec> = data.into_iter().collect(); // This is just a list if h.is_empty() { @@ -231,7 +229,7 @@ fn convert_to_list2(iter: impl IntoIterator) -> Option Date: Thu, 7 Oct 2021 15:20:23 -0700 Subject: [PATCH 13/24] add mkdir command --- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/filesystem/mkdir.rs | 74 +++++++++++++++++++++++ crates/nu-command/src/filesystem/mod.rs | 2 + crates/nu-command/src/filesystem/touch.rs | 30 ++++----- crates/nu-protocol/src/shell_error.rs | 4 ++ 5 files changed, 94 insertions(+), 17 deletions(-) create mode 100644 crates/nu-command/src/filesystem/mkdir.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 06abdd8bbd..b7e5157dcc 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -35,6 +35,7 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(LetEnv)); working_set.add_decl(Box::new(Lines)); working_set.add_decl(Box::new(Ls)); + working_set.add_decl(Box::new(Mkdir)); working_set.add_decl(Box::new(Module)); working_set.add_decl(Box::new(Mv)); working_set.add_decl(Box::new(Ps)); diff --git a/crates/nu-command/src/filesystem/mkdir.rs b/crates/nu-command/src/filesystem/mkdir.rs new file mode 100644 index 0000000000..5635c85538 --- /dev/null +++ b/crates/nu-command/src/filesystem/mkdir.rs @@ -0,0 +1,74 @@ +use std::collections::VecDeque; +use std::env::current_dir; + +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{ShellError, Signature, SyntaxShape, Value, ValueStream}; + +pub struct Mkdir; + +impl Command for Mkdir { + fn name(&self) -> &str { + "mkdir" + } + + fn signature(&self) -> Signature { + Signature::build("mkdir") + .rest( + "rest", + SyntaxShape::Filepath, + "the name(s) of the path(s) to create", + ) + .switch("show-created-paths", "show the path(s) created.", Some('s')) + } + + fn usage(&self) -> &str { + "Make directories, creates intermediary directories as required." + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result { + let path = current_dir()?; + let mut directories = call + .rest::(context, 0)? + .into_iter() + .map(|dir| path.join(dir)) + .peekable(); + + let show_created_paths = call.has_flag("show-created-paths"); + let mut stream: VecDeque = VecDeque::new(); + + if directories.peek().is_none() { + return Err(ShellError::MissingParameter( + "requires directory paths".to_string(), + call.head, + )); + } + + for (i, dir) in directories.enumerate() { + let span = call.positional[i].span; + let dir_res = std::fs::create_dir_all(&dir); + + if let Err(reason) = dir_res { + return Err(ShellError::CreateNotPossible( + format!("failed to create directory: {}", reason), + call.positional[i].span, + )); + } + + if show_created_paths { + let val = format!("{:}", dir.to_string_lossy()); + stream.push_back(Value::String { val, span }); + } + } + + let stream = ValueStream::from_stream(stream.into_iter()); + let span = call.head; + Ok(Value::Stream { stream, span }) + } +} diff --git a/crates/nu-command/src/filesystem/mod.rs b/crates/nu-command/src/filesystem/mod.rs index 926ffe2e54..afbbac5250 100644 --- a/crates/nu-command/src/filesystem/mod.rs +++ b/crates/nu-command/src/filesystem/mod.rs @@ -1,6 +1,7 @@ mod cd; mod cp; mod ls; +mod mkdir; mod mv; mod touch; mod util; @@ -8,5 +9,6 @@ mod util; pub use cd::Cd; pub use cp::Cp; pub use ls::Ls; +pub use mkdir::Mkdir; pub use mv::Mv; pub use touch::Touch; diff --git a/crates/nu-command/src/filesystem/touch.rs b/crates/nu-command/src/filesystem/touch.rs index c895538858..bb23d47189 100644 --- a/crates/nu-command/src/filesystem/touch.rs +++ b/crates/nu-command/src/filesystem/touch.rs @@ -32,25 +32,21 @@ impl Command for Touch { call: &Call, _input: Value, ) -> Result { - touch(context, call) - } -} + let target: String = call.req(context, 0)?; + let rest: Vec = call.rest(context, 1)?; -fn touch(context: &EvaluationContext, call: &Call) -> Result { - let target: String = call.req(context, 0)?; - let rest: Vec = call.rest(context, 1)?; - - for (index, item) in vec![target].into_iter().chain(rest).enumerate() { - match OpenOptions::new().write(true).create(true).open(&item) { - Ok(_) => continue, - Err(err) => { - return Err(ShellError::CreateNotPossible( - format!("Failed to create file: {}", err), - call.positional[index].span, - )); + for (index, item) in vec![target].into_iter().chain(rest).enumerate() { + match OpenOptions::new().write(true).create(true).open(&item) { + Ok(_) => continue, + Err(err) => { + return Err(ShellError::CreateNotPossible( + format!("Failed to create file: {}", err), + call.positional[index].span, + )); + } } } - } - Ok(Value::Nothing { span: call.head }) + Ok(Value::Nothing { span: call.head }) + } } diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 1b1c172bc6..4be91303d0 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -27,6 +27,10 @@ pub enum ShellError { #[diagnostic(code(nu::shell::unknown_operator), url(docsrs))] UnknownOperator(String, #[label = "unsupported operator"] Span), + #[error("Missing parameter: {0}.")] + #[diagnostic(code(nu::shell::missing_parameter), url(docsrs))] + MissingParameter(String, #[label = "missing parameter: {0}"] Span), + #[error("External commands not yet supported")] #[diagnostic(code(nu::shell::external_commands), url(docsrs))] ExternalNotSupported(#[label = "external not supported"] Span), From c4dabe832703127a8d15a90965e13493bedf4ae1 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Fri, 8 Oct 2021 08:14:32 -0500 Subject: [PATCH 14/24] some cleanup, extra_usage --- crates/nu-command/src/viewers/griddle.rs | 33 ++++++++++-------------- crates/nu-command/src/viewers/table.rs | 16 ++++++++---- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index d7747ca288..f16ec4a743 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -15,7 +15,7 @@ impl Command for Griddle { } fn usage(&self) -> &str { - "Render the grid." + "Renders the output to a textual terminal grid." } fn signature(&self) -> nu_protocol::Signature { @@ -27,6 +27,15 @@ impl Command for Griddle { ) } + fn extra_usage(&self) -> &str { + r#"grid was built to give a concise gridded layout for ls. however, +it determines what to put in the grid by looking for a column named +'name'. this works great for tables and records but for lists we +need to do something different. such as with '[one two three] | grid' +it creates a fake column called 'name' for these values so that it +prints out the list properly."# + } + fn run( &self, context: &EvaluationContext, @@ -57,21 +66,11 @@ impl Command for Griddle { } Value::Record { cols, vals, .. } => { // dbg!("value::record"); - - // let mut items = vec![]; - - // for (c, v) in cols.into_iter().zip(vals.into_iter()) { - // items.push(vec![c, v.into_string()]) - // } - // dbg!(&items); - - // Ok(create_grid_output(items, call, columns_param)) let mut items = vec![]; for (i, (c, v)) in cols.into_iter().zip(vals.into_iter()).enumerate() { items.push((i, c, v.into_string())) } - // dbg!(&items); Ok(create_grid_output2(items, call, columns_param)) } @@ -213,8 +212,6 @@ fn convert_to_list2(iter: impl IntoIterator) -> Option = headers.into_iter().map(|x| x.trim().to_string()).collect(); // let d: Vec> = data.into_iter().map(|x| x.into_iter().collect()).collect(); - // dbg!(&headers); - // dbg!(&data); let mut h: Vec = headers.into_iter().collect(); // let d: Vec> = data.into_iter().collect(); @@ -224,26 +221,22 @@ fn convert_to_list2(iter: impl IntoIterator) -> Option Result { + let term_width = if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { + w as usize + } else { + 80usize + }; + match input { Value::List { vals, .. } => { let table = convert_to_table(vals); if let Some(table) = table { - let result = nu_table::draw_table(&table, 80, &HashMap::new()); + let result = nu_table::draw_table(&table, term_width, &HashMap::new()); Ok(Value::String { val: result, @@ -46,7 +52,7 @@ impl Command for Table { let table = convert_to_table(stream); if let Some(table) = table { - let result = nu_table::draw_table(&table, 80, &HashMap::new()); + let result = nu_table::draw_table(&table, term_width, &HashMap::new()); Ok(Value::String { val: result, @@ -78,7 +84,7 @@ impl Command for Table { theme: nu_table::Theme::rounded(), }; - let result = nu_table::draw_table(&table, 80, &HashMap::new()); + let result = nu_table::draw_table(&table, term_width, &HashMap::new()); Ok(Value::String { val: result, From 5ddf0d209d0a9dceda54dfbc0713b62dc0cbe7fa Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Fri, 8 Oct 2021 09:40:20 -0500 Subject: [PATCH 15/24] respect lscolors env var; measure width minus ansi --- Cargo.lock | 54 ++++++++++++++++++++++++ crates/nu-command/Cargo.toml | 3 +- crates/nu-command/src/viewers/griddle.rs | 6 ++- crates/nu-term-grid/Cargo.toml | 1 + crates/nu-term-grid/src/grid.rs | 21 ++++++--- 5 files changed, 78 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 433c8b4a2d..6d6b7893b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "assert_cmd" version = "1.0.8" @@ -402,6 +408,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lscolors" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0aa49b10c47f9a4391a99198b5e65c74f9ca771c0dcc856bb75a3f46c8627d" +dependencies = [ + "ansi_term", + "crossterm", +] + [[package]] name = "memchr" version = "2.4.1" @@ -519,6 +535,7 @@ version = "0.1.0" dependencies = [ "chrono", "glob", + "lscolors", "nu-engine", "nu-json", "nu-path", @@ -594,6 +611,7 @@ dependencies = [ name = "nu-term-grid" version = "0.36.0" dependencies = [ + "strip-ansi-escapes", "unicode-width", ] @@ -979,6 +997,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +[[package]] +name = "strip-ansi-escapes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8" +dependencies = [ + "vte", +] + [[package]] name = "supports-color" version = "1.3.0" @@ -1148,6 +1175,33 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b" +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + +[[package]] +name = "vte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 4d0f13c78b..2120c39384 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -17,5 +17,6 @@ nu-term-grid = { path = "../nu-term-grid" } glob = "0.3.0" thiserror = "1.0.29" sysinfo = "0.20.4" -chrono = { version="0.4.19", features=["serde"] } +chrono = { version = "0.4.19", features = ["serde"] } terminal_size = "0.1.17" +lscolors = { version = "0.8.0", features = ["crossterm"] } diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index f16ec4a743..77cdb92e8f 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -1,3 +1,4 @@ +use lscolors::{LsColors, Style}; use nu_engine::CallExt; use nu_protocol::{ ast::{Call, PathMember}, @@ -88,6 +89,7 @@ fn create_grid_output2( call: &Call, columns_param: Option, ) -> Value { + let ls_colors = LsColors::from_env().unwrap_or_default(); let mut grid = Grid::new(GridOptions { direction: Direction::TopToBottom, filling: Filling::Text(" | ".into()), @@ -96,7 +98,9 @@ fn create_grid_output2( for (_row_index, header, value) in items { // only output value if the header name is 'name' if header == "name" { - let mut cell = Cell::from(value); + let style = ls_colors.style_for_path(value.clone()); + let ansi_style = style.map(Style::to_crossterm_style).unwrap_or_default(); + let mut cell = Cell::from(ansi_style.apply(value).to_string()); cell.alignment = Alignment::Right; grid.add(cell); } diff --git a/crates/nu-term-grid/Cargo.toml b/crates/nu-term-grid/Cargo.toml index 183de8ecc9..aa24839b94 100644 --- a/crates/nu-term-grid/Cargo.toml +++ b/crates/nu-term-grid/Cargo.toml @@ -13,3 +13,4 @@ path = "src/main.rs" [dependencies] unicode-width = "0.1.9" +strip-ansi-escapes = "0.1.1" diff --git a/crates/nu-term-grid/src/grid.rs b/crates/nu-term-grid/src/grid.rs index 212005354c..5c10ef86a1 100644 --- a/crates/nu-term-grid/src/grid.rs +++ b/crates/nu-term-grid/src/grid.rs @@ -94,10 +94,21 @@ use std::cmp::max; use std::fmt; use std::iter::repeat; - -// extern crate unicode_width; +use strip_ansi_escapes::strip; use unicode_width::UnicodeWidthStr; +fn unicode_width_strip_ansi(astring: &str) -> usize { + let stripped_string: String = { + if let Ok(bytes) = strip(astring) { + String::from_utf8_lossy(&bytes).to_string() + } else { + astring.to_string() + } + }; + + UnicodeWidthStr::width(&stripped_string[..]) +} + /// Alignment indicate on which side the content should stick if some filling /// is required. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -129,7 +140,7 @@ pub struct Cell { impl From for Cell { fn from(string: String) -> Self { Self { - width: UnicodeWidthStr::width(&*string), + width: unicode_width_strip_ansi(&*string), contents: string, alignment: Alignment::Left, } @@ -139,7 +150,7 @@ impl From for Cell { impl<'a> From<&'a str> for Cell { fn from(string: &'a str) -> Self { Self { - width: UnicodeWidthStr::width(&*string), + width: unicode_width_strip_ansi(&*string), contents: string.into(), alignment: Alignment::Left, } @@ -177,7 +188,7 @@ impl Filling { fn width(&self) -> Width { match *self { Filling::Spaces(w) => w, - Filling::Text(ref t) => UnicodeWidthStr::width(&t[..]), + Filling::Text(ref t) => unicode_width_strip_ansi(&t[..]), } } } From c636c30a19a17f440fb31445a50af0af8838b903 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Fri, 8 Oct 2021 09:53:26 -0500 Subject: [PATCH 16/24] added a switch to enable coloring --- crates/nu-command/src/viewers/griddle.rs | 44 +++++++++++++++--------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index 77cdb92e8f..42a0e58f33 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -20,12 +20,14 @@ impl Command for Griddle { } fn signature(&self) -> nu_protocol::Signature { - Signature::build("grid").named( - "columns", - SyntaxShape::Int, - "number of columns wide", - Some('c'), - ) + Signature::build("grid") + .named( + "width", + SyntaxShape::Int, + "number of columns wide", + Some('w'), + ) + .switch("color", "draw output with color", Some('c')) } fn extra_usage(&self) -> &str { @@ -43,14 +45,15 @@ prints out the list properly."# call: &Call, input: Value, ) -> Result { - let columns_param: Option = call.get_flag(context, "columns")?; + let width_param: Option = call.get_flag(context, "width")?; + let color_param: bool = call.has_flag("color"); match input { Value::List { vals, .. } => { // dbg!("value::list"); let data = convert_to_list2(vals); if let Some(items) = data { - Ok(create_grid_output2(items, call, columns_param)) + Ok(create_grid_output2(items, call, width_param, color_param)) } else { Ok(Value::Nothing { span: call.head }) } @@ -59,7 +62,7 @@ prints out the list properly."# // dbg!("value::stream"); let data = convert_to_list2(stream); if let Some(items) = data { - Ok(create_grid_output2(items, call, columns_param)) + Ok(create_grid_output2(items, call, width_param, color_param)) } else { // dbg!(data); Ok(Value::Nothing { span: call.head }) @@ -73,7 +76,7 @@ prints out the list properly."# items.push((i, c, v.into_string())) } - Ok(create_grid_output2(items, call, columns_param)) + Ok(create_grid_output2(items, call, width_param, color_param)) } x => { // dbg!("other value"); @@ -87,7 +90,8 @@ prints out the list properly."# fn create_grid_output2( items: Vec<(usize, String, String)>, call: &Call, - columns_param: Option, + width_param: Option, + color_param: bool, ) -> Value { let ls_colors = LsColors::from_env().unwrap_or_default(); let mut grid = Grid::new(GridOptions { @@ -98,15 +102,21 @@ fn create_grid_output2( for (_row_index, header, value) in items { // only output value if the header name is 'name' if header == "name" { - let style = ls_colors.style_for_path(value.clone()); - let ansi_style = style.map(Style::to_crossterm_style).unwrap_or_default(); - let mut cell = Cell::from(ansi_style.apply(value).to_string()); - cell.alignment = Alignment::Right; - grid.add(cell); + if color_param { + let style = ls_colors.style_for_path(value.clone()); + let ansi_style = style.map(Style::to_crossterm_style).unwrap_or_default(); + let mut cell = Cell::from(ansi_style.apply(value).to_string()); + cell.alignment = Alignment::Right; + grid.add(cell); + } else { + let mut cell = Cell::from(value); + cell.alignment = Alignment::Right; + grid.add(cell); + } } } - let cols = if let Some(col) = columns_param { + let cols = if let Some(col) = width_param { col.parse::().unwrap_or(80) } else if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { w From 42113a767a1bb5300e6f31731515b356041f145e Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Fri, 8 Oct 2021 10:15:07 -0500 Subject: [PATCH 17/24] allow one to specify a custom separator --- crates/nu-command/src/viewers/griddle.rs | 55 ++++++++++++++++++------ 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index 42a0e58f33..55ad98c4e7 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -28,6 +28,12 @@ impl Command for Griddle { Some('w'), ) .switch("color", "draw output with color", Some('c')) + .named( + "separator", + SyntaxShape::String, + "character to separate grid with", + Some('s'), + ) } fn extra_usage(&self) -> &str { @@ -47,13 +53,20 @@ prints out the list properly."# ) -> Result { let width_param: Option = call.get_flag(context, "width")?; let color_param: bool = call.has_flag("color"); + let seperator_param: Option = call.get_flag(context, "separator")?; match input { Value::List { vals, .. } => { // dbg!("value::list"); let data = convert_to_list2(vals); if let Some(items) = data { - Ok(create_grid_output2(items, call, width_param, color_param)) + Ok(create_grid_output2( + items, + call, + width_param, + color_param, + seperator_param, + )) } else { Ok(Value::Nothing { span: call.head }) } @@ -62,7 +75,13 @@ prints out the list properly."# // dbg!("value::stream"); let data = convert_to_list2(stream); if let Some(items) = data { - Ok(create_grid_output2(items, call, width_param, color_param)) + Ok(create_grid_output2( + items, + call, + width_param, + color_param, + seperator_param, + )) } else { // dbg!(data); Ok(Value::Nothing { span: call.head }) @@ -76,7 +95,13 @@ prints out the list properly."# items.push((i, c, v.into_string())) } - Ok(create_grid_output2(items, call, width_param, color_param)) + Ok(create_grid_output2( + items, + call, + width_param, + color_param, + seperator_param, + )) } x => { // dbg!("other value"); @@ -92,11 +117,25 @@ fn create_grid_output2( call: &Call, width_param: Option, color_param: bool, + separator_param: Option, ) -> Value { let ls_colors = LsColors::from_env().unwrap_or_default(); + let cols = if let Some(col) = width_param { + col.parse::().unwrap_or(80) + } else if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { + w + } else { + 80u16 + }; + let sep = if let Some(seperator) = separator_param { + seperator + } else { + " │ ".to_string() + }; + let mut grid = Grid::new(GridOptions { direction: Direction::TopToBottom, - filling: Filling::Text(" | ".into()), + filling: Filling::Text(sep), }); for (_row_index, header, value) in items { @@ -116,14 +155,6 @@ fn create_grid_output2( } } - let cols = if let Some(col) = width_param { - col.parse::().unwrap_or(80) - } else if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { - w - } else { - 80u16 - }; - if let Some(grid_display) = grid.fit_into_width(cols as usize) { Value::String { val: grid_display.to_string(), From 1b977c658cfda49f0d9a3c6fd8940ea2a91d98d7 Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Oct 2021 08:38:42 +1300 Subject: [PATCH 18/24] Improve the alias expansion --- crates/nu-parser/src/parser.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 8cf6351c57..f6f8ed2315 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -359,7 +359,7 @@ fn parse_multispan_value( (arg, error) } SyntaxShape::Expression => { - let (arg, err) = parse_expression(working_set, &spans[*spans_idx..]); + let (arg, err) = parse_expression(working_set, &spans[*spans_idx..], true); error = error.or(err); *spans_idx = spans.len() - 1; @@ -586,7 +586,7 @@ pub fn parse_call( new_spans.extend(&spans[(pos + 1)..]); } - let (result, err) = parse_call(working_set, &new_spans, false); + let (result, err) = parse_expression(working_set, &new_spans, false); let expression = match result { Expression { @@ -631,7 +631,7 @@ pub fn parse_call( new_spans.extend(&spans[(pos + 1)..]); } - let (result, err) = parse_call(working_set, &new_spans, false); + let (result, err) = parse_expression(working_set, &new_spans, false); let expression = match result { Expression { @@ -2832,13 +2832,14 @@ pub fn parse_math_expression( pub fn parse_expression( working_set: &mut StateWorkingSet, spans: &[Span], + expand_aliases: bool, ) -> (Expression, Option) { let bytes = working_set.get_span_contents(spans[0]); match bytes[0] { b'0' | b'1' | b'2' | b'3' | b'4' | b'5' | b'6' | b'7' | b'8' | b'9' | b'(' | b'{' | b'[' | b'$' | b'"' | b'\'' | b'-' => parse_math_expression(working_set, spans, None), - _ => parse_call(working_set, spans, true), + _ => parse_call(working_set, spans, expand_aliases), } } @@ -2878,7 +2879,7 @@ pub fn parse_statement( ), b"hide" => parse_hide(working_set, spans), _ => { - let (expr, err) = parse_expression(working_set, spans); + let (expr, err) = parse_expression(working_set, spans, true); (Statement::Pipeline(Pipeline::from_vec(vec![expr])), err) } } @@ -2914,7 +2915,7 @@ pub fn parse_block( .commands .iter() .map(|command| { - let (expr, err) = parse_expression(working_set, &command.parts); + let (expr, err) = parse_expression(working_set, &command.parts, true); if error.is_none() { error = err; From dea9c1482bb13d2cab166196872aaebec8dbc338 Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Oct 2021 10:51:47 +1300 Subject: [PATCH 19/24] Allow vars and subexprs in extern args --- crates/nu-cli/src/syntax_highlight.rs | 8 ++++-- crates/nu-engine/src/eval.rs | 38 +++++++++++++++++---------- crates/nu-parser/src/flatten.rs | 14 +++++++++- crates/nu-parser/src/parser.rs | 22 +++++++++++++--- crates/nu-protocol/src/ast/expr.rs | 2 +- 5 files changed, 62 insertions(+), 22 deletions(-) diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index 161801e261..d0d5d06431 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -39,8 +39,12 @@ impl Highlighter for NuHighlighter { .to_string(); match shape.1 { FlatShape::Custom(..) => output.push((Style::new().bold(), next_token)), - FlatShape::External => output.push((Style::new().bold(), next_token)), - FlatShape::ExternalArg => output.push((Style::new().bold(), next_token)), + FlatShape::External => { + output.push((Style::new().fg(nu_ansi_term::Color::Green), next_token)) + } + FlatShape::ExternalArg => { + output.push((Style::new().fg(nu_ansi_term::Color::Green), next_token)) + } FlatShape::Garbage => output.push(( Style::new() .fg(nu_ansi_term::Color::White) diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index d59ff4b3e0..ca755c665f 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -2,6 +2,8 @@ use nu_protocol::ast::{Block, Call, Expr, Expression, Operator, Statement}; use nu_protocol::engine::EvaluationContext; use nu_protocol::{Range, ShellError, Span, Type, Unit, Value}; +use crate::FromValue; + pub fn eval_operator(op: &Expression) -> Result { match op { Expression { @@ -71,7 +73,7 @@ fn eval_call(context: &EvaluationContext, call: &Call, input: Value) -> Result Result { @@ -84,20 +86,28 @@ fn eval_external( let command = engine_state.get_decl(decl_id); let mut call = Call::new(); - call.positional = [*name] - .iter() - .chain(args.iter()) - .map(|span| { - let contents = engine_state.get_span_contents(span); - let val = String::from_utf8_lossy(contents); - Expression { - expr: Expr::String(val.into()), - span: *span, - ty: Type::String, - custom_completion: None, - } + + let name_span = name; + let name = engine_state.get_span_contents(name); + call.positional.push(Expression { + expr: Expr::String(String::from_utf8_lossy(name).to_string()), + span: *name_span, + ty: Type::String, + custom_completion: None, + }); + + for arg in args { + let span = arg.span; + let result = eval_expression(context, arg)?; + let result: String = FromValue::from_value(&result)?; + + call.positional.push(Expression { + expr: Expr::String(result), + span, + ty: Type::String, + custom_completion: None, }) - .collect(); + } if last_expression { call.named.push(("last_expression".into(), None)) diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index 90cfc5092d..26eae7e911 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -67,7 +67,19 @@ pub fn flatten_expression( let mut output = vec![(*name, FlatShape::External)]; for arg in args { - output.push((*arg, FlatShape::ExternalArg)); + //output.push((*arg, FlatShape::ExternalArg)); + match arg { + Expression { + expr: Expr::String(..), + span, + .. + } => { + output.push((*span, FlatShape::ExternalArg)); + } + _ => { + output.extend(flatten_expression(working_set, arg)); + } + } } output diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index f6f8ed2315..2f1ae7b508 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -104,14 +104,28 @@ pub fn check_name<'a>( } pub fn parse_external_call( - _working_set: &mut StateWorkingSet, + working_set: &mut StateWorkingSet, spans: &[Span], ) -> (Expression, Option) { - // TODO: add external parsing let mut args = vec![]; let name = spans[0]; + let mut error = None; + for span in &spans[1..] { - args.push(*span); + let contents = working_set.get_span_contents(*span); + + if contents.starts_with(b"$") || contents.starts_with(b"(") { + let (arg, err) = parse_expression(working_set, &[*span], true); + error = error.or(err); + args.push(arg); + } else { + args.push(Expression { + expr: Expr::String(String::from_utf8_lossy(contents).to_string()), + span: *span, + ty: Type::String, + custom_completion: None, + }) + } } ( Expression { @@ -120,7 +134,7 @@ pub fn parse_external_call( ty: Type::Unknown, custom_completion: None, }, - None, + error, ) } diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index 4a9b42527d..6eb0b539d5 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -14,7 +14,7 @@ pub enum Expr { ), Var(VarId), Call(Box), - ExternalCall(Span, Vec), + ExternalCall(Span, Vec), Operator(Operator), RowCondition(VarId, Box), BinaryOp(Box, Box, Box), //lhs, op, rhs From 64d83142c3f8068116be6f0f019b0d91d671cec4 Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Oct 2021 11:30:10 +1300 Subject: [PATCH 20/24] More external cleanup --- crates/nu-engine/src/eval.rs | 29 +++++++++-------------------- crates/nu-parser/src/flatten.rs | 4 ++-- crates/nu-parser/src/parser.rs | 5 +++-- crates/nu-protocol/src/ast/expr.rs | 2 +- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index ca755c665f..1a44d6fffb 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -2,8 +2,6 @@ use nu_protocol::ast::{Block, Call, Expr, Expression, Operator, Statement}; use nu_protocol::engine::EvaluationContext; use nu_protocol::{Range, ShellError, Span, Type, Unit, Value}; -use crate::FromValue; - pub fn eval_operator(op: &Expression) -> Result { match op { Expression { @@ -72,7 +70,8 @@ fn eval_call(context: &EvaluationContext, call: &Call, input: Value) -> Result eval_expression(context, expr), Expr::Call(call) => eval_call(context, call, Value::nothing()), - Expr::ExternalCall(name, args) => { - eval_external(context, name, args, Value::nothing(), true) + Expr::ExternalCall(name, span, args) => { + eval_external(context, name, span, args, Value::nothing(), true) } Expr::Operator(_) => Ok(Value::Nothing { span: expr.span }), Expr::BinaryOp(lhs, op, rhs) => { @@ -283,12 +271,13 @@ pub fn eval_block( input = eval_call(context, call, input)?; } Expression { - expr: Expr::ExternalCall(name, args), + expr: Expr::ExternalCall(name, name_span, args), .. } => { input = eval_external( context, name, + name_span, args, input, i == pipeline.expressions.len() - 1, diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index 26eae7e911..db94d622b3 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -63,8 +63,8 @@ pub fn flatten_expression( } output } - Expr::ExternalCall(name, args) => { - let mut output = vec![(*name, FlatShape::External)]; + Expr::ExternalCall(_, name_span, args) => { + let mut output = vec![(*name_span, FlatShape::External)]; for arg in args { //output.push((*arg, FlatShape::ExternalArg)); diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 2f1ae7b508..dbd2ddc298 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -108,7 +108,8 @@ pub fn parse_external_call( spans: &[Span], ) -> (Expression, Option) { let mut args = vec![]; - let name = spans[0]; + let name_span = spans[0]; + let name = String::from_utf8_lossy(working_set.get_span_contents(name_span)).to_string(); let mut error = None; for span in &spans[1..] { @@ -129,7 +130,7 @@ pub fn parse_external_call( } ( Expression { - expr: Expr::ExternalCall(name, args), + expr: Expr::ExternalCall(name, name_span, args), span: span(spans), ty: Type::Unknown, custom_completion: None, diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index 6eb0b539d5..718dec5f6a 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -14,7 +14,7 @@ pub enum Expr { ), Var(VarId), Call(Box), - ExternalCall(Span, Vec), + ExternalCall(String, Span, Vec), Operator(Operator), RowCondition(VarId, Box), BinaryOp(Box, Box, Box), //lhs, op, rhs From 4ddc953e38bb941deb241bf2395b9b00087373a6 Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Oct 2021 14:02:01 +1300 Subject: [PATCH 21/24] Port help and start porting split --- Cargo.lock | 1 + crates/nu-command/src/core_commands/do_.rs | 2 +- crates/nu-command/src/core_commands/help.rs | 99 +++--- crates/nu-command/src/default_context.rs | 2 + crates/nu-command/src/strings/mod.rs | 2 + crates/nu-command/src/strings/split/chars.rs | 157 +++++++++ .../nu-command/src/strings/split/command.rs | 48 +++ crates/nu-command/src/strings/split/mod.rs | 9 + crates/nu-engine/Cargo.toml | 3 +- crates/nu-engine/src/documentation.rs | 315 ++++++++++++++++++ crates/nu-engine/src/lib.rs | 2 + crates/nu-protocol/src/engine/engine_state.rs | 19 +- .../src/engine/evaluation_context.rs | 10 +- crates/nu-protocol/src/shell_error.rs | 14 + 14 files changed, 633 insertions(+), 50 deletions(-) create mode 100644 crates/nu-command/src/strings/split/chars.rs create mode 100644 crates/nu-command/src/strings/split/command.rs create mode 100644 crates/nu-command/src/strings/split/mod.rs create mode 100644 crates/nu-engine/src/documentation.rs diff --git a/Cargo.lock b/Cargo.lock index 6d6b7893b6..b70b7cf907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -551,6 +551,7 @@ dependencies = [ name = "nu-engine" version = "0.1.0" dependencies = [ + "itertools", "nu-parser", "nu-path", "nu-protocol", diff --git a/crates/nu-command/src/core_commands/do_.rs b/crates/nu-command/src/core_commands/do_.rs index 20bd0eb7d2..d87148fa1a 100644 --- a/crates/nu-command/src/core_commands/do_.rs +++ b/crates/nu-command/src/core_commands/do_.rs @@ -15,7 +15,7 @@ impl Command for Do { } fn signature(&self) -> nu_protocol::Signature { - Signature::build("do").required( + Signature::build("do").desc(self.usage()).required( "block", SyntaxShape::Block(Some(vec![])), "the block to run", diff --git a/crates/nu-command/src/core_commands/help.rs b/crates/nu-command/src/core_commands/help.rs index 0313f25534..6bc36ef542 100644 --- a/crates/nu-command/src/core_commands/help.rs +++ b/crates/nu-command/src/core_commands/help.rs @@ -1,10 +1,10 @@ use nu_protocol::{ ast::Call, engine::{Command, EvaluationContext}, - Example, ShellError, Signature, Spanned, SyntaxShape, Value, + span, Example, ShellError, Signature, Spanned, SyntaxShape, Value, }; -use nu_engine::CallExt; +use nu_engine::{get_full_help, CallExt}; pub struct Help; @@ -73,11 +73,11 @@ impl Command for Help { } fn help(context: &EvaluationContext, call: &Call) -> Result { - let span = call.head; + let head = call.head; let find: Option> = call.get_flag(context, "find")?; let rest: Vec> = call.rest(context, 0)?; - let full_commands = context.get_commands_info(); + let full_commands = context.get_signatures_with_examples(); if let Some(f) = find { let search_string = f.item; @@ -87,29 +87,36 @@ fn help(context: &EvaluationContext, call: &Call) -> Result { let mut cols = vec![]; let mut vals = vec![]; - let key = cmd.name.clone(); - let c = cmd.usage.clone(); - let e = cmd.extra_usage.clone(); + let key = cmd.0.name.clone(); + let c = cmd.0.usage.clone(); + let e = cmd.0.extra_usage.clone(); if key.to_lowercase().contains(&search_string) || c.to_lowercase().contains(&search_string) || e.to_lowercase().contains(&search_string) { cols.push("name".into()); - vals.push(Value::String { val: key, span }); + vals.push(Value::String { + val: key, + span: head, + }); cols.push("usage".into()); - vals.push(Value::String { val: c, span }); + vals.push(Value::String { val: c, span: head }); cols.push("extra_usage".into()); - vals.push(Value::String { val: e, span }); + vals.push(Value::String { val: e, span: head }); - found_cmds_vec.push(Value::Record { cols, vals, span }); + found_cmds_vec.push(Value::Record { + cols, + vals, + span: head, + }); } } return Ok(Value::List { vals: found_cmds_vec, - span, + span: head, }); } @@ -121,25 +128,38 @@ fn help(context: &EvaluationContext, call: &Call) -> Result { let mut cols = vec![]; let mut vals = vec![]; - let key = cmd.name.clone(); - let c = cmd.usage.clone(); - let e = cmd.extra_usage.clone(); + let key = cmd.0.name.clone(); + let c = cmd.0.usage.clone(); + let e = cmd.0.extra_usage.clone(); cols.push("name".into()); - vals.push(Value::String { val: key, span }); + vals.push(Value::String { + val: key, + span: head, + }); cols.push("usage".into()); - vals.push(Value::String { val: c, span }); + vals.push(Value::String { val: c, span: head }); cols.push("extra_usage".into()); - vals.push(Value::String { val: e, span }); + vals.push(Value::String { val: e, span: head }); - found_cmds_vec.push(Value::Record { cols, vals, span }); + found_cmds_vec.push(Value::Record { + cols, + vals, + span: head, + }); } + + Ok(Value::List { + vals: found_cmds_vec, + span: head, + }) } else { let mut name = String::new(); + let mut output = String::new(); - for r in rest { + for r in &rest { if !name.is_empty() { name.push(' '); } @@ -147,31 +167,24 @@ fn help(context: &EvaluationContext, call: &Call) -> Result { } for cmd in full_commands { - let mut cols = vec![]; - let mut vals = vec![]; - - let key = cmd.name.clone(); - let c = cmd.usage.clone(); - let e = cmd.extra_usage.clone(); - - if key.starts_with(&name) { - cols.push("name".into()); - vals.push(Value::String { val: key, span }); - - cols.push("usage".into()); - vals.push(Value::String { val: c, span }); - - cols.push("extra_usage".into()); - vals.push(Value::String { val: e, span }); - - found_cmds_vec.push(Value::Record { cols, vals, span }); + if cmd.0.name == name { + let help = get_full_help(&cmd.0, &cmd.1, context); + output.push_str(&help); } } + + if !output.is_empty() { + Ok(Value::String { + val: output, + span: call.head, + }) + } else { + Err(ShellError::CommandNotFound(span(&[ + rest[0].span, + rest[rest.len() - 1].span, + ]))) + } } - Ok(Value::List { - vals: found_cmds_vec, - span, - }) // FIXME: the fancy help stuff needs to be reimplemented /* @@ -341,7 +354,7 @@ You can also learn more at https://www.nushell.sh/book/"#; Ok(Value::String { val: msg.into(), - span, + span: head, }) } } diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index e35574a7ab..9eb6c1c0bc 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -41,6 +41,8 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(Mv)); working_set.add_decl(Box::new(Ps)); working_set.add_decl(Box::new(Select)); + working_set.add_decl(Box::new(Split)); + working_set.add_decl(Box::new(SplitChars)); working_set.add_decl(Box::new(Sys)); working_set.add_decl(Box::new(Table)); working_set.add_decl(Box::new(Touch)); diff --git a/crates/nu-command/src/strings/mod.rs b/crates/nu-command/src/strings/mod.rs index 8691acce1e..d3df18f05a 100644 --- a/crates/nu-command/src/strings/mod.rs +++ b/crates/nu-command/src/strings/mod.rs @@ -1,3 +1,5 @@ mod build_string; +mod split; pub use build_string::BuildString; +pub use split::*; diff --git a/crates/nu-command/src/strings/split/chars.rs b/crates/nu-command/src/strings/split/chars.rs new file mode 100644 index 0000000000..f77f873692 --- /dev/null +++ b/crates/nu-command/src/strings/split/chars.rs @@ -0,0 +1,157 @@ +use nu_protocol::{ + ast::Call, + engine::{Command, EvaluationContext}, + Example, IntoValueStream, ShellError, Signature, Span, Type, Value, +}; + +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "split chars" + } + + fn signature(&self) -> Signature { + Signature::build("split chars") + } + + fn usage(&self) -> &str { + "splits a string's characters into separate rows" + } + + fn run( + &self, + _context: &EvaluationContext, + call: &Call, + input: Value, + ) -> Result { + split_chars(call, input) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Split the string's characters into separate rows", + example: "echo 'hello' | split chars", + result: Some(vec![ + Value::String { + val: "h".into(), + span: Span::unknown(), + }, + Value::String { + val: "e".into(), + span: Span::unknown(), + }, + Value::String { + val: "l".into(), + span: Span::unknown(), + }, + Value::String { + val: "l".into(), + span: Span::unknown(), + }, + Value::String { + val: "o".into(), + span: Span::unknown(), + }, + ]), + }] + } +} + +fn split_chars(call: &Call, input: Value) -> Result { + let name = call.head; + + Ok(match input { + Value::List { vals, span } => Value::List { + vals: vals + .iter() + .flat_map(move |v| { + if let Ok(s) = v.as_string() { + let v_span = v.span(); + s.chars() + .collect::>() + .into_iter() + .map(move |x| Value::String { + val: x.to_string(), + span: v_span, + }) + .collect() + } else { + vec![Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: name, + origin: v.span(), + }, + }] + } + }) + .collect(), + span, + }, + Value::Stream { stream, span } => Value::Stream { + stream: stream + .flat_map(move |v| { + if let Ok(s) = v.as_string() { + let v_span = v.span(); + s.chars() + .collect::>() + .into_iter() + .map(move |x| Value::String { + val: x.to_string(), + span: v_span, + }) + .collect() + } else { + vec![Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: name, + origin: v.span(), + }, + }] + } + }) + .into_value_stream(), + span, + }, + v => { + let v_span = v.span(); + if let Ok(s) = v.as_string() { + Value::List { + vals: s + .chars() + .collect::>() + .into_iter() + .map(move |x| Value::String { + val: x.to_string(), + span: v_span, + }) + .collect(), + span: v_span, + } + } else { + Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: name, + origin: v.span(), + }, + } + } + } + }) +} + +// #[cfg(test)] +// mod tests { +// use super::ShellError; +// use super::SubCommand; + +// #[test] +// fn examples_work_as_expected() -> Result<(), ShellError> { +// use crate::examples::test as test_examples; + +// test_examples(SubCommand {}) +// } +// } diff --git a/crates/nu-command/src/strings/split/command.rs b/crates/nu-command/src/strings/split/command.rs new file mode 100644 index 0000000000..87df229275 --- /dev/null +++ b/crates/nu-command/src/strings/split/command.rs @@ -0,0 +1,48 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EvaluationContext}, + Signature, Value, +}; + +#[derive(Clone)] +pub struct SplitCommand; + +impl Command for SplitCommand { + fn name(&self) -> &str { + "split" + } + + fn signature(&self) -> Signature { + Signature::build("split") + } + + fn usage(&self) -> &str { + "Split contents across desired subcommand (like row, column) via the separator." + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result { + Ok(Value::String { + val: get_full_help(&SplitCommand.signature(), &SplitCommand.examples(), context), + span: call.head, + }) + } +} + +// #[cfg(test)] +// mod tests { +// use super::Command; +// use super::ShellError; + +// #[test] +// fn examples_work_as_expected() -> Result<(), ShellError> { +// use crate::examples::test as test_examples; + +// test_examples(Command {}) +// } +// } diff --git a/crates/nu-command/src/strings/split/mod.rs b/crates/nu-command/src/strings/split/mod.rs new file mode 100644 index 0000000000..3a65f0e124 --- /dev/null +++ b/crates/nu-command/src/strings/split/mod.rs @@ -0,0 +1,9 @@ +pub mod chars; +// pub mod column; +pub mod command; +// pub mod row; + +pub use chars::SubCommand as SplitChars; +// pub use column::SubCommand as SplitColumn; +pub use command::SplitCommand as Split; +// pub use row::SubCommand as SplitRow; diff --git a/crates/nu-engine/Cargo.toml b/crates/nu-engine/Cargo.toml index 6bb588bc0a..35ebfa91ca 100644 --- a/crates/nu-engine/Cargo.toml +++ b/crates/nu-engine/Cargo.toml @@ -6,4 +6,5 @@ edition = "2018" [dependencies] nu-parser = { path = "../nu-parser" } nu-protocol = { path = "../nu-protocol" } -nu-path = { path = "../nu-path" } \ No newline at end of file +nu-path = { path = "../nu-path" } +itertools = "0.10.1" \ No newline at end of file diff --git a/crates/nu-engine/src/documentation.rs b/crates/nu-engine/src/documentation.rs new file mode 100644 index 0000000000..b0269f340b --- /dev/null +++ b/crates/nu-engine/src/documentation.rs @@ -0,0 +1,315 @@ +use itertools::Itertools; +use nu_protocol::{engine::EvaluationContext, Example, Signature, Span, Value}; +use std::collections::HashMap; + +const COMMANDS_DOCS_DIR: &str = "docs/commands"; + +pub struct DocumentationConfig { + no_subcommands: bool, + //FIXME: + #[allow(dead_code)] + no_color: bool, + brief: bool, +} + +impl Default for DocumentationConfig { + fn default() -> Self { + DocumentationConfig { + no_subcommands: false, + no_color: false, + brief: false, + } + } +} + +fn generate_doc(name: &str, context: &EvaluationContext) -> (Vec, Vec) { + let mut cols = vec![]; + let mut vals = vec![]; + + let engine_state = context.engine_state.borrow(); + + let command = engine_state + .find_decl(name.as_bytes()) + .map(|decl_id| engine_state.get_decl(decl_id)) + .unwrap_or_else(|| panic!("Expected command '{}' from names to be in registry", name)); + + cols.push("name".to_string()); + vals.push(Value::String { + val: name.into(), + span: Span::unknown(), + }); + + cols.push("usage".to_string()); + vals.push(Value::String { + val: command.usage().to_owned(), + span: Span::unknown(), + }); + + if let Some(link) = retrieve_doc_link(name) { + cols.push("doc_link".into()); + vals.push(Value::String { + val: link, + span: Span::unknown(), + }); + } + + cols.push("documentation".to_owned()); + vals.push(Value::String { + val: get_documentation( + &command.signature(), + &command.examples(), + context, + &DocumentationConfig { + no_subcommands: true, + no_color: true, + brief: false, + }, + ), + span: Span::unknown(), + }); + + (cols, vals) +} + +// generate_docs gets the documentation from each command and returns a Table as output +pub fn generate_docs(context: &EvaluationContext) -> Value { + let signatures = context.get_signatures(); + + // cmap will map parent commands to it's subcommands e.g. to -> [to csv, to yaml, to bson] + let mut cmap: HashMap> = HashMap::new(); + for sig in &signatures { + if sig.name.contains(' ') { + let mut split_name = sig.name.split_whitespace(); + let parent_name = split_name.next().expect("Expected a parent command name"); + if cmap.contains_key(parent_name) { + let sub_names = cmap + .get_mut(parent_name) + .expect("Expected an entry for parent"); + sub_names.push(sig.name.to_owned()); + } + } else { + cmap.insert(sig.name.to_owned(), Vec::new()); + }; + } + // Return documentation for each command + // Sub-commands are nested under their respective parent commands + let mut table = Vec::new(); + for sig in &signatures { + // Must be a sub-command, skip since it's being handled underneath when we hit the parent command + if !cmap.contains_key(&sig.name) { + continue; + } + let mut row_entries = generate_doc(&sig.name, context); + // Iterate over all the subcommands of the parent command + let mut sub_table = Vec::new(); + for sub_name in cmap.get(&sig.name).unwrap_or(&Vec::new()) { + let (cols, vals) = generate_doc(sub_name, context); + sub_table.push(Value::Record { + cols, + vals, + span: Span::unknown(), + }); + } + + if !sub_table.is_empty() { + row_entries.0.push("subcommands".into()); + row_entries.1.push(Value::List { + vals: sub_table, + span: Span::unknown(), + }); + } + table.push(Value::Record { + cols: row_entries.0, + vals: row_entries.1, + span: Span::unknown(), + }); + } + Value::List { + vals: table, + span: Span::unknown(), + } +} + +fn retrieve_doc_link(name: &str) -> Option { + let doc_name = name.split_whitespace().join("_"); // Because .replace(" ", "_") didn't work + let mut entries = + std::fs::read_dir(COMMANDS_DOCS_DIR).expect("Directory for command docs are missing!"); + entries.find_map(|r| { + r.map_or(None, |de| { + if de.file_name().to_string_lossy() == format!("{}.{}", &doc_name, "md") { + Some(format!("/commands/{}.{}", &doc_name, "html")) + } else { + None + } + }) + }) +} + +#[allow(clippy::cognitive_complexity)] +pub fn get_documentation( + sig: &Signature, + examples: &[Example], + context: &EvaluationContext, + config: &DocumentationConfig, +) -> String { + let cmd_name = &sig.name; + let mut long_desc = String::new(); + + let usage = &sig.usage; + if !usage.is_empty() { + long_desc.push_str(usage); + long_desc.push_str("\n\n"); + } + + let extra_usage = if config.brief { "" } else { &sig.extra_usage }; + if !extra_usage.is_empty() { + long_desc.push_str(extra_usage); + long_desc.push_str("\n\n"); + } + + let mut subcommands = vec![]; + if !config.no_subcommands { + let signatures = context.get_signatures(); + for sig in signatures { + if sig.name.starts_with(&format!("{} ", cmd_name)) { + subcommands.push(format!(" {} - {}", sig.name, sig.usage)); + } + } + } + + let mut one_liner = String::new(); + one_liner.push_str(&sig.name); + one_liner.push(' '); + + for positional in &sig.required_positional { + one_liner.push_str(&format!("<{}> ", positional.name)); + } + for positional in &sig.optional_positional { + one_liner.push_str(&format!("({}) ", positional.name)); + } + + if sig.rest_positional.is_some() { + one_liner.push_str("...args "); + } + + if !subcommands.is_empty() { + one_liner.push_str(" "); + } + + if !sig.named.is_empty() { + one_liner.push_str("{flags} "); + } + + long_desc.push_str(&format!("Usage:\n > {}\n", one_liner)); + + if !subcommands.is_empty() { + long_desc.push_str("\nSubcommands:\n"); + subcommands.sort(); + long_desc.push_str(&subcommands.join("\n")); + long_desc.push('\n'); + } + + if !sig.required_positional.is_empty() + || !sig.optional_positional.is_empty() + || sig.rest_positional.is_some() + { + long_desc.push_str("\nParameters:\n"); + for positional in &sig.required_positional { + long_desc.push_str(&format!(" <{}> {}\n", positional.name, positional.desc)); + } + for positional in &sig.optional_positional { + long_desc.push_str(&format!(" ({}) {}\n", positional.name, positional.desc)); + } + + if let Some(rest_positional) = &sig.rest_positional { + long_desc.push_str(&format!(" ...args: {}\n", rest_positional.desc)); + } + } + if !sig.named.is_empty() { + long_desc.push_str(&get_flags_section(sig)) + } + + if !examples.is_empty() { + long_desc.push_str("\nExamples:"); + } + for example in examples { + long_desc.push('\n'); + long_desc.push_str(" "); + long_desc.push_str(example.description); + + // if config.no_color { + long_desc.push_str(&format!("\n > {}\n", example.example)); + // } else { + // let colored_example = + + // crate::shell::painter::Painter::paint_string(example.example, scope, &palette); + // long_desc.push_str(&format!("\n > {}\n", colored_example)); + // } + } + + long_desc.push('\n'); + + long_desc +} + +fn get_flags_section(signature: &Signature) -> String { + let mut long_desc = String::new(); + long_desc.push_str("\nFlags:\n"); + for flag in &signature.named { + let msg = if let Some(arg) = &flag.arg { + if let Some(short) = flag.short { + if flag.required { + format!( + " -{}, --{} (required parameter){:?} {}\n", + short, flag.long, arg, flag.desc + ) + } else { + format!(" -{}, --{} {:?} {}\n", short, flag.long, arg, flag.desc) + } + } else if flag.required { + format!( + " --{} (required parameter){:?} {}\n", + flag.long, arg, flag.desc + ) + } else { + format!(" --{} {:?} {}\n", flag.long, arg, flag.desc) + } + } else if let Some(short) = flag.short { + if flag.required { + format!( + " -{}, --{} (required parameter) {}\n", + short, flag.long, flag.desc + ) + } else { + format!(" -{}, --{} {}\n", short, flag.long, flag.desc) + } + } else if flag.required { + format!(" --{} (required parameter) {}\n", flag.long, flag.desc) + } else { + format!(" --{} {}\n", flag.long, flag.desc) + }; + long_desc.push_str(&msg); + } + long_desc +} + +pub fn get_brief_help( + sig: &Signature, + examples: &[Example], + context: &EvaluationContext, +) -> String { + get_documentation( + sig, + examples, + context, + &DocumentationConfig { + no_subcommands: false, + no_color: false, + brief: true, + }, + ) +} + +pub fn get_full_help(sig: &Signature, examples: &[Example], context: &EvaluationContext) -> String { + get_documentation(sig, examples, context, &DocumentationConfig::default()) +} diff --git a/crates/nu-engine/src/lib.rs b/crates/nu-engine/src/lib.rs index 2cbbbee638..cc7e7a1236 100644 --- a/crates/nu-engine/src/lib.rs +++ b/crates/nu-engine/src/lib.rs @@ -1,7 +1,9 @@ mod call_ext; +mod documentation; mod eval; mod from_value; pub use call_ext::CallExt; +pub use documentation::{generate_docs, get_brief_help, get_documentation, get_full_help}; pub use eval::{eval_block, eval_expression, eval_operator}; pub use from_value::FromValue; diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 3d8f2c8229..c93c0ace7d 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -1,5 +1,5 @@ use super::Command; -use crate::{ast::Block, BlockId, DeclId, Signature, Span, Type, VarId}; +use crate::{ast::Block, BlockId, DeclId, Example, Signature, Span, Type, VarId}; use core::panic; use std::{ collections::{HashMap, HashSet}, @@ -178,7 +178,7 @@ impl EngineState { .expect("internal error: missing declaration") } - pub fn get_decls(&self) -> Vec { + pub fn get_signatures(&self) -> Vec { let mut output = vec![]; for decl in self.decls.iter() { if decl.get_block_id().is_none() { @@ -193,6 +193,21 @@ impl EngineState { output } + pub fn get_signatures_with_examples(&self) -> Vec<(Signature, Vec)> { + let mut output = vec![]; + for decl in self.decls.iter() { + if decl.get_block_id().is_none() { + let mut signature = (*decl).signature(); + signature.usage = decl.usage().to_string(); + signature.extra_usage = decl.extra_usage().to_string(); + + output.push((signature, decl.examples())); + } + } + + output + } + pub fn get_block(&self, block_id: BlockId) -> &Block { self.blocks .get(block_id) diff --git a/crates/nu-protocol/src/engine/evaluation_context.rs b/crates/nu-protocol/src/engine/evaluation_context.rs index 721c21c811..07a71ff91a 100644 --- a/crates/nu-protocol/src/engine/evaluation_context.rs +++ b/crates/nu-protocol/src/engine/evaluation_context.rs @@ -1,7 +1,7 @@ use super::EngineState; use std::{cell::RefCell, collections::HashMap, rc::Rc}; -use crate::{ShellError, Signature, Value, VarId}; +use crate::{Example, ShellError, Signature, Value, VarId}; #[derive(Clone)] pub struct EvaluationContext { @@ -47,8 +47,12 @@ impl EvaluationContext { self.stack.print_stack(); } - pub fn get_commands_info(&self) -> Vec { - self.engine_state.borrow().get_decls() + pub fn get_signatures(&self) -> Vec { + self.engine_state.borrow().get_signatures() + } + + pub fn get_signatures_with_examples(&self) -> Vec<(Signature, Vec)> { + self.engine_state.borrow().get_signatures_with_examples() } } diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 4be91303d0..87f15fc3b1 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -19,6 +19,16 @@ pub enum ShellError { rhs_span: Span, }, + #[error("Pipeline mismatch.")] + #[diagnostic(code(nu::shell::pipeline_mismatch), url(docsrs))] + PipelineMismatch { + expected: Type, + #[label("expected: {expected}")] + expected_span: Span, + #[label("value originates from here")] + origin: Span, + }, + #[error("Unsupported operator: {0}.")] #[diagnostic(code(nu::shell::unsupported_operator), url(docsrs))] UnsupportedOperator(Operator, #[label = "unsupported operator"] Span), @@ -79,6 +89,10 @@ pub enum ShellError { #[diagnostic(code(nu::shell::unsupported_input), url(docsrs))] UnsupportedInput(String, #[label("{0}")] Span), + #[error("Command not found")] + #[diagnostic(code(nu::shell::command_not_found), url(docsrs))] + CommandNotFound(#[label("command not found")] Span), + #[error("Flag not found")] #[diagnostic(code(nu::shell::flag_not_found), url(docsrs))] FlagNotFound(String, #[label("{0} not found")] Span), From 60f9fe1aa4b3f0e8a69b162b75e2c0a921295d69 Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Oct 2021 15:41:39 +1300 Subject: [PATCH 22/24] Port split column and split row --- crates/nu-command/src/default_context.rs | 2 + crates/nu-command/src/strings/split/chars.rs | 78 +++++++------------- crates/nu-command/src/strings/split/mod.rs | 8 +- 3 files changed, 32 insertions(+), 56 deletions(-) diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 9eb6c1c0bc..8b28172a0a 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -43,6 +43,8 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(Select)); working_set.add_decl(Box::new(Split)); working_set.add_decl(Box::new(SplitChars)); + working_set.add_decl(Box::new(SplitColumn)); + working_set.add_decl(Box::new(SplitRow)); working_set.add_decl(Box::new(Sys)); working_set.add_decl(Box::new(Table)); working_set.add_decl(Box::new(Touch)); diff --git a/crates/nu-command/src/strings/split/chars.rs b/crates/nu-command/src/strings/split/chars.rs index f77f873692..108190c6aa 100644 --- a/crates/nu-command/src/strings/split/chars.rs +++ b/crates/nu-command/src/strings/split/chars.rs @@ -65,69 +65,21 @@ fn split_chars(call: &Call, input: Value) -> Result Value::List { vals: vals .iter() - .flat_map(move |v| { - if let Ok(s) = v.as_string() { - let v_span = v.span(); - s.chars() - .collect::>() - .into_iter() - .map(move |x| Value::String { - val: x.to_string(), - span: v_span, - }) - .collect() - } else { - vec![Value::Error { - error: ShellError::PipelineMismatch { - expected: Type::String, - expected_span: name, - origin: v.span(), - }, - }] - } - }) + .flat_map(|x| split_chars_helper(x, name)) .collect(), span, }, Value::Stream { stream, span } => Value::Stream { stream: stream - .flat_map(move |v| { - if let Ok(s) = v.as_string() { - let v_span = v.span(); - s.chars() - .collect::>() - .into_iter() - .map(move |x| Value::String { - val: x.to_string(), - span: v_span, - }) - .collect() - } else { - vec![Value::Error { - error: ShellError::PipelineMismatch { - expected: Type::String, - expected_span: name, - origin: v.span(), - }, - }] - } - }) + .flat_map(move |x| split_chars_helper(&x, name)) .into_value_stream(), span, }, v => { let v_span = v.span(); - if let Ok(s) = v.as_string() { + if v.as_string().is_ok() { Value::List { - vals: s - .chars() - .collect::>() - .into_iter() - .map(move |x| Value::String { - val: x.to_string(), - span: v_span, - }) - .collect(), + vals: split_chars_helper(&v, name), span: v_span, } } else { @@ -143,6 +95,28 @@ fn split_chars(call: &Call, input: Value) -> Result Vec { + if let Ok(s) = v.as_string() { + let v_span = v.span(); + s.chars() + .collect::>() + .into_iter() + .map(move |x| Value::String { + val: x.to_string(), + span: v_span, + }) + .collect() + } else { + vec![Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: name, + origin: v.span(), + }, + }] + } +} + // #[cfg(test)] // mod tests { // use super::ShellError; diff --git a/crates/nu-command/src/strings/split/mod.rs b/crates/nu-command/src/strings/split/mod.rs index 3a65f0e124..c6e6da0171 100644 --- a/crates/nu-command/src/strings/split/mod.rs +++ b/crates/nu-command/src/strings/split/mod.rs @@ -1,9 +1,9 @@ pub mod chars; -// pub mod column; +pub mod column; pub mod command; -// pub mod row; +pub mod row; pub use chars::SubCommand as SplitChars; -// pub use column::SubCommand as SplitColumn; +pub use column::SubCommand as SplitColumn; pub use command::SplitCommand as Split; -// pub use row::SubCommand as SplitRow; +pub use row::SubCommand as SplitRow; From 5c29a83a7ace824789a79137cf5fbdad116ecf6e Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Oct 2021 15:45:25 +1300 Subject: [PATCH 23/24] Add tests --- crates/nu-command/src/strings/split/column.rs | 164 ++++++++++++++++++ crates/nu-command/src/strings/split/row.rs | 116 +++++++++++++ src/tests.rs | 13 ++ 3 files changed, 293 insertions(+) create mode 100644 crates/nu-command/src/strings/split/column.rs create mode 100644 crates/nu-command/src/strings/split/row.rs diff --git a/crates/nu-command/src/strings/split/column.rs b/crates/nu-command/src/strings/split/column.rs new file mode 100644 index 0000000000..fb3836ee3e --- /dev/null +++ b/crates/nu-command/src/strings/split/column.rs @@ -0,0 +1,164 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EvaluationContext}, + IntoValueStream, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, +}; + +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "split column" + } + + fn signature(&self) -> Signature { + Signature::build("split column") + .required( + "separator", + SyntaxShape::String, + "the character that denotes what separates columns", + ) + .switch("collapse-empty", "remove empty columns", Some('c')) + .rest( + "rest", + SyntaxShape::String, + "column names to give the new columns", + ) + } + + fn usage(&self) -> &str { + "splits contents across multiple columns via the separator." + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + input: Value, + ) -> Result { + split_column(context, call, input) + } +} + +fn split_column( + context: &EvaluationContext, + call: &Call, + input: Value, +) -> Result { + let name_span = call.head; + let separator: Spanned = call.req(context, 0)?; + let rest: Vec> = call.rest(context, 1)?; + let collapse_empty = call.has_flag("collapse-empty"); + + Ok(match input { + Value::List { vals, span } => Value::List { + vals: vals + .iter() + .map(move |x| split_column_helper(x, &separator, &rest, collapse_empty, name_span)) + .collect(), + span, + }, + Value::Stream { stream, span } => Value::Stream { + stream: stream + .map(move |x| split_column_helper(&x, &separator, &rest, collapse_empty, name_span)) + .into_value_stream(), + span, + }, + v => { + if v.as_string().is_ok() { + Value::List { + vals: vec![split_column_helper( + &v, + &separator, + &rest, + collapse_empty, + name_span, + )], + span: call.head, + } + } else { + Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: call.head, + origin: v.span(), + }, + } + } + } + }) +} + +fn split_column_helper( + v: &Value, + separator: &Spanned, + rest: &[Spanned], + collapse_empty: bool, + head: Span, +) -> Value { + if let Ok(s) = v.as_string() { + let splitter = separator.item.replace("\\n", "\n"); + + let split_result: Vec<_> = if collapse_empty { + s.split(&splitter).filter(|s| !s.is_empty()).collect() + } else { + s.split(&splitter).collect() + }; + + let positional: Vec<_> = rest.iter().map(|f| f.item.clone()).collect(); + + // If they didn't provide column names, make up our own + + let mut cols = vec![]; + let mut vals = vec![]; + if positional.is_empty() { + let mut gen_columns = vec![]; + for i in 0..split_result.len() { + gen_columns.push(format!("Column{}", i + 1)); + } + + for (&k, v) in split_result.iter().zip(&gen_columns) { + cols.push(v.to_string()); + vals.push(Value::String { + val: k.into(), + span: head, + }); + } + } else { + for (&k, v) in split_result.iter().zip(&positional) { + cols.push(v.into()); + vals.push(Value::String { + val: k.into(), + span: head, + }) + } + } + Value::Record { + cols, + vals, + span: head, + } + } else { + Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: head, + origin: v.span(), + }, + } + } +} + +// #[cfg(test)] +// mod tests { +// use super::ShellError; +// use super::SubCommand; + +// #[test] +// fn examples_work_as_expected() -> Result<(), ShellError> { +// use crate::examples::test as test_examples; + +// test_examples(SubCommand {}) +// } +// } diff --git a/crates/nu-command/src/strings/split/row.rs b/crates/nu-command/src/strings/split/row.rs new file mode 100644 index 0000000000..a4602e2ca4 --- /dev/null +++ b/crates/nu-command/src/strings/split/row.rs @@ -0,0 +1,116 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EvaluationContext}, + IntoValueStream, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, +}; + +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "split row" + } + + fn signature(&self) -> Signature { + Signature::build("split row").required( + "separator", + SyntaxShape::String, + "the character that denotes what separates rows", + ) + } + + fn usage(&self) -> &str { + "splits contents over multiple rows via the separator." + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + input: Value, + ) -> Result { + split_row(context, call, input) + } +} + +fn split_row( + context: &EvaluationContext, + call: &Call, + input: Value, +) -> Result { + let name_span = call.head; + let separator: Spanned = call.req(context, 0)?; + + Ok(match input { + Value::List { vals, span } => Value::List { + vals: vals + .iter() + .flat_map(move |x| split_row_helper(x, &separator, name_span)) + .collect(), + span, + }, + Value::Stream { stream, span } => Value::Stream { + stream: stream + .flat_map(move |x| split_row_helper(&x, &separator, name_span)) + .into_value_stream(), + span, + }, + v => { + let v_span = v.span(); + if v.as_string().is_ok() { + Value::List { + vals: split_row_helper(&v, &separator, name_span), + span: v_span, + } + } else { + Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: call.head, + origin: v.span(), + }, + } + } + } + }) +} + +fn split_row_helper(v: &Value, separator: &Spanned, name: Span) -> Vec { + if let Ok(s) = v.as_string() { + let splitter = separator.item.replace("\\n", "\n"); + s.split(&splitter) + .filter_map(|s| { + if s.trim() != "" { + Some(Value::String { + val: s.into(), + span: v.span(), + }) + } else { + None + } + }) + .collect() + } else { + vec![Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: name, + origin: v.span(), + }, + }] + } +} + +// #[cfg(test)] +// mod tests { +// use super::ShellError; +// use super::SubCommand; + +// #[test] +// fn examples_work_as_expected() -> Result<(), ShellError> { +// use crate::examples::test as test_examples; + +// test_examples(SubCommand {}) +// } +// } diff --git a/src/tests.rs b/src/tests.rs index 0efd0b513a..b3921acbf4 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -540,3 +540,16 @@ fn string_cell_path() -> TestResult { "c", ) } + +#[test] +fn split_row() -> TestResult { + run_test(r#""hello world" | split row " " | get 1"#, "world") +} + +#[test] +fn split_column() -> TestResult { + run_test( + r#""hello world" | split column " " | get "Column1".0"#, + "hello", + ) +} From 2cd1f634d04b8a9cecaae423948a0083fa6ca0b4 Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Oct 2021 19:20:32 +1300 Subject: [PATCH 24/24] Add map and flat_map to value --- crates/nu-command/src/strings/split/chars.rs | 37 +--------- crates/nu-command/src/strings/split/column.rs | 42 ++---------- crates/nu-command/src/strings/split/row.rs | 37 ++-------- crates/nu-protocol/src/value/mod.rs | 67 +++++++++++++++++++ 4 files changed, 78 insertions(+), 105 deletions(-) diff --git a/crates/nu-command/src/strings/split/chars.rs b/crates/nu-command/src/strings/split/chars.rs index 108190c6aa..39df74ab9f 100644 --- a/crates/nu-command/src/strings/split/chars.rs +++ b/crates/nu-command/src/strings/split/chars.rs @@ -1,7 +1,7 @@ use nu_protocol::{ ast::Call, engine::{Command, EvaluationContext}, - Example, IntoValueStream, ShellError, Signature, Span, Type, Value, + Example, ShellError, Signature, Span, Type, Value, }; pub struct SubCommand; @@ -59,40 +59,9 @@ impl Command for SubCommand { } fn split_chars(call: &Call, input: Value) -> Result { - let name = call.head; + let span = call.head; - Ok(match input { - Value::List { vals, span } => Value::List { - vals: vals - .iter() - .flat_map(|x| split_chars_helper(x, name)) - .collect(), - span, - }, - Value::Stream { stream, span } => Value::Stream { - stream: stream - .flat_map(move |x| split_chars_helper(&x, name)) - .into_value_stream(), - span, - }, - v => { - let v_span = v.span(); - if v.as_string().is_ok() { - Value::List { - vals: split_chars_helper(&v, name), - span: v_span, - } - } else { - Value::Error { - error: ShellError::PipelineMismatch { - expected: Type::String, - expected_span: name, - origin: v.span(), - }, - } - } - } - }) + Ok(input.flat_map(span, move |x| split_chars_helper(&x, span))) } fn split_chars_helper(v: &Value, name: Span) -> Vec { diff --git a/crates/nu-command/src/strings/split/column.rs b/crates/nu-command/src/strings/split/column.rs index fb3836ee3e..b8efed3775 100644 --- a/crates/nu-command/src/strings/split/column.rs +++ b/crates/nu-command/src/strings/split/column.rs @@ -2,7 +2,7 @@ use nu_engine::CallExt; use nu_protocol::{ ast::Call, engine::{Command, EvaluationContext}, - IntoValueStream, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, + ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; pub struct SubCommand; @@ -51,43 +51,9 @@ fn split_column( let rest: Vec> = call.rest(context, 1)?; let collapse_empty = call.has_flag("collapse-empty"); - Ok(match input { - Value::List { vals, span } => Value::List { - vals: vals - .iter() - .map(move |x| split_column_helper(x, &separator, &rest, collapse_empty, name_span)) - .collect(), - span, - }, - Value::Stream { stream, span } => Value::Stream { - stream: stream - .map(move |x| split_column_helper(&x, &separator, &rest, collapse_empty, name_span)) - .into_value_stream(), - span, - }, - v => { - if v.as_string().is_ok() { - Value::List { - vals: vec![split_column_helper( - &v, - &separator, - &rest, - collapse_empty, - name_span, - )], - span: call.head, - } - } else { - Value::Error { - error: ShellError::PipelineMismatch { - expected: Type::String, - expected_span: call.head, - origin: v.span(), - }, - } - } - } - }) + Ok(input.map(name_span, move |x| { + split_column_helper(&x, &separator, &rest, collapse_empty, name_span) + })) } fn split_column_helper( diff --git a/crates/nu-command/src/strings/split/row.rs b/crates/nu-command/src/strings/split/row.rs index a4602e2ca4..4467598536 100644 --- a/crates/nu-command/src/strings/split/row.rs +++ b/crates/nu-command/src/strings/split/row.rs @@ -2,7 +2,7 @@ use nu_engine::CallExt; use nu_protocol::{ ast::Call, engine::{Command, EvaluationContext}, - IntoValueStream, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, + ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; pub struct SubCommand; @@ -42,38 +42,9 @@ fn split_row( let name_span = call.head; let separator: Spanned = call.req(context, 0)?; - Ok(match input { - Value::List { vals, span } => Value::List { - vals: vals - .iter() - .flat_map(move |x| split_row_helper(x, &separator, name_span)) - .collect(), - span, - }, - Value::Stream { stream, span } => Value::Stream { - stream: stream - .flat_map(move |x| split_row_helper(&x, &separator, name_span)) - .into_value_stream(), - span, - }, - v => { - let v_span = v.span(); - if v.as_string().is_ok() { - Value::List { - vals: split_row_helper(&v, &separator, name_span), - span: v_span, - } - } else { - Value::Error { - error: ShellError::PipelineMismatch { - expected: Type::String, - expected_span: call.head, - origin: v.span(), - }, - } - } - } - }) + Ok(input.flat_map(name_span, move |x| { + split_row_helper(&x, &separator, name_span) + })) } fn split_row_helper(v: &Value, separator: &Spanned, name: Span) -> Vec { diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index afc1f90161..b003be1678 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -364,6 +364,73 @@ impl Value { _ => vec![], } } + + pub fn map(self, span: Span, mut f: F) -> Value + where + Self: Sized, + F: FnMut(Self) -> Value + 'static, + { + match self { + Value::List { vals, .. } => Value::List { + vals: vals.into_iter().map(f).collect(), + span, + }, + Value::Stream { stream, .. } => Value::Stream { + stream: stream.map(f).into_value_stream(), + span, + }, + v => { + if v.as_string().is_ok() { + Value::List { + vals: vec![f(v)], + span, + } + } else { + Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: span, + origin: v.span(), + }, + } + } + } + } + } + + pub fn flat_map(self, span: Span, mut f: F) -> Value + where + Self: Sized, + U: IntoIterator, + F: FnMut(Self) -> U + 'static, + { + match self { + Value::List { vals, .. } => Value::List { + vals: vals.into_iter().map(f).flatten().collect(), + span, + }, + Value::Stream { stream, .. } => Value::Stream { + stream: stream.map(f).flatten().into_value_stream(), + span, + }, + v => { + if v.as_string().is_ok() { + Value::List { + vals: f(v).into_iter().collect(), + span, + } + } else { + Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: span, + origin: v.span(), + }, + } + } + } + } + } } impl PartialEq for Value {