diff --git a/README.md b/README.md index 5baa3e5ce0..1e804d22c1 100644 --- a/README.md +++ b/README.md @@ -281,7 +281,7 @@ Nu adheres closely to a set of goals that make up its design philosophy. As feat | inc (column-or-column-path) | Increment a value or version. Optionally use the column of a table | | insert column-or-column-path value | Insert a new column to the table | | last amount | Show only the last number of rows | -| nth row-number | Return only the selected row | +| nth ...row-numbers | Return only the selected rows | | pick ...columns | Down-select table to only these columns | | pivot --header-row | Pivot the tables, making columns into rows and vice versa | | prepend row-data | Prepend a row to the beginning of the table | diff --git a/src/commands/classified.rs b/src/commands/classified.rs deleted file mode 100644 index da857135d4..0000000000 --- a/src/commands/classified.rs +++ /dev/null @@ -1,513 +0,0 @@ -use crate::parser::{hir, TokenNode}; -use crate::prelude::*; -use bytes::{BufMut, BytesMut}; -use derive_new::new; -use futures::stream::StreamExt; -use futures_codec::{Decoder, Encoder, Framed}; -use log::{log_enabled, trace}; -use nu_source::PrettyDebug; -use std::io::{Error, ErrorKind}; -use subprocess::Exec; - -/// A simple `Codec` implementation that splits up data into lines. -pub struct LinesCodec {} - -impl Encoder for LinesCodec { - type Item = String; - type Error = Error; - - fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> { - dst.put(item); - Ok(()) - } -} - -impl Decoder for LinesCodec { - type Item = String; - type Error = Error; - - fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - match src.iter().position(|b| b == &b'\n') { - Some(pos) if !src.is_empty() => { - let buf = src.split_to(pos + 1); - String::from_utf8(buf.to_vec()) - .map(Some) - .map_err(|e| Error::new(ErrorKind::InvalidData, e)) - } - _ if !src.is_empty() => { - let drained = src.take(); - String::from_utf8(drained.to_vec()) - .map(Some) - .map_err(|e| Error::new(ErrorKind::InvalidData, e)) - } - _ => Ok(None), - } - } -} - -pub(crate) struct ClassifiedInputStream { - pub(crate) objects: InputStream, - pub(crate) stdin: Option, -} - -impl ClassifiedInputStream { - pub(crate) fn new() -> ClassifiedInputStream { - ClassifiedInputStream { - objects: vec![UntaggedValue::nothing().into_value(Tag::unknown())].into(), - stdin: None, - } - } - - pub(crate) fn from_input_stream(stream: impl Into) -> ClassifiedInputStream { - ClassifiedInputStream { - objects: stream.into(), - stdin: None, - } - } - - pub(crate) fn from_stdout(stdout: std::fs::File) -> ClassifiedInputStream { - ClassifiedInputStream { - objects: VecDeque::new().into(), - stdin: Some(stdout), - } - } -} - -#[derive(Debug, Clone)] -pub struct Commands { - pub list: Vec, - pub span: Span, -} - -impl std::ops::Deref for Commands { - type Target = [ClassifiedCommand]; - - fn deref(&self) -> &Self::Target { - &self.list - } -} - -#[derive(Debug, Clone)] -pub(crate) struct ClassifiedPipeline { - pub commands: Commands, -} - -impl ClassifiedPipeline { - pub fn commands(list: Vec, span: impl Into) -> ClassifiedPipeline { - ClassifiedPipeline { - commands: Commands { - list, - span: span.into(), - }, - } - } -} - -impl PrettyDebugWithSource for ClassifiedPipeline { - fn pretty_debug(&self, source: &str) -> DebugDocBuilder { - b::intersperse( - self.commands.iter().map(|c| c.pretty_debug(source)), - b::operator(" | "), - ) - .or(b::delimit("<", b::description("empty pipeline"), ">")) - } -} - -impl HasSpan for ClassifiedPipeline { - fn span(&self) -> Span { - self.commands.span - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum ClassifiedCommand { - #[allow(unused)] - Expr(TokenNode), - #[allow(unused)] - Dynamic(hir::Call), - Internal(InternalCommand), - External(ExternalCommand), -} - -impl PrettyDebugWithSource for ClassifiedCommand { - fn pretty_debug(&self, source: &str) -> DebugDocBuilder { - match self { - ClassifiedCommand::Expr(token) => b::typed("command", token.pretty_debug(source)), - ClassifiedCommand::Dynamic(call) => b::typed("command", call.pretty_debug(source)), - ClassifiedCommand::Internal(internal) => internal.pretty_debug(source), - ClassifiedCommand::External(external) => external.pretty_debug(source), - } - } -} - -impl HasSpan for ClassifiedCommand { - fn span(&self) -> Span { - match self { - ClassifiedCommand::Expr(node) => node.span(), - ClassifiedCommand::Internal(command) => command.span(), - ClassifiedCommand::Dynamic(call) => call.span, - ClassifiedCommand::External(command) => command.span(), - } - } -} - -#[derive(new, Debug, Clone, Eq, PartialEq)] -pub struct InternalCommand { - pub(crate) name: String, - pub(crate) name_tag: Tag, - pub(crate) args: hir::Call, -} - -impl PrettyDebugWithSource for InternalCommand { - fn pretty_debug(&self, source: &str) -> DebugDocBuilder { - b::typed( - "internal command", - b::description(&self.name) + b::space() + self.args.pretty_debug(source), - ) - } -} - -impl HasSpan for InternalCommand { - fn span(&self) -> Span { - let start = self.name_tag.span; - - start.until(self.args.span) - } -} - -#[derive(new, Debug, Eq, PartialEq)] -pub(crate) struct DynamicCommand { - pub(crate) args: hir::Call, -} - -impl InternalCommand { - pub(crate) fn run( - self, - context: &mut Context, - input: ClassifiedInputStream, - source: Text, - ) -> Result { - if log_enabled!(log::Level::Trace) { - trace!(target: "nu::run::internal", "->"); - trace!(target: "nu::run::internal", "{}", self.name); - trace!(target: "nu::run::internal", "{}", self.args.debug(&source)); - } - - let objects: InputStream = - trace_stream!(target: "nu::trace_stream::internal", "input" = input.objects); - - let command = context.expect_command(&self.name); - - let result = - { context.run_command(command, self.name_tag.clone(), self.args, &source, objects) }; - - let result = trace_out_stream!(target: "nu::trace_stream::internal", "output" = result); - let mut result = result.values; - let mut context = context.clone(); - - let stream = async_stream! { - let mut soft_errs: Vec = vec![]; - let mut yielded = false; - - while let Some(item) = result.next().await { - match item { - Ok(ReturnSuccess::Action(action)) => match action { - CommandAction::ChangePath(path) => { - context.shell_manager.set_path(path); - } - CommandAction::Exit => std::process::exit(0), // TODO: save history.txt - CommandAction::Error(err) => { - context.error(err); - break; - } - CommandAction::EnterHelpShell(value) => { - match value { - Value { - value: UntaggedValue::Primitive(Primitive::String(cmd)), - tag, - } => { - context.shell_manager.insert_at_current(Box::new( - HelpShell::for_command( - UntaggedValue::string(cmd).into_value(tag), - &context.registry(), - ).unwrap(), - )); - } - _ => { - context.shell_manager.insert_at_current(Box::new( - HelpShell::index(&context.registry()).unwrap(), - )); - } - } - } - CommandAction::EnterValueShell(value) => { - context - .shell_manager - .insert_at_current(Box::new(ValueShell::new(value))); - } - CommandAction::EnterShell(location) => { - context.shell_manager.insert_at_current(Box::new( - FilesystemShell::with_location(location, context.registry().clone()).unwrap(), - )); - } - CommandAction::PreviousShell => { - context.shell_manager.prev(); - } - CommandAction::NextShell => { - context.shell_manager.next(); - } - CommandAction::LeaveShell => { - context.shell_manager.remove_at_current(); - if context.shell_manager.is_empty() { - std::process::exit(0); // TODO: save history.txt - } - } - }, - - Ok(ReturnSuccess::Value(v)) => { - yielded = true; - yield Ok(v); - } - - Ok(ReturnSuccess::DebugValue(v)) => { - yielded = true; - - let doc = PrettyDebug::pretty_doc(&v); - let mut buffer = termcolor::Buffer::ansi(); - - doc.render_raw( - context.with_host(|host| host.width() - 5), - &mut crate::parser::debug::TermColored::new(&mut buffer), - ).unwrap(); - - let value = String::from_utf8_lossy(buffer.as_slice()); - - yield Ok(UntaggedValue::string(value).into_untagged_value()) - } - - Err(err) => { - context.error(err); - break; - } - } - } - }; - - Ok(stream.to_input_stream()) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ExternalArg { - pub arg: String, - pub tag: Tag, -} - -impl std::ops::Deref for ExternalArg { - type Target = str; - - fn deref(&self) -> &str { - &self.arg - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ExternalArgs { - pub list: Vec, - pub span: Span, -} - -impl ExternalArgs { - pub fn iter(&self) -> impl Iterator { - self.list.iter() - } -} - -impl std::ops::Deref for ExternalArgs { - type Target = [ExternalArg]; - - fn deref(&self) -> &[ExternalArg] { - &self.list - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ExternalCommand { - pub(crate) name: String, - - pub(crate) name_tag: Tag, - pub(crate) args: ExternalArgs, -} - -impl PrettyDebug for ExternalCommand { - fn pretty(&self) -> DebugDocBuilder { - b::typed( - "external command", - b::description(&self.name) - + b::preceded( - b::space(), - b::intersperse( - self.args.iter().map(|a| b::primitive(format!("{}", a.arg))), - b::space(), - ), - ), - ) - } -} - -impl HasSpan for ExternalCommand { - fn span(&self) -> Span { - self.name_tag.span.until(self.args.span) - } -} - -#[derive(Debug)] -pub(crate) enum StreamNext { - Last, - External, - Internal, -} - -impl ExternalCommand { - pub(crate) async fn run( - self, - context: &mut Context, - input: ClassifiedInputStream, - stream_next: StreamNext, - ) -> Result { - let stdin = input.stdin; - let inputs: Vec = input.objects.into_vec().await; - - trace!(target: "nu::run::external", "-> {}", self.name); - trace!(target: "nu::run::external", "inputs = {:?}", inputs); - - let mut arg_string = format!("{}", self.name); - for arg in self.args.iter() { - arg_string.push_str(&arg); - } - - trace!(target: "nu::run::external", "command = {:?}", self.name); - - let mut process; - if arg_string.contains("$it") { - let input_strings = inputs - .iter() - .map(|i| { - i.as_string().map(|s| s.to_string()).map_err(|_| { - let arg = self.args.iter().find(|arg| arg.contains("$it")); - if let Some(arg) = arg { - ShellError::labeled_error( - "External $it needs string data", - "given row instead of string data", - &arg.tag, - ) - } else { - ShellError::labeled_error( - "$it needs string data", - "given something else", - self.name_tag.clone(), - ) - } - }) - }) - .collect::, ShellError>>()?; - - let commands = input_strings.iter().map(|i| { - let args = self.args.iter().filter_map(|arg| { - if arg.chars().all(|c| c.is_whitespace()) { - None - } else { - Some(arg.replace("$it", &i)) - } - }); - - format!("{} {}", self.name, itertools::join(args, " ")) - }); - - process = Exec::shell(itertools::join(commands, " && ")) - } else { - process = Exec::cmd(&self.name); - for arg in self.args.iter() { - let arg_chars: Vec<_> = arg.chars().collect(); - if arg_chars.len() > 1 - && arg_chars[0] == '"' - && arg_chars[arg_chars.len() - 1] == '"' - { - // quoted string - let new_arg: String = arg_chars[1..arg_chars.len() - 1].iter().collect(); - process = process.arg(new_arg); - } else { - process = process.arg(arg.arg.clone()); - } - } - } - - process = process.cwd(context.shell_manager.path()); - - trace!(target: "nu::run::external", "cwd = {:?}", context.shell_manager.path()); - - let mut process = match stream_next { - StreamNext::Last => process, - StreamNext::External | StreamNext::Internal => { - process.stdout(subprocess::Redirection::Pipe) - } - }; - - trace!(target: "nu::run::external", "set up stdout pipe"); - - if let Some(stdin) = stdin { - process = process.stdin(stdin); - } - - trace!(target: "nu::run::external", "set up stdin pipe"); - trace!(target: "nu::run::external", "built process {:?}", process); - - let popen = process.popen(); - - trace!(target: "nu::run::external", "next = {:?}", stream_next); - - let name_tag = self.name_tag.clone(); - if let Ok(mut popen) = popen { - match stream_next { - StreamNext::Last => { - let _ = popen.detach(); - loop { - match popen.poll() { - None => { - let _ = std::thread::sleep(std::time::Duration::new(0, 100000000)); - } - _ => { - let _ = popen.terminate(); - break; - } - } - } - Ok(ClassifiedInputStream::new()) - } - StreamNext::External => { - let _ = popen.detach(); - let stdout = popen.stdout.take().unwrap(); - Ok(ClassifiedInputStream::from_stdout(stdout)) - } - StreamNext::Internal => { - let _ = popen.detach(); - let stdout = popen.stdout.take().unwrap(); - let file = futures::io::AllowStdIo::new(stdout); - let stream = Framed::new(file, LinesCodec {}); - let stream = stream.map(move |line| { - UntaggedValue::string(line.unwrap()).into_value(&name_tag) - }); - Ok(ClassifiedInputStream::from_input_stream( - stream.boxed() as BoxStream<'static, Value> - )) - } - } - } else { - return Err(ShellError::labeled_error( - "Command not found", - "command not found", - name_tag, - )); - } - } -} diff --git a/src/commands/classified/dynamic.rs b/src/commands/classified/dynamic.rs new file mode 100644 index 0000000000..8e6e7d6510 --- /dev/null +++ b/src/commands/classified/dynamic.rs @@ -0,0 +1,7 @@ +use crate::parser::hir; +use derive_new::new; + +#[derive(new, Debug, Eq, PartialEq)] +pub(crate) struct Command { + pub(crate) args: hir::Call, +} diff --git a/src/commands/classified/external.rs b/src/commands/classified/external.rs new file mode 100644 index 0000000000..f668abad12 --- /dev/null +++ b/src/commands/classified/external.rs @@ -0,0 +1,259 @@ +use super::ClassifiedInputStream; +use crate::prelude::*; +use bytes::{BufMut, BytesMut}; +use futures::stream::StreamExt; +use futures_codec::{Decoder, Encoder, Framed}; +use log::trace; +use std::io::{Error, ErrorKind}; +use subprocess::Exec; + +/// A simple `Codec` implementation that splits up data into lines. +pub struct LinesCodec {} + +impl Encoder for LinesCodec { + type Item = String; + type Error = Error; + + fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> { + dst.put(item); + Ok(()) + } +} + +impl Decoder for LinesCodec { + type Item = String; + type Error = Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + match src.iter().position(|b| b == &b'\n') { + Some(pos) if !src.is_empty() => { + let buf = src.split_to(pos + 1); + String::from_utf8(buf.to_vec()) + .map(Some) + .map_err(|e| Error::new(ErrorKind::InvalidData, e)) + } + _ if !src.is_empty() => { + let drained = src.take(); + String::from_utf8(drained.to_vec()) + .map(Some) + .map_err(|e| Error::new(ErrorKind::InvalidData, e)) + } + _ => Ok(None), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Command { + pub(crate) name: String, + + pub(crate) name_tag: Tag, + pub(crate) args: ExternalArgs, +} + +impl HasSpan for Command { + fn span(&self) -> Span { + self.name_tag.span.until(self.args.span) + } +} + +impl PrettyDebug for Command { + fn pretty(&self) -> DebugDocBuilder { + b::typed( + "external command", + b::description(&self.name) + + b::preceded( + b::space(), + b::intersperse( + self.args.iter().map(|a| b::primitive(format!("{}", a.arg))), + b::space(), + ), + ), + ) + } +} + +#[derive(Debug)] +pub(crate) enum StreamNext { + Last, + External, + Internal, +} + +impl Command { + pub(crate) async fn run( + self, + context: &mut Context, + input: ClassifiedInputStream, + stream_next: StreamNext, + ) -> Result { + let stdin = input.stdin; + let inputs: Vec = input.objects.into_vec().await; + + trace!(target: "nu::run::external", "-> {}", self.name); + trace!(target: "nu::run::external", "inputs = {:?}", inputs); + + let mut arg_string = format!("{}", self.name); + for arg in &self.args.list { + arg_string.push_str(&arg); + } + + trace!(target: "nu::run::external", "command = {:?}", self.name); + + let mut process; + if arg_string.contains("$it") { + let input_strings = inputs + .iter() + .map(|i| { + i.as_string().map_err(|_| { + let arg = self.args.iter().find(|arg| arg.arg.contains("$it")); + if let Some(arg) = arg { + ShellError::labeled_error( + "External $it needs string data", + "given row instead of string data", + &arg.tag, + ) + } else { + ShellError::labeled_error( + "$it needs string data", + "given something else", + self.name_tag.clone(), + ) + } + }) + }) + .collect::, ShellError>>()?; + + let commands = input_strings.iter().map(|i| { + let args = self.args.iter().filter_map(|arg| { + if arg.chars().all(|c| c.is_whitespace()) { + None + } else { + Some(arg.replace("$it", &i)) + } + }); + + format!("{} {}", self.name, itertools::join(args, " ")) + }); + + process = Exec::shell(itertools::join(commands, " && ")) + } else { + process = Exec::cmd(&self.name); + for arg in &self.args.list { + let arg_chars: Vec<_> = arg.chars().collect(); + if arg_chars.len() > 1 + && arg_chars[0] == '"' + && arg_chars[arg_chars.len() - 1] == '"' + { + // quoted string + let new_arg: String = arg_chars[1..arg_chars.len() - 1].iter().collect(); + process = process.arg(new_arg); + } else { + process = process.arg(arg.arg.clone()); + } + } + } + + process = process.cwd(context.shell_manager.path()); + + trace!(target: "nu::run::external", "cwd = {:?}", context.shell_manager.path()); + + let mut process = match stream_next { + StreamNext::Last => process, + StreamNext::External | StreamNext::Internal => { + process.stdout(subprocess::Redirection::Pipe) + } + }; + + trace!(target: "nu::run::external", "set up stdout pipe"); + + if let Some(stdin) = stdin { + process = process.stdin(stdin); + } + + trace!(target: "nu::run::external", "set up stdin pipe"); + trace!(target: "nu::run::external", "built process {:?}", process); + + let popen = process.popen(); + + trace!(target: "nu::run::external", "next = {:?}", stream_next); + + let name_tag = self.name_tag.clone(); + if let Ok(mut popen) = popen { + match stream_next { + StreamNext::Last => { + let _ = popen.detach(); + loop { + match popen.poll() { + None => { + let _ = std::thread::sleep(std::time::Duration::new(0, 100000000)); + } + _ => { + let _ = popen.terminate(); + break; + } + } + } + Ok(ClassifiedInputStream::new()) + } + StreamNext::External => { + let _ = popen.detach(); + let stdout = popen.stdout.take().unwrap(); + Ok(ClassifiedInputStream::from_stdout(stdout)) + } + StreamNext::Internal => { + let _ = popen.detach(); + let stdout = popen.stdout.take().unwrap(); + let file = futures::io::AllowStdIo::new(stdout); + let stream = Framed::new(file, LinesCodec {}); + let stream = stream.map(move |line| { + UntaggedValue::string(line.unwrap()).into_value(&name_tag) + }); + Ok(ClassifiedInputStream::from_input_stream( + stream.boxed() as BoxStream<'static, Value> + )) + } + } + } else { + return Err(ShellError::labeled_error( + "Command not found", + "command not found", + name_tag, + )); + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ExternalArg { + pub arg: String, + pub tag: Tag, +} + +impl std::ops::Deref for ExternalArg { + type Target = str; + + fn deref(&self) -> &str { + &self.arg + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ExternalArgs { + pub list: Vec, + pub span: Span, +} + +impl ExternalArgs { + pub fn iter(&self) -> impl Iterator { + self.list.iter() + } +} + +impl std::ops::Deref for ExternalArgs { + type Target = [ExternalArg]; + + fn deref(&self) -> &[ExternalArg] { + &self.list + } +} diff --git a/src/commands/classified/internal.rs b/src/commands/classified/internal.rs new file mode 100644 index 0000000000..582f7d7986 --- /dev/null +++ b/src/commands/classified/internal.rs @@ -0,0 +1,147 @@ +use crate::parser::hir; +use crate::prelude::*; +use derive_new::new; +use log::{log_enabled, trace}; + +use super::ClassifiedInputStream; + +#[derive(new, Debug, Clone, Eq, PartialEq)] +pub struct Command { + pub(crate) name: String, + pub(crate) name_tag: Tag, + pub(crate) args: hir::Call, +} + +impl HasSpan for Command { + fn span(&self) -> Span { + let start = self.name_tag.span; + + start.until(self.args.span) + } +} + +impl PrettyDebugWithSource for Command { + fn pretty_debug(&self, source: &str) -> DebugDocBuilder { + b::typed( + "internal command", + b::description(&self.name) + b::space() + self.args.pretty_debug(source), + ) + } +} + +impl Command { + pub(crate) fn run( + self, + context: &mut Context, + input: ClassifiedInputStream, + source: Text, + ) -> Result { + if log_enabled!(log::Level::Trace) { + trace!(target: "nu::run::internal", "->"); + trace!(target: "nu::run::internal", "{}", self.name); + trace!(target: "nu::run::internal", "{}", self.args.debug(&source)); + } + + let objects: InputStream = + trace_stream!(target: "nu::trace_stream::internal", "input" = input.objects); + + let command = context.expect_command(&self.name); + + let result = + { context.run_command(command, self.name_tag.clone(), self.args, &source, objects) }; + + let result = trace_out_stream!(target: "nu::trace_stream::internal", "output" = result); + let mut result = result.values; + let mut context = context.clone(); + + let stream = async_stream! { + let mut soft_errs: Vec = vec![]; + let mut yielded = false; + + while let Some(item) = result.next().await { + match item { + Ok(ReturnSuccess::Action(action)) => match action { + CommandAction::ChangePath(path) => { + context.shell_manager.set_path(path); + } + CommandAction::Exit => std::process::exit(0), // TODO: save history.txt + CommandAction::Error(err) => { + context.error(err); + break; + } + CommandAction::EnterHelpShell(value) => { + match value { + Value { + value: UntaggedValue::Primitive(Primitive::String(cmd)), + tag, + } => { + context.shell_manager.insert_at_current(Box::new( + HelpShell::for_command( + UntaggedValue::string(cmd).into_value(tag), + &context.registry(), + ).unwrap(), + )); + } + _ => { + context.shell_manager.insert_at_current(Box::new( + HelpShell::index(&context.registry()).unwrap(), + )); + } + } + } + CommandAction::EnterValueShell(value) => { + context + .shell_manager + .insert_at_current(Box::new(ValueShell::new(value))); + } + CommandAction::EnterShell(location) => { + context.shell_manager.insert_at_current(Box::new( + FilesystemShell::with_location(location, context.registry().clone()).unwrap(), + )); + } + CommandAction::PreviousShell => { + context.shell_manager.prev(); + } + CommandAction::NextShell => { + context.shell_manager.next(); + } + CommandAction::LeaveShell => { + context.shell_manager.remove_at_current(); + if context.shell_manager.is_empty() { + std::process::exit(0); // TODO: save history.txt + } + } + }, + + Ok(ReturnSuccess::Value(v)) => { + yielded = true; + yield Ok(v); + } + + Ok(ReturnSuccess::DebugValue(v)) => { + yielded = true; + + let doc = PrettyDebug::pretty_doc(&v); + let mut buffer = termcolor::Buffer::ansi(); + + doc.render_raw( + context.with_host(|host| host.width() - 5), + &mut crate::parser::debug::TermColored::new(&mut buffer), + ).unwrap(); + + let value = String::from_utf8_lossy(buffer.as_slice()); + + yield Ok(UntaggedValue::string(value).into_untagged_value()) + } + + Err(err) => { + context.error(err); + break; + } + } + } + }; + + Ok(stream.to_input_stream()) + } +} diff --git a/src/commands/classified/mod.rs b/src/commands/classified/mod.rs new file mode 100644 index 0000000000..b3add6eccb --- /dev/null +++ b/src/commands/classified/mod.rs @@ -0,0 +1,74 @@ +use crate::parser::{hir, TokenNode}; +use crate::prelude::*; + +mod dynamic; +mod external; +mod internal; +mod pipeline; + +#[allow(unused_imports)] +pub(crate) use dynamic::Command as DynamicCommand; +#[allow(unused_imports)] +pub(crate) use external::{Command as ExternalCommand, ExternalArg, ExternalArgs, StreamNext}; +pub(crate) use internal::Command as InternalCommand; +pub(crate) use pipeline::Pipeline as ClassifiedPipeline; + +pub(crate) struct ClassifiedInputStream { + pub(crate) objects: InputStream, + pub(crate) stdin: Option, +} + +impl ClassifiedInputStream { + pub(crate) fn new() -> ClassifiedInputStream { + ClassifiedInputStream { + objects: vec![UntaggedValue::nothing().into_untagged_value()].into(), + stdin: None, + } + } + + pub(crate) fn from_input_stream(stream: impl Into) -> ClassifiedInputStream { + ClassifiedInputStream { + objects: stream.into(), + stdin: None, + } + } + + pub(crate) fn from_stdout(stdout: std::fs::File) -> ClassifiedInputStream { + ClassifiedInputStream { + objects: VecDeque::new().into(), + stdin: Some(stdout), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ClassifiedCommand { + #[allow(unused)] + Expr(TokenNode), + #[allow(unused)] + Dynamic(hir::Call), + Internal(InternalCommand), + External(ExternalCommand), +} + +impl PrettyDebugWithSource for ClassifiedCommand { + fn pretty_debug(&self, source: &str) -> DebugDocBuilder { + match self { + ClassifiedCommand::Expr(token) => b::typed("command", token.pretty_debug(source)), + ClassifiedCommand::Dynamic(call) => b::typed("command", call.pretty_debug(source)), + ClassifiedCommand::Internal(internal) => internal.pretty_debug(source), + ClassifiedCommand::External(external) => external.pretty_debug(source), + } + } +} + +impl HasSpan for ClassifiedCommand { + fn span(&self) -> Span { + match self { + ClassifiedCommand::Expr(node) => node.span(), + ClassifiedCommand::Internal(command) => command.span(), + ClassifiedCommand::Dynamic(call) => call.span, + ClassifiedCommand::External(command) => command.span(), + } + } +} diff --git a/src/commands/classified/pipeline.rs b/src/commands/classified/pipeline.rs new file mode 100644 index 0000000000..f40b627437 --- /dev/null +++ b/src/commands/classified/pipeline.rs @@ -0,0 +1,40 @@ +use super::ClassifiedCommand; +use crate::prelude::*; + +#[derive(Debug, Clone)] +pub(crate) struct Pipeline { + pub(crate) commands: ClassifiedCommands, +} + +impl Pipeline { + pub fn commands(list: Vec, span: impl Into) -> Pipeline { + Pipeline { + commands: ClassifiedCommands { + list, + span: span.into(), + }, + } + } +} + +#[derive(Debug, Clone)] +pub struct ClassifiedCommands { + pub list: Vec, + pub span: Span, +} + +impl HasSpan for Pipeline { + fn span(&self) -> Span { + self.commands.span + } +} + +impl PrettyDebugWithSource for Pipeline { + fn pretty_debug(&self, source: &str) -> DebugDocBuilder { + b::intersperse( + self.commands.list.iter().map(|c| c.pretty_debug(source)), + b::operator(" | "), + ) + .or(b::delimit("<", b::description("empty pipeline"), ">")) + } +} diff --git a/src/commands/nth.rs b/src/commands/nth.rs index 8e7e0828bf..b57b7a413f 100644 --- a/src/commands/nth.rs +++ b/src/commands/nth.rs @@ -6,7 +6,8 @@ use nu_source::Tagged; #[derive(Deserialize)] struct NthArgs { - amount: Tagged, + row_number: Tagged, + rest: Vec>, } pub struct Nth; @@ -17,15 +18,17 @@ impl WholeStreamCommand for Nth { } fn signature(&self) -> Signature { - Signature::build("nth").required( - "row number", - SyntaxShape::Any, - "the number of the row to return", - ) + Signature::build("nth") + .required( + "row number", + SyntaxShape::Any, + "the number of the row to return", + ) + .rest(SyntaxShape::Any, "Optionally return more rows") } fn usage(&self) -> &str { - "Return only the selected row" + "Return only the selected rows" } fn run( @@ -38,10 +41,35 @@ impl WholeStreamCommand for Nth { } fn nth( - NthArgs { amount }: NthArgs, + NthArgs { + row_number, + rest: and_rows, + }: NthArgs, RunnableContext { input, .. }: RunnableContext, ) -> Result { - Ok(OutputStream::from_input( - input.values.skip(amount.item as u64).take(1), - )) + let stream = input + .values + .enumerate() + .map(move |(idx, item)| { + let row_number = vec![row_number.clone()]; + + let row_numbers = vec![&row_number, &and_rows] + .into_iter() + .flatten() + .collect::>>(); + + let mut result = VecDeque::new(); + + if row_numbers + .iter() + .any(|requested| requested.item == idx as u64) + { + result.push_back(ReturnSuccess::value(item.clone())); + } + + result + }) + .flatten(); + + Ok(stream.to_output_stream()) } diff --git a/src/evaluate/evaluator.rs b/src/evaluate/evaluator.rs index fc9c702811..3700ce50f3 100644 --- a/src/evaluate/evaluator.rs +++ b/src/evaluate/evaluator.rs @@ -1,5 +1,6 @@ use crate::data::base::Block; use crate::errors::ArgumentError; +use crate::evaluate::operator::apply_operator; use crate::parser::hir::path::{ColumnPath, UnspannedPathMember}; use crate::parser::{ hir::{self, Expression, RawExpression}, @@ -71,8 +72,8 @@ pub(crate) fn evaluate_baseline_expr( trace!("left={:?} right={:?}", left.value, right.value); - match left.compare(binary.op(), &right) { - Ok(result) => Ok(UntaggedValue::boolean(result).into_value(tag)), + match apply_operator(binary.op(), &left, &right) { + Ok(result) => Ok(result.into_value(tag)), Err((left_type, right_type)) => Err(ShellError::coerce_error( left_type.spanned(binary.left().span), right_type.spanned(binary.right().span), diff --git a/src/evaluate/mod.rs b/src/evaluate/mod.rs index 21a3b369d8..f8133808e0 100644 --- a/src/evaluate/mod.rs +++ b/src/evaluate/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod evaluator; +pub(crate) mod operator; pub(crate) use evaluator::{evaluate_baseline_expr, Scope}; diff --git a/src/evaluate/operator.rs b/src/evaluate/operator.rs new file mode 100644 index 0000000000..ba450a5137 --- /dev/null +++ b/src/evaluate/operator.rs @@ -0,0 +1,39 @@ +use crate::data::base::{Primitive, UntaggedValue, Value}; +use crate::parser::Operator; +use crate::traits::ShellTypeName; +use std::ops::Not; + +pub fn apply_operator( + op: &Operator, + left: &Value, + right: &Value, +) -> Result { + match *op { + Operator::Equal + | Operator::NotEqual + | Operator::LessThan + | Operator::GreaterThan + | Operator::LessThanOrEqual + | Operator::GreaterThanOrEqual => left.compare(op, right).map(UntaggedValue::boolean), + Operator::Dot => Ok(UntaggedValue::boolean(false)), + Operator::Contains => contains(left, right).map(UntaggedValue::boolean), + Operator::NotContains => contains(left, right) + .map(Not::not) + .map(UntaggedValue::boolean), + } +} + +fn contains( + left: &UntaggedValue, + right: &UntaggedValue, +) -> Result { + if let ( + UntaggedValue::Primitive(Primitive::String(l)), + UntaggedValue::Primitive(Primitive::String(r)), + ) = (left, right) + { + Ok(l.contains(r)) + } else { + Err((left.type_name(), right.type_name())) + } +} diff --git a/src/parser/parse/operator.rs b/src/parser/parse/operator.rs index 88b99ec5b8..f98097f374 100644 --- a/src/parser/parse/operator.rs +++ b/src/parser/parse/operator.rs @@ -13,6 +13,8 @@ pub enum Operator { LessThanOrEqual, GreaterThanOrEqual, Dot, + Contains, + NotContains, } impl PrettyDebug for Operator { @@ -35,6 +37,8 @@ impl Operator { Operator::LessThanOrEqual => "<=", Operator::GreaterThanOrEqual => ">=", Operator::Dot => ".", + Operator::Contains => "=~", + Operator::NotContains => "!~", } } } @@ -56,6 +60,8 @@ impl FromStr for Operator { "<=" => Ok(Operator::LessThanOrEqual), ">=" => Ok(Operator::GreaterThanOrEqual), "." => Ok(Operator::Dot), + "=~" => Ok(Operator::Contains), + "!~" => Ok(Operator::NotContains), _ => Err(()), } } diff --git a/src/parser/parse/parser.rs b/src/parser/parse/parser.rs index 0f5be81aa7..50cf4112bc 100644 --- a/src/parser/parse/parser.rs +++ b/src/parser/parse/parser.rs @@ -30,7 +30,7 @@ macro_rules! operator { #[tracable_parser] pub fn $name(input: NomSpan) -> IResult { let start = input.offset; - let (input, tag) = tag(stringify!($token))(input)?; + let (input, tag) = tag($token)(input)?; let end = input.offset; Ok(( @@ -41,13 +41,15 @@ macro_rules! operator { }; } -operator! { gt: > } -operator! { lt: < } -operator! { gte: >= } -operator! { lte: <= } -operator! { eq: == } -operator! { neq: != } -operator! { dot: . } +operator! { gt: ">" } +operator! { lt: "<" } +operator! { gte: ">=" } +operator! { lte: "<=" } +operator! { eq: "==" } +operator! { neq: "!=" } +operator! { dot: "." } +operator! { cont: "=~" } +operator! { ncont: "!~" } #[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)] pub enum Number { @@ -205,7 +207,7 @@ pub fn raw_number(input: NomSpan) -> IResult { #[tracable_parser] pub fn operator(input: NomSpan) -> IResult { - let (input, operator) = alt((gte, lte, neq, gt, lt, eq))(input)?; + let (input, operator) = alt((gte, lte, neq, gt, lt, eq, cont, ncont))(input)?; Ok((input, operator)) } @@ -805,6 +807,16 @@ mod tests { "!=" -> b::token_list(vec![b::op("!=")]) } + + equal_tokens! { + + "=~" -> b::token_list(vec![b::op("=~")]) + } + + equal_tokens! { + + "!~" -> b::token_list(vec![b::op("!~")]) + } } #[test] diff --git a/tests/commands_test.rs b/tests/commands_test.rs index 59621539db..89ab430b36 100644 --- a/tests/commands_test.rs +++ b/tests/commands_test.rs @@ -3,6 +3,45 @@ mod helpers; use helpers as h; use helpers::{Playground, Stub::*}; +#[test] +fn nth_selects_a_row() { + Playground::setup("nth_test_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("notes.txt"), EmptyFile("arepas.txt")]); + + let actual = nu!( + cwd: dirs.test(), h::pipeline( + r#" + ls + | sort-by name + | nth 0 + | get name + | echo $it + "# + )); + + assert_eq!(actual, "arepas.txt"); + }); +} + +#[test] +fn nth_selects_many_rows() { + Playground::setup("nth_test_2", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("notes.txt"), EmptyFile("arepas.txt")]); + + let actual = nu!( + cwd: dirs.test(), h::pipeline( + r#" + ls + | get name + | nth 1 0 + | count + | echo $it + "# + )); + + assert_eq!(actual, "2"); + }); +} #[test] fn default_row_data_if_column_missing() { Playground::setup("default_test_1", |dirs, sandbox| { diff --git a/tests/filter_where_tests.rs b/tests/filter_where_tests.rs new file mode 100644 index 0000000000..e802607d85 --- /dev/null +++ b/tests/filter_where_tests.rs @@ -0,0 +1,112 @@ +mod helpers; + +use helpers as h; + +#[test] +fn test_compare() { + let actual = nu!( + cwd: "tests/fixtures/formats", h::pipeline( + r#" + open sample.db + | where table_name == ints + | get table_values + | first 4 + | where z > 4200 + | get z + | echo $it + "# + )); + + assert_eq!(actual, "4253"); + + let actual = nu!( + cwd: "tests/fixtures/formats", h::pipeline( + r#" + open sample.db + | where table_name == ints + | get table_values + | first 4 + | where z >= 4253 + | get z + | echo $it + "# + )); + + assert_eq!(actual, "4253"); + + let actual = nu!( + cwd: "tests/fixtures/formats", h::pipeline( + r#" + open sample.db + | where table_name == ints + | get table_values + | first 4 + | where z < 10 + | get z + | echo $it + "# + )); + + assert_eq!(actual, "1"); + + let actual = nu!( + cwd: "tests/fixtures/formats", h::pipeline( + r#" + open sample.db + | where table_name == ints + | get table_values + | first 4 + | where z <= 1 + | get z + | echo $it + "# + )); + + assert_eq!(actual, "1"); + + let actual = nu!( + cwd: "tests/fixtures/formats", h::pipeline( + r#" + open sample.db + | where table_name == ints + | get table_values + | where z != 1 + | first 1 + | get z + | echo $it + "# + )); + + assert_eq!(actual, "42"); +} + +#[test] +fn test_contains() { + let actual = nu!( + cwd: "tests/fixtures/formats", h::pipeline( + r#" + open sample.db + | where table_name == strings + | get table_values + | where x =~ ell + | count + | echo $it + "# + )); + + assert_eq!(actual, "4"); + + let actual = nu!( + cwd: "tests/fixtures/formats", h::pipeline( + r#" + open sample.db + | where table_name == strings + | get table_values + | where x !~ ell + | count + | echo $it + "# + )); + + assert_eq!(actual, "2"); +}